fix(server): a queued message that names no model keeps the model picked ahead of it#33591

merged6
main
fix/issue-33574
opened1d agobyJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 13h ago

Problem

A model picked on a queued message only reaches the thread when that message's run starts. A message that names no model and is queued behind it (an MCP t3_thread_send, a server or agent message, a scheduled wake) captured the thread's old model. When the queue drained, it switched the whole thread back to that model after the picked run, with avoidable cache misses. Reproduction: a thread on model A runs /compact. The user picks model B and sends with Steer, which falls back to the queue because compaction cannot be steered. Another thread then calls t3_thread_send. The user's run starts on B, then the agent run starts on A and moves the thread back to A.

Change

When dispatch queues a message without its own modelSelection, the message takes the selection of the newest queued run ahead of it (queueTailModelSelection in Orchestrator.ts). That is the model the thread will be on when the message starts. When nothing is queued, it falls back to the thread's selection. The inherited selection is applied before the queued run is built, so the provider thread, capability checks and the stored run all use it.

Runs that already resolve their model at promotion (delegated completions, monitor notices) are skipped when finding that selection, and do not inherit one themselves. Two kinds of message keep the model they have today:

  • A /goal command that stops the active run, which goes to the front of the queue.
  • A buffered provider wake, which drains output its own provider already produced.

A /goal pause or /goal resume is checked against the model it will run on, so a Claude thread no longer refuses one that inherits a Codex pick, and one that inherits a Claude pick is refused at dispatch. The check now runs once, after that decision and before the message stops, queues or releases anything.

Messages queued before a pick keep their pinned selection as before.

Scope and approval

Fixes pingdotgg/t3code#33574

Triaged via the automated issue flow.

Verification

Four focused tests at head 6c797ff4ad:

  • runtimeLayer.test.ts › "keeps a model picked ahead of a queued agent message that names none": a web pick B queued behind an active run on A, then an agent/MCP message with no model. It fails with main's orchestrator (the agent run starts on A) and passes with the fix (the agent run and the thread stay on B).
  • runtimeLayer.test.ts › "checks a queued goal pause against the model picked ahead of it": a Claude thread with a Codex pick queued accepts a model-less /goal pause on Codex, while a /goal resume that stops the active run stays on Claude and is refused. It fails with main's orchestrator.
  • runtimeLayer.test.ts › "keeps a buffered provider wake on its own provider behind a queued pick" and SteeringCompletion.integration.test.ts › "starts a goal that names no model on the thread's model ahead of a queued pick" guard the two exclusions. Both pass on main, and each fails when its exclusion is removed from the fix.

Further scenarios, run as uncommitted scratch tests against the head, cover the empty-queue fallback, delivery order with reordered and held runs, skipping monitor notices, and inheritance across provider instances. Each passes at the head and fails under a targeted mutation of the fix.

The fix is server-only, with no render path, so the proof is the red and green runs below. Earlier transcript at 973246366b (red at the test-only commit, red for the wake exclusion, green).

Runtime transcript at 6c797ff4ad

Runtime transcript at head 6c797ff4ad (merge-base 9110dc2c93). Ubuntu 24.04.5 LTS, Linux 6.1.102 x86_64, Node v26.12.1, vp test (Vitest 5.0.5), run from apps/server. Real orchestrator dispatch, persisted projections, queue promotion and the effect worker, with fake provider adapters. Model strings are test fixtures.

Committed tests, green at head (runtimeLayer.test.ts, SteeringCompletion.integration.test.ts):

text
1✓ keeps a model picked ahead of a queued agent message that names none
2✓ keeps a buffered provider wake on its own provider behind a queued pick
3✓ checks a queued goal pause against the model picked ahead of it
4✓ starts a goal that names no model on the thread's model ahead of a queued pick
5Tests 4 passed | 150 skipped (154)

Same tests, head tests with main's Orchestrator.ts and NotificationMailbox.ts: the two fix tests fail and the two exclusion tests pass, as on main.

text
1× keeps a model picked ahead of a queued agent message that names none
2- "model": "gpt-5.5", (the pick queued ahead)
3+ "model": "gpt-5.4", (the stale thread model)
4× checks a queued goal pause against the model picked ahead of it
5Caused by: Claude goals cannot be paused or resumed. Send /goal clear to remove the goal.
6✓ keeps a buffered provider wake on its own provider behind a queued pick
7✓ starts a goal that names no model on the thread's model ahead of a queued pick
8Tests 2 failed | 2 passed | 150 skipped (154)

Exclusions, red under a mutation at head:

text
1drop `goalStopRunId === undefined` from the inheritance guard
2× starts a goal that names no model on the thread's model ahead of a queued pick
3 → expected { instanceId: 'codex', …(2) } to deeply equal { instanceId: 'codex', …(2) }
4drop `!isBufferedProviderNotification(incomingMessage)` from the inheritance guard
5× keeps a buffered provider wake on its own provider behind a queued pick
6 → expected { instanceId: 'codex_alternate', …(1) } to deeply equal { instanceId: 'codex', …(1) }

Scratch scenarios (not committed), green at head, each red under one mutation of the fix:

RowScenarioGreen at headMutation → red
R2Empty queue: a model-less message falls back to the thread's A, and stays A through promotion after a later pick B is queued✓drop ?? projection.thread.modelSelection → TypeError … reading 'instanceId'
R5Picks B(high), C(low) queued, C reordered ahead of B, B held, a later A cancelled: the model-less follower takes B(high) with its options✓findLast→find; array order instead of delivery order; held runs filtered out → each gets C(low)
R6Pick B, then a server monitor notice stored on A, then a model-less follower → B; monitor-only queue → current thread selection C✓ ✓treat followers as candidates → both get A
R10Thread active on instance A, pick on instance B queued, model-less follower: after draining the worker, the follower starts on B on the pick's provider thread (one B provider thread; run, attempt and checkpoint agree)✓inherit the stale thread selection → instanceId: 'evidence-a' instead of 'evidence-b'
R10Thread on A, Claude pick queued, model-less /goal pause → refused with the Claude goal error, no run created✓check the goal against the thread's instance → expected 'Success' to equal 'Failure'
text
1Tests 6 passed (6) (scratch file, unmodified head)

Focused files at head (runtimeLayer.test.ts, SteeringCompletion.integration.test.ts, testkit/ProviderSwitch.integration.test.ts, QueuedRunOrder.test.ts): 204/204 passed. vp run check:changed: format, lint, knip, typecheck of 3 packages, 174 tests in 6 files passed.

Results at 6c797ff4ad:

  • runtimeLayer.test.ts, SteeringCompletion.integration.test.ts, testkit/ProviderSwitch.integration.test.ts and QueuedRunOrder.test.ts: 204/204 passed.
  • vp run check:changed passed: format, lint, knip, typecheck of 3 packages, and 174 tests in 6 files.
  • The final head 6136b5f462 only merges main, with no changes under orchestration-v2; vp run check:changed passed again there (174 tests in 6 files).

Not checked: a live end-to-end run with a real provider account. The tests drive the real orchestrator dispatch and queue promotion with fake adapters.

AI-assisted; verified with the tests and tools listed above.

Checks

21 successful · 20 skipped
All checks passed
6136b5f

Added 3 commits

  • fix(server): buffered provider wakes stay on their provider behind a queued pick9732463
  • fix(server): a queued message that names no model keeps the pick queued ahead of itb384b9f
  • test(server): reproduce #33574, a queued agent message undoes a queued model pickd4523d6
Synced from GitHub

Approvability

Verdict: Needs Human Approval

Macroscope's review found this PR requires human approval — Queued messages that omit a model now inherit the selection queued ahead of them instead of the thread's current selection. Human confirmation is needed for this change to implicit model/provider selection.

No code changes detected at 6136b5f. Prior analysis still applies.

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

Synced from GitHub

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✅

Baseline: bbdc7dc · PR result: 6136b5f · 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.

Synced from GitHub

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

Queued messages without an explicit model selection can inherit a selection from the queue tail when they meet the dispatch conditions. Goal-stop commands and buffered provider notifications are excluded. Goal-command support checks use the resulting model selection. Tests cover these paths.

Changes: Queued model selection

Layer / File(s)Summary
Queue selection and goal validation
apps/server/src/orchestration-v2/NotificationMailbox.ts, apps/server/src/orchestration-v2/Orchestrator.ts
queueTailModelSelection selects the last eligible queued run’s model selection, or falls back to the thread selection. Dispatch applies it only to qualifying queued messages without an explicit selection. Goal-command support validation runs before steering and the remaining dispatch path.
Selection regression tests
apps/server/src/orchestration-v2/SteeringCompletion.integration.test.ts, apps/server/src/orchestration-v2/runtimeLayer.test.ts
Tests cover queued agent messages, buffered provider notifications, goal-pause and goal-resume commands, and an unselected /goal message sent while another message is queued.

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

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: t3dotgg

Merge Risk: External image Low · up to 6c797

In a narrow background-continuation scenario, the thread may switch models before a held user message runs. The change is mergeable with owner awareness and follow-up.

Pre-merge checks: Passed 4

Check nameStatusExplanation
Title checkPassedThe title clearly summarizes the main change: queued messages without a model selection keep the model selected ahead of them.
Linked Issues checkPassedIssue #33574 requires an ordinary queued agent/MCP message without a model override to retain a newer model pick ahead of it. The reviewed PR summary and prior assessment establish that dispatch inher…
Out of Scope Changes checkPassedThe latest changes adjust goal-command validation to use the resolved model selection and add focused tests and a provider fixture for that behavior. The other reported changes implement queue selecti…
Description checkPassedThe description covers the problem, change, linked issue and triage, and focused verification. It also states that a live run with a real provider was not checked.
Full details: Linked Issues check

Explanation

Issue #33574 requires an ordinary queued agent/MCP message without a model override to retain a newer model pick ahead of it. The reviewed PR summary and prior assessment establish that dispatch inherits the newest eligible queued selection before building the run, and the regression test verifies that the agent run and thread use the picked model. The latest changes move goal-command support validation after selection inheritance and add coverage for queued goal commands, including a command that must be refused on the still-active provider. This supports the queue behavior and does not leave a concrete issue requirement unmet.

Full details: Out of Scope Changes check

Explanation

The latest changes adjust goal-command validation to use the resolved model selection and add focused tests and a provider fixture for that behavior. The other reported changes implement queue selection inheritance and its documented exceptions. These changes have a direct connection to issue #33574. No unrelated change is evident in the reviewed summary or incremental diff.

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
External image Recent review info

External image Run configuration

External image Commits

Reviewing files that changed from the base of the PR and between abe222c73a4425fe0a77a54a4d775cc562e9ae97 and 6c797ff4adb46654b714ee4d622697995a4346e6.

External image Files selected for processing: 2

  • apps/server/src/orchestration-v2/Orchestrator.ts
  • apps/server/src/orchestration-v2/runtimeLayer.test.ts

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

Added 3 commits

  • Merge remote-tracking branch 'origin/main' into pr-33591abe222c
  • fix(server): check a queued goal command against the model it will run on6c797ff
  • Merge remote-tracking branch 'origin/main' into pr-335916136b5f