Repository navigation
Make turn/steer retries idempotent by clientUserMessageId #32254
Description
What variant of Codex are you using?
Codex app-server v2 through an external client.
What feature would you like to see?
Make clientUserMessageId a durable, thread-scoped idempotency key for turn/steer.
External app-server clients can receive input from durable ingress queues. A client may lose its connection or exit after Codex accepts a steer but before the client commits its own acknowledgement. Retrying the request must not deliver the same user message to the active turn twice.
The requested contract is:
- The first
turn/steerrequest for an ID and payload durably records the acceptance before returning success. - A concurrent or later request with the same thread, ID, and payload returns the original accepted result without enqueueing another user message.
- A request with the same thread and ID but different input returns a machine-readable idempotency conflict.
- The accepted ID survives app-server and client restarts through Codex thread or rollout persistence.
item/started,item/completed,thread/read, and resume history preserve the client ID so clients can reconcile an uncertain request.
The protocol already exposes clientUserMessageId on turn/start and turn/steer. Codex also persists the ID in user-message history and emits it in item notifications. PR #24653 describes that field as a correlation mechanism. This request extends that mechanism with retry semantics at the component that owns steer acceptance.
Suggested public integration coverage:
- Send two concurrent steers with the same ID and payload; assert one user item and equivalent successful responses.
- Restart app-server and replay the same request; assert no second user item.
- Reuse the ID with different input; assert a structured conflict.
- Interrupt the client after acceptance and reconcile the result through
thread/reador resume history.
Additional information
This arose while integrating Codex steering with OpenClaw's durable Telegram ingress and SQLite transcript. OpenClaw must finalize an ingress entry only after the steered message is durable in its authoritative transcript. Codex emits a correlated item/started after persisting the user item, but a client crash between Codex persistence and client-side persistence leaves the request outcome uncertain. Retrying can duplicate model delivery because Codex does not deduplicate the client ID.
Adjacent work:
- [codex] Add user input client ids #24653 adds and persists user input client IDs for correlation.
- Preserve late steer after turn finalization #30341 preserves a late steer during turn finalization, but does not deduplicate repeated client requests.
- Codex Mobile: queued message disappears after app background/thread reload #25268 discusses durable queued input and notes that stable client IDs and idempotent retry would support reconnect recovery.
An OpenClaw follow-up fallback prevents message loss when this guarantee is unavailable, but same-turn steering still requires Codex-owned idempotent acceptance.
Activity
FayAndXan commentedon Jul 20, 2026
Downstream production reproduction reinforcing the need for durable, idempotent
turn/steer:On 2026-07-20, OpenClaw 2026.7.1-beta.5 sent a Telegram message into an active native Codex turn. The message was echoed externally 748 ms after ingress, while the real assistant answer arrived separately later. The retained Codex transcript shows the text only as a
userMessage, so the echo appears to be an OpenClaw delivery/provenance defect rather than Codex generating that answer.However, recovery analysis still reaches the gap described here: after
turn/steeracceptance, the downstream client cannot safely distinguish “persisted but acknowledgement/notification was lost” from “not persisted.” Retrying the sameclientUserMessageIdmay duplicate the user item; not retrying may lose it.Is durable, thread-scoped idempotency for
clientUserMessageIdstill planned, and will app-server advertise it as a protocol capability rather than requiring version detection? The interoperability tests we need are:thread/reador resume historyuserMessage.clientIdemitted only after durable adoptionWe can provide further sanitized downstream evidence if helpful.