app-server turn/start returns a submission ID when it steers into an active turn
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/startresponse reports submission IDS; turn/started,item/*,turn/completed, and interruption continue to use the active turn IDA;- the second user message is correctly delivered to
A, soSnever 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?
- Start an app-server v2 thread against a mock Responses API.
- Send
turn/startfor turn A and keep it active with a controllable long-running shell call. - Wait for
turn/startedand record A. - While A is active, send a second
turn/startwith a distinctclientUserMessageId. - Read the second
turn/startresponse and theitem/startednotification for that second user message. - 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/startwas not wired to it. - #36389 adds complementary cross-process single-writer protection; it does not correct same-process
turn/startresponse correlation. - #18516 concerns a different
review/startlifecycle 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.
1 Comment
Confirmed against Codex 0.146.0 with a real app-server client.
In the observed thread, native goal continuation first started active turn
Aas UUIDv4. A laterturn/startreturned a new UUIDv7 submission IDS, whileturn/started,item/*,turn/completed, rollout metadata, andturn/interruptall continued to useA. The client therefore discarded every notification when correlating them withTurnStartResponse.turn.id, then timed out; interrupting withSfailed withexpected 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:
turn_start_innercallsubmit_user_input_and_wait_for_admission.UserMessageAdmission::StartedandUserMessageAdmission::Steeredwhile preserving the existing{ turn }response shape.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?