app-server turn/start returns a submission ID when it steers into an active turn

Open 💬 1 comment Opened Aug 4, 2026 by luvs01

What version of Codex CLI is running?

openai/codex main at d4fb78bfc59009a2bbc3245d125bf8ba92a8e33e (source-level codex-app-server regression test). No released binary or live provider is required to reproduce it.

What subscription do you have?

ChatGPT Pro. This bug is local app-server state handling and is not plan-dependent.

Which model were you using?

A mock Responses API provider. The behavior is not model-dependent.

What platform is your computer?

Windows 11 x64. The affected Rust path is platform-independent; the regression fixture has Windows and Unix hold-open commands.

What terminal emulator and version are you using (if applicable)?

PowerShell 7. The reproducer exercises app-server v2 JSON-RPC directly rather than terminal rendering.

Codex doctor report

{
  "status": "not_applicable",
  "reason": "source-level app-server regression with a mock provider; no auth, network, MCP, or local-state dependency"
}

What issue are you seeing?

If a thread already has an active turn and a client sends another turn/start, Core admits that user message by steering it into the active turn. However, app-server currently returns the new submission UUID as Turn.id before it knows whether Core started a new turn or steered the input.

The result is one logical turn with two IDs from the client's perspective:

  • the second turn/start response reports submission ID S;
  • turn/started, item/*, turn/completed, and interruption continue to use the active turn ID A;
  • the second user message is correctly delivered to A, so S never becomes the lifecycle turn ID.

Clients that correlate pending requests, stop actions, or resumed state using the response ID can therefore track a turn that does not exist as an independent lifecycle.

The direct cause is turn_start_inner calling submit_user_input_with_client_user_message_id, which returns Submission.id. Core already exposes the acknowledged API added in #36385: submit_user_input_and_wait_for_admission returns UserMessageAdmission::Started { turn_id } or Steered { turn_id }.

What steps can reproduce the bug?

  1. Start an app-server v2 thread against a mock Responses API.
  2. Send turn/start for turn A and keep it active with a controllable long-running shell call.
  3. Wait for turn/started and record A.
  4. While A is active, send a second turn/start with a distinct clientUserMessageId.
  5. Read the second turn/start response and the item/started notification for that second user message.
  6. On current main, the response ID is a fresh submission ID S, while the user item and subsequent lifecycle remain on A.

A deterministic integration regression can assert that both turn/start responses return A and that the second client message is emitted under the active turn before explicitly interrupting and awaiting A's terminal notification.

What is the expected behavior?

With Core's current admission semantics, a second turn/start that is steered into an active turn should return that active turn's ID. The response shape should remain { turn }; only turn.id should be correlated with the lifecycle that will actually emit notifications.

If admission fails, the JSON-RPC request should return the corresponding invalid-request or internal error instead of a provisional submission ID.

Additional information

I searched current issues, PRs, and main history. I did not find an exact duplicate.

  • #36385 added the Core admission acknowledgement used by the minimal fix, but app-server turn/start was not wired to it.
  • #36389 adds complementary cross-process single-writer protection; it does not correct same-process turn/start response correlation.
  • #18516 concerns a different review/start lifecycle ID mismatch.
  • #34767 and #34034 cover broader overlapping-turn/cross-client behavior; this report is the narrower deterministic response-ID defect.

I have a focused local patch that switches turn/start to the acknowledged Core API, maps both Started and Steered to the canonical turn ID, preserves the public response shape, and adds the deterministic app-server regression above. The new regression and related turn-start/Core turn-ID tests pass; scoped Clippy and formatting checks also pass. Per the invitation-only contribution policy, I can submit the small PR if a maintainer confirms that this approach is wanted.

View original on GitHub ↗

1 Comment

TopGrd · 23 days ago

Confirmed against Codex 0.146.0 with a real app-server client.

In the observed thread, native goal continuation first started active turn A as UUIDv4. A later turn/start returned a new UUIDv7 submission ID S, while turn/started, item/*, turn/completed, rollout metadata, and turn/interrupt all continued to use A. The client therefore discarded every notification when correlating them with TurnStartResponse.turn.id, then timed out; interrupting with S failed with expected active turn id S but found A.

This confirms the issue affects external app-server clients, not only the synthetic reproducer. The focused fix described here looks correct:

  1. Have app-server turn_start_inner call submit_user_input_and_wait_for_admission.
  2. Return the admitted turn ID for both UserMessageAdmission::Started and UserMessageAdmission::Steered while preserving the existing { turn } response shape.
  3. Propagate admission failures as JSON-RPC errors instead of returning a provisional ID.
  4. Add an app-server integration regression that holds turn A active, sends a second turn/start, and verifies the second response, client-tagged user item, interrupt, and terminal notification all use A.

Per the invitation-only contribution policy, would a maintainer like a PR for this implementation and regression test?