Cross-session send_message silently lost after worktree recycle (reports sent, never delivered)
Summary
Cross-session messages (mcp__ccd_session_mgmt__send_message) sent to a session whose worktree was recycled are silently lost: the sender gets a success result ("Message sent" / "Message queued"), but the recipient session never receives the message. After the recycle, even messages addressed to what appears to be the correct successor session are lost.
Environment
- Claude Code (desktop app harness with CCD session management MCP), Windows 11 (win32)
- Long-running multi-session project: several concurrent local sessions coordinating via
send_message/list_events
Steps to reproduce (as observed over several days)
- Session A (long-lived "coordinator" session) runs in a git worktree. The harness recycles the worktree mid-conversation ("The git worktree ... was recycled. This session now operates on a fresh worktree ...", same branch, new path). The session continues normally.
- Session B calls
send_messagetargeting Session A by its sessionId (obtained earlier fromlist_sessionsor from a previously received message's "from" attribution). - Session B receives a success result:
Message sent to session ...(orMessage queued for session ...). - Session A never receives the message — no
<cross-session-message>block ever appears in its context on any subsequent user turn.
Observed 4 independent losses over 3 days, from two different sender sessions. One loss occurred even when the sender explicitly replied to a freshly received message from Session A (i.e. addressing the "from" session), so the stale-id explanation may not cover all cases.
Additional observation: the pre-recycle session still appears in list_sessions output of other sessions (with its old branch/PR metadata), so senders keep resolving the dead id as a valid target.
Expected
Either:
- delivery to the successor session (the recycle keeps the conversation alive, so logically the recipient still exists), or
- an explicit error to the sender ("session not found / recycled"), so the sender knows to re-resolve the target.
Silent success with no delivery is the worst of both: both sides believe the message went through, and coordination silently breaks (in our case, two agent "strategist" sessions lost three days of hand-off packages before noticing).
Workaround we use
Recipients periodically read correspondents' transcripts via list_events instead of relying on delivery; senders treat >1 day of silence after a substantive message as a probable recycle.
3 Comments
The worktree-recycle trigger here lands in the same pipeline — #86298 documents the same failure chain with root-cause analysis (Windows desktop, regression at app 1.28929.0 / bundled CLI 2.1.227, still present on app 1.30096.1.0 / CLI 2.1.229): the desktop bridge sends the message with a class-less
origin:{kind:'peer'}, the recipient CLI/SDK's cross-session consent gate holds it for an approval no desktop surface offers, anddialogExpiry(~5 min) then drops it — while the sender keeps a success receipt. A memory-dump repro in that thread shows the payload reaching the recipient CLI process but never its transcript. SettingcrossSessionInbound: "accept"in user settings does not unblock it (tested on running, cold-started, and post-restart fresh sessions).Adding a data point from a different environment (macOS desktop app, local sessions all sharing one project directory — not a git-worktree setup, so the recycle trigger described above doesn't apply here).
Same symptom, different/no trigger: over one working day, 5 independent peer sessions (in a coordinator + ~15-peer fleet) could reliably receive
send_messagefrom the coordinator, but their own replies back to the coordinator silently never arrived — one-directional, not a full bidirectional loss. The coordinator only recovered the real content by reading each peer's transcript directly (list_events) instead of relying on delivery — same workaround already described in this issue.Possibly useful new data point — respawn does not fix it: one affected session was archived and replaced with a brand-new session (fresh conversation, no prior history, new sessionId). Its first message to the coordinator (an identification ping) delivered successfully; every message after that reverted to the same one-directional failure within minutes. So whatever's wrong isn't tied to an individual session's age/state/history — a session that was never "recycled" at all (it never existed before) hit the identical failure almost immediately. That seems relevant to narrowing down the root cause regardless of platform.
One affected session's own transcript logged, unprompted, after a failed send: "Relay failed again as expected (consistent pattern all session)" — so the failure is consistent enough that the session itself started expecting it.
Happy to share session IDs/timestamps if useful for correlating with server-side logs.
The "reports sent, never delivered" symptom after worktree recycle is a rough one - the sender has no way to know the message is being silently dropped, so multi-session coordination falls apart quietly rather than loudly.
I've run into a related pattern (not worktree-specific): in a coordinator + multiple peer session setup, messages sent to a session that's been idle for a while are accepted by the
send_messagecall but the recipient never processes them. The session's event log shows the message arrived but no turn starts. The only reliable recovery is to attach to the recipient session interactively and prompt it manually, which defeats the purpose of autonomous multi-session coordination.One thing that's helped slightly: before sending a coordinating message, having the sender first call
list_eventson the target session to confirm it's actually in a receiving state. Iflist_eventsshows recent activity, delivery is more reliable. If the session has been quiet for a while, the message tends to get dropped. Still not a real fix - you shouldn't need to pre-check liveness before everysend_message- but it's surfaced a correlation worth logging.The core issue feels like missing liveness semantics on the send_message surface: the caller needs to know whether delivery is "queued and guaranteed" vs "best-effort-and-may-drop", and right now it's the latter with a success status code.