Skip to content
Source
Sign inSign up

fix(server): ACP threads accept a model change after their session detaches #35951

Mergedjuliusmarmingemerged 2 commits intomain fromrefs/pull/35951/headmerged by juliusmarminge View on GitHub

juliusmarmingecommented

Settling a thread emits provider-session.detached, which removes the session record, so ProviderSwitchService.plan had no negotiated capabilities. It then asked the ACP adapter to classify the change against the instance's base capabilities, where supportsModelSwitchInSession is false, and acpSelectionTransition rejected it. The planner now skips the adapter when no session record exists for the current instance (only a native provider thread remains) and treats the change as apply_on_next_turn, which the transition policy maps to switch_model_in_session. When a stopped or errored session record still exists, the adapter still decides from that session's negotiated capabilities, so the existing 'rejects a model change the dead record never negotiated support for' case is unchanged. The ACP adapter already applies the thread's model selection after it loads or resumes a session, so the saved selection takes effect when the next session starts. No regex or parser was added.

Fixes pingdotgg/t3code#35938

Reproduction on main

Commit 0a333a69e7 (origin/main). Dev server with T3CODE_DEV_SEED=0 (fresh DB). Added a local-command acpRegistry provider instance running the repo's apps/server/scripts/acp-mock-agent.ts, which advertises a model config option in session/new. Drove the server through its MCP tools because the preview browser was blocked on the host. 1) t3 thread start --provider mockAcp --model default --wait: turn completed. 2) t3_thread_configure model=composer-2: accepted. 3) t3_thread_organize settle, then unsettle: the thread's provider session binding row was gone. 4) t3_thread_configure model=gpt-5.4: refused; the server trace shows thread.model-selection.set -> ProviderSwitchService.plan -> ProviderSwitchPlanError 'The active ACP session does not expose a model-switch capability.' 5) t3 thread send --wait, then the same configure: accepted. Matches every step of the report. Failing unit test pushed: ProviderSwitchService.test.ts 'applies a model change on next turn after a settled thread's session detached' fails on main with the same error.

Evidence

Complete an ACP replay turn → change its live model to Plan and reads → settle and un-settle → select Multi turn while the session is stopped. Both sides use fresh synthetic databases, Chromium at 1280×800, and dark theme.

BeforeAfter
0a333a69e721d76f81a8
Before: model change rejected with ACP capability errorAfter: model change accepted without error
Before recordingAfter recording
Server retained todo-list; next turn used todo-list.Server saved multi-turn; next turn used multi-turn.

Browser and seed driver · Next-turn verification driver · Real-server runtime transcript

The composer retains its local attempted pick even on base; the error toast and authoritative saved/next-run models establish the difference.

Not shown:

  • The triage mock's config-option negotiation and literal composer-2/gpt-5.4 choices were not independently exercised; this proof uses the replay ACP agent's model-switch capability.

Tests

vp test run src/orchestration-v2/ProviderSwitchService.test.ts (apps/server): 9/9 pass with the fix; without it the repro test 'applies a model change on next turn after a settled thread's session detached' fails (1 failed, 8 passed). vp run check:changed: format, lint, knip, graphql, typecheck (3 packages) and tests (2 files, 11 tests) all pass.

Not in this change

  • Detaching a session drops its negotiated capabilities from the projection. Keeping them would let the planner judge detached ACP threads by what the agent actually advertised, but that changes projection behaviour and is not needed for this fix.

Who did the work

  • Written by Claude Opus 5.5, in Claude Code
  • Reviewed by GPT-6.1 Sol, in Codex, and by Claude Opus 5.5, each on its own
  • Proof captured by GPT-6.1 Sol and checked by Claude Opus 5.5

Run by the t3code factory in T3 Code.

    1. github-actions[bot](GitHub profile, opens in a new tab) added labelsvouch:trusted

    2. tanstack-source added labelsbug

    3. Approvability

      Verdict: Approved at 21d76f8

      Macroscope's review found this PR approvable — Detached threads can accept a requested model change for their next session without relying on unavailable session capabilities. Existing availability and continuation safeguards remain, keeping the change narrowly scoped.

      You can add or adjust custom eligibility rules. [Learn more](https://docs.macroscope.com/approvability#example-custom-eligibility-rules).
    4. Thread transfer impact

      ✅ Thread transfer remains within every enforced ceiling.

      ProviderMetricMain baselineThis PRImpactPR ceiling
      CodexTotal thread wire5.0 KiB5.0 KiB0 B (0.0%)6.8 KiB✅
      CodexThread snapshot wire3.8 KiB3.8 KiB0 B (0.0%)4.9 KiB✅
      CodexLive turn WebSocket wire1.2 KiB1.2 KiB0 B (0.0%)2.0 KiB✅
      CodexLive turn WebSocket decoded21.5 KiB21.5 KiB0 B (0.0%)24.4 KiB✅
      CodexLive turn messages110 (0.0%)8✅
      ClaudeTotal thread wire5.0 KiB5.0 KiB0 B (0.0%)6.8 KiB✅
      ClaudeThread snapshot wire3.8 KiB3.8 KiB0 B (0.0%)4.9 KiB✅
      ClaudeLive turn WebSocket wire1.2 KiB1.2 KiB0 B (0.0%)2.0 KiB✅
      ClaudeLive turn WebSocket decoded21.9 KiB21.9 KiB0 B (0.0%)24.4 KiB✅
      ClaudeLive turn messages110 (0.0%)8✅

      Other scenarios

      All 64 scenario measurements match main.

      All 64 scenario measurements
      ProviderScenarioMetricMain baselineThis PRImpactPR ceiling
      Codexshort turn resumeWebSocket decoded10.5 KiB10.5 KiB0 B (0.0%)12.7 KiB✅
      Codexshort turn resumeWebSocket messages220 (0.0%)2✅
      Codexlong turn running, cold openHTTP decoded281.7 KiB281.7 KiB0 B (0.0%)302.7 KiB✅
      Codexlong turn running, cold openrows75750 (0.0%)75✅
      Codexlong turn running, history pageHTTP decoded92.4 KiB92.4 KiB0 B (0.0%)107.4 KiB✅
      Codexlong turn running, history pagerows75750 (0.0%)75✅
      Codexlong turn finished, cold openHTTP decoded381.5 KiB381.5 KiB0 B (0.0%)410.2 KiB✅
      Codexlong turn finished, cold openrows1511510 (0.0%)151✅
      Codexlong turn finished, socket cold openWebSocket decoded381.6 KiB381.6 KiB0 B (0.0%)410.2 KiB✅
      Codexlong turn finished, socket cold openWebSocket messages220 (0.0%)2✅
      Codexlong turn finished, socket cold openrows1511510 (0.0%)151✅
      Codexlong turn finished, work pageHTTP decoded186.2 KiB186.2 KiB0 B (0.0%)200.2 KiB✅
      Codexlong turn finished, work pagerows1501500 (0.0%)150✅
      Codexidle thread resumeWebSocket decoded65 B65 B0 B (0.0%)200 B✅
      Codexidle thread resumeWebSocket messages110 (0.0%)1✅
      Codexfleet shell snapshotHTTP decoded42.4 KiB42.4 KiB0 B (0.0%)53.7 KiB✅
      Codexfleet shell snapshotrows27270 (0.0%)27✅
      Codexfleet shell subscribeWebSocket decoded65 B65 B0 B (0.0%)200 B✅
      Codexfleet shell subscribeWebSocket messages110 (0.0%)1✅
      Codexfleet shell resume checkHTTP decoded18 B18 B0 B (0.0%)100 B✅
      Codexfleet shell resume checkrows0000✅
      Codexstale shell reloadHTTP decoded42.4 KiB42.4 KiB0 B (0.0%)53.7 KiB✅
      Codexstale shell reloadrows27270 (0.0%)27✅
      Codexstale shell resumeWebSocket decoded140 B140 B0 B (0.0%)400 B✅
      Codexstale shell resumeWebSocket messages220 (0.0%)2✅
      Codexstale todo list cold openHTTP decoded72.4 KiB72.4 KiB0 B (0.0%)83.0 KiB✅
      Codexstale todo list cold openrows75750 (0.0%)75✅
      Codexstale todo list cold opennodes220 (0.0%)4✅
      Codexstale todo list cold opensubagents220 (0.0%)4✅
      Codexstale todo list cold openplans110 (0.0%)2✅
      Codexstale todo list cold openproviderTurns110 (0.0%)2✅
      Codexstale todo list cold opencheckpoints110 (0.0%)2✅
      Claudeshort turn resumeWebSocket decoded10.6 KiB10.6 KiB0 B (0.0%)12.7 KiB✅
      Claudeshort turn resumeWebSocket messages220 (0.0%)2✅
      Claudelong turn running, cold openHTTP decoded283.3 KiB283.3 KiB0 B (0.0%)302.7 KiB✅
      Claudelong turn running, cold openrows75750 (0.0%)75✅
      Claudelong turn running, history pageHTTP decoded93.4 KiB93.4 KiB0 B (0.0%)107.4 KiB✅
      Claudelong turn running, history pagerows75750 (0.0%)75✅
      Claudelong turn finished, cold openHTTP decoded384.2 KiB384.2 KiB0 B (0.0%)410.2 KiB✅
      Claudelong turn finished, cold openrows1511510 (0.0%)151✅
      Claudelong turn finished, socket cold openWebSocket decoded384.3 KiB384.3 KiB0 B (0.0%)410.2 KiB✅
      Claudelong turn finished, socket cold openWebSocket messages220 (0.0%)2✅
      Claudelong turn finished, socket cold openrows1511510 (0.0%)151✅
      Claudelong turn finished, work pageHTTP decoded188.1 KiB188.1 KiB0 B (0.0%)200.2 KiB✅
      Claudelong turn finished, work pagerows1501500 (0.0%)150✅
      Claudeidle thread resumeWebSocket decoded66 B66 B0 B (0.0%)200 B✅
      Claudeidle thread resumeWebSocket messages110 (0.0%)1✅
      Claudefleet shell snapshotHTTP decoded43.0 KiB43.0 KiB0 B (0.0%)53.7 KiB✅
      Claudefleet shell snapshotrows27270 (0.0%)27✅
      Claudefleet shell subscribeWebSocket decoded66 B66 B0 B (0.0%)200 B✅
      Claudefleet shell subscribeWebSocket messages110 (0.0%)1✅
      Claudefleet shell resume checkHTTP decoded18 B18 B0 B (0.0%)100 B✅
      Claudefleet shell resume checkrows0000✅
      Claudestale shell reloadHTTP decoded43.0 KiB43.0 KiB0 B (0.0%)53.7 KiB✅
      Claudestale shell reloadrows27270 (0.0%)27✅
      Claudestale shell resumeWebSocket decoded140 B140 B0 B (0.0%)400 B✅
      Claudestale shell resumeWebSocket messages220 (0.0%)2✅
      Claudestale todo list cold openHTTP decoded73.4 KiB73.4 KiB0 B (0.0%)83.0 KiB✅
      Claudestale todo list cold openrows75750 (0.0%)75✅
      Claudestale todo list cold opennodes220 (0.0%)4✅
      Claudestale todo list cold opensubagents220 (0.0%)4✅
      Claudestale todo list cold openplans110 (0.0%)2✅
      Claudestale todo list cold openproviderTurns110 (0.0%)2✅
      Claudestale todo list cold opencheckpoints110 (0.0%)2✅

      Baseline: 97cfc07 · PR result: 21d76f8 · Source CI: success

      Scenario and decoded snapshot size

      10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

      • Codex decoded thread snapshot: 108.5 KiB
      • Claude decoded thread snapshot: 108.8 KiB

      Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

    5. Explain Change →

      No actionable comments were generated in the recent review.

      Walkthrough

      When negotiated session capabilities are absent, same-instance model-selection planning now uses apply_on_next_turn instead of calling the transition planner with current-instance capabilities. A test covers a settled thread with no recorded sessions.

      Changes: Model selection planning

      Layer / File(s)Summary
      Plan model changes without negotiated capabilities
      apps/server/src/orchestration-v2/ProviderSwitchService.ts, apps/server/src/orchestration-v2/ProviderSwitchService.test.ts
      When negotiated session capabilities are absent, the plan uses apply_on_next_turn. The test checks that a settled thread with no recorded sessions uses switch_model_in_session for an acpSelectionTransition plan and releases no provider sessions.

      Estimated code review effort: 2 (Simple) | ~10 minutes

      Change: Bug fix · Severity of issue fixed: Low

      Suggested reviewers: maria-rcks, t3dotgg

      Merge Risk: Minimal · up to 21d76

      This change lets an ACP thread accept a model change after its session detaches, applying the saved selection when the next session starts instead of rejecting it. No concrete regression was found in the reviewed change, so it looks ready to merge.

      Pre-merge checks: Passed 4

      Check nameStatusExplanation
      Title checkPassedThe title names the main change: ACP threads accept a model change after their session detaches. It is a conventional commit with the fix(server) scope. The changeset adds a planner path that treats a…
      Linked Issues checkPassed[#35938] The issue asks that a model change on an ACP thread with no live session be stored and applied when the next session starts, not rejected. The diff in apps/server/src/orchestration-v2/Provide…
      Out of Scope Changes checkPassedThe only code change is the planner branch in ProviderSwitchService.plan and one regression test in ProviderSwitchService.test.ts. Both serve #35938. No unrelated change was found in the whole-PR diff…
      Description checkPassedThe description explains the problem (settling emits provider-session.detached, removing the session record, so ACP planning rejects a model change), the change (skip the adapter when no session recor…
      Full details: Title check

      Explanation

      The title names the main change: ACP threads accept a model change after their session detaches. It is a conventional commit with the fix(server) scope. The changeset adds a planner path that treats a model change with no session record as apply_on_next_turn, which matches the title.

      Full details: Linked Issues check

      Explanation

      [#35938] The issue asks that a model change on an ACP thread with no live session be stored and applied when the next session starts, not rejected. The diff in apps/server/src/orchestration-v2/ProviderSwitchService.ts returns { type: "apply_on_next_turn" } when negotiatedCapabilities is undefined. Before the change, the adapter received negotiatedCapabilities ?? currentInstance.value.capabilities, and the base ACP capabilities set supportsModelSwitchInSession to false, so acpSelectionTransition rejected the change. The new test 'applies a model change on next turn after a settled thread's session detached' asserts switch_model_in_session with no released sessions for a settled thread whose session records are empty. The existing dead-record rejection test is kept. Residual uncertainty: the claim that the ACP adapter applies the saved model selection on session load or resume was not verified in this review. The planner-level behavior is verified by the test; the end-to-end apply-on-resume behavior is taken from the PR description.

      Full details: Out of Scope Changes check

      Explanation

      The only code change is the planner branch in ProviderSwitchService.plan and one regression test in ProviderSwitchService.test.ts. Both serve #35938. No unrelated change was found in the whole-PR diff against the merge base 0a333a69e7. Note for the author: the PR description says the planner skips the adapter when no session record exists. The code instead skips the adapter when negotiatedCapabilities is undefined. That value is also undefined when a session record exists without a capabilities field, including a live session. In that case the change now returns apply_on_next_turn without asking the adapter. This wider condition is related to the objective but is not described in the PR. Whether a live session can lack capabilities was not established from the contracts schema.

      Full details: Description check

      Explanation

      The description explains the problem (settling emits provider-session.detached, removing the session record, so ACP planning rejects a model change), the change (skip the adapter when no session record exists and treat the change as apply_on_next_turn), and the scope (links issue #35938 and gives a reproduction on main). It also gives a detailed verification section with focused test results and manual repro steps, plus evidence links, and it names the tools and models used. Required template sections are all filled in. Minor gaps: the Problem section is folded into the body rather than under a '## Problem' heading, the title is not shown in the provided description, and the evidence includes external links whose availability cannot be verified here. The author also lists the work agents, which the template asks for. Overall the description is mostly complete and specific.

      Finishing Touches

      Generate unit tests (beta)

      • Commit to this branch
      • Create a new PR
      • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
      Recent review info

      Run configuration

      Commits

      Reviewing files that changed from the base of the PR and between 97cfc07759fed2a67edef22de4370ed74ff40030 and 21d76f81a835d589f24066d7e06f8279db91e4de.

      Files selected for processing: 2

      • apps/server/src/orchestration-v2/ProviderSwitchService.test.ts
      • apps/server/src/orchestration-v2/ProviderSwitchService.ts

      Included review availability: This review used your included allowance. 0 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 10 reviews per hour.

    6. juliusmarminge merged this0b12767

    7. juliusmarminge closed this

Sign in to join this conversation.