send_message (ccd_session_mgmt) silently stops delivering and wedges the recipient session — regression since CLI 2.1.227

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

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_message calls 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:

  1. delivered means "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.
  1. The queue is in-memory and is discarded on teardown. Mid-turn sends are parked on o.deferredSends, and teardownQuery does:

``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

  1. Open two local CCD sessions, A and B, in the same working directory.
  2. From A, call send_message targeting B with any body.
  3. Observe the tool result: Message sent… or Message queued….
  4. Inspect B's transcript in ~/.claude/projects/<project>/<cliSessionId>.jsonl for a user turn whose text starts with <cross-session-message from=. It is absent.
  5. Ask B directly whether it received anything. It has not.
  6. Check ~/Library/Logs/Claude/main.log for Session <B> timed out after Ns … reason=no_response roughly 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=\", not from=".
  • The origin field is not a usable discriminator: delivered peer messages are recorded with origin: {kind: "human"}, even those that arrived successfully before the regression.

Suggested fixes

  1. 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.
  2. Persist deferredSends alongside session metadata and re-inject on respawn, instead of deferredSends = void 0 on teardown.
  3. 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.
  4. Surface reason=no_response teardowns to the sender, so a dropped handoff is at least observable.

View original on GitHub ↗

3 Comments

mrpoele · 10 days ago

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 the 2.1.227 you identify as the regression point and below your 2.1.222 last-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 | lastActivityAt after 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 — isRunning flipped to true and lastActivityAt stamped 4–16s after delivery — and then that timestamp never advanced again. Nothing was ever written. Some sessions later dropped back to isRunning: false with the timestamp still frozen at the wake instant, i.e. the turn ended without executing anything.

Ruled out here:

  • Write-permission / path. Round 1 asked each session to write outside its cwd, round 2 inside its own cwd. Both 0/5.
  • A blocking permission prompt. Sessions that returned to isRunning: false with a frozen timestamp had no prompt outstanding — the turn ended, it did not stall awaiting input.
  • Stale long-lived sessions. A session created 90 seconds before being pinged wedged identically. Age is irrelevant.
  • Delivery. Sends succeed and targets demonstrably wake. Delivery reaches the session record; execution never starts.

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.

mrpoele · 10 days ago

Follow-up to my comment above: still broken on 2.1.237 after a clean upgrade and a full restart.

I was on 2.1.114 previously. Upgraded via npm install -g @anthropic-ai/claude-code@latest (2.1.114 → 2.1.237), confirmed with claude --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, lastActivityAt stamped), 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:

  • It rules out "upgrade to current" as a workaround — current is where I now am.
  • It rules out restart as a workaround. The wedged sessions were all stopped and relaunched; the very first ping after that wedged again.
  • Combined with 2.1.114 being affected, the fault spans the oldest and newest builds in play here, which sits awkwardly with a regression bounded at 2.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, effort high.

Happy to run any specific diagnostic if it would help — I have a reliable repro here, though each attempt costs the target session.

mrpoele · 10 days ago

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_message did 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:

  • The payload is fine. Identical content.
  • The session is fine. Never wedged in any lasting sense; it accepts and executes human-typed input normally, before and after a failed peer send.
  • Only the peer-delivery path fails. The message never reaches the model's context when sent via 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 + frozen lastActivityAt symptom. 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 on 2.1.114), desktop app.

Showing cached comments. Read the full discussion on GitHub ↗