Repository navigation
[Bug]: Input sent to a Claude thread around a provider crash is never delivered (steered message lost; queued message held and overtaken) #22423
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/server
Steps to reproduce
A. Steer:
- Run the t3 server (v0.1.0) with the Claude provider.
- Start a Claude thread with a multi-step task that takes a few minutes.
- While the turn is running, send a second message to the thread (default delivery, which steers into the running turn).
- About 3 s later, kill the Claude CLI process (
kill -9 <pid>).
B. Queue: same as A, but send the second message with mode: "queue". After the kill, send two more queued messages.
Expected behavior
A message the server accepted for the thread is eventually delivered to the agent once the provider recovers, or is clearly surfaced as undelivered. Queued messages start in the order they were sent.
Actual behavior
- A (steer): the run fails as expected and the thread continues on a resumed session, but the steered message is never delivered. It stays stored on the failed run. The Claude session transcript shows it was enqueued by the CLI but never dequeued before the kill, and the resumed CLI does not replay it. Nothing in the UI indicates it was lost.
- B (queue): after the provider failure the queued message is marked
queueHeld: trueand stays held. The two later queued messages start their own runs and complete while the older one is still held, so the order is inverted and the first message is never delivered unless someone resumes the queue.
Where it seems to come from (v0.1.0, source reading):
- A steer counts as done once it is offered to the provider (
ClaudeAdapterV2.steerTurn,apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts:9116-9163, ending inquery.offer); there is no "consumed" state. - Re-dispatch only happens when the steer failed because the turn had already completed (
ProviderTurnControlService.ts:133-140,EffectWorker.ts:232-330). Process loss terminalizes provider-bound work (ProviderSessionManager.ts:74-79), andprovider-turn.steeris not inREPLAY_SAFE_EFFECT_TYPES_AFTER_PROCESS_LOSS(EffectOutbox.ts:123-133). - The detection for this case already exists for delegated-task notices:
isUndeliveredMailboxSteer(NotificationMailbox.ts:25-39) finds a message steered into a run that then ended and re-offers it (Orchestrator.ts:1065-1100). Ordinary user messages don't go through it. - After a provider failure, queued runs on that instance are marked
queueHeld(Orchestrator.ts:1366-1383), new queued messages inherit the hold (Orchestrator.ts:5083-5088), and starting the next queued run is refused while any queued run is held (Orchestrator.ts:1300-1310). Yet in our run the later queued messages started and completed while the older one stayed held. We haven't traced which path started them, so this may be a second, separate inconsistency.
Possible fix: treat a user message steered into a run that then failed by provider process loss the way delegated notices are treated (re-queue it as its own turn, or at least surface it as undelivered), and never start a newer queued message ahead of an older held one. Since t3 can't know whether the provider acted on the message before dying, surfacing it to the user may be safer than an automatic resend.
Related: #15708, #15358, #21040.
Impact
Minor bug or occasional failure
Version or commit
v0.1.0 (81493ef); relevant code unchanged on main as of 2026-10-20.
Environment
Ubuntu 24.04 (VM), Node 24.22, Claude Code 2.1.304, Codex CLI 0.163.0, headless t3 server driven over its MCP server.
Activity
dpearson2699 commentedon Oct 21, 2026
Another case of part B (a held queued message overtaken by later work), with the overtaking paths traced. Here the hold came from a server restart, not a provider crash. Messages sent over MCP can't release it, and nothing tells the sender it exists.
Version: T3 Code (Nightly) 0.1.1-nightly.20261020.3438, desktop app on macOS, local server. The relevant code is unchanged on
mainat 448b8a9 (apps/server/src/orchestration-v2/Orchestrator.tsL1404, L1474, L5264-5281, L5395-5397;ProviderRuntimeRecoveryService.tsL326-334).What happened
A coordinator thread drives a long-running
delegate_taskchild (Claude) entirely over MCP. Nobody types into the child.queued, not held).runtime-reconcile:shutdowncancelled the active run and setqueueHeld: trueon the queued run.fixture:delegated-completion:dispatch:*) started new runs whenever the thread was idle: 20 runs.t3_thread_sendmessages that arrived while the thread was idle started immediately. The repro below shows this happens even withmode: "queue".t3_thread_sendwithmode: "queue"that arrived while a run was active was created alreadyqueueHeld: true(queuePosition 2, 3, 4, then 5, 6) and never started. When the active run ended, a later send and four completion wakes started ahead of them.startedAt: nulluntil the coordinator noticed andt3_queue_canceled them, then resent them withmode: "auto"(which started at once).Nothing failed.
t3_thread_sendreturned{status: "queued", delivery: "queued"}.task_statuson the queued send's task saidstatus: "queued", workState: "working", andt3_queue_listlisted the items with no held flag. From the sender's side, nothing distinguishes this from a normal queue that will drain.Why (traced)
queueHeld: true.startNextQueuedRunrefuses to drain while any queued run is held.queue.resumeandheldRunsToRelease(added by fix(server,mobile): a message you send resumes a held queue, and mobile can resume one #21473).heldRunsToReleaseapplies only whencreatedBy === "user"andcreationSourceiswebormobile. MCP sends (createdBy: "agent",creationSource: "mcp"), delegated-completion wakes, and scheduled tasks never release. No MCP tool exposesqueue.resume.queueHeld: truewhenever a held run exists and nothing was released.So #21473 fixed this for people typing in the app, but any thread driven by an agent over MCP (delegated children especially) gets stuck behind one restart or Stop until a human resumes it. The day before, on another delegated child, a cancelled run left the next queued run held for ~37 h while a restart continuation started ahead of it.
Minimal repro (scratch, Haiku, ~2 min)
delegate_taska child that delegates two async grandchildren and ends its turn.t3_thread_sendthe child a message withmode: "auto"(it starts), then two more withmode: "queue"(runs N+1, N+2queued).task_cancelthe task for the running message. That stops the thread like a user Stop: N isinterrupted, and N+1 and N+2 becomequeueHeld: true.t3_thread_sendthe now-idle child a message withmode: "queue". It starts immediately (delivery: "started").mode: "queue". It is created already held.Control: without step 3 the two queued sends drain in order right after the active run, ahead of the completion wakes, so the hold is what causes this.
Expected
Any one of these would do:
queue.resume(or a held flag plus resume) to MCP callers. Maket3_thread_send/task_status/t3_queue_listsay when a message is queued behind a held queue.Related: #19233 (restart hold behind a cut
/compact), #22606 (held queue with no explanation after a failed run), #21473 / #18792 (the user-message release).nkoynov commentedon Oct 25, 2026
Another restart hold of part B, this time after an unclean stop (power loss), where the server's own restart continuation ran without asking while the held queue waited three and a half hours. 0.1.3-nightly.20261024.3617 on Linux, OpenCode provider, restart continuations on. Times are UTC, from
orchestration_v2_projection_runs.queueHeld: trueon the queued one. The restart continuation started at 09:13:20 and finished normally.So the consent the hold waits for wasn't asked anywhere else: the server continued the cut turn on its own, and the task kept running through its newer occurrences. The held ones only came back hours late and out of order. The hold is set for every queued run in
ProviderRuntimeRecoveryService.ts(L317-332 on main 3d00499), andheldRunsToReleaseinOrchestrator.tsexcludes restart continuations and scheduled tasks on purpose (#21473).One way out: when startup recovery continues the cut run, don't hold the queue behind it (or release the hold once the continuation settles). A held scheduled-task occurrence that a newer occurrence of the same task has overtaken could be dropped rather than delivered late (related: #16818).
Model and harness: Claude Opus 5.5 via OpenCode 2 in T3 Code.