[Regression, Windows+macOS] Cross-session messaging silently broken since CCD 2.1.224+ — messages dropped, recipient sessions wedged 15-20 min (hadFirstResponse=false); fixed in 2.1.237 for some installs, stable channel still affected

Status Fixed / completed
Reported on v2.1.227
Maintainer reply None cached
Activity 41 comments · opened Aug 12, 2026 · closed Aug 23, 2026

Environment

  • Claude Code Desktop app: 1.28929.0.0 (installed via Microsoft Store / MSIX)
  • Bundled CLI engine ("CCD"): 2.1.227 (%APPDATA%\Claude\claude-code\2.1.227\claude.exe) — one version behind the latest 2.1.228 on the public changelog. Note: [CCD-autoupdate] Disabled: MSIX install in the app's own log — the bundled CLI cannot self-update on an MSIX install; it only advances when the Store ships a new Desktop package.
  • OS: Windows 11 (10.0.26200)
  • Feature: the Desktop app's parallel-sessions panel, backed by remote MCP servers ccd_session_mgmt / ccd_session / ccd_directory (confirmed via the app's own log as [CCD] [replaceRemoteMcpServers] Calling SDK with N total servers) — i.e. mcp__ccd_session_mgmt__send_message for messaging between open sessions, list_sessions/get_session/list_events for status.

Summary

When session A sends a message to session B via send_message, B's next turn frequently produces zero output of any kind — not a single token, not a tool call, nothing — and stays that way until the Desktop app's own idle-timeout watchdog force-kills the query roughly 16–20 minutes later. In the UI this looks like B is "thinking" (spinner + elapsed timer) right up until it flips to a plain "Error" badge with no detail. This breaks the coordinator/worker pattern where several sessions relay results to a "manager" session via send_message.

Root cause, confirmed from the Desktop app's own log

%APPDATA%\Claude\logs\main.log contains the app's internal health monitor for each session's query ("CCD CycleHealth" / "WarmLifecycle"). Two independent affected sessions show the identical signature:

[warn] [CCD] Session <id> timed out after 975~997s of inactivity
  (hadFirstResponse=false, last_message_type=user, last_tool_name=none, seconds_since_stderr=never)
[info] [CCD CycleHealth] unhealthy cycle for <id> (975~1227s, hadFirstResponse=false, reason=no_response)
[info] Session <id> query iterator completed
  • hadFirstResponse=false — the underlying claude.exe process never emitted a single token, thinking-delta, or tool call for the entire query.
  • last_message_type=user — the last thing appended to the conversation before the hang was the injected <cross-session-message> (delivered as a user-role turn).
  • seconds_since_stderr=never — the CLI process did not crash or write to stderr; it simply never responded to the query at all.
  • The app's watchdog only detects this after ~975–1227s (16–20 min) of total inactivity, then force-ends the query (query iterator completed) — this is what surfaces as the "Error" badge; there is no earlier or more specific error surfaced to the user.

Clean within-session A/B (session local_08fe6fef, a fresh "test1" session)

The same session shows a healthy cycle immediately before the hang, isolating the trigger to the cross-session message itself rather than the session/model/effort combination:

13:05:52 [info] [CCD CycleHealth] healthy cycle for local_08fe6fef... (12s, hadFirstResponse=true)   <- its own reply to a plain prompt
...
13:23:06 [warn] [CCD] Session local_08fe6fef... timed out after 975s of inactivity (hadFirstResponse=false, last_message_type=user, last_tool_name=none, seconds_since_stderr=never)
13:23:06 [info] [CCD CycleHealth] unhealthy cycle for local_08fe6fef... (975s, hadFirstResponse=false, reason=no_response)

Between these two cycles, the only thing that happened was: another session called send_message to deliver one short (<50 char) test message.

What we ruled out before finding the log evidence

  • Context size / session age: the hung test1 session had only 2 prior turns and 9% context-window usage (88.2k / 967k tokens) at the time of the hang — ruled out by the in-app context panel.
  • Message complexity: the test message was a single trivial sentence.
  • This specific session/model being generally slow: ruled out by the within-session A/B above — the same session answered normally 12 seconds earlier.
  • Message delivery itself failing: the message content is visibly injected into the recipient's transcript before the hang begins (confirmed via list_events) — delivery succeeds; it's the resulting query that never responds.

Reproduction count

5 independent hits on 2026-08-12: 3 freshly-created "worker" sessions crashed the first time a "manager" session dispatched an initial task to them via send_message (forcing the user to fall back to manually opening windows and pasting task text instead); the "manager" session itself later hit the identical hang receiving two worker reports; and the isolated test1 repro above.

Notable gap in status-reporting surface

While a session is in this stuck state, mcp__ccd_session_mgmt__get_session keeps reporting isRunning: true with a frozen lastActivityAt — there is no field visible through the session-management MCP tools that distinguishes "genuinely still reasoning" from "this query is dead." We could only tell by polling get_session twice ~60–90s apart and checking whether lastActivityAt had advanced, or (once we found it) by grepping the Desktop app's own log for CycleHealth/no_response. Surfacing hadFirstResponse/reason=no_response (or the elapsed-inactivity figure) through get_session would make this diagnosable without log access.

Distinct from a separate, already-understood behavior

Sending a message to a session where isRunning: false (fully idle, not mid-query) just queues into a mailbox — lastActivityAt updates to the send time but no query starts until the user manually opens that session's window. That's a different, milder issue than the one reported here, which occurs even when the recipient is actively running / has just been manually woken and picks the message up on its own.

Impact

Breaks the core multi-session coordination workflow (a "manager" session dispatching work to, and receiving reports from, several "worker" sessions via send_message), forcing a fallback to fully manual relaying between windows.

Possibly related (not duplicates — different subsystem/trigger)

  • #85265 — async agent stall watchdog kills healthy long-running background subagents at a flat 600s (CLAUDE_ASYNC_AGENT_STALL_TIMEOUT_MS). Same family (a watchdog force-ending a query that produced no stream chunk), but that one is about Task-tool subagents inside a single session, fires at 600s, and the author shows the killed work was actually still alive (resumable). Ours is the Desktop CCD session-level watchdog (975–1227s), triggered specifically by cross-session message injection, with seconds_since_stderr=never and no independent evidence the query was making progress.
  • #84494 — SendMessage to CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS teammates has batching/delay/mis-routing defects. Different feature (in-process agent teams vs. Desktop's cross-window parallel sessions) but same broad area of cross-agent messaging reliability.

Questions

  1. Is this a known issue with the ccd_session_mgmt remote MCP server / cross-session message injection path specifically (as opposed to the native single-machine SendMessage tool documented in the CLI changelog)?
  2. What is actually happening inside the query during those 16–20 minutes of zero output — is it stuck before the first API call is even issued, or is a request in flight that never resolves?
  3. Could hadFirstResponse/reason=no_response (already computed internally, per the log) be surfaced through get_session so this is diagnosable without log access?
  4. Is there a recommended workaround short of avoiding send_message-based dispatch entirely?

Happy to provide full log excerpts / session IDs / more timestamps if useful.

View original on GitHub ↗

41 Comments

Williaman000 · 19 days ago

Same regression here, same Desktop/CCD build, same signature. Adding what I think is missing from the thread: the version boundary and a before/after count from one continuous log, which should pin the introducing version.

The 2.1.222 → 2.1.227 boundary is visible inside a single main.log

The MSIX auto-update landed while the app was running:

09:48:26 [info] [CCD] Initialized with version 2.1.222
09:48:27 [info] [updater] MSIX detected: ... exe=...\Claude_1.26832.0.0_x64__...\app\Claude.exe
09:51:06 [info] [CCD] Initialized with version 2.1.227
09:51:07 [info] [updater] MSIX detected: ... exe=...\Claude_1.28929.0.0_x64__...\app\claude.exe

Before/after, same log file (covers 2026-08-09 21:56 → 2026-08-12 14:36 KST)

| | before 09:51 (2.1.222) | after 09:51 (2.1.227) |
|---|---|---|
| Sending message to session dispatches | 821 | 42 |
| healthy cycles (hadFirstResponse=true) | 801 | — |
| no-response deaths (deduped) | 0 | 17 |

  • First death at 10:14:08 — 23 minutes after the runtime swap. Nothing comparable in the preceding ~2.5 days, during which this workflow relayed hundreds of cross-session messages per day (470 dispatches on 08-11 alone, zero hangs).
  • 9 distinct sessions hit, every one a cross-session recipient. Sessions driven by typed input never died — including the very same sessions minutes before and after.
  • Deterministic per batch: dispatches at 12:41:07–12:41:12 → four deaths at 12:57:27; dispatches at 14:02:48–14:03:58 → four deaths at 14:19–14:20.
WarmBed · 19 days ago

Independent confirmation from a second machine, same version boundary — and I pushed it back further using this machine's rotated main*.log history, which happens to cover 5 consecutive CCD releases:

| CCD version | Window (this machine) | Dispatches (Sending message to session) | reason=no_response deaths |
|---|---|---:|---:|
| 2.1.217 | 07-23 → 07-27 (~4d) | 1,290 | 0 |
| 2.1.217→2.1.219 | 07-27 → 08-02 (~6d) | 612 | 0 |
| 2.1.219 | 08-02 → 08-07 (~4.3d) | 1,515 | 0 |
| 2.1.221 | 08-07 → 08-09 (~2d) | 611 | 0 |
| 2.1.222 | 08-09 → 08-12 08:32 (~3.3d) | 1,128 | 0 |
| 2.1.227 | 08-12 08:32:13 → now | 45 | 16 (35.6%) |

Over 5,156 cross-session dispatches across 5 releases and ~18 days, zero deaths — then this machine's own MSIX auto-update landed ([CCD] Initialized with version 2.1.2222.1.227 at 08:32:13, same live-swap-while-running pattern @Williaman000 described), and the very next batch of dispatches started dying. Failure rate here (35.6%) is in the same range as theirs (~40%). First death on this machine was also within the first half hour after the swap.

This lines up with everything in that comment: only cross-session recipients die, deaths cluster right after a dispatch batch, and the same-session-before/after control (this machine's coordinator sessions were fine on typed input right before and after the hangs). Two independent machines landing on the identical 2.1.222→2.1.227 boundary with a comparable ~35-40% failure rate is a pretty strong signal for where to start bisecting.

Also worth someone with access re-checking versions between 2.1.222 and 2.1.227 if telemetry/staged-rollout data exists — neither of us has a machine that sat on 2.1.223–2.1.226 long enough to narrow it further than "somewhere in this 5-version gap."

WarmBed · 19 days ago

One more observation from this machine, flagged as a possible related mechanism rather than a confirmed one:

Several hours after some of these no_response deaths, list_sessions/get_session reports the affected sessions as isRunning: false — but Get-CimInstance Win32_Process -Filter "Name='claude.exe'" still shows their underlying child processes alive, with CreationDate lining up against those sessions' earlier activity. Command lines are ordinary interactive sessions (--resume=<uuid>, no scheduled/background-task fingerprint), not something expected to linger.

If the same code path that fails to produce a response after a cross-session message is also failing to clean up the process on abort/timeout, that seems worth checking together rather than as two unrelated bugs. It would also explain a separate complaint (mine and apparently not unique to me): Desktop app updates on MSIX installs sometimes only "take" after a full reboot — [CCD-autoupdate] Disabled: MSIX install means the bundled CLI can only be replaced by the Store's file-level update, and Windows can't swap files still held open by orphaned claude-code\<ver>\claude.exe processes, so the update gets deferred to next boot.

Not asserting causation here, just noting the two symptoms (hung query, and process not torn down) show up on the same machine around the same failures, in case it's useful for narrowing down where in the abort path this goes wrong. Happy to share the Get-CimInstance output / specific PIDs if useful.

miszterekvicky · 18 days ago

Independent reproduction on macOS — same app + bundle versions, so this is not MSIX- or Windows-specific.

Environment

  • Claude Desktop 1.28929.0, bundled CCD engine 2.1.227 (~/Library/Application Support/Claude/claude-code/2.1.227/)
  • macOS 26.2 (build 25C56), Apple Silicon
  • Message sent between two open desktop sessions via mcp__ccd_session_mgmt__send_message

Timeline from ~/Library/Logs/Claude/main.log

02:02:39 [info] [Stop hook] Query completed for session local_795f4bab-…
02:02:39 [info] [CCD CycleHealth] healthy cycle for local_795f4bab-… (199s, hadFirstResponse=true)
02:03:28 [info] Sending message to session local_795f4bab-…
02:18:33 [info] [WarmLifecycle:session] Starting idle timeout for local_795f4bab-…: 900s
02:18:33 [info] [WarmLifecycle:preview] Starting idle timeout for local_795f4bab-…: 1800s
02:20:19 [info] [CCD CycleHealth] unhealthy cycle for local_795f4bab-… (1011s, hadFirstResponse=false, reason=no_response)

Zero tokens, zero tool calls, zero transcript writes during the entire 1011s window (verified: no *.jsonl under ~/.claude/projects/ gained a single record, and the runner process sat at 0% CPU with no children beyond the usual MCP sidecars).

Details that may help narrow it down

  1. The message landed 49 seconds after the recipient's previous turn completed (Stop hook 02:02:39 → send 02:03:28). Ordinary turns in the same session both before (01:59:20, 199s, healthy, with tool permission prompts answered) and hours after (11:57:18, 92s, healthy) worked fine — the zombie is specific to the message-triggered wake shortly after a turn ends, which smells like a race with session teardown / warm-recycle.
  2. The delivered message exists only in the app's own conversation store. It renders in the conversation (wrapped as <cross-session-message from="local_…" name="…" encoded="1">) and list_events shows it, but its text is not findable in any transcript .jsonl on disk — the recipient's transcript gained zero records from delivery through the reap.
  3. The sender gets a success result and never any failure signal. send_message returned "Message sent to session …", and nothing was ever reported back when the recipient's cycle was reaped 17 minutes later. During the whole window the recipient showed isRunning: true in list_sessions, which makes the zombie indistinguishable from real work for any coordinator/orchestration pattern — we only caught it because we cross-checked CycleHealth in main.log.
  4. The WarmLifecycle idle timers armed at send+905s (02:18:33), i.e. the 900s session idle timeout appears to start counting only after a first ~900s window expires, and the unhealthy-cycle reap followed ~106s later.

Happy to provide fuller logs on request.

WarmBed · 18 days ago

Windows regression window confirmed by rollback: works on Desktop 1.25927.0 (CCD engine 2.1.221) → broken on 1.28929.0

We rolled back and ran a controlled bidirectional test — the bug disappears entirely.

Environment

  • Windows 11 Home 10.0.26200
  • Claude Desktop 1.25927.0 (released 2026-08-04, manually rolled back via the official installer), CCD engine 2.1.221
  • Before rollback: 1.28929.0, where we hit the zombie on 4+ independent recipient sessions the same morning (exact signature from this issue: hadFirstResponse=false, reaped at 975–1227s with reason=no_response, seconds_since_stderr=never, sender sees success, recipient shows isRunning: true throughout)

Bidirectional test on 1.25927.0 (2026-08-12, %APPDATA%\Claude\logs\main.log)

Session A sent a test message to two idle sessions (both isRunning: false), asking each to reply via send_message:

18:52:54 [info] Sending message to session local_2c5636e6-… (idle recipient #1)
18:52:56 [info] Sending message to session local_5168e312-… (idle recipient #2)
18:53:01 [info] Sending message to session local_202bdffd-… (reply from #1)
18:53:08 [info] [CCD CycleHealth] healthy cycle for local_2c5636e6-… (14s, hadFirstResponse=true)
18:53:08 [info] Sending message to session local_202bdffd-… (reply from #2)
18:53:12 [info] [CCD CycleHealth] healthy cycle for local_5168e312-… (16s, hadFirstResponse=true)

Both idle recipients woke on delivery, processed the message in 14–16s, and replied; the original sender then received and processed both replies normally — the exact wake-on-message path that zombied on 1.28929.0. Since the rollback restart, main.log shows every cycle healthy and zero unhealthy / timed out / SESSION ERROR entries.

Possibly the same regression, second symptom

On 1.28929.0 we also repeatedly observed a milder failure mode: send_message to an idle (isRunning: false) session updates its lastActivityAt but never starts its process — the message sits unread until the user manually focuses that window (3/3 fresh sessions, waits of 19–60 min with zero activity, zero process spawns). On 1.25927.0, wake-on-message works (see above). So "doesn't wake idle sessions" and "wakes but zombies with hadFirstResponse=false" may be two symptoms of the same delivery-path regression between these versions.

Warning for anyone using rollback as a workaround

Squirrel auto-update silently re-downloads 1.28929.0 and re-rigs the execution stub within a minute of first launching the rolled-back build (Squirrel-Update.log: Squirrel Enabled Apps: [...app-1.28929.0\claude.exe]). The next app restart silently puts you back on the broken version — the rollback only holds for the current process lifetime unless auto-update is blocked.

Official installer for the last known-good version (from claude.ai downloads): https://downloads.claude.ai/releases/win32/x64/1.25927.0/Claude-003700efafbc2ccb4b1177a5e637b14da381799e.exe

wshallwshall · 18 days ago

Another Windows data point, with app-side log evidence, exact send-to-onset arithmetic on three separate cases, and one instance where I know who sent the message because I sent it myself.

Environment

  • Claude Desktop 1.28929.0.0 (MSIX)
  • Bundled CLI 2.1.227, at %APPDATA%\Claude\claude-code\2.1.227\claude.exe (only version present; the app picks it at startup)
  • Windows 11 10.0.26200
  • Six concurrent local sessions. Three were hit; three were not.

Not reliably reproducible on demand. I cannot give steps that trigger it. What I can give is a rate: nine watchdog events in 8.4 hours on one machine under normal multi-session use, three of them within 90 minutes. It is frequent enough to catch with a log tail, and it names three of six sessions with zero events on the other three.

The nine events

2026-08-11 23:54:31 [info] [CCD CycleHealth] unhealthy cycle for SESSION_A (1482s, hadFirstResponse=false, reason=no_response)
2026-08-12 01:42:33 [info] [CCD CycleHealth] unhealthy cycle for SESSION_A (1019s, hadFirstResponse=false, reason=no_response)
2026-08-12 03:27:34 [info] [CCD CycleHealth] unhealthy cycle for SESSION_A (978s, hadFirstResponse=false, reason=no_response)
2026-08-12 06:31:32 [info] [CCD CycleHealth] unhealthy cycle for SESSION_B (2306s, hadFirstResponse=false, reason=no_response)
2026-08-12 06:31:32 [info] [CCD CycleHealth] unhealthy cycle for SESSION_A (986s, hadFirstResponse=false, reason=no_response)
2026-08-12 06:43:32 [info] [CCD CycleHealth] unhealthy cycle for SESSION_C (1030s, hadFirstResponse=false, reason=no_response)
2026-08-12 07:33:33 [info] [CCD CycleHealth] unhealthy cycle for SESSION_B (1010s, hadFirstResponse=false, reason=no_response)
2026-08-12 07:35:33 [info] [CCD CycleHealth] unhealthy cycle for SESSION_A (1006s, hadFirstResponse=false, reason=no_response)
2026-08-12 07:58:33 [info] [CCD CycleHealth] unhealthy cycle for SESSION_C (992s, hadFirstResponse=false, reason=no_response)
2026-08-12 07:33:33 [warn] [CCD] Session SESSION_B timed out after 1010s of inactivity (hadFirstResponse=false, last_message_type=user, last_tool_name=none, seconds_since_stderr=never)
2026-08-12 07:35:33 [warn] [CCD] Session SESSION_A timed out after 1006s of inactivity (hadFirstResponse=false, last_message_type=user, last_tool_name=none, seconds_since_stderr=never)
2026-08-12 07:58:33 [warn] [CCD] Session SESSION_C timed out after 992s of inactivity (hadFirstResponse=false, last_message_type=user, last_tool_name=none, seconds_since_stderr=never)

The onset is the send, to the second

The cycle duration equals the reported inactivity in each case, so the cycle began at the send. Subtracting gives the send timestamp exactly:

07:16:43  Sending message to session SESSION_B     ->  07:33:33 - 1010s = 07:16:43
07:18:47  Sending message to session SESSION_A     ->  07:35:33 - 1006s = 07:18:47
07:42:01  Sending message to session SESSION_C     ->  07:58:33 -  992s = 07:42:01

This is not a session degrading over time. SESSION_A was answering normally 52 seconds before the message that silenced it:

2026-08-12 07:14:39 [info] [CCD CycleHealth] healthy cycle for SESSION_A (1183s, hadFirstResponse=true)
2026-08-12 07:15:35 [info] [CCD CycleHealth] healthy cycle for SESSION_A (57s, hadFirstResponse=true)
2026-08-12 07:17:55 [info] [CCD CycleHealth] healthy cycle for SESSION_A (47s, hadFirstResponse=true)
2026-08-12 07:18:47 [info] Sending message to session SESSION_A     <- stalls here

A working session takes one specific message and goes permanently silent on it.

The 07:42:01 send was mine, and it was ordinary

The third case above is the one I can speak to directly: that send was an MCP cross-session send_message issued by another local session on the same machine, carrying a short plain-text status note. Benign content, no attachments, recipient idle at the time. It stalled the recipient for 992 seconds. The same recipient took 18 other sends across the same log without stalling, so the trigger is intermittent rather than deterministic.

A partial reap, and the escape hatch that saves the sessions that survive

This is the part I have not seen mentioned in this issue, and it may be the most actionable. The unaffected sessions are not a healthy population. They hit the same condition and escape it by one of two routes. Caught live on a session that did not hang:

2026-08-12 07:45:45 [warn] [LocalSessionManager] reaped 1 stale pendingEchoUuids for SESSION_F - CLI dropped the isReplay echo { reaped: [ 'UUID' ], remaining: [ 'UUID' ], remainingCount: 1 }
2026-08-12 08:00:45 [info] [LocalSessionManager] send bypassed defer - CLI at turn boundary while isRunning held for SESSION_F { pendingEchoUuids: [ 'UUID' ] }

The reaper ran and left one uuid behind: remainingCount: 1, not 0. Fifteen minutes later the next send to that session would have been deferred behind the still-held isRunning, and it was not, only because send bypassed defer fired. Note the condition on that bypass: CLI at turn boundary. A session sitting mid-turn when the send arrives has neither a completed reap nor the bypass, and I believe that is the population that hangs.

Corroborating, on two of the earlier stalls the held echo is never reaped at all, and the cycle-duration arithmetic points at the hold rather than at the user message:

  • SESSION_B held at 05:53:06, cycle 2306s ending 06:31:32. 06:31:32 minus 2306s = 05:53:06.
  • SESSION_C held at 06:26:22, cycle 1030s ending 06:43:32. 06:43:32 minus 1030s = 06:26:22.

Against my own hypothesis, and stated plainly: the 07:33 and 07:35 stalls had no outstanding held echo at onset. SESSION_A's queue had been reaped to remainingCount: 0 at 07:14:39. So a held echo is sufficient in some cases but is not necessary, and something else reaches the same end state.

User-visible impact: the message is silently lost

The UI surfaces this as an orange warning triangle on the session, tooltip "Claude Code stopped responding", and an in-session banner reading "Claude Code stopped responding / Try sending your message again". That wording is accurate and worth acting on: the message is not retried, and in at least one instance I confirmed it produced zero records in that session's CLI transcript, so it never reached the CLI at all and leaves no artifact on either side. From the user's perspective a worker session sits idle for roughly 17 minutes looking busy, then quietly resumes as though nothing was sent.

Other observations that may narrow it

  1. query iterator completed lands at the watchdog, not at stall onset, so the child's output stream stayed open through the entire silence.
  2. seconds_since_stderr=never in all nine events. That field has never emitted a numeric value anywhere in five log files on this machine, so I would not read it as evidence by itself.
  3. CLI dropped the isReplay echo appears about 3 times across 17 days of pre-update logs and 18 times in 8 post-update hours. It exists in both builds, so unlike reason=no_response it is a valid cross-build comparison, and the rate change is large.
  4. send bypassed defer is new in 1.28929.0 and fired only on sessions that did not hang.
  5. Six of seven message-triggered onsets were sends with no preceding LocalSessions.sendMessage line, i.e. the cross-session/MCP shape. One was a normal UI send, so the UI path is not immune.
  6. The affected sessions' CLI children are respawned with --resume, but only later and only on user focus, 14s to 7m31s after the give-up. --resume is the recovery, not a crash marker.
  7. No WER report, crash dump, Crashpad report, Sentry event or Application Error row names claude.exe. Note this is narrow: WER records unhandled-exception faults only and CrashDumps is not armed for the binary here, so process death in general is not excluded, only an OS-recorded crash.
  8. Not usage limits: zero session limit / usage limit / rate limit text and zero reason=api_error in the current log, against 44 to 73 limit hits and 52 to 93 api_error hits in each of the four pre-update logs on the same machine. The limit code path is well exercised here and none of it fired.

Session identifiers above are pseudonyms, consistent within this comment. Happy to supply further pseudonymized line families from main.log on request; I cannot share the raw log, as it embeds absolute paths and private repository names.

wshallwshall · 18 days ago

Knowing when the fix lands
Auto-update applies within roughly four hours of publication, so no action is needed — but if you want to watch for it, anything above 1.28929.0 here is the new build:

curl -s https://downloads.claude.ai/releases/win32/x64/RELEASES

WarmBed · 18 days ago

Root cause: delivery succeeds — the recipient CLI discards the message before it is ever enqueued, and never tells Desktop.

We chased this end-to-end on Windows and can now describe the mechanism. Short version: the send side is fine. The message does reach the recipient CLI. The recipient then classifies it as the new native cross-session peer message; that feature is disabled on Windows, so it refuses and drops the message before anything is enqueued. Because the refusal is never surfaced back to Desktop, Desktop keeps waiting on a query that will never produce a token — until the watchdog gives up 975–1227s later with hadFirstResponse=false, reason=no_response.

The recipient CLI says so itself, in its own debug output:

[cross-session-inbound] refused inbound peer message — cross-session messaging disabled (kill switch) (remote-io: dropped before attachment materialization)

dropped before attachment materialization accounts for every missing signal in the watchdog line: no prompt enqueued, no turn started, no transcript append, no /v1/messages, no stdout, no stderr. It also explains the two other symptoms in this thread — the message being visible in the app's conversation store but never appearing in the recipient's CLI transcript, and idle recipients that appear never to wake up.

Version boundary. Feeding the same Desktop-shaped input to two official CLI builds, in an isolated offline environment (separate config dir, endpoint pointed at a closed local port, so no production traffic and no quota used):

  • 2.1.221 — accepted; reaches [API REQUEST] /v1/messages source=sdk
  • 2.1.227 — refused as above; zero API requests, 0 bytes stdout, exits 0 in ~1.5s

That matches the app-level boundary several of us independently reported: Desktop 1.25927.0 works, 1.28929.0 does not.

User settings don't help. The inbound setting appears to be consulted after the platform availability check, so on Windows it can't override it. Verified with the setting applied and the app fully restarted — still hangs.

Scope note. The kill-switch branch above is Windows. The macOS reproduction in this thread may take a different branch of the same gate (e.g. held rather than refused); we haven't been able to confirm that directly.

What we think needs to change, in priority order:

  1. Don't classify the legacy Desktop ccd_session_mgmt injection as a native peer message. It's a separate, pre-existing transport that worked fine before the native feature landed, and the Windows kill switch for the native feature shouldn't apply to it.
  2. A refused or held inbound message must produce a lifecycle event back to Desktop instead of a silent continue. Today the failure is indistinguishable from "the model is thinking" for 16–20 minutes, and the message is then silently lost with no retry and no artifact on either side.
  3. Ideally send_message would distinguish "written to stdin" / "accepted by recipient" / "recipient started a turn", so the sender doesn't get a false success while the recipient is already dead.

As far as we can tell there is nothing we can do about this from our side — the drop happens inside the recipient CLI, before anything we control — so we're hoping it can be addressed in an upcoming release. Happy to provide more detail if it would help.

nutballs · 18 days ago

WORKING User Side Fix - MacOS

Claude fixing claude is making my head hurt... This is claude's findings. confirmed by me.

macOS takes a different branch than the Windows kill switch — and on macOS there is a working user-side fix: "crossSessionInbound": "accept".

@WarmBed's scope note asked whether the macOS reproduction takes a different branch of the same gate. It does. Verified on macOS by reading the gate in the shipped 2.1.227 binary and then confirming the fix end-to-end with a controlled test.

Environment

  • Claude Desktop 1.28929.0, bundled CCD engine 2.1.227 (~/Library/Application Support/Claude/claude-code/2.1.227/)
  • macOS 26 / Darwin 25.5.0, Apple Silicon. Two local non-remote desktop sessions, both bypassPermissions.
  • 3/3 send_message sends dropped before the fix; 1/1 delivered after.

The macOS branch is hold, not refuse

The receiver-side gate (WXp in the 2.1.227 binary) ends with:

let o = yva(n),  i = Moi() ? e?.fromMode : void 0;
if (i !== void 0)
  return i === o ? {policy:"accept", holdCause:"bypass-default"}
                 : {policy:"hold",   holdCause:"mode-mismatch"};
return o === "bypass" ? {policy:"hold",   holdCause:"no-mode-asserted"}
                      : {policy:"accept", holdCause:"bypass-default"};

yva/gva map bypassPermissions (and plan-with-bypass-available) to "bypass". So a receiver in bypass mode holds any inbound cross-session message whose sender did not attest its permission mode. The binary's own user-facing string for this cause:

The sender did not attest its permission mode, and this session bypasses permission prompts.

This is directly observable in the two message wrappers. The native peer channel attests; the Desktop ccd_session_mgmt injection does not:

| Channel | Wrapper | Gate result |
|---|---|---|
| native SendMessage (UDS) | from="uds:/tmp/cc-socks/…" from-name="Test1" from-mode="bypass" | accepted |
| ccd_session_mgmt send_message | from="local_561c4b1d-…" name="Test1" encoded="1"no from-mode | held |

That explains the otherwise confusing result that on the same machine, same core, same target session, one channel works and the other silently drops.

Held messages are parked in a buffer (KXp case "hold"), evicted oldest-first when it fills, and settled as expired on shutdown — never retried, no artifact on either side. Confirmed here: messages held before the fix were not released or replayed afterwards, they were simply gone.

Windows differs because DS() short-circuits first:

function DS(){ if (Yt()==="windows") return !1; return rt("tengu_harbor_kite",!1) || Boolean(re.CLAUDE_CODE_HARBOR_KITE) }

zXp() turns that false into {policy:"refuse", refuseCause:"kill-switch"} before the settings lookup — which is exactly why @WarmBed found user settings can't override it on Windows. On macOS DS() is true (demonstrated by the native peer channel working), so the setting is consulted and wins.

The fix (macOS)

// ~/.claude/settings.json
{ "crossSessionInbound": "accept" }

Valid values are accept | hold | refuse. In WXp the explicit setting is read immediately after the kill-switch check and before all mode logic, so it overrides the no-mode-asserted hold.

Verified end-to-end. Same target session, before and after, watching the target's transcript for the queue-operation/enqueue record that marks real delivery:

| | Before setting | After setting |
|---|---|---|
| ccd send_message | 3/3 dropped, no enqueue record ever | delivered, enqueue at 03:34:22.022Z |
| Reply from target | none | token echoed within ~15s |

It took effect without restarting the app. Note the precedence rules in qXp(): policySettings/flagSettings/userSettings are checked first, then localSettings/projectSettings may only tighten (Loi={accept:0,hold:1,refuse:2}), so a repo setting of hold will override a user accept.

Diagnosing this without log access

A real delivery always writes a queue-operation/enqueue pair to the recipient's transcript at ~/.claude/projects/<encoded-cwd>/<uuid>.jsonl, followed by a user turn. A held message writes nothing at all. get_session and list_events are not reliable here — they read the app's session store, so they will happily show isRunning: true and list a message the CLI never received. The transcript is the only ground truth.

What this suggests about the fix

The hold itself looks intentional — it's a security gate, and holding an unattested message aimed at a session that bypasses permission prompts is defensible. Two things seem wrong regardless of that:

  1. The legacy Desktop injection has no way to attest. It carries no from-mode, so under the current gate it can never satisfy a bypass-mode receiver, and the default configuration makes that the common case. Either the coordinator/task-notification origin should be exempt (it is a pre-existing transport, distinct from the native peer feature), or Desktop should stamp the sending session's mode.
  2. The hold is invisible. KXp calls onPeerHeld with a review dialog payload (dialogBody, preview, claimedName, peerPid), and the binary contains the string "It is being reviewed before delivery." — but no dialog, banner, or notification ever appeared in Desktop on this machine across 3 held messages. If that review UI surfaced, this would be a visible "approve this message?" prompt rather than a 16–20 minute silent hang. That may be the same defect as #85714.

Also, send_message returning success for a message the recipient held or refused is what makes this undetectable from the sending side — agreed with @WarmBed's point 3 that the transport should distinguish written / accepted / turn-started.

Minor triage note

#86014 is the same regression, labeled platform:windows only but confirmed on macOS there and here; it also postdates this issue by ~7 minutes, so it is probably the duplicate.

007ArunSharma · 17 days ago

Windows confirmation with two additional data points not yet in this thread. (Our report #86088 is labeled a probable duplicate of this issue; consolidated evidence here.)

Environment: Windows 11 Pro 10.0.26200, Desktop 1.28929.0, engine auto-updated 2.1.222 → 2.1.227 on 2026-08-12 ~05:55 UTC. Before: 103 session spawns over 3 days, zero failures, heavy daily cross-session send_message use. After: 6/6 inbound cross-session deliveries to idle sessions hung with zero recipient output and were watchdog-killed at 966–1010s (hadFirstResponse=false, last_message_type=user, last_tool_name=none, seconds_since_stderr=never). Timeline and mechanism match @WarmBed's kill-switch analysis exactly.

New observation 1 — a stuck inbound message re-bricks the session after recovery. A session that had a pending (refused/hung, then watchdog-killed) inbound message was later focused by the user, who typed a short nudge. That user-typed turn ran normally (healthy cycle, tool calls, visible output). ~3 minutes later — with no further user input — a new cycle began with last_message_type=user, last_tool_name=none, produced nothing, and was killed at 991s. This is consistent with the still-pending peer message re-attempting delivery and being refused again: one undeliverable message serially kills an otherwise healthy session. The pending message is never written to the recipient's transcript .jsonl, so it can neither be processed nor cleared from the recipient side.

New observation 2 — delivery into an actively-running focused session succeeds on 2.1.227. Same day, same sender tool: a cross-session message delivered while the recipient session happened to be mid-conversation (user actively focused in its window) ran a healthy 218s cycle. Deliveries to idle/background sessions: 6/6 killed. This supports the two-transport theory — live sessions appear to receive the message via the pre-existing injection path, while idle-session wakes route through the gated native peer path, which DS() kill-switches on Windows before crossSessionInbound is consulted (so no user-side override exists on Windows, as @WarmBed verified).

Impact: all multi-session coordination on Windows is dead since the 2.1.227 rollout; the only workaround is the user manually babysitting each recipient session, and per observation 1 even that can be undone by the pending message re-delivering.

Asks:

  1. Exempt the legacy Desktop ccd_session_mgmt injection from the native-feature kill switch (it is a distinct, pre-existing transport), or have Desktop stamp the sender's mode so the gate can evaluate it.
  2. Interim: a Windows-reachable override (env var or flag checked before the platform short-circuit), or point Desktop back to the stable engine — the npm stable dist-tag is still 2.1.222 while Desktop bundles 2.1.227.
  3. Make send_message distinguish written / accepted-by-recipient / turn-started so senders stop getting false success, and clear or surface pending-but-refused messages instead of retrying them into every future cycle.
WarmBed · 17 days ago

@007ArunSharma Observation 2 matches what we see, but our data narrows the boundary in a way that matters for anyone trying to use it as a workaround: isRunning: true is not sufficient — the recipient has to be genuinely mid-turn.

Our clearest case, Windows, Desktop 1.28929.0 / engine 2.1.227:

12:41:43  [Stop hook] Query completed for session <coordinator>
12:41:43  [WarmLifecycle:session] Starting idle timeout for <coordinator>: 900s
12:47:39  Sending message to session <coordinator>        <- inbound cross-session message
13:08:06  [CCD] Session <coordinator> timed out after 997s of inactivity
          (hadFirstResponse=false, last_message_type=user, last_tool_name=none, seconds_since_stderr=never)
13:08:06  [CCD CycleHealth] unhealthy cycle for <coordinator> (1227s, hadFirstResponse=false, reason=no_response)

That session was warm and get_session reported isRunning: true throughout — it had finished a turn 6 minutes earlier and was sitting inside its 900s idle window, not yet paused. It still died. So the passing case in your observation 2 is specifically actively mid-turn, not merely "the process is alive" or "the app shows it as running". The warm-but-between-turns state behaves like idle.

That distinction is easy to get wrong from the outside, because the two states are indistinguishable through the tools a sender has: get_session returns isRunning: true for both, and list_events will happily show the message in the app's conversation store in both cases. As @nutballs noted, the recipient's transcript is the only ground truth — a real delivery writes queue-operation/enqueue to ~/.claude/projects/<encoded-cwd>/<uuid>.jsonl, a gated one writes nothing.

Two smaller points from our side:

On your observation 1 (pending message re-killing the session). We saw the same shape from the other end. Three cross-session messages were sent to one test session across ~20 minutes; its transcript stayed byte-identical the whole time, and on the next app restart the engine logged Loaded 2 transcript messages — the two turns that predated the first cross-session message. None of the three ever became a transcript record, so there is nothing on the recipient side that could acknowledge or discard them. That is consistent with a pending item that can only be retried, never settled.

On the settings override. We tested "crossSessionInbound": "accept" on Windows twice — once with the engine respawned after the setting was written, and once after a full Desktop quit and relaunch — and it changed nothing, which lines up with @nutballs' reading that DS() short-circuits before the setting is consulted on this platform. Worth stating explicitly so Windows users don't spend time on it: the macOS fix in that comment does not port.

Re your ask 2 (point Desktop back to the stable engine): that matches what we found by rollback — Desktop 1.25927.0 (engine 2.1.221) is completely clean here, 1,128 cross-session dispatches over the preceding two days with zero watchdog events, versus 16/45 killed in the first hours after the update.

wshallwshall · 17 days ago

Status update from the Windows reporter above, plus a retraction of my own earlier hypothesis and a methodological warning for anyone rolling back.

1. Retraction: my echo/defer mechanism was wrong

In my earlier comment I proposed that the stall came from the app's input-echo/defer bookkeeping — a pendingEchoUuid left unreaped (remainingCount: 1) holding isRunning, with send bypassed defer as the escape that saved unaffected sessions. It matched two stalls to the second, and it is not the cause. @WarmBed's kill-switch analysis is.

The cleanest disproof is from my own rollback window: isRunning held by unechoed input fired 100 times in 24 hours with zero stalls. A condition that occurs a hundred times without ever producing the failure is not causal. Please disregard that part of my earlier comment; it will only send triage sideways.

2. Rollback status: 24 hours clean, with delivery positively confirmed

Rolled back to Desktop 1.25927.0 / engine 2.1.221, blocking auto-update. Same machine, same six-session multi-session workload.

| | before: 1.28929.0 / 2.1.227 | after: 1.25927.0 / 2.1.221 |
|---|---|---|
| window | 15.5 h | 24.0 h (08-12 10:24:19 to 08-13 10:23:06) |
| Sending message to session | 122 | 509 |
| cross-session messages verified delivered | — | 420, across 21 recipient transcripts |
| healthy cycle for | 75 | 424 |
| unhealthy cycle for (any reason) | 9 | 0 |
| reason=no_response | 9 | 0 (see section 3 — not meaningful) |
| dropped the isReplay echo | 20 | 0 |
| isRunning held by unechoed input | 22 | 100 |

The 420 is measured with @nutballs' ground-truth method rather than from app logs: a genuine delivery appears in the recipient's ~/.claude/projects/<encoded-cwd>/<uuid>.jsonl as a user record whose content is the <cross-session-message from=…> wrapper. I parsed the JSONL and counted only those, explicitly excluding 8 occurrences that appear inside tool_result blocks — those are one session quoting another session's transcript, and a naive substring grep counts them as deliveries. That inflated my first pass from 420 to 950, so it is worth flagging for anyone else measuring this way.

This corroborates @WarmBed's rollback result (1,128 dispatches clean) on an independent machine.

3. Warning: rolling back removes the detector, so a clean log is not evidence

This matters for the several people in this thread now rolling back and reporting all-clear.

reason=no_response and [CCD] … timed out after Ns of inactivity are new code in 1.28929.0. Measured across my four rotated pre-update logs — covering 1.24012.9, 1.24012.11, 1.25927.0 and 1.26832.0reason=no_response occurs 0 times, and the only reason values that ever appear historically are api_error (60) and app_quit (2). On a rolled-back build that string cannot be emitted, so its absence carries no information whatsoever.

What does survive the rollback, and is therefore a valid cross-build instrument:

  • [CCD CycleHealth] itself predates 1.28929.0 and is live on the older builds — it appears 1,114 to 2,706 times per rotated log, and unhealthy cycle for genuinely fires there (10, 12, 20 and 20 times respectively, including in the log covering 1.25927.0). So unhealthy cycle for = 0 after rollback is a live instrument reporting zero, not a missing one. That is the number to quote.
  • The recipient-transcript enqueue check in section 2, which is entirely build-independent.

If you roll back and report "no more no_response", you have measured nothing. Quote unhealthy cycle counts and delivered-message counts instead.

4. Fix status, as of 2026-08-13 10:20 UTC-5

For anyone waiting rather than rolling back:

  • No Claude Desktop build above 1.28929.0 exists. The Squirrel feed at downloads.claude.ai/releases/win32/x64/RELEASES still lists only AnthropicClaude-1.28929.0-full.nupkg, and the enterprise MSIX latest redirect still resolves to 1.28929.0.
  • The engine has moved but does not fix this. latest is now 2.1.231, stable is 2.1.223. I read the changelogs for 2.1.228, 2.1.229 and 2.1.231 in full: none mentions the cross-session inbound gate, the kill switch, or the legacy ccd_session_mgmt injection. 2.1.229 does carry Windows fixes, but for extended-length/UNC paths and the scheduled-tasks watcher.
  • So Desktop 1.28929.0 pins 2.1.227, and no engine that would fix it has shipped yet in any case.

Echoing @007ArunSharma's ask 2: pointing Desktop's pinned engine back to a pre-2.1.224 build would resolve this for every Windows user without waiting on the classification fix, since the native peer feature that the kill switch guards did not exist before then.

wshallwshall · 17 days ago

Desktop 1.30096.1 shipped, and it does not fix this. It pins engine 2.1.229.

Flagging early because the update is being offered now and the "Relaunch to update" prompt reads like a fix has landed.

What the new build actually carries

Read out of the new package's own resources/app.asar (%LOCALAPPDATA%\AnthropicClaude\app-1.30096.1, downloaded and fully extracted here at 2026-08-13 18:54 local, 2400 files / 561 MB):

{"version":"2.1.229","manifest":{"version":"2.1.229",
 "commit":"10b3e93beb60b9548432371b3de664a4190e8da7",
 "buildDate":"2026-08-12T17:59:47Z"}}     // appVersion "1.30096.1"

Four things follow, and I want to be clear which are measured and which are not:

  • MEASURED. 2.1.229 is inside the affected 2.1.224+ range.
  • MEASURED. 2.1.229's entire changelog is one entry, about documenting claude remote-control --continue. Nothing touches the inbound gate. Same for 2.1.228 and 2.1.231, which I read in full earlier in this thread.
  • MEASURED. The engine was built 2026-08-12T17:59:47Z — roughly 15 hours before @WarmBed posted the binary-level kill-switch analysis on 08-13 at 08:54Z. It could not have been built in response to a root cause that was not yet public.
  • INFERRED, not measured. That 2.1.229 still refuses. I do not have that binary; only 2.1.221 is on this machine.

A cheap discriminator, if someone has 2.1.229 handy

I validated a string test against the version @WarmBed measured as accepting, with both controls, so it discriminates rather than merely matching everywhere:

| grep over engine 2.1.221 (accepting) | matches |
|---|---|
| cross-session-inbound | 0 |
| cross-session messaging disabled | 0 |
| any other cross-session* string (positive control) | 15+ |

The gate's own debug string is absent from the accepting build and reported present in 2.1.227. So on any 2.1.229 install, rg -a -c 'cross-session-inbound' <engine>/claude.exe returning non-zero is good evidence the gate shipped again. Someone who has taken 1.30096.1 can settle in one command what I can only infer.

On the title

@WarmBed — would you consider retitling? The current title is accurate but front-loads the telemetry fields, and I think it undersells the impact enough to affect triage priority. The thing that actually happens to users is that a running session is killed and its in-flight work is lost. Something like:

Regression (Windows/macOS): an inbound cross-session message is dropped before enqueue, leaving the recipient session dead and its in-flight work lost — still shipping in Desktop 1.30096.1 / engine 2.1.229

Entirely your call, it's your issue — just flagging that several of us have now independently lost session work to this and the title doesn't convey that.

Heeyoung-Ahn · 17 days ago

Reproduced on the latest versions: desktop app 1.30096.0.0, bundled runtime 2.1.229, Windows 11 (10.0.26200), after a clean reboot.

What changed vs 2.1.227: the message now actually arrives and renders in the recipient session (the 2.1.228 delivery-failure fix appears to work — sender gets success and the message is visible as '총괄의 메시지' / cross-session-message block in the recipient transcript).

What did NOT change: immediately after the message is injected, the recipient session's turn produces zero output — spinner running 1m31s+ with no token, no tool call, exactly the hadFirstResponse=false hang described here. Screenshot-confirmed by the human operator watching the recipient session.

So on 2.1.229 the failure mode shifted from 'silent non-delivery' to 'delivered, then recipient hangs'. Happy to provide logs.

arthurmoraesfernandes-afk · 17 days ago

This looks like the "wedge" mechanism documented in the follow-up comment on #86298, with app-log evidence: an injected cross-session message that the CLI holds (consent gate) gets drained into the CLI at a turn boundary and is never "echoed" back as a user turn; the app then logs isRunning held by unechoed input at result … pendingEchoUuids: […] and treats the session as busy indefinitely — phantom turn, watchdog kills, and the user's own typed prompts queue/stall behind a turn that never ends until a priority=next send (typing another message force-flushes) or a restart reclaims it. hadFirstResponse=false cycles are the app-side signature of this state.

DEzioA · 17 days ago

@wshallwshall -- here is the 2.1.229 measurement you asked for. I have 1.30096.1 installed, so I ran your discriminator directly. The gate did ship again, and on every gate string I tested 2.1.229 returns the same counts as 2.1.227. (String counts only -- I did not diff the binaries themselves.)

1. Your discriminator, run on 2.1.229

Windows 11 10.0.26200, Claude Desktop 1.30096.1.0 (MSIX / Microsoft Store install), bundled engine 2.1.229 at %APPDATA%\Claude\claude-code\2.1.229\claude.exe. 2.1.227 is still on disk, so both are measured on the same machine with the same command.

rg -a -c <string> <engine>\claude.exe (lines containing the string):

| string | 2.1.221 (yours, accepting) | 2.1.227 (mine) | 2.1.229 (mine) |
|---|---|---|---|
| cross-session-inbound | 0 | 10 | 10 |
| cross-session messaging disabled | 0 | 2 | 2 |
| cross-session (positive control) | 15+ | 45 | 45 |

Cross-checked by match count (grep -ao ... | wc -l) rather than line count, in case of packing artifacts: cross-session-inbound 27 vs 27, cross-session messaging disabled 3 vs 3. Two further gate strings are also identical across the two builds: kill switch 8 vs 8, refused inbound peer message 2 vs 2.

So your INFERRED line can be marked MEASURED: 2.1.229 still carries the gate, with counts identical to 2.1.227 on every string tested. Nothing in this build touched that code path.

Caveat on my side: I do not have an accepting build on disk (2.1.222 was reaped by the installer), so I cannot reproduce your negative control locally -- I am relying on your 2.1.221 baseline for discrimination. My positive control (cross-session = 45) at least confirms the search reaches strings in this binary.

2. Behavioural retest on 1.30096.1 / 2.1.229, sender and recipient both on the new build

Independent confirmation of @Heeyoung-Ahn's and @arthurmoraesfernandes-afk's retests, with one difference noted below.

Method: unique-token probe, verdict taken only from the recipient's CLI transcript JSONL, never from list_events or the rendered card.

  • Both endpoints restarted after the update; verified new engine on both sides (AI_AGENT=claude-code_2-1-229_agent).
  • Sender: send_message returned Message sent to session ....
  • App log shows the recipient genuinely woken: Resuming session ... -> Loaded 209 transcript messages at T+0s, then [WarmLifecycle:session] Starting idle timeout ... 900s 13 seconds later -- i.e. it woke and went idle without ever starting a turn.
  • Recipient transcript after 10 minutes: record count unchanged (337), last record still from the previous day, token absent. Watched continuously; zero writes.

Difference worth flagging: @Heeyoung-Ahn reported that on 2.1.229 the message now does reach the recipient transcript and the hang moves to the turn. On this machine it still does not reach the transcript at all -- the older 2.1.227 shape. Same app build, so there may be a second variable (MSIX vs Squirrel install, or recipient warm/cold state).

3. Two smaller confirmations

  • crossSessionInbound: "accept" is not a workaround here either -- consistent with @arthurmoraesfernandes-afk and @Bianco24siete. Given the string counts above are identical to 2.1.227, this is expected rather than surprising.
  • Rollback is not universally available. This machine is a Microsoft Store (MSIX) install -- [CCD-autoupdate] Disabled: MSIX install in main.log -- and the Store does not offer downgrade. The 1.25927.0 installer route several of you used is not reachable from an MSIX install, so for Store users there is currently no workaround at all, only avoidance.

4. Two verification traps that cost me a day, in case they save someone else

Both produced confident but wrong conclusions before I caught them:

  1. list_events / get_session / the rendered card are not delivery evidence. I used them to "confirm" delivery after a restart and reported the bug fixed; it was not. Re-auditing with transcript enqueue records showed 0 cross-session deliveries in that window -- every delivery I had "verified" was app-store-only. This matches @nutballs' and @WarmBed's point; I am adding it because I made exactly the mistake they warned about, one day later.
  2. A recipient transcript's mtime changes on a held message while its content does not. Watching file modification time reports activity that never happened. Only new records in the file count.

Also, on this machine the app log's watchdog signature is a clean discriminator: timed out after ...s of inactivity (hadFirstResponse=false ...) occurred 4 times, all after the engine swap, 0 times before it, over the same workload.

Happy to run any further one-command checks on 2.1.229 / MSIX -- the binary is here.

batesyx · 16 days ago

Delivery and turn execution are separable: payload reaches the recipient's transcript, then the turn produces zero tokens

Windows 11 Pro 10.0.26200, desktop app, bundled CLI 2.1.229, native Windows, no WSL. Sender and recipient are both local interactive sessions. crossSessionInbound and dialogExpiry were unset (defaults) for this test. Sender was in auto mode, so it identifies as prompting rather than bypassing.

Recipient: opus-5, xhigh effort, created three minutes before the test, idle (isRunning: false) at send time.

Method. Send a tokened probe via send_message, then read the recipient's own transcript with list_events rather than relying on the UI card, and poll get_session for isRunning / lastActivityAt.

Result.

| Step | Observed |
|---|---|
| Send receipt | Message sent to session … ("…") |
| Recipient transcript | Message present as a [user] event, wrapped in <cross-session-message from="local_71a…" name="…" encoded="1"> |
| Wake | isRunning flipped falsetrue |
| lastActivityAt | Froze at the arrival timestamp and did not advance for 3+ minutes |
| Assistant output | None. No tokens, no [result] record |
| Hold notice / approval dialog | Neither, on either side |

Second send, testing the force-flush claim. A follow-up send ~3.5 minutes later returned different wording:

Message queued for session …; it will be processed after the in-flight turn finishes if that session stays healthy.

That message also landed in the transcript as a [user] event, and lastActivityAt advanced to its arrival time. Still zero assistant output. Since the in-flight turn never finishes, a queued follow-up is never processed. A second send does not unwedge the session.

Why this may help narrow it. The payload is not lost in the app layer for this instance, and it is not held-then-expired: it is written to the recipient's transcript as a user turn within seconds, with no dialog and no hold notice. The wake path works, and message arrival updates lastActivityAt. What fails is strictly the turn, which starts and emits nothing.

One limit on that claim: transcript presence is not proof the model received the text. Because the turn emitted zero tokens, whether it entered the model's context is indeterminate from outside. What is ruled out is loss before the transcript.

This suggests reports describing "never written to the transcript" and reports describing "delivered then hangs" may be two distinct failures being merged, and that on 2.1.229 the second is what reproduces.

Bianco24siete · 16 days ago

The gate is on the SENDER side, not the recipient — and that is why none of the recipient-side workarounds in this thread work.

Windows 11 10.0.26200, Desktop 1.30096.1 (MSIX / Microsoft Store), bundled engine 2.1.229. Broken here since 2026-08-12T02:40:42Z (last real delivery), measured against 1,945 historical inbound deliveries. Yesterday: 9 probes to 4 sessions, zero delivered.

Today it works again, both pipes, 9-second delivery, round trip. What changed was on the sending side only.

---

The measurement that isolates it

The decisive test was sending to a session that does not have the fix:

recipient: Desktop session whose CLI process started the previous night
           -> no messagingSocketPath in ~/.claude/sessions/<pid>.json
           -> does not appear in ListAgents
           -> i.e. the recipient-side gate is still fully closed for it

sender:    session restarted after the change

result:    delivered in 9 s, model produced a turn, wrote the requested file,
           and replied

A recipient with the gate closed receives fine. What was broken was the outbound path.

That inverts the working theory in this thread. The reports here — including mine in #86237 and #86298 — describe it as "the recipient holds it", "the recipient's consent gate refuses it", "the recipient never receives it". Every remedy tried against that reading failed, and now there is a reason: they all target the recipient. crossSessionInbound: "accept", recipient permissionMode: "default", waiting for the 2.1.228 inbox fix, restarting the recipient — all recipient-side, all negative (I tested each).

Both transports recover together, which is consistent with a single shared gate rather than two bugs:

| transport | result |
|---|---|
| mcp__ccd_session_mgmt__send_message (the desktop/MCP path this thread is about) | delivered, 9 s |
| ListAgents + SendMessage (native peer messaging, previously absent on Windows) | delivered, 9 s, and the reply came back through it |

ListAgents did not exist as a tool in my sessions before the change; it now lists peers. So native cross-session messaging is built and functional on native Windows — it was gated off, not missing. The desktop's older MCP path appears to have been failing because it was being caught by that same gate.

---

How it was enabled — with the caveat stated up front

⚠️ This is disabled deliberately for Windows, and I do not know why. It may be untested there, or have known problems. I am reporting it as diagnostic evidence, not recommending it — anyone enabling it does so at their own risk, and the real fix is on Anthropic's side.

The engine's gate function resolves to false on Windows unless a flag overrides it. The override is a documented-in-binary env var, so this is not a patch or a bypass of authentication — no binary was modified, no credentials were touched:

// ~/.claude/settings.json   (USER settings — in project settings this var is ignored)
"env": { "CLAUDE_CODE_HARBOR_KITE": "1" }

A full app restart is required, and this matters: the gate is evaluated when each session's CLI process starts, not when the app starts. This also explains @psangiovanni86-max's three-hour lag — sessions whose processes predated the swap kept working. In my case, sessions the app relaunched picked the flag up; one I opened afterwards, whose process survived the restart, did not — and that is precisely the session I used as the "recipient without the fix" above.

Note for MSIX/Store users specifically: we cannot downgrade ([CCD-autoupdate] Disabled: MSIX install), so the 2.1.222 rollback several of you used is not reachable. For that population this was the only available path.

---

Limits of this measurement, declared

  • It does not prove a gated session can SEND. I measured receive-without-the-flag only.
  • It does not prove the flag is the sole cause. The app was also restarted; I cannot separate the two from outside. What I can say is that a restart alone did not fix it yesterday — I restarted and retested twice, both negative, on the same build.
  • It is one machine, MSIX, 2.1.229. @psangiovanni86-max's Squirrel install and @batesyx's environment show two different failure shapes; whether the sender-side gate explains both is untested.

Two verification traps, both of which cost me hours

  1. list_events / get_session read the app's store, not the transcript. They render the message and report isRunning: true for a message the engine never received. I confidently reported "the message reaches the recipient's transcript" three times based on this. It was false — measured by parsing the .jsonl, zero inbound records for the entire outage. This matches what @psangiovanni86-max and @nutballs already warned; I am repeating it because I made the mistake anyway.
  2. A plain grep for a marker string in the recipient's .jsonl produces false positives — the sender's transcript also contains the message text. The valid criterion is a type:"user" record whose content contains the <cross-session-message opening tag.
wshallwshall · 16 days ago

Full reproduction on 1.30096.1 / engine 2.1.229, plus the thing that cost me a whole revert cycle: rolling back the APP does not roll back the ENGINE.

Windows 11 Pro 10.0.26200, Squirrel install (not MSIX). I took 1.30096.1 deliberately to test it, reproduced the bug, and rolled back. Everything below is measured on this machine today.

1. End-to-end reproduction in one log

Upgraded 09:48 local. Ten cross-session sends fired 09:50:09-09:50:44 (counted from main.log, Sending message to session). Zero arrived — no recipient transcript gained a type:"user" record containing the <cross-session-message tag. Then, ~1000 s later:

10:07:27 [warn] Session local_3dca70bf... timed out after 1004s of inactivity
                (hadFirstResponse=false, last_message_type=user,
                 last_tool_name=none, seconds_since_stderr=never)
10:07:27 [info] unhealthy cycle for local_3dca70bf... (1004s, reason=no_response)
10:07:27 [warn] Session local_550a5e48... timed out after  992s of inactivity
10:07:27 [info] unhealthy cycle for local_550a5e48... ( 992s, reason=no_response)

Work it backwards: 1004 s before 10:07:27 is 09:50:43; 992 s before is 09:50:55. The send burst was 09:50:09-09:50:44. Those two sessions took a message from that burst, produced nothing, and were killed by the watchdog. Both were live sessions doing real work.

So this is not only "the message is lost" — it destroys the recipient.

One caution on reading these logs: hadFirstResponse=false on its own is not the signal. It appears in healthy cycles too — I have three in the same file. Only reason=no_response paired with the ~1000 s timeout discriminates.

2. The gate is present in 2.1.229, and it short-circuits before the settings lookup

Earlier I offered a string test but could only infer the result. I now have both binaries locally, so here it is measured, with the positive control:

| grep | 2.1.221 (accepting) | 2.1.229 |
|---|---|---|
| cross-session-inbound | 0 | 10 |
| cross-session messaging disabled | 0 | 2 |
| cross-session (positive control) | 11 | 45 |

And the gate itself, out of the shipped 2.1.229 binary:

LHf(){ return Gb() ? void 0
       : {policy:"refuse", holdCause:"explicit-setting", refuseCause:"kill-switch"} }

o6v(){ let e = LHf(); if (e) return e;          // kill switch returns FIRST
       let t = LUr(); if (t !== void 0) ...     // crossSessionInbound never consulted

This is why every crossSessionInbound remedy in this thread fails on Windows. The kill switch returns before LUr(), so the setting is never read. Consistent with @WarmBed's analysis, and unchanged in 2.1.229.

3. Rolling back the app does NOT roll back the engine

This is the part I most want people to see, because a failed rollback looks exactly like a successful one.

I reverted app-1.30096.1 -> app-1.25927.0 and relaunched. The app version read correctly. It was still broken. Process lineage shows why:

Desktop app-1.25927.0  process started  10:16:24
engine 2.1.229         process started  10:16:31   <- 7 s later, spawned BY the reverted app
engine 2.1.221         processes        none

The engine lives in %APPDATA%\Claude\claude-code\<version>\, separately from the app, and the app appears to select the highest engine version present on disk. config.json carries no engine pin (updaterLastSeenVersion is app-level only). So an app-only downgrade leaves the defective engine in place while reporting the version you wanted.

The gate is in the engine. Verify the engine, not the app:

Get-Process claude | Where Path -like '*claude-code*' | Select -Expand Path -Unique

I suspect this explains at least some of the "I rolled back and it still fails" reports here. It is not enough for the old engine to exist — the new engine directory has to be gone or occupied, or it wins.

4. How to actually downgrade (verified, both halves)

Nobody has posted this, and it is not discoverable from the UI. Both artifacts are addressable by version.

Engine. Manifest and binary:

https://downloads.claude.ai/claude-code-releases/<version>/manifest.json
https://downloads.claude.ai/claude-code-releases/<version>/win32-x64/claude.exe        (265 MB)
https://downloads.claude.ai/claude-code-releases/<version>/win32-x64/claude.exe.zst    ( 62 MB)

Channel pointers, as of now: .../claude-code-releases/stable returns 2.1.223, .../latest returns 2.1.232.

The manifest carries a per-platform SHA256, and I verified it describes exactly what ships:

manifest win32-x64 checksum : 0f73196359a07f9ad435a8c83ca7c741b9f2adbc0ad9b05f71a0c2f4aabe906a
SHA256 of my running 2.1.221 : 0f73196359a07f9ad435a8c83ca7c741b9f2adbc0ad9b05f71a0c2f4aabe906a

Engines install to %APPDATA%\Claude\claude-code\<version>\claude.exe.

App (Squirrel only). The package URL pattern from Squirrel-Update.log:

https://downloads.claude.ai/releases/win32/x64/AnthropicClaude-<version>-full.nupkg

Verified reachable for 1.25927.0 (224.5 MB), 1.28929.0, and 1.30096.1. Old versions are not pruned.

Two limits, stated rather than papered over. Each engine directory has a .verified sidecar holding a hash that is not the SHA256 of claude.exe; I have not worked out what it covers, so a hand-placed binary may be re-verified or re-fetched. And MSIX/Store installs cannot downgrade the app at all ([CCD-autoupdate] Disabled: MSIX install), as @Bianco24siete noted — though the engine half above is independent of install type and may still help that population.

5. The stable channel (2.1.223) does not contain the gate — measured

Rather than tell people to hand-pin 2.1.221, I fetched stable and tested it. Downloaded from the URL above, SHA256 verified against its own manifest before testing (a708ba81...fe722, 280,233,632 bytes, commit 4535f697, built 2026-08-05):

| grep | 2.1.221 (known accepting) | 2.1.223 (stable) | 2.1.229 (broken) |
|---|---|---|---|
| cross-session-inbound | 0 | 0 | 10 |
| cross-session messaging disabled | 0 | 0 | 2 |
| cross-session — positive control | 11 | 11 | 45 |
| SendMessage — positive control | — | 52 | — |

The gate strings are absent from 2.1.223. The same test separates the known-good build from the known-broken one, so it is discriminating rather than merely matching nothing.

Stated precisely: the absence of the gate code in 2.1.223 is measured. That 2.1.223 delivers is inferred — I did not run it, I rolled back to 2.1.221 which I had already measured end to end. But 2.1.223 predates 2.1.224, where the native cross-session path landed, and it is a supported channel pointer rather than a version someone has to know to ask for.

So the practical recommendation for anyone stuck on this is: pin the engine to stable, not to a specific old build. That also survives the app-version churn described in section 3, because the engine is selected independently of the app.

Current state here

Back on Desktop 1.25927.0 / engine 2.1.221, engine process verified. 2.1.229 and 1.30096.1 are both blocked by occupying their paths with files, since deleting them alone just invites re-download.

WarmBed · 16 days ago

Independent confirmation of the engine/app split, plus one negative CLAUDE_CODE_HARBOR_KITE result on the Desktop MCP path

I agree with @wshallwshall that the running engine must be verified independently of the Desktop app version. My earlier tests already isolated the defect to the engine layer, but did not independently reproduce the specific app-rollback trap:

  • With the same machine, input frame, isolated config, and closed local endpoint, official CLI 2.1.221 accepted the Desktop-shaped <cross-session-message> and reached [API REQUEST] /v1/messages.
  • Official CLI 2.1.227 rejected the same frame before any API request with cross-session messaging disabled (kill switch) and produced zero stdout.

So our earlier evidence independently established that the behavioral boundary is in the engine. What @wshallwshall adds is the important operational proof that rolling back the app alone may still launch a newer broken engine. We had not previously tested that exact old-app/new-engine combination.

I also tested the sender-side environment override reported above:

  • Windows 11, Desktop 1.30096.1 MSIX, official engine 2.1.229.
  • CLAUDE_CODE_HARBOR_KITE=1 set in user ~/.claude/settings.json.
  • Full Desktop restart; all tested CLI processes started after the setting change.

The flag was not simply ignored: new session registration files contained messagingSocketPath, the corresponding cc-msg-* named pipes existed, and SendMessage appeared in the advertised tool set.

However, one controlled mcp__ccd_session_mgmt__send_message probe still did not deliver:

  • The sender received Message sent to session ..., and Desktop logged Sending message to session ....
  • The idle recipient still had a live CLI process and messaging socket.
  • After about five minutes, the recipient's real CLI transcript had not changed and contained zero type:"user" records with a <cross-session-message opening tag.

Therefore, on this machine the flag changed the exposed capability surface but was not sufficient to restore the Desktop MCP path in this probe. This is not a claim that the env var failed to load, and it does not disprove the successful report above. I have not yet independently tested a native ListAgents + SendMessage send, so that transport remains open.

I also confirmed the current channel pointers directly: stable is 2.1.223, and latest is 2.1.232. I have not yet run end-to-end delivery on either, so I am not treating either version as fixed until the recipient transcript, first response, result, and round trip all pass.

007ArunSharma · 15 days ago

Confirmed data point (Windows): the CLAUDE_CODE_HARBOR_KITE=1 flag DID restore the Desktop MCP send_message path on our machine — transcript-verified.

Environment: Windows 11 Pro 10.0.26200, Desktop 1.30096.5, bundled engine 2.1.229 (the kill-switch gate strings are still present in the binary here — so this is a workaround, not a fixed engine).

We were fully dark for ~3 working days: every cross-session send_message to an idle session was refused, the recipient hung with hadFirstResponse=false, and the app watchdog killed it at ~980s.

Following @Bianco24siete's finding, we set in user ~/.claude/settings.json:

"env": { "CLAUDE_CODE_HARBOR_KITE": "1" }

and did a full restart (a machine reboot in our case).

Result — messaging restored, verified at the transcript level (not the app's "message sent" popup, which we'd already learned lies):

  • The inbound message landed in the recipient's real .jsonl as a proper type:"user" <cross-session-message …> record — delivered.
  • The recipient (idle for 3 days) woke and produced a normal turn ~2 minutes later — hadFirstResponse=true, no ~980s hang — processed.
  • Round-trip confirmed bidirectional: the peer's reply subsequently arrived in our sender session the same way.

This is an independent positive on the Desktop MCP send_message path specifically — @Bianco24siete reported it working; @WarmBed reported the flag loading (sockets/cc-msg-* pipes/SendMessage tool all appeared) but the MCP path still not delivering in one probe. On our machine it delivered on that path. So the flag's effect on the legacy Desktop path appears machine/timing-dependent, but it can and did fully restore it here.

Huge thanks to @Bianco24siete for isolating the flag and the sender-side framing — that's what got us unblocked after three lost days — and to @WarmBed, @wshallwshall, @DEzioA and @Heeyoung-Ahn for the binary-level forensics and the transcript-verification discipline.

Ask to Anthropic: this thread still has no maintainer response, while the regression has caused multi-day outages for anyone relying on cross-session coordination on Windows. Two requests:

  1. Please prioritize a proper fix so the legacy Desktop ccd_session_mgmt injection isn't caught by the native-feature Windows kill switch — or ship a supported, documented setting that works on Windows. The CLAUDE_CODE_HARBOR_KITE route enables a feature that is deliberately disabled on Windows "at our own risk," which shouldn't be the only way to restore basic messaging.
  2. Please make send_message report delivery/refusal truthfully — the false "sent" confirmation (while the recipient never received anything) is what made this so costly to diagnose.
DonPuls · 15 days ago

Negative datapoint on the exact same app+engine as the restoration report above — Desktop 1.30096.5 / bundled engine 2.1.229, STOCK config (no flag), Windows Server: message rendered in the recipient's pane but never reached its transcript

Environment

  • Claude Code Desktop 1.30096.5, bundled engine 2.1.229 (verified from the running process: %APPDATA%\Claude\claude-code\2.1.229\claude.exe; 2.1.227 staged beside it) — the identical app+engine pair as @007ArunSharma's flag-restored positive.
  • Windows Server 2022 Datacenter 21H2, build 20348.5499 (I believe the first non-Win11 Windows report here).
  • STOCK configuration: settings.json contains no CLAUDE_CODE_HARBOR_KITE and no crossSessionInbound key; engine not pinned or swapped. (A standalone CLI 2.1.220 on PATH is a separate install, not what Desktop runs.)
  • Two Desktop sessions, split panes in one window, same cwd; recipient idle at send time.

What happened (2026-08-15, one controlled probe as part of a planned channel re-test)

  • Sender called mcp__ccd_session_mgmt__send_message with a ~430-character plain-text message; the call returned success.
  • The recipient's PANE rendered the message ("Message from <sender>" card, full text visible) and showed a working turn. It then produced zero output — no tokens, no tool calls — for 10+ minutes (10m14s on the turn timer when I checked; I pressed Escape shortly after rather than waiting for the watchdog; Escape did stop it).
  • Transcript-level verification (recipient's real .jsonl, per the discipline established upthread): NOT delivered. The recipient's last transcript record is timestamped ~30 seconds BEFORE the send — zero type:"user" <cross-session-message> records, zero queue-operations, nothing at all after the send.
  • So on this machine the recipient-side pane rendering lies too, one level deeper than the sender's "sent" popup: the app painted the inbound message in the UI while the engine never received it. Consistent with @arthurmoraesfernandes-afk's held-at-the-gate description.
  • Matching detail: for the entire wedge the desktop pane showed the session's working indicator, while the mobile remote-control view showed that same session as NOT running — the mobile surface apparently read the engine's truth while the desktop pane showed app-side state.
  • This was the first and only cross-session message in either session's history (fresh sessions). The wedged session is preserved; happy to pull %APPDATA%\Claude\logs\main.log excerpts and session IDs if useful.

Net: with app, engine, and (per @DEzioA) the gate strings all identical to the restoration report, stock config still wedges here — the flag (or something machine-dependent it interacts with) is doing the restoring, not the build. OS is the other delta: Windows Server 2022 vs Windows 11 Pro. I'll try the CLAUDE_CODE_HARBOR_KITE route on this machine next and report the result either way, since its effect looks machine-dependent (one negative probe from @WarmBed, two transcript-verified positives).

+1 to both asks: a supported fix for the Windows path, and truthful delivery/refusal reporting from send_message — the false "sent" (and now, a false pane render on the recipient) is what makes this expensive to diagnose.

DonPuls · 15 days ago

Follow-up datapoint (promised in my 2026-08-15 comment): CLAUDE_CODE_HARBOR_KITE=1 restores cross-session send_message on the same machine that reproduced the wedge flag-off.

Environment: Windows Server 2022 Datacenter (10.0.20348), Claude Desktop 1.30096.5, bundled engine 2.1.229. Same two-pane Desktop setup (two sessions, same window, same cwd) as my earlier report.

Flag-off (earlier the same day): first delivered handshake (~430 chars) auto-started the recipient's turn, then the recipient wedged — 10m+ zero output, spinner active — matching this issue's profile. Session kept un-archived as evidence.

Flag-on: added "env": { "CLAUDE_CODE_HARBOR_KITE": "1" } to ~/.claude/settings.json, rebooted the VM, re-ran the same graduated protocol. Result: 4/4 legs delivered and verified against the recipients' transcript .jsonl files (not the pane render, which produced a false positive in my flag-off test):

  • A→B handshake ~430 chars: user-turn record ~9s after send, real turn produced, no wedge
  • B→A one-line ack: returned "Message queued…" (recipient mid-turn), delivered when it went idle (~77s), no wedge
  • A→B full-size ~2.9k multi-paragraph message with a fenced code block and non-ASCII (historically the killer size): delivered ~22s, character-exact after the encoded="1" wrapper's entity decode, no wedge
  • B→A one-line ack: queued, delivered on idle (~97s), no wedge

Notes: delivery to a busy session queues until that session goes idle (expected behavior as far as I can tell, but it's the exact spot where a wedge turns into silent message loss, so worth covering in any fix's tests). The wrapper's XML-entity escaping of < > & decodes cleanly.

So on this machine the kill-switch bypass fully restores the path — consistent with the two positive reports up-thread, and it suggests the gated code path itself works on Server 2022 and the kill-switch is the only thing standing between Windows users and a working feature. Happy to run further diagnostics on either configuration; I've kept the flag-off wedged session's transcript.

Heeyoung-Ahn · 15 days ago

Mixed result after today's Desktop update — active-session delivery now clean, but idle-session wake-up still hangs (recovery-via-new-input also failed this time)

Environment: Windows 11 Pro 10.0.26200, Desktop app updated 2026-08-16 (Owner-confirmed update, exact new version not independently verified from this session), bundled engine directory still shows 2.1.229. Two coordinator-managed CCD sessions in the same workspace, tested via the mcp__ccd_session_mgmt__send_message / list_events / get_session tools (not the pane render — we verify against the target session's own transcript, since a prior incident here taught us render ≠ delivery).

Test 1 — target session busy (in-flight turn) at send time

  • send_message returned: "Message queued ... will be processed after the in-flight turn finishes if that session stays healthy."
  • Message appeared in the target's real transcript (list_events) immediately.
  • Target kept working normally on its own task (tool calls continued, event count climbing) — no hang.
  • This looks like a genuine improvement over the 2.1.229 behavior reported earlier in this thread.

Test 2 — target session idle at send time

  • send_message returned an unconditional success (no queuing language).
  • Message appeared in the target's transcript immediately.
  • get_session showed isRunning flip false → true right after send (so the session did register the wake trigger).
  • Then: zero new turns for ~2 minutes — no assistant text, no tool calls, isRunning staying true the whole time. Matches this issue's original profile (spinner active / zero output).
  • We then tried the workaround we'd verified on 2026-08-15 (sending a second message acts as "new input" and unwedges the recipient). This time it did not work: the second message landed in the transcript too, but the session was still producing zero output when we stopped observing (~1 more minute, isRunning still true, lastActivityAt frozen at the first message's timestamp).

Net: the fix appears to help the "recipient already mid-turn" path, but the "recipient idle, needs to be woken from a cold state" path — which is the more common real-world case for cross-session handoffs — still reproduces the hang, and our previously-working recovery trick no longer reliably clears it. Happy to leave the hung session alive if a maintainer wants any additional diagnostics pulled from it before we archive/restart it.

wshallwshall · 13 days ago

The Windows gate is implemented differently in stable than in latest, and the CLAUDE_CODE_HARBOR_KITE workaround in this thread only works on one of them.

Windows 11 Pro 10.0.26200, Squirrel (non-MSIX) install. Everything below is measured on this machine today unless marked inferred.

1. The two gate functions, verbatim

All three binaries SHA256-verified against their own manifests before reading — 2.1.224 = 879f0d7e7eee…, 2.1.229 = 5736c66be98a…, 2.1.233 = 8ae35d4125….

// 2.1.224  -- currently /claude-code-releases/stable
function sA(){ if(Gt()==="windows") return !1;
               return ot("tengu_harbor_kite",!1) || Boolean(ee.CLAUDE_CODE_HARBOR_KITE) }

// 2.1.229
function Gb(){ if(Q.CLAUDE_CODE_HARBOR_KITE) return !0;
               if(Kt()==="windows" && !rt("tengu_harbor_kite_win",!1)) return !1;
               return rt("tengu_harbor_kite",!1) }

// 2.1.233  -- currently /claude-code-releases/latest
function dg(){ if(q.CLAUDE_CODE_HARBOR_KITE) return !0;
               if(Kt()==="windows" && !rt("tengu_harbor_kite_win",!1)) return !1;
               return rt("tengu_harbor_kite",!1) }

On stable, the platform check returns first and no environment variable can reach past it. On latest, the env check runs first and short-circuits to true.

So latest is more permissive on Windows than stable — it added a server-side flag (tengu_harbor_kite_win, default false) and moved the override ahead of the platform check. The CLAUDE_CODE_HARBOR_KITE=1 workaround reported by @007ArunSharma and @DonPuls works on 2.1.229/2.1.233 and silently does nothing on 2.1.224.

A warning on method, because it nearly caught me: a string-count pass on 2.1.224 returns cross-session messaging disabled = 0 and tengu_harbor_kite_win = 0, which reads as "stable is clean." That is wrong. 2.1.224 gates Windows with a bare early return carrying no flag and no message string, so the discriminating strings don't exist there. For a gate question, extract the function; adjacent string counts fail exactly when the implementation changes shape.

2. Delivery, measured against recipient transcripts

| engine | probes | delivered |
|---|---|---|
| 2.1.229 (Desktop 1.30096.5) | 8 | 0 |
| 2.1.221 (Desktop 1.25927.0) | 1 | 1, round trip in 2.1 s |

The 8 covered busy and idle recipients across four repositories. The 1 used the same sender and the same recipient as one of the failures, so only the engine differed.

Successful delivery, from the recipient's own .jsonl:

16:26:17.259  queue-operation      <- enqueue
16:26:17.384  user                 <- <cross-session-message ...>
16:26:25.244  assistant            <- recipient echoed the token back

On all 8 failures there was no queue-operation record and no user record — nothing at all in the recipient's transcript. The drop precedes the queue.

Stated honestly: that A/B moved two variables (app and engine), so by itself it does not localise the defect. What localises it to the engine is the gate functions above plus @WarmBed's standalone isolation of 2.1.221 vs 2.1.227.

3. A cheap test anyone can run

Send one tokened probe, then search all transcripts for that token. If it appears only in the sender's transcript, it was not delivered. No census, no rate, no waiting — and it is immune to the contamination that ruins substring counts once sessions start discussing the outage.

Do not trust the sender-side "Message sent to session X" popup. It reports success on every one of the 8 failures.

4. The framing, which I think is the actual bug

The documentation states: available on macOS and Linux, including Linux inside WSL 2. Claude Code doesn't offer cross-session messaging on native Windows.

If that is intended, then this issue is not a regression — and the defect is that the feature is advertised as working rather than being cleanly absent. On native Windows SendMessage is exposed, the sender is told the message was sent, the recipient wakes and starts a turn, and nothing is ever enqueued. The docs say a session without the feature should not even recognise /list-agents.

A documented-unavailable feature that reports success is worse than one that errors, and it is a smaller fix than making messaging work.

Related: #24798 was closed on 2026-08-17 with "as of v2.1.224 your Claude Code sessions can message each other", with no platform qualification.

5. The wedge is not limited to peer messages

Recipients that never receive a message still wedge, and so do sessions receiving ordinary typed input:

[warn] Session local_a21167… timed out after 984s of inactivity
       (hadFirstResponse=false, last_message_type=user, last_tool_name=none)
[info] unhealthy cycle (1012s, hadFirstResponse=false, reason=no_response)
[info] [LocalSessionManager] isRunning held by unechoed input at result for local_a21167…

That is @arthurmoraesfernandes-afk's isRunning held by unechoed input signature. Inferred, not instrumented: the triggering LocalSessions.sendMessage carried messageLength=9, imageCount=2, which exactly fingerprints a 9-character prompt plus two screenshots I typed myself — not a peer message. If that reading is right, the wedge is broader than this issue's title suggests.

A resend with priority=next reclaims the session; priority=(default) does not.

Aturion31 · 12 days ago

Data point from a Windows Desktop setup: the 2.1.234 fix cannot reach Desktop users yet.

  • Claude Desktop 1.32352.1 (latest Microsoft Store build, Windows 11) still embeds engine 2.1.229 — read from the transcript JSONL version field, including sessions created fresh after a full app restart.
  • Symptom exactly as described in this issue: the message is displayed in the recipient session, but the recipient's turn dies silently after ~12-15 min (hadFirstResponse=false, reason=no_response in main.log), no error on either side. Reproduced 6+ times (Aug 16-18), on brand-new sessions, outside any platform incident.
  • Tried "crossSessionInbound": "accept" in ~/.claude/settings.json (supported since v2.1.224 per the docs) + full app restart → no effect on 2.1.229.
  • The engine 2.1.234 changelog entry ("Fixed Claude Desktop inter-session messages being silently dropped by the recipient session when cross-session messaging read as disabled…") matches this issue word for word.
  • Aggravating detail worth documenting: sending a second message to a session that already has a pending undelivered one silently overwrites the first.

Could a Desktop build embedding engine >= 2.1.234 be shipped? Until then, Desktop users cannot benefit from the upstream fix.

Aturion31 · 11 days ago

Update, and likely resolution for our variant: today's Store build 1.32885.1 ships engine 2.1.234 (verified in the transcript version field). Cross-session messaging works again on our machine — full round-trip verified: send triggers the recipient's turn, and the reply triggers a turn on the sender's side. Both directions, first try, after 7 days of silent drops on 2.1.229.

For anyone stuck: check Get-AppxPackage Claude for 1.32885.1+, and confirm the engine version in any fresh transcript. Note that sessions still holding an undelivered "ghost" message from the broken engine keep showing the stale card — those messages are gone, but the sessions themselves work fine.

CptCabbageee · 11 days ago

Adding a quantified before/after for the regression, confirmation of the fix, and a testing trap that produced a false negative for me.

1. Quantified regression — 82% → ~0% on the affected builds

I paired every Resuming session <id> in the desktop logs with the next event for that id — either Mapping internal session <id> (a CLI process attached) or a hadFirstResponse=false timeout (it never did):

| Log | Desktop version | Result |
|---|---|---|
| main1.log (09–16 Aug) | 1.26832.0.0 / 1.28929.0.0 | 121 attached / 26 failed = 82% |
| main.log (from 16 Aug 12:49) | 1.30096.5.0 | 11 attached / 36 failed = 23% |
| — rate-limit-blocked sessions only | 1.30096.5.0 | 0 of 12 |

The 82% comes from rotated logs, so it's a real before-baseline on the same machine and the same sessions. On 11 Aug at 01:32, three sessions woken by send_message attached within 1–2 seconds each.

Practical impact of the broken window: two sessions stranded by a 5-hour rate limit stayed dead for ~11 hours overnight. Mapping internal session <id> is the reliable indicator of an attached process — note isRunning reports true throughout the failed state, so the UI shows dead sessions as live. That cost me several hours of misdiagnosis.

2. ✅ Confirmed fixed in Desktop 1.32885.1.0 / engine 2.1.234

Corroborating @Aturion31. Tested against a session that had been paused ~10 hours, with the app up ~50 minutes:

14:08:40  Mapping internal session local_32b91bb2-... to CLI session 35b59675-...

Attached within seconds. Same machine, same session type that went 0-for-12 the night before.

3. ⚠️ Testing trap: don't test in the first minutes after the update relaunch

My first test on this build reported it as still broken, and that was wrong. It ran 70 seconds after the app relaunched for the update — the app logged 43 startup lines in the surrounding two-minute window. The message was swallowed and the session died at 999s with hadFirstResponse=false, which looks identical to the regression.

That is almost certainly #86326 (a message delivered to a session that is still starting up is silently lost), not this issue. Anyone verifying the fix should let the app settle for a few minutes and prefer a long-idle target, or they'll reproduce the startup bug and conclude the regression is still live.

4. Diagnostics that were useful, for the archive

  • Mapping internal session <id> — the only reliable "a CLI process really attached" signal. isRunning is not.
  • LocalSessions.setFocusedSession — tells you a human opened the session, vs a bare Resuming session meaning a message triggered it. Essential for telling a real wake from an accidental click during testing.
  • Message sent vs Message queued from send_message — "queued" means the target was mid-turn, so that send tests nothing.
  • On the broken build, focusing a session was not sufficient while a message was pending (no attach), whereas focusing an idle session attached in seconds. Unverified on 2.1.234.

5. One caution for anyone who built retry tooling during the outage

On the broken build I ran a recovery loop that fired 19 retries at two rate-limited sessions over ~6 hours on a ~16-minute cadence. Since each undelivered message occupied the session for ~1000s, that plausibly kept both unrecoverable for the whole period — the retries were likely doing harm rather than helping. I have not re-verified this on 2.1.234, so treat it as a property of the broken builds only; but if you wrote a retry loop while this was broken, it's worth revisiting rather than leaving armed.

Environment: Windows 11 (26100), 15.7 GB RAM. Broken on Desktop 1.30096.5.0; working on 1.32885.1.0 / engine 2.1.234.

WarmBed · 11 days ago

Another negative datapoint on Desktop 1.32885.1 / engine 2.1.234 (Windows 11, MSIX Store build) — single-shot protocol, transcript + log verified.

Environment: Windows 11 Home 10.0.26200, Desktop 1.32885.1 via MSIX auto-update landing 2026-08-19 11:51 local, engine dir %APPDATA%\Claude\claude-code\2.1.234 (fresh install by the update, Authenticode Valid). Recipient session idle at send time.

Following @CptCabbageee's warning that a failed delivery jams the recipient (~1000s) and timer-based retries hold it jammed, this was one send, zero retries:

  • 13:14 local — Desktop main.log logs Sending message to session <id>.
  • 10 minutes of watching the recipient's transcript JSONL: the message never arrives. No queue-operation/enqueue, no user turn, nothing.
  • No Mapping internal session <id> line ever follows the send — while in the same minutes, other (human-focused) sessions' Mapping internal session lines fire normally, matching the setFocusedSession discriminator described above.
  • isRunning reported true for the recipient throughout, consistent with every prior report that the session store lies while the CLI never attached.

So on this machine 1.32885.1 / 2.1.234 reproduces the original signature exactly — corroborating @CptCabbageee's 2.1.234 result and contradicting none of the mechanics; combined with @Aturion31's working 2.1.234 round-trip, the same-version/different-outcome split is still unexplained. Happy to run a specific diagnostic if someone from the team wants data from a failing 2.1.234 machine.

InfiniteBSOD · 11 days ago

Authored by AI (Claude - Opus 5 w. ultracode); approved by me - a human :)

---

Negative datapoint on Desktop 1.32885.1 / engine 2.1.234, run specifically to avoid the false-negative traps @CptCabbageee documented, since the fix status is currently split.

I also have to withdraw an earlier negative of my own from tonight — it was confounded. Details in §3, flagged so it isn't counted as a second failure.

Environment: Windows 11 Pro 26200 · Desktop 1.32885.1 (MSIX, auto-update landed 21:23:47 local) · engine 2.1.234 (Using Claude Code binary at: …\claude-code\2.1.234\claude.exe) · Max subscription.

1. Protocol — controlled against the known traps

  • Settle time: 36 minutes after the update relaunch (not 70 s — @CptCabbageee's #86326 trap).
  • Target idle 4 days, and ghost-free: its full log history shows 2 prior sends, 2 prior Mapping internal session, 0 timeouts — i.e. every earlier delivery to it attached cleanly, so it carried no undelivered message from the broken engine.
  • Tool returned Message sent, not Message queued — the target was not mid-turn, so the send actually tests something.
  • Single send, zero retries (retries occupy the target ~1000 s each).
  • No focus click — I did not open the session; no setFocusedSession preceded the send.
  • Verdict taken from Mapping internal session, not isRunning.

2. Result: message lost

Complete log record for the target, start to finish:

22:00:00  send_message called -> "Message sent to session … (…)"
22:00:08  Resuming session <T> in <cwd>
22:00:08  Starting local session <T>
22:00:08  Loaded 51 transcript messages for session <T>
22:16:08  [warn] Session <T> timed out after 960s of inactivity
          (hadFirstResponse=false, last_message_type=user,
           last_tool_name=none, seconds_since_stderr=never)
22:16:08  [CCD CycleHealth] unhealthy cycle for <T> (960s, hadFirstResponse=false, reason=no_response)
22:16:09  Session <T> query iterator completed

Counts for the whole evening, scoped to that session id:

| signal | count |
|---|---|
| Mapping internal session <T> | 0 |
| Sending message to session <T> | 0 |
| [CCD start-timing] <T> | 0 |
| drained N deferred send(s) for <T> | 0 |

The wake fires and nothing attaches. A unique token in the message body appears only in the sender's transcript, never in the recipient's.

One refinement to the "transcript mtime frozen" description in #87615: the recipient's .jsonl mtime did move, to 22:16:08 — exactly the teardown second — while the file gained no message record (token count 0, still 96 records). So mtime is not a reliable "did it arrive" probe on its own; the file is touched at cycle teardown regardless. Byte-size/record-count or a token grep is the safer check.

3. ⚠️ Withdrawing my own earlier negative from tonight

I ran an earlier probe on this same build at 21:32 which also failed, and it is not sound evidence. Three defects, all of which @CptCabbageee's notes predict:

  1. It ran 8m58s after the update relaunch — probably past the startup window, but not demonstrably.
  2. setFocusedSession fired 4 s before the send, so the target was not in a clean idle state.
  3. The target still held a ghost from a failed probe the previous day on the old engine (send → no attach → 977 s watchdog). Per @Aturion31's note about sessions holding an undelivered ghost, re-using it was the wrong choice — I picked it precisely because it was already sacrificed, which is what invalidated it.

Only the §2 run should be counted.

4. Two diagnostics that may explain why reports disagree

The log signature depends on the target's state, not only on the build. Across four builds I get three distinct signatures, and two probes on this build four minutes apart produced two of them:

| target state | Sending message | Resuming/Loaded N | outcome |
|---|---|---|---|
| mid-turn | ✅ logged | — | drained 1 deferred send(s), lost |
| warm / recently bound | ✅ logged | ❌ | no Mapping, lost |
| cold / unbound | ❌ never | ✅ | no turn, watchdog, lost |

This is independently visible in @JLWsoftware's data on #86498 (busy recipient → bare Sending message; paused recipient → resume with no send line). It means "no Sending message line" and "a bare Sending message line" are the same defect seen against different target states — worth controlling for before attributing a signature change to a version.

seconds_since_stderr=never does not mean the CLI process never started. On an earlier build I have a cold-target case where the binary is logged and Loaded 827 transcript messages succeeds, and the field still reads never 997 s later. It only means the CLI emitted no stderr — true both when it never starts and when it starts with nothing to run.

Minor caution for #87615's echo-reaping chain: isRunning held by unechoed input at result fires here on days with zero cross-session sends (2 of 5 occurrences), and reaped … stale pendingEchoUuids never appears in my logs at all. It looks like general echo tracking rather than a cross-session-specific symptom.

5. Summary

On this machine, with the traps controlled for, 1.32885.1 / 2.1.234 does not deliver cross-session messages to a cold target. That is one clean negative, not a claim that the fix is universally absent — @Aturion31 and @CptCabbageee's positives are on the same versions, so something environmental or path-dependent still separates the two outcomes. The target-state table in §4 is my best guess at where to look.

wshallwshall · 10 days ago

Unfortunately, I think the title of this bug does not indicate its severity. This is a serious regression.

CptCabbageee · 10 days ago

Following up on the CLAUDE_CODE_HARBOR_KITE findings above (thanks @nutballs, @WarmBed, @Bianco24siete, @DEzioA) — rather than just asking for the Windows gate to be lifted, it might be worth asking a more specific question: is the underlying pipe already access-controlled, or is the gate standing in for that missing check?

Concretely, for cc-msg-* / messagingSocketPath to be safe to enable by default, I'd expect two things:

  1. OS-level restriction on the pipe itself — Windows named pipes support a security descriptor limiting which user/process SID can connect. If the pipe is created without an explicit restrictive ACL, any process running under the same Windows account could potentially open it and write to it, not just other Claude Code sessions.
  2. A per-message secret, not just a session ID — each session should hold a freshly generated, random token (not a predictable/sequential ID) that a legitimate sender must include for a message to be accepted. Ideally single-use or timestamp-bound, so a captured message can't be replayed later.

If both of those are already implemented under the hood, that would go a long way to explaining why it's safe to flip this on, and might be worth documenting so people aren't just discovering it via binary string analysis. If they're not both in place yet, that seems like the actual blocker worth fixing, rather than the flag being toggled on as-is — since as WarmBed's test showed, HARBOR_KITE=1 doesn't even reliably fix delivery yet, so there's not an urgent reason to trade an unverified security boundary for an unconfirmed fix.

Would a maintainer be able to confirm which of the two (if either) is currently in place? Happy to help test once there's something concrete to verify.

InfiniteBSOD · 9 days ago

Authored by AI (Claude - Opus 5 w. ultracode); approved by me - a human :)

---

✅ Fixed here on Desktop 1.34493.1 / engine 2.1.237 — all three delivery paths, 3 of 3. This supersedes my negative datapoint above, which was on 1.32885.1 / 2.1.234.

Environment: Windows 11 Pro 26200 · Desktop 1.34493.1 (MSIX, auto-update landed 18:40 local) · engine 2.1.237 · Max subscription. Same machine, same protocol, same target class as the failing test I posted two days ago.

1. Results — cold, warm and queued all deliver and run a turn

One target (idle 5 days, ghost-free: 7 prior sends / 7 attaches / 0 timeouts across both log roots), probed in three states back to back. Each success left the session in the state needed for the next test.

| path | tool returned | attach | transcript | turn | verdict |
|---|---|---|---|---|---|
| cold (idle 5 d) | Message sent | Mapping +2 s | queue-operation enqueue + user record | start-timing, healthy 178 s | ✅ |
| queued (target mid-turn) | Message queued | drained 1 deferred send(s)Mapping same second | enqueue + user | healthy 59 s | ✅ |
| warm (bound, idle between turns) | Message sent | Mapping same second | enqueue + user | healthy 46 s | ✅ |

Cold path, in full:

19:05:29  send_message -> "Message sent to session … (…)"
19:05:35  Resuming session <T> in <cwd>
19:05:35  Starting local session <T>
19:05:35  Loaded 153 transcript messages for session <T>
19:05:37  Mapping internal session <T> to CLI session <cli>
19:06:04  [CCD start-timing] <T> preflight=9ms worktree=1ms mcp=45ms query=5ms enqueue=20ms

The warm probe's transcript records include assistant thinking, text and tool_use — the recipient read the message and acted on it, not merely received it.

2. The queued path is the one worth highlighting

On 1.32352.1 I logged the deferred-send variant failing silently: the drain fired and the payload simply evaporated, with the target left perfectly healthy —

19:34:13  [Stop hook] Query completed for session <B>
19:34:13  [LocalSessionManager] drained 1 deferred send(s) for <B>
19:34:13  [CCD CycleHealth] healthy cycle for <B> (166s, hadFirstResponse=true)

— no spinner, no watchdog, no symptom anywhere, sender told "queued". That was the nastiest variant precisely because nothing surfaced it.

On 2.1.237 the same drained 1 deferred send(s) line is now followed by a Mapping and a real responding turn. Anyone verifying this fix should test that path explicitly: a build could attach cold targets correctly and still lose everything sent to a busy session, and the failure would be invisible.

3. Clean before/after on one machine

| date | build | engine | target | result |
|---|---|---|---|---|
| 2026-08-19 22:00 | 1.32885.1 | 2.1.234 | cold, ghost-free, idle 4 d | ❌ no attach, 960 s watchdog, payload lost |
| 2026-08-21 19:05 | 1.34493.1 | 2.1.237 | cold, ghost-free, idle 5 d | ✅ attach +2 s, turn ran |

Both runs used the same controls: settled well past the last app restart, long-idle ghost-free target, Message sent (not queued), single send, no retries, no focus click, verdict taken from Mapping internal session rather than isRunning.

⚠️ This does not retroactively validate the 2.1.234 fix reports. @mouarg, @Aturion31 and @CptCabbageee reported success on 2.1.234, and 2.1.234 failed here under exactly the protocol above. Either the fix was partial or environment-dependent on 2.1.234 and complete in 2.1.237, or those machines differed in some way that mattered. I cannot distinguish those from here, so I am not asserting either.

⚠️ App and engine moved together (1.32885.1 → 1.34493.1, 2.1.234 → 2.1.237), so I cannot attribute the fix to one or the other.

4. ⚠️ 1.34493.1 MOVED THE LOG DIRECTORY — this will break your tooling

Anyone still grepping the old path will read a dead file and may conclude nothing is happening.

  • Old (now dead): %LOCALAPPDATA%\Packages\Claude_<pkg>\LocalCache\Roaming\Claude\logs\ — its last line is beforeQuitForUpdate handler fired, going down for update.
  • New (live): %LOCALAPPDATA%\Claude\Logs\main.log

The app is still MSIX (windowsStore=true), so this is not a packaging change — just a relocation. History is now split across two roots: any before/after census or "has this session ever failed a delivery" check must enumerate both, or it silently undercounts.

5. Minor correction for the echo-reaping chain in #87615

That report presents isRunning held by unechoed input at resultreaped N stale pendingEchoUuids as part of the loss chain. On this machine that line fires:

  • on days with zero cross-session sends (2 of 5 occurrences), and
  • at 19:08:33 today — on a successful delivery, immediately before the drain that worked.

reaped … stale pendingEchoUuids never appears here at all. It looks like general echo tracking rather than a cross-session loss signal, so it is probably not the thread to pull.

6. Scope

One machine, one target, one probe per path, on the cold/warm/queued states as defined above. Not a claim that every environment is fixed — the unreconciled 2.1.234 split in §3 is reason enough for others to re-verify rather than take this as universal.

wshallwshall · 7 days ago

@WarmBed ... I second the above suggestions... It would be helpful if you retitle this issue to convey the urgency.

WarmBed · 7 days ago

@wshallwshall Done — retitled to convey the severity and current split state.

Status on my machine, for the record: Desktop 1.34493.1 (MSIX Store channel) / engine 2.1.237 — cross-session messaging verified working stock (no flag, no workaround): single-shot receipt test, message reached the idle recipient's transcript, session woke, replied, and the ack round-tripped back. This matches @InfiniteBSOD's 3-of-3 report on the same build.

So the picture as I read the thread today:

  • MSIX/latest channel, engine 2.1.237: fixed (two independent machines verified, transcript-level).
  • Engine 2.1.229/2.1.234: broken for most reporters; CLAUDE_CODE_HARBOR_KITE=1 works there (env check precedes the platform gate, per your decompile).
  • Stable channel (2.1.224-class): still broken AND the flag is dead on Windows (platform check short-circuits first, again per your decompile) — this is the population the title now calls out.

I'll keep the issue open until the stable channel ships a fixed engine and the thread goes quiet on new reproductions. If anyone still hits this on 2.1.237+, please post the build/channel — that would be important evidence of a staged rollout rather than a version fix.

wshallwshall · 7 days ago

Ran a structured attempt to reproduce this. It did not reproduce here — but the scope is narrow enough that I'd rather lead with two methodological findings than with the negative result, because I think both affect how everyone else in this thread is measuring.

Scope, up front: Claude Code engine 2.1.237, Claude Desktop 1.34493.1 (MSIX/Store), Windows Server 2025 (10.0.26100). That build is neither stable (2.1.231) nor latest (2.1.241), so nothing below speaks to "stable channel still affected."

---

1. The transcript oracle is blind during deferred holds

This is the finding I'd most like other people to check, because it may mean some reproductions have been filed as the wrong bug.

When a message is delivered to a session that is mid-turn, Desktop holds it and the recipient's transcript JSONL gets no record at all until the in-flight turn ends and the send drains. Two measurements:

| Delivery | Sent | Record written | Blind window |
|---|---|---|---|
| into a mid-turn session | 13:08:15.384Z | 13:08:38.014Z | 22.6 s |
| into a mid-turn session | 13:10:14Z | 13:12:33.9Z | 139 s |

In both cases the drain is marked by [LocalSessionManager] drained 1 deferred send(s), and the peer record's timestamp lands within ~100 ms of that drain — not of receipt. For contrast, a send to an idle session wrote its record within ~50 ms of receipt, so the oracle is sound there.

The window scales with the recipient's turn length.

Why this matters: if a session wedges on the deferred path, the transcript contains no peer record whatsoever. Anyone whose detection logic is "did a cross-session envelope appear, and was it answered?" will score that as message never arrived — i.e. a silent-drop bug — rather than as this one. The issue's own signature (last_message_type=user, hadFirstResponse=false) is Desktop-side in-memory buffer state, which is exactly the state that has no transcript counterpart during the hold.

Also worth noting: the MCP return string differs by path, which is a cheap way to tell which branch you're on.

  • idle target → Message sent to session ...
  • mid-turn target → Message queued for session ...; it will be processed after the in-flight turn finishes if that session stays healthy.

2. On Windows, the log path in this issue is dead

Desktop 1.34493.1 relocated the log directory. Anyone grepping the path named in the issue body is reading a file frozen at their last major version bump, and will conclude "no log data" with full confidence.

DEAD:  %APPDATA%\Claude\logs\main.log        (mine froze 2026-07-02, still reads version 2.1.187)
LIVE:  %LOCALAPPDATA%\Claude\Logs\main.log   (actively written)

Mine was stale by ~7 weeks and I nearly reported "the confirmation path is unavailable" on that basis. The live file is also truncated at every app launch (line 1 is Starting app), so capture it with tail -F rather than reading it after the fact.

Two corrections to the signature itself, from the live log:

  • hadFirstResponse=false is not diagnostic on its own. My log contains [CCD CycleHealth] healthy cycle for local_… (5s, hadFirstResponse=false) — a perfectly healthy 5-second cycle. The discriminator is unhealthy plus reason=no_response plus a long duration.
  • Grep the bare literal timed out after, without the tag or the extra fields. The emitted line does not carry the full field set quoted in the issue body, so grepping the quoted line matches nothing even when the event fires.

---

3. The negative result, with honest scoping

30 deliveries, 0 wedges. Per-cell rather than pooled, because the hazard in this thread is clearly state-conditional:

| Cell | Trials | Wedges |
|---|---|---|
| warm / idle (~2 min old) | 20 | 0 |
| deferred / mid-turn | 2 | 0 |
| unbound / re-attach (~6 770 s since last Stop hook) | 5 | 0 |
| cold (no live CLI process) | 0 | untestable — see below |

Watchdog across the full capture: 65 healthy cycle, 0 timed out after, 0 unhealthy, 0 reason=no_response. The warn/error channel was demonstrably alive in the same window, so those zeros are real zeros rather than a silent logger.

What I think this does and doesn't support. The ~35% per-dispatch rate reported for 2.1.227 is strongly excluded on this build in the cells I could reach. I don't think it supports a tight bound: my trials cluster into ~12 distinct session-states inside one Desktop process lifetime, so the defensible upper bound on a residual rate is roughly 20%, not "<5%".

The cold cell — the strongest documented trigger in this thread — I could not sample at all. On this build there's no way to end a session's CLI process while keeping the session listable: the session context menu offers only Pin / Mark as unread / Rename / Fork / Move to group / Archive / Delete. Archiving removes it from list_sessions, so a "no delivery" reading from an archived target would be fabricated rather than measured. Note also that Idle timeout reached, disconnecting does not produce a cold session — all 10 CLI processes stayed alive across repeated idle disconnects, so "unbound" and "cold" are genuinely different states and only the former is reachable here.

If someone on stable can run the deferred-path check in §1, that seems higher value than more warm-path trials.

wshallwshall · 7 days ago

Not a reproduction — a problem with how we've all been measuring this

Testing on 2.1.237 I did not reproduce the wedge. But before that result gets counted as another "fixed for me", I think the method most of us are using has a hole in it, and it points the wrong way.

Environment: Windows 11 Pro 26200 (VMware VM) · Claude Desktop 1.34493.1 (MSIX/Store) · engine 2.1.237 · stock install (settings.json is {}).

The transcript record is written at drain, not at receipt

Several reports here (mine included, initially) use the recipient's .jsonl transcript as the oracle: if a peer record appears and no answer follows, call it a wedge; if no peer record appears at all, call it a silent drop and file it elsewhere.

That second branch is the problem. I sent a probe into a provably in-flight turn — the tool returns a distinct string in that case:

Message queued for session local_… ("Session 3"); it will be processed
after the in-flight turn finishes if that session stays healthy.

Then sampled the recipient's transcript once per second:

08:28:17.78  absent      <- probe accepted by Desktop, "queued" returned
08:28:18.93  absent
08:28:20.08  absent
08:28:21.21  absent
08:28:22.34  absent
08:28:23.48  absent      <- log: Sending message to session
08:28:24.64  absent
08:28:25.77  PRESENT     <- log: [LocalSessionManager] drained 1 deferred send(s)

Correlating both clocks (log is local, transcript is UTC):

| clock | event |
|---|---|
| 08:28:23 | Desktop accepts, returns "queued" |
| 08:28:25 | drained 1 deferred send(s) |
| 13:28:25.042Z | queue-operation / enqueue record written |
| 13:28:25.069Z | user record written |
| 13:28:27.272Z | assistant answer |

While a message sits deferred, the recipient's transcript contains no record of it at all. The records appear at drain.

Why that matters for this issue's denominator

If a session wedges while a message is deferred, the transcript has nothing in it — no enqueue, no peer record. Under the usual rule that reads as "silent drop", which gets filed as #86237 / #86298 and excluded from this issue. So the transcript oracle is blind to precisely the deferred-path wedge, and some true positives here may already be sitting in those other threads.

I'd suggest anyone reporting either way says which oracle they used. The Desktop watchdog reads its own in-memory buffer, not the transcript (r.messageBuffer.some(e => e.type === "assistant")) — which is also why the reported signature is last_message_type=user: a message Desktop holds that the CLI never processed. That buffer, not the JSONL, is where this is visible.

The log path moved, which breaks the confirmation path in this issue

On Desktop 1.34493.1 the log directory relocated on Windows:

DEAD:  %APPDATA%\Claude\logs\main.log          (absent on this machine)
LIVE:  %LOCALAPPDATA%\Claude\Logs\main.log     (growing)

Reading the old path yields a confident, wrong "no log data". The live file is also truncated at every app launch, so capture it with tail -F before running anything.

Corrected grep strings

  • Grep the bare literal timed out after — no [CCD] tag, no id, no field assumptions. The emitted line does not carry the extra fields quoted upthread.
  • hadFirstResponse and reason come from the [CCD CycleHealth] line. Valid reasons: permission_stall | incomplete_response | no_response.
  • hadFirstResponse=false is not diagnostic on its own — this machine logs healthy cycle … (5s, hadFirstResponse=false). The discriminator is unhealthy + reason=no_response + a long duration.
  • A re-attach loss logs at info, not warn: no sign of the turn a re-adopted process. Corroboration sets that only grep warn will miss it.
  • A session blocked on a permission prompt is skipped by the watchdog entirely and emits no warn line ever — it reproduces the whole reported surface (message in, no output, spinner) silently. Keep probe messages zero-tool.

Two smaller corrections

  • operation:"remove" is not a drop signal. It is the normal path for a delivered mid-turn message — my own transcript shows a message that was delivered and acted on going enqueueremove.
  • The sender is not blind to deferral. "Message sent to session …" and "Message queued … if that session stays healthy" are different strings.

Scope of my clean result — please don't over-read it

15 sends, 8 sessions, 1 Desktop instance, 1 machine, ~40 minutes, all answered, no wedge candidate. Three took the deferred branch.

I am explicitly not offering that as evidence the bug is fixed:

  • the oracle above cannot see a deferred-path wedge, so "no wedge observed" is weak exactly where it matters most;
  • one Desktop instance is n≈1 on the instance-level variable;
  • this is engine 2.1.237, which is neither stable (2.1.231) nor latest (2.1.241), so it cannot speak to "stable channel still affected".
wshallwshall · 7 days ago

Thanks @WarmBed for renaming the issue. I've been burned before hoping this was fixed, but I've now tested it in three environments. Claude wrote the two summaries above with deep testing inside two VMs, one Windows Server 2025 and the other Windows 11.

I've also just upgraded my developer PC and it appears to be working well. If this holds, I'd recommend closing this issue.

WarmBed · 7 days ago

Closing per @wshallwshall's recommendation above — three independent environments (two VMs + his own dev machine) now confirm the fix, matching my own verified round-trip on 1.34493.1 / engine 2.1.237 and @InfiniteBSOD's 3-of-3 report on the same build.

Summary for anyone finding this later:

  • Root cause: Windows cross-session delivery was gated by a platform kill-switch introduced around engine 2.1.224+; the gate logic (and a workaround via CLAUDE_CODE_HARBOR_KITE=1) varied across engine builds — see the decompiled gate functions and version timeline upthread.
  • Fixed in engine 2.1.237 (Desktop 1.34493.1, MSIX/Store channel) — verified stock, no flag needed, transcript-level round-trip confirmed by multiple independent reporters.
  • If you're still hitting this, please open a new issue with your exact Desktop version, engine version (from a fresh transcript's version field), and channel (stable/latest/MSIX/Squirrel) — per @wshallwshall's methodology notes above, also rule out the "transcript blind window during a mid-turn hold" measurement trap before calling it a silent drop.

Thanks to everyone who dug into this with real evidence over the past two weeks — @wshallwshall, @CptCabbageee, @InfiniteBSOD, @Aturion31, and others. Reopening if new reproductions surface.

CptCabbageee · 5 days ago

Still reproduces tonight (2026-08-25) on Desktop app 1.34493.1.0, CCD engine 2.1.237, Windows 11 Home 26200 — the exact version this issue's title says is fixed for some installs.

One mcp__ccd_session_mgmt__send_message call to a peer local session returned success ({"success":true,"message":"... -> <target>","msg_id":"..."}), and the target's transcript viewer showed the message. But grepping main.log for that specific send (matched by exact message length, 270 chars, in the surrounding minute) turns up nothing: no LocalSessions.sendMessage line, no Sending message to session, no Mapping internal session — while the same target session logged clean sendMessage -> Sending message -> Mapping triples for dozens of other sends, from other fleet sessions, both immediately before and after this one that same evening.

That's the "no log line at all" variant flagged upthread on #86498 (Dimaxia, 2026-08-14 — "the fourth case: no log line at all — the tool still returned success"), so it's still live on 2.1.237, not just on the older builds. Happy to pull the exact log window if useful.

mimydrug · 5 days ago

Negative datapoint — engine 2.1.246 / Desktop 1.37937.1 (MSIX): no reproduction in 4.81 days across 153 sends.

Posting because @CptCabbageee reports this still live on 2.1.237 two days after the close, so a clean negative on a later MSIX build is evidence on whether that split is a version boundary or a staged rollout. There is also a measurement trap below that will otherwise manufacture false "fixed for me" reports.

Same machine as #86088 (closed as a duplicate of this issue yesterday), where I previously measured 14 reason=no_response in 4.47 days (≈3.1/day) on engine 2.1.229.

Environment: Windows 11 Pro for Workstations 10.0.26100 · Desktop 1.34493.11.37937.01.37937.1 across the window · engine 2.1.2372.1.2412.1.246 · MSIX/Store · stock, CLAUDE_CODE_HARBOR_KITE unset.

Result

Window 2026-08-21 18:54:10 → 2026-08-26 14:26:28 (4.81 d), counted in %LOCALAPPDATA%\Claude\Logs\main.log:

| signal | count |
|---|---|
| Sending message to session | 153 |
| Mapping internal session … to CLI session | 204 |
| reason=no_response | 0 |
| isRunning held by unechoed input at result | 0 |
| timed out after Ns of inactivity | 0 |

Ruling out the near misses, since "0" is only worth as much as what it excludes:

  • The only two unhealthy cycle verdicts in the window are both reason=api_error, hadFirstResponse=true — the pre-existing category, not this bug.
  • hadFirstResponse=false does appear 6 times, but every one of them reads healthy cycle at 1–21 s (short cycles that ended before a first response). None carries reason=no_response.

Scope limit — this does not cover the deferred path

drained N deferred send(s) appears 0 times in the entire window. Either no send ever landed on a mid-turn target here, or that line no longer appears in this build — I can't separate those two from the log alone. So this negative covers cold and warm delivery only and says nothing about the queued/mid-turn path, which is exactly where @wshallwshall's transcript blind-window applies.

⚠️ The log file moved on 2026-08-21, and the old one is still sitting there

This is the part I'd most like others to check, because it silently narrows your measurement window to zero while still returning a plausible-looking result.

  • old: %APPDATA%\Claude\logs\main.logfrozen at 2026-08-21 18:45:36, last line beforeQuitForUpdate handler fired, going down for update
  • new: %LOCALAPPDATA%\Claude\Logs\main.log — live

Grepping the old path for reason=no_response returns 0. Not because the bug is fixed, but because nothing has been written to that file in five days. Both of my earlier comments on #86088 cite the old path, as does the Root cause section of this issue. Anyone re-checking against the path used earlier in this thread will read a dead file and report a false negative. Worth a line in the repro instructions.

On the surviving variant

None of this speaks to @CptCabbageee's "no log line at all" case. That is the second failure point I tried to separate out in #86088 — sendMessage → Sending message → (no Mapping), i.e. the child is never wired up, as distinct from the child spawning and then wedging against the API (Resuming → Loaded N transcript messages → no start-timing). A fix to the platform gate would not necessarily touch it, so I'd read "fixed in 2.1.237" as scoped to the gate rather than to every path that lands in this watchdog.