send_message (ccd_session_mgmt) silently stops delivering and wedges the recipient session — regression since CLI 2.1.227
Summary
Cross-session send_message (the ccd_session_mgmt MCP tool in Claude Desktop) silently stopped delivering on this machine on 2026-08-12 ~06:14 UTC. Since then, 197 consecutive sends returned a success acknowledgement and none were delivered — no error, no retry, no trace.
Worse, the same code path wedges the recipient session: after a peer message, the target enters timed out … hadFirstResponse=false, reason=no_response 37.4% of the time, versus 1.9% for human-typed messages on the same machine in the same period.
This is not a "message shows as Queued in the UI" cosmetic issue. The messages never reach the recipient model's context at all.
Environment
| | |
|---|---|
| Claude Desktop | 1.32885.1 |
| Claude Code CLI | 2.1.234 (current); regression first observed under 2.1.227 |
| Last known-good CLI | 2.1.222 (running 2026-08-09 → 08-10) |
| macOS | 26.5.2 (25F84), arm64 (M4) |
| Usage pattern | 8–11 concurrent local CCD sessions in one working tree, exchanging handoffs via send_message |
Impact
Multi-session workflows that rely on send_message for coordination lose work silently. Because the tool acknowledges success, neither the sender nor the user has any signal. In our case this went unnoticed for eight days and cost ~197 handoffs, including deployment requests that were never actioned.
Symptom 1 — silent non-delivery, with a success ack
Delivered peer messages appear in the recipient's transcript as a user turn whose text begins with <cross-session-message from="local_…">. Counting those strictly across all 211 transcript files in ~/.claude/projects/:
- 1,661 envelopes delivered between 2026-07-06 and 2026-08-12T06:14:19Z
- 0 delivered after that timestamp
- 197
send_messagecalls in that window, all returning either
Message sent to session <id> ("<title>"). or
Message queued for session <id> ("<title>"); it will be processed after the in-flight turn finishes if that session stays healthy.
Both acknowledgements fail equally — queued is not a distinct, worse state; both simply mean "the desktop accepted it".
Behavioural confirmation (not just file inspection): when asked directly, a recipient session replied "I have not received anything … nothing has reached me", and had taken no action on any of the four handoffs sent to it over 14 hours. So the content never entered the recipient's context — this is not merely a transcript-persistence gap.
Symptom 2 — peer messages hang the recipient (this is the more serious one)
From ~/Library/Logs/Claude/main*.log, classifying each Sending message to session <id> as peer (matched to a send_message tool call within ±90 s) or human (the remainder), and checking whether a Session <id> timed out after Ns of inactivity followed within 25 minutes:
| trigger | followed by a no_response timeout |
|---|---|
| peer (send_message) | 64 / 171 = 37.4 % |
| human-typed | 21 / 1099 = 1.9 % |
A ~20× difference on the same machine, same sessions, same period. Aggregate over the log window: 91 reason=no_response timeouts, 186 cycles with hadFirstResponse=false.
Representative sequence — the recipient CLI process was alive, idle, and had answered its human 90 minutes earlier:
21:27:04 [info] Sending message to session local_715d… <- peer handoff
21:43:39 [warn] [CCD] Session local_715d… timed out after 995s of inactivity
(hadFirstResponse=false, last_message_type=user,
last_tool_name=none, seconds_since_stderr=never)
21:43:39 [info] [CCD CycleHealth] unhealthy cycle for local_715d…
(995s, hadFirstResponse=false, reason=no_response)
21:43:40 [info] Session local_715d… query iterator completed
995 s is measured exactly from the peer message. The CLI accepted the input and produced no first response. The session surfaces a yellow warning badge and appears "stuck doing nothing" — which is how the user first noticed. The same session answers human input normally.
Why the tool cannot tell the truth (root cause analysis)
From the shipped app.asar, the tool handler returns:
let d = await i.sendMessage(t, u, undefined, { origin: { kind: 'peer', from: r.sessionId, … } })
return d.delivered
? { text: d.queued ? `Message queued for session …` : `Message sent to session …` }
: { text: `Message could not be delivered …: ${d.reason}`, isError: true }
Two structural problems:
deliveredmeans "handed off to the CLI", not "delivered". The transcript is written by the recipient's CLI. If that CLI never responds, nothing is written and nothing is reported back. The acknowledgement is generated before any evidence of delivery exists.
- The queue is in-memory and is discarded on teardown. Mid-turn sends are parked on
o.deferredSends, andteardownQuerydoes:
``js``
e.query = null, e.inputStream = null, …, e.deferredSends = void 0, …
drainDeferredSends only runs at a turn boundary and requires a live inputStream. So any teardown (timeout, crash-respawn, stale-credential cold respawn, cwd gone, app quit) destroys the queued messages with no persistence, no retry and no notification. The acknowledgement string even hedges this — "if that session stays healthy" — but the sender has no way to learn that it didn't.
Note the two symptoms compound: Symptom 2 causes the teardown that Symptom 1's in-memory queue cannot survive.
Reproduction
- Open two local CCD sessions, A and B, in the same working directory.
- From A, call
send_messagetargeting B with any body. - Observe the tool result:
Message sent…orMessage queued…. - Inspect B's transcript in
~/.claude/projects/<project>/<cliSessionId>.jsonlfor auserturn whose text starts with<cross-session-message from=. It is absent. - Ask B directly whether it received anything. It has not.
- Check
~/Library/Logs/Claude/main.logforSession <B> timed out after Ns … reason=no_responseroughly 995 s later.
How to verify a fix
# count genuinely delivered envelopes (must start the turn text; a plain
# substring grep also matches prose mentions and compaction summaries)
grep -c 'cross-session-message from=\\"' ~/.claude/projects/*/*.jsonl
Two measurement traps we hit, in case they help:
- In the JSONL the quotes are escaped, so the probe is
from=\", notfrom=". - The
originfield is not a usable discriminator: delivered peer messages are recorded withorigin: {kind: "human"}, even those that arrived successfully before the regression.
Suggested fixes
- Don't report success without evidence of delivery. Resolve the tool call only once the recipient CLI has echoed/committed the injected turn, or return a distinct "accepted, delivery unconfirmed" state the sender can act on.
- Persist
deferredSendsalongside session metadata and re-inject on respawn, instead ofdeferredSends = void 0on teardown. - Investigate why a peer-origin message yields no first response while a human-origin message on the same live CLI is handled normally. This is the primary defect; the silent loss is its consequence.
- Surface
reason=no_responseteardowns to the sender, so a dropped handoff is at least observable.
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Confirming this on Windows, and with one data point that contradicts the stated regression window.
Claude Code CLI
2.1.114, Windows 11 Pro 10.0.26200, desktop app. That is below the2.1.227you identify as the regression point and below your2.1.222last-known-good — yet the wedge behaviour is fully present here. So either the regression is considerably older than 2.1.227, or two distinct faults are being conflated under the same symptom.Controlled repro, 11 pings across 6 sessions, 0 successes. Each session was sent an identical trivial task ("write one line to one file, do nothing else"):
| session cwd | session age at ping |
lastActivityAtafter ping | file written ||---|---|---|---|
| ProjectA | 2 months | +7s, then frozen | no |
| ProjectB | — | +10s, then frozen | no |
| ProjectC | 2 months | +13s, then frozen | no |
| ProjectD | 2 weeks | +14s, then frozen | no |
| ProjectE (git worktree) | 3 months | +16s, then frozen | no |
| (same five, second round) | — | +5s to +14s, then frozen | no |
| ProjectA (second session) | 90 seconds | +4s, then frozen | no |
Every send returned
Message sent to session .... Every target measurably woke —isRunningflipped totrueandlastActivityAtstamped 4–16s after delivery — and then that timestamp never advanced again. Nothing was ever written. Some sessions later dropped back toisRunning: falsewith the timestamp still frozen at the wake instant, i.e. the turn ended without executing anything.Ruled out here:
isRunning: falsewith a frozen timestamp had no prompt outstanding — the turn ended, it did not stall awaiting input.Wedge rate 100%, not 37.4%. Every one of the 11 pings wedged its target. That may be a platform difference, or it may be that this machine sits on the more severe end of the same fault.
Same-day regression window. One of these sessions was working normally earlier the same day — it received messages and wrote output files at 11:08, 11:22 and 11:40 SAST (UTC+2). First failure on that same session was ~12:46 SAST. No restart, no settings change and no version change in between, which suggests whatever flips this is runtime state rather than a build.
Practical note matching your "silently" point: because the sender gets a success ack, a multi-session workflow just stops producing output with no error anywhere. It took a deliberate ping test to find it — and each ping costs a session, since the target has to be restarted afterwards.
Follow-up to my comment above: still broken on
2.1.237after a clean upgrade and a full restart.I was on
2.1.114previously. Upgraded vianpm install -g @anthropic-ai/claude-code@latest(2.1.114 → 2.1.237), confirmed withclaude --version, then restarted the app so every session was stopped and re-launched. Pinged a session with the same trivial one-file task.Result identical to every previous attempt: send returned
Message sent to session ..., the target woke 5 seconds later (isRunning: true,lastActivityAtstamped), and then froze there. No file written, timestamp never advanced.Running tally on this machine:
| build | fresh install | app restarted | pings | replies |
|---|---|---|---|---|
|
2.1.114| no | no | 11 | 0 ||
2.1.237| yes | yes | 1 | 0 |Why this may be worth noting for triage:
2.1.114being affected, the fault spans the oldest and newest builds in play here, which sits awkwardly with a regression bounded at2.1.227. Either the bound is wrong, or there are two separate faults presenting the same way.Environment unchanged from my earlier comment: Windows 11 Pro 10.0.26200, desktop app, target session model
claude-fable-5, efforthigh.Happy to run any specific diagnostic if it would help — I have a reliable repro here, though each attempt costs the target session.
Correction and a useful discriminator.
First, correcting myself: in my earlier comment I said each ping "costs the target session, since the target has to be restarted afterwards." That is wrong. The session is not damaged at all.
The machine owner here points out something that had escaped me during testing: a session that failed to act on a
send_messagedid the work correctly the moment he manually copied and pasted the same text into it. Same text, same session, same task — the only variable was how the message was delivered.So:
send_message.This lines up exactly with the 37.4% vs 1.9% split in the original report — peer messages versus human-typed messages on the same machine. Reproduced here independently, and on this machine the peer path is at 100% failure.
That also reframes the
isRunning: true+ frozenlastActivityAtsymptom. It is not the session hanging on work it cannot finish. It looks more like the session being brought up to receive something that never arrives, then sitting in that state — consistent with #86498's "phantom turn" description.Workaround for anyone blocked on this: paste the message into the target session by hand. The receiving session behaves completely normally. Multi-session workflows still function; they just need a human for each handoff instead of running autonomously.
Environment unchanged: Windows 11 Pro 10.0.26200, Claude Code
2.1.237(also reproduced on2.1.114), desktop app.