Claude thread permanently wedged after MCP registration timeout: stale pending handoff forces fresh session, full-thread handoff exceeds budget, /compact can't recover#22198

opened7d agobySsweepies

What happened

Was just coding and started getting provider errors, then compact errors etc. Tried updating/restarting t3code.

A long Claude thread (Opus 5.5, 1M window, ~553k tokens used) was managing an OpenCode subagent. The subagent's OpenCode event stream was lost. The next two Claude turns failed with "The provider could not start this turn". After an update and restart, every turn failed with "Insufficient context allowance for the provider handoff. Compact the target conversation…". /compact returned "Error: No messages to compact", and the thread stayed stuck on every following turn.

Diagnosis

From boot-service.log, server.trace.ndjson*, the provider event log and statev2.sqlite, checked against source at v0.1.1-nightly.20261020.3386:

  1. Trigger. OpenCode lost its event stream (HeadersTimeoutError on /api/event), and the OpenCode provider refresh for the project timed out (20s). The next two Claude turns (runs 470 and 471) each failed after 90s in ClaudeAdapterV2 with ClaudeAgentSdkQueryRunnerError → Claude Code did not answer the MCP server registration. (CLAUDE_MCP_REGISTRATION_TIMEOUT). Claude never received the prompt.
  2. A stale pending marker. Before run 471 started, deliverContextHandoffs saved its delta_since_target_last_seen handoff with delivery: { nativeThreadId: <original session>, status: "pending" }. When startTurn fails, ProviderTurnStartService restores the marker (delivery.unsent) only if refusedBeforePrompt(error) is true. That check (ProviderTurnStartService.ts:70; PROMPT_REFUSAL_TAGS on main) does not recognise the MCP registration timeout, even though it happens before the prompt is sent. So the marker stayed pending.
  3. The real session is thrown away. On the next turn, uncertainDelivery was true. T3 skipped resumeThread on purpose and logged Provider resume failed; attempting a fresh native session with reason: "uncertain_history_delivery". It then bound a new, empty Claude session and prepared a full_thread_summary handoff covering runs 1–471 (~168 KB payload).
  4. The handoff doesn't fit. deliverContextHandoffs threw ContextHandoffBudgetError, so every turn fails with "Insufficient context allowance…".
  5. /compact can't recover. /compact goes to the new, empty native session ("No messages to compact"), and its run completes. The turn-start handoff filter (ProviderTurnStartService.ts:243) then keeps retrying any handoff whose target run was a completed /compact with no delivery. So the oversized full_thread_summary comes back on every later turn, and the thread can't recover in the app.

So a transient failure before the prompt was sent (a 90s MCP registration timeout) permanently replaced a healthy native session holding ~553k tokens with an empty one, and the advice in the error ("Compact the target conversation") cannot work.

Suggested fixes:

  • Treat the MCP registration timeout, and any failure before query.open or before the prompt is sent, as refused-before-prompt, so delivery.unsent clears the marker.
  • An uncertain delivery of a tiny delta handoff should not force dropping a native session. At minimum, try resuming and re-deliver the delta.
  • A full_thread_summary that doesn't fit should be deferred or replaced with a t3_thread_read pointer instead of failing the turn (overlaps with #19414). /compact against a fresh, empty session should not count as having compacted the target conversation.

Steps to reproduce

Not reproduced deterministically. The sequence:

  1. Run a long Claude thread (a large native session; here ~553k tokens) that delegates to an OpenCode subagent.
  2. Make the turn fail during setMcpServers (here OpenCode had hung, and Claude did not answer the MCP server registration within 90s). The failure has to come after deliverContextHandoffs saved a pending handoff, which happens when a previous run failed, so a missed-run delta exists.
  3. Send another message. The resume is skipped (uncertain_history_delivery), a fresh session is created, and the turn fails with ContextHandoffBudgetError.
  4. Run /compact ("No messages to compact"), then send again. It fails the same way, permanently.

Version

Trigger (runs 470–471) on 0.1.1-nightly.20261017.3272. The stuck state after the update is on 0.1.1-nightly.20261020.3386, and the relevant code is unchanged on main as of 2026-10-20. The t3 triage CLI was 0.1.0.

Environment

Server: Linux x64 (Proxmox kernel 7.0.14), Node v26.8.2, systemd user service. Client: T3 Code desktop app on macOS. claude CLI 2.1.301, model claude-opus-5-5 (1M), OpenCode subagent.

Evidence

shell
1# run 470/471 (nightly.20261017.3272), ClaudeAdapterV2.startTurn, ~90s each
2ProviderAdapterTurnStartError: Failed to start run run:thread:<thread>:ordinal:471 on claudeAgent ...
3 [cause]: ClaudeAgentSdkQueryRunnerError: Claude Code did not answer the MCP server registration.
4
5# context handoff row for run 471, left behind
6strategy=delta_since_target_last_seen status=ready
7delivery={"nativeThreadId":"<original session>","status":"pending","itemIds":[<run 470 failure item>]}
8
9# after restart (nightly.20261020.3386)
10[07:23:00.864] WARN Provider resume failed; attempting a fresh native session
11 { driver: 'claudeAgent', runId: '...:ordinal:472', reason: 'uncertain_history_delivery', errorTag: 'ProviderAdapterTurnStartError' }
12# run 472 = "/compact" on new session <fresh session>: result success, num_turns 0 -> UI "Error: No messages to compact"
13# handoff for run 472: strategy=full_thread_summary coveredRunOrdinals 1-471, payload ~168 KB, no delivery
14[07:23:04.109] ERROR orchestration V2 provider turn start failed runId ...:ordinal:473
15 [cause]: ContextHandoffBudgetError: Insufficient context allowance for the provider handoff. ...
16[07:23:19.226] ERROR orchestration V2 provider turn start failed runId ...:ordinal:474 (same)
17
18# last usage on the original native session (provider turn for run 469)
19{"usedTokens":552836,"maxTokens":1000000}

Related issues

  • #19875: the same permanent wedge, where /compact can't recover, on OpenCode. There the trigger is missed-run replay with budget 0 from byte-estimated usage. Here the trigger is a stale pending delivery marker after a failure before the prompt was sent, which forces a fresh native session plus a full_thread_summary.
  • #19414: an open PR that defers oversized handoffs. It would stop the endless failures here, but the healthy native session would still be lost.
  • #21608: uncertain_history_delivery on a Claude thread, closed as a duplicate. This report gives a concrete cause for the pending marker.
  • #17952, #16147: the same error with different triggers.

Fix applied or workaround

To recover the thread, we edited the database by hand with the server stopped, after taking a backup. We appended context-handoff.updated events marking the run 471 and 472 handoffs superseded (with the pending delivery removed), and a provider-thread.updated event pointing nativeThreadRef back at the original Claude session, with its last contextUsage. We made the matching projection-row changes and moved orchestration_v2_projection_metadata.last_sequence forward. After the server restarted, the thread resumed its original Claude session and works again. This isn't a recommended fix. Without it, the only workaround is a new thread that reads the old one via t3_thread_read.

Filed by

Claude Code (claude-opus-5-5) via t3 triage

Synced from GitHub

I hit this today on T3 Code desktop 0.1.0 (Beta), macOS 27.0.1 arm64, Claude Agent SDK / Opus 5.5. Same recovery loop, but it started with a SQLite lock rather than an MCP registration timeout. It connects this report with #15542.

The thread showed a provider error, but its Claude process and subagents were still working. Sending another message failed immediately. /compact failed too. I had other threads running, so restarting the whole app wasn't a useful way out.

Here's what we found in the local trace and database:

  • At 19:34:17–19:34:22 UTC on October 20, orchestrationV2.EventSink.write failed after about 5.18 seconds with LockTimeoutError → database is locked. We haven't established what held the lock.
  • App run 236 became failed, while its provider turn stayed running. New runs failed with Claude provider turn …ordinal…236… is still active.
  • Subsequent recovery logged Provider resume failed; attempting a fresh native session, with reason: uncertain_history_delivery. T3 replaced the original native-session reference with a fresh one and left full-thread handoffs ready to deliver.
  • After interrupting the stuck turn, the next message failed with Insufficient context allowance for the provider handoff.
  • /compact then completed with Not enough messages to compact. It was acting on the empty replacement session. Sending another message brought back the context-allowance error.

The original native conversation was still on disk. Its last recorded usage was 307,706 / 1,000,000 tokens. It hadn't run out of context.

We recovered this one thread without restarting T3: backed up its state, interrupted the orphaned turn through T3's command API, detached its provider session, then repaired the affected projection rows to point back to the original native conversation, superseded the two unused full-thread handoffs, and reconciled the interrupted provider turn. A diagnostic message then got a normal answer. The T3 server PID stayed the same. This stopped the thread's two remaining subagents; other chats kept running.

That was a one-off repair, not a workaround I'd want people to have to repeat. The app should be able to stop the orphaned turn and resume the existing conversation. If recovery creates an empty session, telling the user to compact it just sends them in circles.

I don't have a deterministic reproduction, and I haven't tested a newer build. The sequence above is from this incident. Conversation contents, local paths and session IDs are omitted.

Synced from GitHub

Additional occurrence of this recovery loop on Windows x64, inspected on T3 Code server 0.1.1-nightly.20261020.3411, using the native claudeAgent provider and claude-opus-5-5.

The initial trigger is unconfirmed here. The durable timeline records a T3 restart/resume notification after a cancelled long-running turn, followed by provider-start failures. I have not verified an MCP registration timeout, a stale pending delivery marker, or replacement of the original native-session reference in this case.

Observed sequence

All times are UTC on 2026-10-20, verified from T3's durable run/activity records:

  • Runs 56 and 57 failed at 19:55:50.088 and 19:58:30.222 with the generic provider-start error below.
  • Run 59, /compact, recorded Context compaction failed and returned Not enough messages to compact. at 21:07:14.220. Its run status was nevertheless completed.
  • Run 60, a continuation message, failed at 21:07:15.895 with the context-handoff error below.
  • Run 61, another /compact, again recorded failed compaction and returned Not enough messages to compact. at 21:07:54.873, while its run status was completed.
  • Run 62, another continuation, failed at 21:07:56.079 with the same handoff error.
  • For run 63, the thread was configured to claude-opus-4-6 with contextWindow: 1m, effort: high, and fastMode: false. Reading back the configuration confirmed those settings. The run still failed before assistant output at 21:09:46.791 with the same handoff error. This did not establish that model capacity was the cause.
text
1The provider could not start this turn. Retry the turn; if it keeps failing, check the provider setup and server logs.
2
3Not enough messages to compact.
4
5Insufficient context allowance for the provider handoff. Compact the target conversation or use a larger-context model; the current request has not been truncated.

Workaround and limits

A fork from the latest stable point, using the original claudeAgent / claude-opus-5-5 selection, accepted a continuation and produced assistant output and command activity. The user confirmed the fork worked and settled the original thread. The fork's assistant said it had no earlier messages in its native session and reconstructed progress from project files, so I am not claiming that the complete conversation was successfully transferred.

The user reports similar failures in other threads, but only this occurrence was inspected. No deterministic reproduction or underlying server exception is available from this inspection. The error text alone does not establish that the original conversation exhausted its context window.

Synced from GitHub

Ran into this today on 0.1.2-nightly.20261021.3499 (macOS, claudeAgent). My long Opus thread hit the usage limit, I tried switching to Fable and Sonnet, and after that every message failed with "Insufficient context allowance" while /compact just said "No messages to compact". Forking the thread didn't work either. I had Claude go through the logs and the source, and it found a workaround that got the thread back. Its notes are below.


Notes from Claude Opus 5.5

Trigger. Same as above: a turn that failed after the model switch left a pending delivery marker on the original session. The next turn (a /compact) took the uncertain_history_delivery path and started an empty session with a full_thread_summary.

Why it never recovers. The summary was only 13.8 KB, so its size isn't the problem. nativeContextEstimate is. After the fallback run, the replacement session counts as the same native thread and has no measured usage, so the estimate adds up every item ever recorded on the provider thread: about 4 MB here, mostly command output. handoffBudget then returns 0 for any model, 1M windows included. Normal turns fail before reaching Claude, and /compact runs on the empty session, so no usage is ever recorded.

On the fallback run itself the estimate is 0, so if that first turn had been a normal message instead of /compact, the handoff would have been delivered. Counting only items that actually reached the current native session would fix this.

Fork. The fork calls session.fork on the empty replacement session with an upToMessageId from the original session, so the SDK query fails.

Workaround.

  1. Kill the idle claude --resume=<replacement id> process.
  2. Back up <replacement id>.jsonl and delete its user/assistant lines, so resumeThread sees an empty transcript and fails.
  3. Send a normal message, not /compact. T3 falls back again, the estimate is 0 on that run, and the handoffs are delivered.

The code is unchanged on main @ d53873b6a4.

It happened again, Claude-only this time (no OpenCode involved), on preview 0.1.2-preview.20261022.3516. The trigger here was host overload rather than a lost subagent stream.

Timeline (UTC, 2026-10-24), from boot-service.log, the thread's activity and statev2.sqlite:

  1. From 02:38, two check:changed runs on the same host pinned the agent work slice at its memory limit. The host swapped for hours: load around 70, I/O pressure "full" around 75%, and T3 event-loop stalls of up to 12 s. The Claude capability probe timed out every ~35 s from 03:21.
  2. Runs 716 (the server's idle /compact at 04:43), 717 and 718 each failed after 90 s with ProviderAdapterTurnStartError → ClaudeAgentSdkQueryRunnerError → Claude Code did not answer the MCP server registration.
  3. At 05:43:53 the server logged Provider resume failed; attempting a fresh native session with reason: 'uncertain_history_delivery' (run 718). It recorded a provider_resume_fallback transfer, claudeAgent to claudeAgent with no provider switch, and a full_thread_summary handoff over runs 1–717 (~11,900 items).
  4. From then on every turn (720–726) failed within a second with "Insufficient context allowance for the provider handoff…". /compact (run 719) answered "Error: No messages to compact" because it compacted the new, empty native session. The thread is still unusable.

So step 2 of the diagnosis above reproduces without OpenCode: an MCP registration timeout is a refusal before the prompt was sent, but it still leaves the handoff marked pending. The next turn then discards a perfectly resumable session.

What a fix needs: no failure on a single turn may leave a thread permanently unusable.

  • Treat the MCP registration timeout (and any start failure before the prompt was consumed) as refused-before-prompt, so the pending marker is restored and the next turn resumes the original session.
  • A temporary start failure (timeout, overloaded host) must not lead to "abandon the native session". Retry or resume first.
  • If a fresh session is truly needed and the full-thread summary does not fit, defer it and send the prompt (as https://github.com/pingdotgg/t3code/pull/19414 does for oversized handoffs) rather than failing every turn.
  • /compact, or another user action, must be able to clear a stuck pending handoff.
Synced from GitHub

It happened again, Claude-only this time (no OpenCode involved), on preview 0.1.2-preview.20261022.3516. The trigger here was host overload rather than a lost subagent stream.

Timeline (UTC, 2026-10-24), from boot-service.log, the thread's activity and statev2.sqlite:

  1. From 02:38, two check:changed runs on the same host pinned the agent work slice at its memory limit. The host swapped for hours: load around 70, I/O pressure "full" around 75%, and T3 event-loop stalls of up to 12 s. The Claude capability probe timed out every ~35 s from 03:21.
  2. Runs 716 (the server's idle /compact at 04:43), 717 and 718 each failed after 90 s with ProviderAdapterTurnStartError → ClaudeAgentSdkQueryRunnerError → Claude Code did not answer the MCP server registration.
  3. At 05:43:53 the server logged Provider resume failed; attempting a fresh native session with reason: 'uncertain_history_delivery' (run 718). It recorded a provider_resume_fallback transfer, claudeAgent to claudeAgent with no provider switch, and a full_thread_summary handoff over runs 1–717 (~11,900 items).
  4. From then on every turn (720–726) failed within a second with "Insufficient context allowance for the provider handoff…". /compact (run 719) answered "Error: No messages to compact" because it compacted the new, empty native session. The thread is still unusable.

So step 2 of the diagnosis above reproduces without OpenCode: an MCP registration timeout is a refusal before the prompt was sent, but it still leaves the handoff marked pending. The next turn then discards a perfectly resumable session.

What a fix needs: no failure on a single turn may leave a thread permanently unusable.

  • Treat the MCP registration timeout (and any start failure before the prompt was consumed) as refused-before-prompt, so the pending marker is restored and the next turn resumes the original session.
  • A temporary start failure (timeout, overloaded host) must not lead to "abandon the native session". Retry or resume first.
  • If a fresh session is truly needed and the full-thread summary does not fit, defer it and send the prompt (as https://github.com/pingdotgg/t3code/pull/19414 does for oversized handoffs) rather than failing every turn.
  • /compact, or another user action, must be able to clear a stuck pending handoff.

image