App-server: expose atomic idle-only turn admission
What variant of Codex are you using?
CLI / app-server v2 on current main (e766f7598993ce37cf61b9c26c80cc2ba3a4f2d7).
What feature would you like to see?
An experimental app-server request that atomically starts a turn only when a specific thread is idle.
Today, turn/start is intentionally start-or-steer: if a turn becomes active before Core admits the request, the input can be steered into that turn. A client can read an idle status first, but thread/read → turn/start is not atomic. That makes turn/start unsafe for background or event-driven clients that must never alter an active human-owned turn.
A narrow v2 shape would be:
turn/startIfIdle {
threadId,
expectedCwd,
clientUserMessageId?,
input
}
The admission should happen inside the same ordered Core turn-input path as ordinary input and either:
- start one turn and return the existing
TurnStartResponsewith its actual turn ID; or - reject without steering, interrupting, queuing, applying turn settings, recording input, or making a provider request.
The useful fail-closed reasons are:
- thread is busy;
- pending trigger work exists;
- Plan mode is active for this client admission;
- the effective turn cwd differs from
expectedCwd; - the thread is not persistence-enabled.
The cwd check must be part of the same reserved admission operation; checking it in app-server before submission would leave another race.
Core now has most of the required primitive: PR 38275 added ordered CodexThread::start_turn_if_idle / TurnInputMode::StartIfIdle. The remaining work is a small precondition extension plus the experimental app-server protocol/processor surface, schemas, docs, and focused fake-provider regressions.
This does not require a new delivery queue, retry loop, fallback to turn/start, approval response, or replacement client.
Additional information
We ran into this while building a small resident app-server integration at @clevvi: inbound work should wake an idle Codex thread, but must leave an active TUI turn completely alone.
Related, but not duplicates:
- #20312 asks for a broader native event-driven wake facility.
- #36866 documents the existing start-or-steer behavior and turn-ID consequences of
turn/start. - #34767 shows why multi-client admission needs a single authoritative turn boundary.
I have a focused current-main patch and deterministic no-real-model tests in progress. Happy to open the PR if this API shape is welcome.
2 Comments
The fixed source and no-model proof are ready:
turn/startIfIdle, pinned to post-#38275 commit902bd9e; reviewed/canaried production source ise9380d3, and current head17375b1adds test/docs portability only.I tried opening the promised draft from
clevvi:feat/CSF-1085-add-atomic-turn-admissiontoopenai/codex:main, but GitHub rejectedCreatePullRequestfor contribution permissions. If the API shape is welcome, please authorize/invite the upstream PR and I will open it from this fixed branch. I have deliberately not chased movingmain; I can do one maintainer-requested refresh after that signal.Supporting data point: the atomic primitive you're asking for already exists end-to-end in Core, so this is almost purely an API-surface change.
CodexThread::start_turn_if_idlesubmits through the ordered turn-input path withTurnInputMode::StartIfIdleand returnsStarted { turn_id }orNotSubmitted { reason }without steering, queuing, or provider calls (https://github.com/openai/codex/blob/1f41cc5d92/codex-rs/core/src/codex_thread.rs#L290-L313). The fail-closed reasons you enumerated are already the protocol enum —NotIdle,PendingTriggerTurn,PlanMode(protocol/src/turn_input.rs#L182-L200). And app-server already consumes it internally: the thread-queue processor mapsNotSubmitted{NotIdle|PendingTriggerTurn}to an "already has an active or pending turn" error (app-server/src/request_processors/thread_queue_processor.rs#L199-L212), which is exactly the semanticsturn/startIfIdleneeds — it's just not reachable with arbitrary input from an external client.So the implementation is: a v2 request that validates
expectedCwd, then callsstart_turn_if_idleand mapsNotSubmittedreasons to typed JSON-RPC errors, mirroring the queue processor's handling. No new Core admission logic required.