[BUG] Terminal mode: messages enqueued after first turn are never dispatched (CLI-side queue stall, no exthost restart) — v2.1.221, macOS 26.6

Status Open
Reported on v2.1.221
Maintainer reply None cached
Activity 3 comments · opened Aug 4, 2026

Summary

Two distinct silent-message-drop failure modes observed on the same machine on the same day, with measured log evidence for both. Mode A is a CLI-side queue stall in terminal mode (claudeCode.useTerminal: true): the first turn of a session completes normally, then every subsequent user message is enqueued and never dispatched — no API request, no error, no timeout, spinner state forever. Mode B is the known webview/exthost problem (#79760 / #79245): extension host SIGTERM restarts orphan running backends, and restored tabs silently drop messages.

Mode A is the new data point: it shows the enqueued-never-dispatched behavior of #82772 also occurs in terminal mode with no extension host restart involved, so the webview is not the only trigger surface.

Environment

  • Claude Code CLI: 2.1.221 (npm global install)
  • VS Code extension: anthropic.claude-code-2.1.221-darwin-arm64 (pinned, extensions.autoUpdate: off)
  • VS Code: 1.131.0 (unchanged since 2026-07-20, so not an app-update regression)
  • macOS 26.6 (25G72), Apple Silicon (arm64)
  • Node v24.12.0
  • Config: 13 PreToolUse hooks, 15 MCP servers, ToolSearch active (~356 deferred tools)

Mode A — terminal-mode queue stall (new evidence)

Timeline from the session transcript JSONL (timestamps UTC):

11:00:55  session start  (3 s after VS Code window load; VS Code had been
                          restarted twice within the previous 30 s)
11:00:59  user message #1 ("are you working?")
11:01:08  assistant reply  ← first turn works, 9 s latency
11:01:13  system: stop_hook_summary + turn_duration  ← turn closed cleanly
11:02:20  queue-operation enqueue   ← user message #2 — NEVER dispatched
11:02:54  queue-operation enqueue   ← user message #3 — NEVER dispatched
11:13:00  queue-operation enqueue   ← user message #4 — NEVER dispatched
          (no further entries; user gave up after ~17 minutes)

Facts measured for this window:

  • grep "reason 'killed'" main.log for that VS Code window: 0 hits — no extension host restart involved.
  • No Claude webview extension logs in that window at all (terminal mode) — the stall is inside the CLI process.
  • The three enqueued messages produced zero API requests and zero errors.
  • After quitting VS Code and starting a fresh terminal-mode session (~6 min later), everything worked normally again — the very next session ran a long multi-turn workload without issues.

This matches the symptom description of #82772 (enqueued → never dispatched) but in a plain CLI/terminal context, with the turn state machine apparently stuck in "busy" after a cleanly-closed first turn (stop hook summary and turn_duration were both logged).

Mode B — exthost SIGTERM orphaning (same day, measured)

Earlier the same day, in webview mode, we measured the known orphaned-tab behavior:

  • Extension host killed with reason 'killed' in three separate windows (kills=1, kills=2, kills=1); in the last case the exthost was killed 9 seconds after session start.
  • Decision metric used: grep -c "reason 'killed'" main.log and grep -c "API REQUEST" in the extension output log. kills>0 or apiRequests=0 for a submitted prompt → tab is detached.
  • Key observation: the extension log went silent at 12:35:24 while the backend kept writing the session transcript until 12:39:39 — the backend survives, only the webview binding is lost, and messages typed into the restored tab are silently dropped (matches #79760).
  • A clean uninstall/reinstall of the extension (VS Code fully closed) plus version pinning did not stop the SIGTERM restarts.
  • Confirmed exthost-restart triggers on this machine: hot-swap extension reinstall while VS Code runs; built-in Copilot Chat (#81493 — disabling via chat.disableAIFeatures: true helped); settings.json changes that reload the extension.

Ruled out (with control experiment)

  • CLI/extension version mismatch — both sides pinned 2.1.221; CLI 2.1.221 demonstrably healthy.
  • Hooks / MCP / ToolSearch deadlock (#33073) — control: a CLI session with the same 13 PreToolUse hooks, 15 MCP servers, and ToolSearch active ran 18+ consecutive turns without a single stall.
  • Binary integrity / AV — extension native binary signed com.anthropic.claude-code, no quarantine attribute; no endpoint-security involvement demonstrated (note: the "Blocked by endpoint security" toast appears on any SIGKILL, see #58626, so it is not evidence of AV).

Why this report may be useful

Existing reports (#82772, #79760, #79245, #83771) describe the symptoms; this one adds (a) proof the queue stall occurs without the webview and without exthost restarts, (b) a concrete grep-based decision metric other users can run (reason 'killed' count + API REQUEST count), and (c) a control experiment excluding hooks/MCP as the cause. Happy to provide sanitized transcript JSONL excerpts or run additional diagnostics (--debug, --safe-mode) on request.

Workarounds currently in use

  • claudeCode.useTerminal: true (avoids Mode B entirely; terminal survives exthost restarts)
  • Never type into a restored webview tab; open a fresh tab and /resume
  • On stall: Esc to flush the queue and retype; if dead, Ctrl+C and claude --resume (no context loss — backend transcript is intact)

View original on GitHub ↗

3 Comments

Zmidloch · 26 days ago

Update: controlled downgrade experiment, plus fix-side observations from the measured data

Downgrade experiment (running now)

We rolled both sides back to the last incident-free day's versions as a controlled experiment:

  • CLI 2.1.221 → 2.1.220 (npm global), extension 2.1.221 → 2.1.220; VS Code itself unchanged (1.131.0 since 2026-07-20, already ruled out as a variable). Auto-update disabled on both sides, so the pair stays pinned.
  • Rationale: 2.1.220 is the version recorded in this machine's Aug 3 session transcripts (multiple sessions, zero incidents); 2.1.221 was the version in every incident window. We'll report back either way:
  • stall reproduces on 2.1.220 → evidence the bug class is long-standing, not a 2.1.221 regression;
  • stays clean under the identical workload (13 PreToolUse hooks, 15 MCP servers, ToolSearch active) → narrows the regression window to 2.1.220 → 2.1.221.

Fix-side observations, in case they help triage

  1. The stalled queue state is recoverable, so a watchdog could self-heal it. Esc flushes the stuck queue and retyping works; quitting and claude --resume also recovers with zero context loss. In the Mode A window the first turn closed cleanly (stop_hook_summary and turn_duration both logged), yet the dispatcher behaved as busy for the next 3 enqueues over 11 minutes. A watchdog for "message enqueued, no dispatch within N seconds, no turn active" that re-kicks the dispatcher — or at minimum surfaces an error — would turn a 17-minute silent failure into a sub-second retry.
  2. No ack, no spinner. Zero API requests followed the enqueues, and the UI had no state to express that. Treating "enqueued" as "sent" is what makes this failure silent. An ack-based lifecycle (enqueued → dispatched → API request observed), with a visible failure state after a timeout, would make every variant of this bug user-recoverable.
  3. The backend outlives the UI — rebinding is possible. In Mode B we measured the extension log going silent at 12:35:24 while the backend kept writing the session transcript until 12:39:39. Session state is intact on disk; a restored tab could detect the dead binding (heartbeat to the backend) and offer reconnect/resume instead of silently dropping input.
  4. Init race candidate. The Mode A stall happened in a session that started 3 seconds after window load, following two VS Code restarts within 30 seconds, with 15 MCP servers initializing. The next fresh session on the identical config ran 18+ turns cleanly. If dispatcher/turn-state init races MCP startup, gating input dispatch on init completion (or making enqueue re-check dispatcher state) would remove the race.
  5. Hot-swap reinstall orphans backends. Reinstalling the extension while VS Code runs was one of the confirmed exthost-restart triggers on this machine; a drain/handoff step (or a "restart VS Code to finish install" gate) would avoid creating orphaned sessions.

Data available on request

We hold substantially more local telemetry than what's visible in the public reports on this bug class: full session transcript JSONLs including the queue-operation entries, per-window exthost main.log kill counts, and extension output logs for every incident window that day — all preserved. Happy to share sanitized excerpts or run --debug / --safe-mode reproductions on request.

Zmidloch · 26 days ago

New incident — a third failure shape: extension host busy-loop freeze (GUI panel mode, extension + CLI v2.1.221, macOS)

Timeline (local time, all logs archived):

  • 15:49:37 — window load, fresh session created in the panel webview (not a restored tab; zero reason 'killed' entries in main.log for this window)
  • 15:49:55 — turn 1: [API REQUEST] dispatched, reply rendered normally ~7 s later
  • 15:50:15 — turn 2 typed; queue-operation enqueue/dequeue OK; webview time_to_response event for turnIdx: 2 logged
  • 15:50:16.062 — extension output log: [API REQUEST] /v1/messages dispatched — the last line ever written to that log
  • 15:50:22.042 — renderer.log: Extension host (LocalProcess pid: 18936) is unresponsive. then UNRESPONSIVE extension host: starting to profile NOW (the profiler's verdict never appeared in any log)
  • The extension host main thread then spun at ~100 % CPU for 8+ minutes until manually restarted

What the process was doing: a macOS sample of the extension host shows the main thread 100 % inside JIT-compiled JS (no symbols), entered from a libuv fs completion callback — node::fs::FileHandle::CloseReq::Resolve() → microtask drain → a tight JS loop that never yields. The last debug lines before the freeze were FileHistory: Making snapshot for message <uuid>, so the timing is consistent with an fs-callback-triggered loop in the extension's file-history/snapshot path.

Meanwhile the CLI backend was healthy the whole time: the session transcript JSONL shows both user turns arrived and the assistant's turn-2 reply was written at 15:50:22 — the exact moment the exthost was flagged unresponsive. No data loss; the UI just never rendered the reply. Same "backend keeps working while the UI is dead" signature as the earlier incidents in this issue, but a different mechanism: this time the API request did dispatch — the extension host itself froze.

Recovery that works: Developer: Restart Extension Host — the renderer/UI stays alive (only extensions are dead), so the command palette is reachable.

Possibly relevant environment, now removed as a controlled experiment:

  • the built-in Copilot Chat had been inadvertently re-enabled (chat.disableAIFeatures was lost during a settings reset) — the #81493 trigger
  • the Continue.dev extension was installed and active, logging startup errors (#76633 family)

Both are now disabled/uninstalled; I'll report back whether the freezes stop.

Two disclosures for anyone triaging:

  1. extension.js on this machine carries a small local patch to deserializeWebviewPanel (passing the persisted sessionID instead of undefined, see #83322). That code path handles restored tabs only and was not exercised here — the failing session was fresh and no exthost restart preceded the freeze.
  2. The v2.1.220 rollback experiment I promised earlier in this thread was aborted before producing data (the environment got fully reset to latest instead), so no conclusion either way from that.

Happy to share the full sample output, extension log, and transcript timestamps.

Zmidloch · 26 days ago

Follow-up to my earlier comment on this issue, where I described a "confirmed" trigger for the extension-host restarts I was seeing. New data collected since then contradicts part of that comment, and I'd rather correct it than leave it standing.

What I said before, and why it needs correcting

I previously wrote that GitHub Copilot Chat was a confirmed trigger (citing #81493, and noting that setting chat.disableAIFeatures: true "helped"), and separately attributed some Continue.dev startup config errors to "the #76633 family."

Both need retracting:

  • Copilot Chat: across five extension-host-unresponsive events captured since that comment, "copilot" never appears in exthost.log — not once, in any of the five. In the fifth event, chat.disableAIFeatures: true was written to settings 1 second before that extension host started, and the host froze anyway 46 seconds later. My earlier "disabling helped" note looks like it was coincidence, not causation — I didn't have enough data at the time to tell the difference.
  • #76633: that issue is about the extension getting stuck on "Reloading..." after claude --resume on Windows — different mechanism, different platform, different author, and not actually about Continue.dev's startup config errors. That citation was a mistake; please disregard it.

What the new data points to instead

Five extension-host freezes in one session, all 40–73 seconds after an extension host start:

| # | Host start | Freeze detected | Delay to freeze |
|---|---|---|---|
| 1 | 14:39:07 | 14:40:20 | 73s |
| 2 | 14:47:33 | 14:48:13 | 40s |
| 3 | 14:48:56 | 14:49:37 | 41s |
| 4 | 15:49:37 | 15:50:22 | 45s |
| 5 | 16:02:44 | 16:03:30 | 46s |

(Delay to freeze = time from this host's start to the "is unresponsive" report, not total time unresponsive — the host behind row 4 was still spinning at 100% CPU when sampled 8 minutes later.)

In all five, the Continue extension activated (onStartupFinished) roughly 2 seconds after host start, and logged the same configuration errors every time (a YAML schema registration failure, plus an "Embeddings provider google-genai not found" error twice per startup). I uninstalled Continue at 16:03:01, mid-lifetime of host #5 — which still froze 29 seconds after the uninstall completed, consistent with "already active in the running process" rather than "present on disk." The next extension host started about 13 minutes later and has run 40+ minutes and counting under materially heavier load than any of the five freezing sessions (including sustained Claude Code activity), with zero freezes. Every one of the five freezes landed within 73 seconds of host start, so 40 minutes clean is well past the window where they showed up. That's still one clean run, not yet repeated.

A macOS sample of one frozen host showed 100% of samples in a single JS callback chain through node::fs::FileHandle::CloseReq::Resolve, unsymbolized JIT frames throughout — consistent with a busy loop, not a deadlock, but it doesn't name Continue's code directly.

I'm reporting the Continue-side evidence to that project directly rather than duplicating it in full here.

Where Claude Code fits (or doesn't)

Worth being explicit: this correction is largely exculpatory for Claude Code too. The local patch for #83322 (webview session-restore) was present for freezes #4 and #5 but absent for #1–#3, and all five froze the same way regardless of whether the patch was installed. The clean run that followed Continue's removal happened under heavier Claude Code load than any of the five freezing sessions, with zero freezes. Read together, presence of the Claude Code patch doesn't track with whether the freeze happens; Continue's presence does.

What I still can't prove

  • Correlation, not causation: no symbolized profile names Continue's code on the spinning frame.
  • The clean-after-removal run is one occurrence: the next host started ~13 minutes after removal and has run 40+ minutes without a freeze; I haven't repeated the install/uninstall cycle to build real statistical confidence.
  • Detailed CPU sample for only 1 of the 5 freeze events, not all 5.

Apologies for the bad citation in the earlier comment — correcting the record now rather than leaving it while I keep digging.