[BUG] Desktop app: a message delivered to a session that is still starting up is silently lost — the query never starts and the session is dead for the full ~16min watchdog

Status Closed — duplicate
Reported on v2.1.222
Maintainer reply None cached
Activity 2 comments · opened Aug 13, 2026 · closed Aug 25, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

On the desktop app, when a message is delivered to a session that was just activated/resumed (either a cross-session message, or the user activating a window and typing), the session intermittently never starts its query at all. The window looks active but produces nothing — no init, no thinking indicator, no output, no error. It stays dead for the full CCD inactivity watchdog (~970s), then self-recovers. The delivered message is lost — it never reaches the transcript and must be re-sent.

I have log-level evidence that isolates this to "the query never started", rather than "the model was slow" or a network stall:

Mapping internal session <x> to CLI session <y> appears in 731/731 (100%) healthy cycles and 0/14 (0%) stalled cycles.

That line is emitted when CCD binds the CLI session. Its total absence across stall windows of 569–1001s means no query was ever bound or started.

Quantitative evidence (14 stalls vs 731 healthy cycles, from a single main.log)

A "stall" = a cycle with duration > 300s AND hadFirstResponse=false.

| Signal | Stalled (n=14) | Healthy (n=731) |
|---|---|---|
| Mapping internal session … to CLI session present in cycle | 0 / 14 (0%) | 731 / 731 (100%) |
| Median seconds from session START (Resuming/Warming) to message delivery | 1.0 s | 34.0 s |
| Had a Resuming session within 120s before the message | 50% (7/14) | 4% (27/731) |
| Distinct sessions activated in the 90s before the cycle (mean) | 3.36 | 1.51 |

Two conclusions:

  1. It is a startup race. The message lands a median of 1 second after the session starts booting, vs 34s when healthy. Cold Resuming is enriched 13x in stalls. Bursts make it much worse — the worst incident stalled 7 sessions that had all been activated within 90 seconds of each other.
  2. The query never begins. Perfect 0%/100% separation on the Mapping line, across stall windows up to 1001s long.

Process state during a stall

The claude.exe child process for that session is alive but idle — a 6-second sample gives dCPU = 0.06s, working set flat, transcript file not growing. It is blocked, not working.

What this is NOT (ruled out with evidence)

  • Not the network. In 13 of 14 stalls, other sessions completed healthy cycles during the exact stall window over the same egress. Example: while one session was stalled for 972s, five other sessions completed successfully (33s, 86s, 589s, 406s, 276s).
  • Not transcript size. The largest session (79.7 MB / 2683 messages) never stalled; an 8.9 MB one did.
  • Not resources. 462 GB disk free, 46 GB RAM free; the transcript JSONL validated 5021/5021 lines parseable.
  • Not permissions. The CLI is launched with --allow-dangerously-skip-permissions; no permission prompt appears in the logs.
  • Not MCP startup. [CCD start-timing] reports mcp=519ms on healthy starts.
  • Not hooks. Only PreToolUse hooks are configured, and those cannot fire before the first byte. (This rules out the mechanism in #43123 for this reporter.)

Impact

For a multi-session workflow this is severe. A coordinating session dispatching work to several task sessions in quick succession is exactly the burst pattern that triggers it, and those messages are silently lost — with a 16-minute dead window and no error surfaced anywhere in the UI.

Related but distinct

  • #49563 — also a pre-first-byte startup hang, but terminal CLI, permanent (no self-recovery), and on every first message.
  • #43123 — same hadFirstResponse=false / reason=no_response signature, but requires a SessionStart hook holding the stdio pipes.
  • #47958 — some sessions hang while others work, but permanent and bound to a specific project.
  • #32526 — new sessions hang intermittently, but no log evidence was captured.

Secondary observation (happy to file separately)

The same logs show that cancellation does not work while stalled:

[warn] [CCD] query.interrupt() did not resolve within 1500ms for local_<x>
       (hadFirstResponse=false); falling back to stopSession.
       SDK abort not honoured pre-first-byte

So pressing Stop does nothing and the user has no recovery except waiting out the full ~16 minutes. Even once the startup race is fixed, cancellation should work before the first byte. Say the word and I'll open a separate issue for this.

What Should Happen?

A message delivered to a session should always be processed, regardless of whether that session happened to be mid-startup when it arrived.

Concretely:

  1. Don't deliver until the CLI is ready. Hold the queued message until the CLI has emitted its init / the query is bound, then deliver it. (Or re-deliver once binding completes.)
  2. Never lose the message. If the cycle is reaped by the watchdog, the undelivered message should be re-queued, not silently dropped. Today it disappears with no trace in the transcript and no indication in the UI.
  3. Fail loudly, not silently. If no init arrives within a few seconds of delivery, surface an error instead of sitting mute for ~16 minutes.
  4. Make Stop work. query.interrupt() should be honoured before the first byte so the user can recover immediately rather than waiting out the watchdog.

Error Messages/Logs

A stalled cycle. Note there is NO "Mapping internal session ... to CLI session ..." line
between the delivery and the timeout - that line is present in 731/731 healthy cycles.

[info] Sending message to session local_<B>
    ... no "Mapping internal session local_<B> to CLI session <cli>" ...
    ... 972 seconds of complete silence ...
[warn] [CCD] Session local_<B> timed out after 972s of inactivity
       (hadFirstResponse=false, last_message_type=user, last_tool_name=none, seconds_since_stderr=never)
[info] [CCD CycleHealth] unhealthy cycle for local_<B> (972s, hadFirstResponse=false, reason=no_response)
[info] Session local_<B> query iterator completed

A healthy cycle for comparison - the Mapping line appears immediately:

[info] LocalSessions.sendMessage: sessionId=local_<A>, messageLength=36, ...
[info] Sending message to session local_<A>
[info] Mapping internal session local_<A> to CLI session <cli>
[info] [Stop hook] Query completed for session local_<A>
[info] [CCD CycleHealth] healthy cycle for local_<A> (33s, hadFirstResponse=true)

A cold resume that then stalled - transcript loads fine, then nothing ever happens:

[info] Resuming session local_<B> in <cwd>
[info] Starting local session local_<B> in <cwd>
[info] Loaded 2683 transcript messages for session local_<B>
    ... nothing further for this session until the watchdog fires ...

Cancellation does not work while stalled:

[warn] [CCD] query.interrupt() did not resolve within 1500ms for local_<x> (hadFirstResponse=false);
       falling back to stopSession. SDK abort not honoured pre-first-byte

Steps to Reproduce

This is a timing race, so it is probabilistic rather than deterministic — in my logs it hit roughly 2% of cycles overall, but much more often when sessions are activated in a burst. The pattern that reproduces it most reliably:

  1. Open the desktop app with several long-lived local sessions (I have ~20; each has a sizeable transcript, though transcript size turned out not to matter).
  2. Let some of them go idle long enough to be paused by the idle timeout ([CCD] Pausing session ... (idle_timeout)), so that reactivating them requires a cold Resuming session + Loaded N transcript messages.
  3. From one session, use cross-session messaging to dispatch messages to several of those idle sessions in quick succession — within ~90 seconds of each other. (Activating the windows manually and typing immediately also reproduces it; the cross-session path just makes bursts easy.)
  4. Watch %APPDATA%\Claude\logs\main.log.

Expected: every target session starts a query and replies.

Actual: some fraction of them log Sending message to session local_<x> with no following Mapping internal session local_<x> to CLI session <y>, then sit completely silent until timed out after ~970s of inactivity (hadFirstResponse=false, ...). Those messages never appear in the target transcripts.

The single most useful discriminator when triaging a report like this:

# stalls (>300s with hadFirstResponse=false) that contain a Mapping line vs healthy cycles that do

In my data that is 0/14 vs 731/731 — so the presence or absence of the Mapping line cleanly identifies the failure without needing to reproduce interactively.

Timing hint for narrowing it down: in stalled cycles the median gap between the session START event (Resuming / Warming) and the message delivery was 1.0 s, versus 34.0 s in healthy cycles. Delivering while the CLI is still booting appears to be the trigger.

Claude Model

Opus

Is this a regression?

Yes, this worked in a previous version

Last Working Version

2.1.222

Claude Code Version

2.1.227 (Claude Code) — bundled by the desktop app, not user-selectable

Platform

Anthropic API

Operating System

Windows

Terminal/Shell

Other

Additional Information

Which distribution channel this is (relevant for reproducing)

This is the desktop app, which does not use the npm package. CCD downloads its own CLI binary from downloads.claude.ai/claude-code-releases/<version>/win32-x64/claude.exe.zst and installs it silently — there is no update prompt and no version selector in the UI. For reference, from my logs:

[stealth-update] Triggering stealth update after idle timeout
[updater] Version changed since last launch: 1.26832.0 → 1.28929.0
[CCD] Initialized with version 2.1.227
[CCD] Downloading from https://downloads.claude.ai/claude-code-releases/2.1.227/win32-x64/claude.exe.zst
[CCD] Installed at ...\claude-code\2.1.227\claude.exe
[CCD-autoupdate] Disabled: MSIX install

So 2.1.227 is what the desktop channel served me, and it is current for that channel as far as I can tell. I mention it only because npm dist-tags.latest is 2.1.231 at the time of writing — a different channel, so the numbers aren't directly comparable. If a fix already landed in 2.1.228–2.1.231 and simply hasn't reached the desktop channel yet, please close this as fixed; I'll confirm once the app pulls a newer binary.

Environment

  • Claude desktop app 1.28929.0.0 (MSIX / Windows Store package; [CCD-autoupdate] Disabled: MSIX install)
  • Bundled Claude Code CLI 2.1.227 (previously 2.1.222, downloaded 2 days earlier)
  • Windows 11 Enterprise 26200
  • ~20 long-lived local sessions
  • All 11 sessions that stalled were on claude-opus-5 with effort=high (other sessions on this machine run xhigh; both appear in the logs and the stalls are not specific to either)
  • 11 MCP servers; CLI launched with --include-partial-messages, --replay-user-messages, --permission-prompt-tool stdio, --setting-sources=user,project,local
  • Sessions coordinate via the built-in cross-session messaging

On the regression window — deliberately not overclaiming

There were zero occurrences in the two days before the desktop-app + CLI upgrade (one of those days had 508 healthy cycles under heavy use), and 14 occurrences in the ~24 hours after.

I am not claiming 2.1.227 introduced the bug. A startup-timing change that widened a pre-existing race window fits the data equally well, and I have no way to distinguish those from outside. I'm noting the correlation only as a starting point for whoever bisects this — please don't treat it as an attribution.

Methodology note

All numbers above come from parsing a single main.log programmatically (cycles classified by the [CCD CycleHealth] lines, then joined against Sending message / Resuming session / Mapping internal session events). Happy to share the analysis script, or a redacted log excerpt, if that would help triage. I've kept session IDs and local paths out of this report deliberately, but can provide specifics privately.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗