Reproduced on current main 70e5e384da, Linux x64, real web dev server with Claude Opus 5.5. Started an async child, ended the delegating turn, then kept a second parent turn active until the child completion queued. Persisting Sonnet with thread.model-selection.set succeeded at 22:50:09 UTC; queue delivery at 22:50:19 wrote Opus back with no command attached and started the completion on Opus. The picker alone changed the composer draft during the active turn, so the persisted change used the app metadata command. Sanitized runtime transcript.
A queued delegated-task completion reverts a model change made while it waited#22577
What happened
An agent delegated a task with delegate_task. The child finished while the parent thread was busy with another turn, so its completion notice was queued. While the notice waited, the thread's model was changed with t3_thread_configure. The call succeeded. When the notice's turn started, the thread's model switched back to the earlier one. t3_thread_configuration then read back the old model, and that turn and the turns after it ran on the old model.
It happened twice in a row on the same thread. A model change made before a notice was queued held.
Diagnosis
The queued completion run starts on a copy of the thread's selection taken when the notice was queued. If that copy differs from the thread's selection, the start writes the copy back to the thread.
ProviderContinuationServicequeues the completion notice withqueue_after_activeand nomodelSelection(ProviderContinuationService.ts#L119-L135).message.dispatchfalls back to the thread's selection (Orchestrator.ts#L4983), and the queued run stores that copy.- When the queue starts the run,
queuedRunModelSelectionreturns the stored copy for everything except PR-watch monitor updates. Since #21314, those follow the thread's current selection (Orchestrator.ts#L655-L672). - The start compares the copy with the thread's current selection. When they differ, it emits
thread.model-selection-updated(orthread.provider-switched) with the copy, and no command is attached (Orchestrator.ts#L1546-L1549, Orchestrator.ts#L1926-L1941). This event overwrites the newer selection.
The notice had no model of its own. The stored copy was not a user choice, and it was not the model of the run it reports to: on the affected thread, the run that delegated the task was already on the newer model. The copy came from a later turn that ran on the older model.
In #19376, a queued run keeps the model it was sent with, by design. A user message always carries the composer's selection, so it is not affected. A delegated completion queued this way is sent with no model.
Expected: a queued delegated-task completion starts on the thread's current selection and leaves it unchanged, the same as a queued PR-watch update since #21314.
Steps to reproduce
In the app:
- In a thread, have the agent start a
delegate_taskchild that takes a minute or more. - Keep the parent thread in a turn when the child finishes, so that the completion notice is queued behind it.
- While the notice is queued, change the thread's model, either with
t3_thread_configureor the model picker. Both sendthread.model-selection.set. - When the running turn ends and the notice starts, the thread's model switches back and the turn runs on the old model.
Deterministic, against the orchestrator on main (05696c636), with the runtimeLayer.test.ts harness:
- Create a thread on model A and start a user turn.
- Open a delegated-completion cohort on that run. Dispatch the notice the way
ProviderContinuationServicedoes:queue_after_active,createdBy: "agent",creationSource: "server",delegatedCompletion, nomodelSelection. - Dispatch
thread.model-selection.setwith model B. - Complete the active run and call
resumeQueuedRuns. - The completion run starts on model A, and
thread.modelSelectionis A again. This also happens when B is on another provider instance.
Version
0.0.46-nightly.20261015.3145 (the local T3 server). Also reproduced on main at 05696c636 with the test above. Triage CLI: 0.1.0.
Environment
Linux x64 (kernel 6.12), Claude Code provider, local T3 server
Evidence
| 1 | # One thread's orchestration_events on 2026-10-20 (UTC), ids removed. |
| 2 | # A = the older model with effort xhigh; B = the newer model chosen with t3_thread_configure. |
| 3 | 11:10:56.299 run.created run N queued command: delegated-completion dispatch selection A |
| 4 | 11:13:18.274 thread.model-selection-updated command: MCP t3_thread_configure selection B |
| 5 | 11:14:07.399 thread.model-selection-updated command: none selection A |
| 6 | 11:14:07.399 run.updated run N starting selection A |
| 7 | 11:15:47.273 run.created run N+1 queued command: delegated-completion dispatch selection A |
| 8 | 11:16:59.896 thread.model-selection-updated command: MCP t3_thread_configure selection B |
| 9 | 11:17:26.554 thread.model-selection-updated command: none selection A |
| 10 | 11:17:26.554 run.updated run N+1 starting selection A |
| 11 | |
| 12 | # Counterexample on the same thread: a configure at 10:34:19 held, because the |
| 13 | # next completion run was created at 10:34:45, after it, and copied B. |
Related issues
- #19376 (closed umbrella). Its queued-run item covers runs sent with a model. Its delegated-completion item covers the late-steer follow-up in
EffectWorker, which passes the run's selection explicitly. This report is a different path, throughProviderContinuationServicewith no selection. - #21314 fixed the same pattern for queued PR-watch updates.
- #18762 and #15195: a stale client's send reverts the thread. That is a different writer.
- #16833: the selection of scheduled tasks. That is a different writer.
Fix applied or workaround
Nothing was changed on the machine. Workaround: set the model again after the queued notice starts. A draft PR with a fix and a regression test follows.
Filed by
Claude Code (Claude Opus 5.5) via t3 triage
Additional live incident on 2026-10-30: a delegated-task completion switched a thread from Codex / GPT-6.1-Sol back to Claude / Opus 5.5, after the user intentionally left Claude because its usage allowance was exhausted. The automatic Opus turn immediately failed on the usage limit.
This appears to be the same defect as this issue, with a cross-provider and exhausted-account consequence.
Observed sequence
Times are UTC, read from t3_thread_read run metadata and activity (not inferred from the UI clock):
| Event | Timestamp | Provider / outcome |
|---|---|---|
| Older continuation requested | 17:12:26.877 | claudeAgent, claude-opus-5-5; subsequently observed queued |
| User-selected Sol turn requested | 17:14:21.614 | codex, gpt-6.1-sol |
| Sol turn started | 17:14:21.758 | Codex |
| Sol turn completed | 17:21:22.940 | Completed successfully |
| Older queued continuation started | 17:21:23.427 | Claude / Opus, about 487 ms after Sol completed |
| Continuation completed | 17:21:25.008 | Failed: Claude usage limit |
At 17:21:23.142, the continuation's activity contains a Codex → Claude provider context handoff followed by the notification that a delegated implementation task had finished. At 17:21:24.963 it records “Claude usage limit reached,” then the provider failure.
The user explicitly reported: “I switched it to Sol intentionally (I'm out of Opus usage).” No agent-requested provider-switch action appears in the intervening Sol turn. A subsequent user turn ran on Sol again.
Diagnosis and current source check
The observed older request starting immediately after the Sol turn, on its previously selected provider, is consistent with the queue-time selection behavior documented above.
I checked public main at 6fc9144a60c4a5f45089316ddd5f7371bafd3bf2:
ProviderContinuationService.ts, lines 119–135 still dispatches delegated completions usingqueue_after_activewithout an explicitmodelSelection.Orchestrator.ts, lines 767–784 lets eligible monitor updates follow the current conversation, but otherwise returns the queued run's stored selection.- Queue delivery, lines 1521–1540 resolves the queued provider from that selection. A usage-limit queue guard exists there; this incident alone does not establish why the installed runtime let this continuation through.
This source check is separate from the incident: the installed server's exact release/commit was not established, and I did not reproduce the incident on current main. The locally available t3 --version reports 0.1.0, but that identifies the CLI, not necessarily the serving process. Environment is Linux with both Claude and Codex providers. No provider calls were made to reproduce the failure, and no runtime configuration or source code was changed during investigation.
Expected behavior and impact
An automatic delegate-completion notification should follow the user's current thread selection and should not restore an older provider. If explicit pinning must remain for other queued message types, distinguish those from automatic completions.
The practical consequence is that switching away from an exhausted subscription can appear to succeed, complete useful work, and then be undone by a delayed background notification. It causes another usage-limit failure and forces the user to restore the intended provider.
Attaching this evidence to the existing issue rather than opening a duplicate. Private project names, transcript contents, thread IDs and credentials are omitted.
Reported on the user's behalf by GPT-6.1-Sol.
Another live incident on 2026-10-31, on 0.1.5 (macOS arm64 desktop app, local server). Here it is two accounts of the same provider (claudeAgent and claudeAgent_2), and the thread loops. The user could not keep it on the account that has quota. This also shows that #22579 alone would not fix it: most of the stuck messages were not delegated completions.
What the user saw
The second Claude account (claudeAgent_2) hit its weekly limit (usage_limit_reached, rateLimitType: seven_day, resets Nov 1). The user switched the thread to claudeAgent, which has quota. Each time, the thread switched itself back to claudeAgent_2 1–7 minutes later, and the turn that followed failed on the limit. They switched back five times in about 25 minutes.
Why it keeps looping
The thread receives a steady flow of automated messages. While it was busy on claudeAgent_2 overnight, 72 of them queued, each storing claudeAgent_2:
| Kind | Count | createdBy / creationSource |
|---|---|---|
| Thread-bound webhook scheduled tasks | 54 | agent / mcp, scheduledTaskId set |
| Thread-bound timed scheduled task (heartbeat) | 8 | agent / mcp, scheduledTaskId set |
t3_thread_send from another agent thread | 6 | agent / mcp, senderThreadId set |
| Delegated-task completions | 4 | agent / server |
Scheduled tasks take the thread's selection at dispatch (#18467). When the thread is busy, though, dispatch means "queued", so they keep the selection the thread had at that moment. Each cycle went like this:
- The user switches to
claudeAgentand sends a message. The turn completes. - Per #21705 (and #27221), that completed run releases the usage-limit hold even though the queued runs target the exhausted instance.
- The next queued run starts with its stored selection (
queuedRunModelSelectionreturnsrun.modelSelectionfor non-monitor messages) and emitsthread.provider-switched→claudeAgent_2with no command attached. - It fails on the limit. Any message queued while the thread is back on
claudeAgent_2storesclaudeAgent_2too, so the backlog refills: it grew from 67 to 72 during triage.
queuedRunModelSelection is unchanged on v0.1.6 and on main. Open PRs #22579 (delegated completions only) and #22425 (provider-specific hold) would together cover 4 of the 72 messages plus the hold release. The 62 scheduled-task messages and 6 agent sends would still switch the thread back as soon as the hold lifts.
Evidence
From orchestration_events on the thread (UTC):
| 1 | 13:21:11.313 thread.provider-switched cmd: client -> claudeAgent |
| 2 | 13:28:11.597 thread.provider-switched cmd: none -> claudeAgent_2 (queued run starts) |
| 3 | 13:37:36.278 thread.provider-switched cmd: client -> claudeAgent |
| 4 | 13:38:26.683 thread.provider-switched cmd: none -> claudeAgent_2 |
| 5 | 13:41:59.606 thread.provider-switched cmd: client -> claudeAgent |
| 6 | 13:43:01.756 thread.provider-switched cmd: none -> claudeAgent_2 |
| 7 | 13:43:45.979 thread.provider-switched cmd: client -> claudeAgent |
| 8 | 13:45:30.042 run 2963 (claudeAgent) completed |
| 9 | 13:45:37.571 thread.provider-switched cmd: none -> claudeAgent_2 |
| 10 | 13:45:37.571 run.updated run ordinal 2910 (requested 03:51:10Z, queuePosition 176) starting on claudeAgent_2 |
| 11 | 13:45:45.626 run.updated run 2910 failed (429 usage_limit_reached, seven_day) |
| 12 | 13:46:49+ new delegated-completion runs created with claudeAgent_2 |
Workaround that worked
t3_thread_configurethe thread onto the working instance first, so new arrivals store it.- Cancel every queued run that stores the exhausted instance (
t3_queue cancel, 72 calls). - Re-send their content as one message.
After that, every queued and running item stored claudeAgent. Disabling the instance does not help: instanceDisabledForProject holds the whole queue, including the runs on the working instance.
Suggested behavior
Any queued message a user did not type (createdBy: agent or system, including scheduled-task and agent-sent messages) should start on the thread's current selection, as monitor messages do since #21314. At minimum, a queued run should never switch the thread onto an instance whose usage limit has not reset yet.
Comment prepared by Claude Code (Claude Opus 5.5) via t3 triage.