fix(server): ACP threads accept a model change after their session detaches #35951
main fromrefs/pull/35951/head·merged by juliusmarminge View on GitHubSettling 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.
| Before | After |
|---|---|
| 0a333a69e7 | 21d76f81a8 |
![]() | ![]() |
| Before recording | After 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
tanstack-source added labelsbug
github-actions[bot](GitHub profile, opens in a new tab) added labelssize:XS
Approvability#
Verdict: Approved at
21d76f8Macroscope'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).Thread transfer impact#
✅ Thread transfer remains within every enforced ceiling.
Provider Metric Main baseline This PR Impact PR ceiling Codex Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅ Codex Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅ Codex Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅ Codex Live turn WebSocket decoded 21.5 KiB 21.5 KiB 0 B (0.0%) 24.4 KiB ✅ Codex Live turn messages 1 1 0 (0.0%) 8 ✅ Claude Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅ Claude Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅ Claude Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅ Claude Live turn WebSocket decoded 21.9 KiB 21.9 KiB 0 B (0.0%) 24.4 KiB ✅ Claude Live turn messages 1 1 0 (0.0%) 8 ✅ Other scenarios#
All 64 scenario measurements match
main.All 64 scenario measurements
Provider Scenario Metric Main baseline This PR Impact PR ceiling Codex short turn resumeWebSocket decoded10.5 KiB 10.5 KiB 0 B (0.0%) 12.7 KiB ✅ Codex short turn resumeWebSocket messages2 2 0 (0.0%) 2 ✅ Codex long turn running, cold openHTTP decoded281.7 KiB 281.7 KiB 0 B (0.0%) 302.7 KiB ✅ Codex long turn running, cold openrows75 75 0 (0.0%) 75 ✅ Codex long turn running, history pageHTTP decoded92.4 KiB 92.4 KiB 0 B (0.0%) 107.4 KiB ✅ Codex long turn running, history pagerows75 75 0 (0.0%) 75 ✅ Codex long turn finished, cold openHTTP decoded381.5 KiB 381.5 KiB 0 B (0.0%) 410.2 KiB ✅ Codex long turn finished, cold openrows151 151 0 (0.0%) 151 ✅ Codex long turn finished, socket cold openWebSocket decoded381.6 KiB 381.6 KiB 0 B (0.0%) 410.2 KiB ✅ Codex long turn finished, socket cold openWebSocket messages2 2 0 (0.0%) 2 ✅ Codex long turn finished, socket cold openrows151 151 0 (0.0%) 151 ✅ Codex long turn finished, work pageHTTP decoded186.2 KiB 186.2 KiB 0 B (0.0%) 200.2 KiB ✅ Codex long turn finished, work pagerows150 150 0 (0.0%) 150 ✅ Codex idle thread resumeWebSocket decoded65 B 65 B 0 B (0.0%) 200 B ✅ Codex idle thread resumeWebSocket messages1 1 0 (0.0%) 1 ✅ Codex fleet shell snapshotHTTP decoded42.4 KiB 42.4 KiB 0 B (0.0%) 53.7 KiB ✅ Codex fleet shell snapshotrows27 27 0 (0.0%) 27 ✅ Codex fleet shell subscribeWebSocket decoded65 B 65 B 0 B (0.0%) 200 B ✅ Codex fleet shell subscribeWebSocket messages1 1 0 (0.0%) 1 ✅ Codex fleet shell resume checkHTTP decoded18 B 18 B 0 B (0.0%) 100 B ✅ Codex fleet shell resume checkrows0 0 0 0 ✅ Codex stale shell reloadHTTP decoded42.4 KiB 42.4 KiB 0 B (0.0%) 53.7 KiB ✅ Codex stale shell reloadrows27 27 0 (0.0%) 27 ✅ Codex stale shell resumeWebSocket decoded140 B 140 B 0 B (0.0%) 400 B ✅ Codex stale shell resumeWebSocket messages2 2 0 (0.0%) 2 ✅ Codex stale todo list cold openHTTP decoded72.4 KiB 72.4 KiB 0 B (0.0%) 83.0 KiB ✅ Codex stale todo list cold openrows75 75 0 (0.0%) 75 ✅ Codex stale todo list cold opennodes2 2 0 (0.0%) 4 ✅ Codex stale todo list cold opensubagents2 2 0 (0.0%) 4 ✅ Codex stale todo list cold openplans1 1 0 (0.0%) 2 ✅ Codex stale todo list cold openproviderTurns1 1 0 (0.0%) 2 ✅ Codex stale todo list cold opencheckpoints1 1 0 (0.0%) 2 ✅ Claude short turn resumeWebSocket decoded10.6 KiB 10.6 KiB 0 B (0.0%) 12.7 KiB ✅ Claude short turn resumeWebSocket messages2 2 0 (0.0%) 2 ✅ Claude long turn running, cold openHTTP decoded283.3 KiB 283.3 KiB 0 B (0.0%) 302.7 KiB ✅ Claude long turn running, cold openrows75 75 0 (0.0%) 75 ✅ Claude long turn running, history pageHTTP decoded93.4 KiB 93.4 KiB 0 B (0.0%) 107.4 KiB ✅ Claude long turn running, history pagerows75 75 0 (0.0%) 75 ✅ Claude long turn finished, cold openHTTP decoded384.2 KiB 384.2 KiB 0 B (0.0%) 410.2 KiB ✅ Claude long turn finished, cold openrows151 151 0 (0.0%) 151 ✅ Claude long turn finished, socket cold openWebSocket decoded384.3 KiB 384.3 KiB 0 B (0.0%) 410.2 KiB ✅ Claude long turn finished, socket cold openWebSocket messages2 2 0 (0.0%) 2 ✅ Claude long turn finished, socket cold openrows151 151 0 (0.0%) 151 ✅ Claude long turn finished, work pageHTTP decoded188.1 KiB 188.1 KiB 0 B (0.0%) 200.2 KiB ✅ Claude long turn finished, work pagerows150 150 0 (0.0%) 150 ✅ Claude idle thread resumeWebSocket decoded66 B 66 B 0 B (0.0%) 200 B ✅ Claude idle thread resumeWebSocket messages1 1 0 (0.0%) 1 ✅ Claude fleet shell snapshotHTTP decoded43.0 KiB 43.0 KiB 0 B (0.0%) 53.7 KiB ✅ Claude fleet shell snapshotrows27 27 0 (0.0%) 27 ✅ Claude fleet shell subscribeWebSocket decoded66 B 66 B 0 B (0.0%) 200 B ✅ Claude fleet shell subscribeWebSocket messages1 1 0 (0.0%) 1 ✅ Claude fleet shell resume checkHTTP decoded18 B 18 B 0 B (0.0%) 100 B ✅ Claude fleet shell resume checkrows0 0 0 0 ✅ Claude stale shell reloadHTTP decoded43.0 KiB 43.0 KiB 0 B (0.0%) 53.7 KiB ✅ Claude stale shell reloadrows27 27 0 (0.0%) 27 ✅ Claude stale shell resumeWebSocket decoded140 B 140 B 0 B (0.0%) 400 B ✅ Claude stale shell resumeWebSocket messages2 2 0 (0.0%) 2 ✅ Claude stale todo list cold openHTTP decoded73.4 KiB 73.4 KiB 0 B (0.0%) 83.0 KiB ✅ Claude stale todo list cold openrows75 75 0 (0.0%) 75 ✅ Claude stale todo list cold opennodes2 2 0 (0.0%) 4 ✅ Claude stale todo list cold opensubagents2 2 0 (0.0%) 4 ✅ Claude stale todo list cold openplans1 1 0 (0.0%) 2 ✅ Claude stale todo list cold openproviderTurns1 1 0 (0.0%) 2 ✅ Claude stale todo list cold opencheckpoints1 1 0 (0.0%) 2 ✅ Baseline:
97cfc07· PR result:21d76f8· Source CI: successScenario 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.
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_turninstead 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.tsWhen negotiated session capabilities are absent, the plan uses apply_on_next_turn. The test checks that a settled thread with no recorded sessions usesswitch_model_in_sessionfor anacpSelectionTransitionplan 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 to21d76-
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:
4-
Check name Status Explanation Title check 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… Linked Issues check [ #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 check 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…Description check 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 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
#35938and 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-
- Source mode: Auto
- Repository: pingdotgg/t3code
- Branch: factory/issue-35938
- File: .coderabbit.config.ts
- Commit: 21d76f8
- Layers: Repository TS config
- Review profile: CHILL
- Plan: Team
- Run ID:
0ba7835b-ff1a-430a-ad8f-8aaa171a162e
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.tsapps/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.
-
-
juliusmarminge merged this
0b12767juliusmarminge closed this
Sign in to join this conversation.


macroscopeapp[bot](GitHub profile, opens in a new tab) approved these changes