Skip to content

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

Open

Description

@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.

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.

Activity

  1. added
    bugSomething isn't working
    app-serverIssues involving app server protocol or interfaces
    on Aug 4, 2026
  2. TopGrd commented on Aug 5, 2026

    @TopGrd

    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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    app-serverIssues involving app server protocol or interfacesbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions