fix(server): a queued provider switch no longer ends background work#30401

merged1
main
t3/queued-switch-spares-busy-sessions
opened7d agobyJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 7d ago

Problem

When a queued message switches provider or model, startNextQueuedRun detached every live session in the switch plan without probing it for background work. Detaching ends background agents and commands running inside the provider process, such as Claude native subagents and background shells. A send made right away (dispatchMessage) already probes these sessions and refuses with "The previous provider is still running background agents or commands…". The queued path can't refuse, because nobody is waiting on it.

Fix

  • Move the probe from dispatchMessage into a shared sessionHasBackgroundWork helper. The behavior is unchanged: one session-wide probe for single-thread sessions, otherwise one probe per provider thread, and a failed probe counts as busy.
  • In the queued start, probe each session the plan would release, then act on the transition type:
    • create_with_handoff: the queued run gets its own session on another instance with separate native history. Busy sessions stay running, and only idle sessions are detached.
    • restart_and_resume: this is an account switch or a same-instance restart, and the queued run resumes the busy session's native thread. Running both processes on one native thread would fork its history, so the queue is held (queueHeld). The held queue shows Resume, and queue.resume probes again: it starts the run once the work is done, or holds again if it isn't.

The hold reason is only logged. queueHeld has no reason field, so a client sees a held queue with Resume, the same as the other holds. I left adding a reason to contracts and clients out of this fix.

Verification

Two new rows in the existing #20343 table in ProviderSwitch.integration.test.ts queue a switch behind a running turn while the old Claude session reports background work:

  • queued hand-off to an instance with separate history: the run completes on the new instance, and the busy session stays ready and is never closed.
  • queued account switch with shared history: the queue holds without closing the session. Once the work ends, queue.resume delivers the run on the new account and only then closes the old session.

Both rows fail on main, where the busy session is closed, and pass with the fix. The full file passes (46/46), as does vp run check:changed (format, lint, knip, typecheck, tests beside changes).

Done with Claude Opus 5.5 in Claude Code (T3 Code).

🤖 Generated with Claude Code

Checks

23 successful · 14 skipped
All checks passed
f3edb1d

Added 1 commit

  • fix(server): a queued provider switch no longer ends background workf3edb1d
Synced from GitHub

Approvability

Verdict: Approved at f3edb1d

Macroscope's review found this PR approvable — Queued provider switches now preserve background work, retaining busy sessions during handoffs and holding restarts until they can safely resume. The change reuses existing probes and queue controls without changing product defaults.

You can add or adjust custom eligibility rules. Learn more.

Synced from GitHub

Review in Change Stack →

No actionable comments were generated in the recent review.

Walkthrough

The orchestrator now checks provider sessions for background work before detaching them during queued and immediate switches. Integration tests cover queued switches with shared and separate histories.

Changes: Provider session handoff

Layer / File(s)Summary
Background-work checks and handoff behavior
apps/server/src/orchestration-v2/Orchestrator.ts, apps/server/src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts
A shared probe checks sessions for background work. Queued restart-and-resume switches hold queued runs when a releasable session is busy; other queued switches detach only idle releasable sessions. Immediate dispatch rejects a switch when a session slated for detachment is busy. Integration tests cover queued switches with shared and separate histories, as well as background-work blocking and successful handoff.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

Possibly related issues

  • pingdotgg/t3code#16000 — The PR adds a background-work check before detaching sessions during provider switches, covering part of the issue’s requested behavior.
  • pingdotgg/t3code#20343 — The PR adds background-work checks to queued and immediate switch paths, which relate to the issue’s requested send-path guard.

Suggested labels: bugfix, size:M

Suggested reviewers: t3dotgg, macroscopeapp

Merge Risk: External image Minimal · up to f3edb

No actionable merge-blocking risk remains from the reviewed provider-switch changes; normal checks can proceed.

Pre-merge checks: Passed 3 · Failed 1

Failed Failed checks: 1 warning

Check nameStatusExplanationResolution
Description checkWarningThe description covers the required template sections. Problem gives the failure, the queued path detaching sessions without a background-work probe, and the effects on background agents and shells. C…Add the Scope and approval section. Link the triaged bug issue or the discussion with explicit maintainer approval, including the approval comment. If the fix qualifies as a very small, focused fix of an obvious bug without prior approval, …
Passed Passed checks: 3 passed
Check nameStatusExplanation
Title checkPassedThe title names the main change: queued provider switches no longer detach sessions that still run background work. It follows the conventional commit form and is specific. It is a fix, and the body c…
Linked Issues checkPassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes checkPassedCheck skipped because no linked issues were found for this pull request.
Full details: Title check

Explanation

The title names the main change: queued provider switches no longer detach sessions that still run background work. It follows the conventional commit form and is specific. It is a fix, and the body confirms the probe and hold behavior matches the title.

Full details: Description check

Explanation

The description covers the required template sections. Problem gives the failure, the queued path detaching sessions without a background-work probe, and the effects on background agents and shells. Change explains the shared helper and the per-transition behavior, including the hold for restart_and_resume. Verification names the two new integration test rows, states they fail on main and pass with the fix, and reports 46/46 plus check:changed. The Scope and approval section is missing. It has no linked triaged issue or maintainer approval comment, and no justification for skipping one. The template also asks for the model and harness used, and the description includes the Claude Code attribution, so that item is met.

Resolution

Add the Scope and approval section. Link the triaged bug issue or the discussion with explicit maintainer approval, including the approval comment. If the fix qualifies as a very small, focused fix of an obvious bug without prior approval, explain why it qualifies. The linked issue at git.cafe/pingdotgg/t3code/issues/20343 appears only as the test table reference, so it does not show approval for this fix.

Finishing Touches
Generate docstrings
  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.
External image Recent review info
External image Run configuration
External image Commits

Reviewing files that changed from the base of the PR and between aee76a8b473694878e820ed642b4b4018991f378 and f3edb1d7ab79b4a56d5edb7e0b4c76b029beef89.

External image Files selected for processing: 2
  • apps/server/src/orchestration-v2/Orchestrator.ts
  • apps/server/src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 5 remain after this review.

Synced from GitHub

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire5.0 KiB5.0 KiB+35 B (+0.7%)6.8 KiB✅
CodexThread snapshot wire3.8 KiB3.8 KiB0 B (0.0%)4.9 KiB✅
CodexLive turn WebSocket wire1.2 KiB1.3 KiB+35 B (+2.8%)2.0 KiB✅
CodexLive turn WebSocket decoded21.5 KiB21.6 KiB+41 B (+0.2%)24.4 KiB✅
CodexLive turn messages12+1 (+100.0%)8✅
ClaudeTotal thread wire5.0 KiB5.1 KiB+17 B (+0.3%)6.8 KiB✅
ClaudeThread snapshot wire3.8 KiB3.8 KiB0 B (0.0%)4.9 KiB✅
ClaudeLive turn WebSocket wire1.2 KiB1.3 KiB+17 B (+1.3%)2.0 KiB✅
ClaudeLive turn WebSocket decoded21.9 KiB21.9 KiB0 B (0.0%)24.4 KiB✅
ClaudeLive turn messages220 (0.0%)8✅

Baseline: aee76a8 · PR result: f3edb1d · 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.