App-server: expose atomic idle-only turn admission

Open 💬 2 comments Opened Aug 13, 2026 by jrdeck

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 TurnStartResponse with 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.

View original on GitHub ↗

2 Comments

jrdeck · 14 days ago

The fixed source and no-model proof are ready:

  • @clevvi capability PR 2 — experimental turn/startIfIdle, pinned to post-#38275 commit 902bd9e; reviewed/canaried production source is e9380d3, and current head 17375b1 adds test/docs portability only.
  • @clevvi proof PR 202 — exact pinned binary, isolated PID/network namespaces, fake Responses providers, ordering, busy non-interruption, reconnect/decoy binding, two roots, both crash boundaries, and stock remote-TUI approval ownership; no real model calls.

I tried opening the promised draft from clevvi:feat/CSF-1085-add-atomic-turn-admission to openai/codex:main, but GitHub rejected CreatePullRequest for 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 moving main; I can do one maintainer-requested refresh after that signal.

jdcodes1 · 9 days ago

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_idle submits through the ordered turn-input path with TurnInputMode::StartIfIdle and returns Started { turn_id } or NotSubmitted { 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 maps NotSubmitted{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 semantics turn/startIfIdle needs — it's just not reachable with arbitrary input from an external client.

So the implementation is: a v2 request that validates expectedCwd, then calls start_turn_if_idle and maps NotSubmitted reasons to typed JSON-RPC errors, mirroring the queue processor's handling. No new Core admission logic required.