Skip to content

[Bug]: Input sent to a Claude thread around a provider crash is never delivered (steered message lost; queued message held and overtaken) #22423

Open

Description

@blueworkslabs

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:

  1. Run the t3 server (v0.1.0) with the Claude provider.
  2. Start a Claude thread with a multi-step task that takes a few minutes.
  3. While the turn is running, send a second message to the thread (default delivery, which steers into the running turn).
  4. 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: true and 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 in query.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), and provider-turn.steer is not in REPLAY_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

  1. dpearson2699 commented on Oct 21, 2026

    @dpearson2699

    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 main at 448b8a9 (apps/server/src/orchestration-v2/Orchestrator.ts L1404, L1474, L5264-5281, L5395-5397; ProviderRuntimeRecoveryService.ts L326-334).

    What happened

    A coordinator thread drives a long-running delegate_task child (Claude) entirely over MCP. Nobody types into the child.

    1. A scheduled-task message was queued behind an active run on the child (queued, not held).
    2. The desktop app updated and restarted the server. runtime-reconcile:shutdown cancelled the active run and set queueHeld: true on the queued run.
    3. For the next ~13 h the hold was never released, and the child kept working normally:
      • Delegated-completion wakes (fixture:delegated-completion:dispatch:*) started new runs whenever the thread was idle: 20 runs.
      • t3_thread_send messages that arrived while the thread was idle started immediately. The repro below shows this happens even with mode: "queue".
      • Every t3_thread_send with mode: "queue" that arrived while a run was active was created already queueHeld: 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.
    4. Six queued messages sat there with startedAt: null until the coordinator noticed and t3_queue_canceled them, then resent them with mode: "auto" (which started at once).

    Nothing failed. t3_thread_send returned {status: "queued", delivery: "queued"}. task_status on the queued send's task said status: "queued", workState: "working", and t3_queue_list listed the items with no held flag. From the sender's side, nothing distinguishes this from a normal queue that will drain.

    Why (traced)

    • Shutdown reconcile and provider recovery mark every queued run queueHeld: true.
    • startNextQueuedRun refuses to drain while any queued run is held.
    • The only releases are queue.resume and heldRunsToRelease (added by fix(server,mobile): a message you send resumes a held queue, and mobile can resume one #21473). heldRunsToRelease applies only when createdBy === "user" and creationSource is web or mobile. MCP sends (createdBy: "agent", creationSource: "mcp"), delegated-completion wakes, and scheduled tasks never release. No MCP tool exposes queue.resume.
    • A new queued run inherits queueHeld: true whenever a held run exists and nothing was released.
    • Dispatch to an idle thread doesn't look at the queue at all, so later sends and wakes start ahead of the held runs. That answers the "which path started them" question above.

    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)

    1. From a parent thread, delegate_task a child that delegates two async grandchildren and ends its turn.
    2. t3_thread_send the child a message with mode: "auto" (it starts), then two more with mode: "queue" (runs N+1, N+2 queued).
    3. task_cancel the task for the running message. That stops the thread like a user Stop: N is interrupted, and N+1 and N+2 become queueHeld: true.
    4. t3_thread_send the now-idle child a message with mode: "queue". It starts immediately (delivery: "started").
    5. While it runs, send another with mode: "queue". It is created already held.
    6. The grandchildren finish, and their completion wakes start new runs. The three held runs never start, and nothing reports that they are 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:

    • Never start newer work, a send or a wake, ahead of an older held queued message on the same thread. Or release the hold when an agent sends a message, as a web/mobile user message does.
    • Expose queue.resume (or a held flag plus resume) to MCP callers. Make t3_thread_send / task_status / t3_queue_list say 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).

  2. nkoynov commented on Oct 25, 2026

    @nkoynov
    Contributor

    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.

    • A scheduled task bound to the thread (every 30 minutes) queued a run at 08:24 behind the active run.
    • The machine lost power. At 09:13:14 startup recovery cancelled the active run and set queueHeld: true on the queued one. The restart continuation started at 09:13:20 and finished normally.
    • The next two occurrences of the same task, at 09:13:19 and 09:43, were created already held.
    • Over the next 3.5 hours about 20 other runs started right away on the idle thread, later occurrences of the same task among them, overtaking the three held ones.
    • At 12:43:29 a message I sent from the web app released the hold, and the three stale occurrences ran back to back at 12:44.

    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), and heldRunsToRelease in Orchestrator.ts excludes 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions