You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Desktop app (Windows): cross-session messages silently dropped — held for an approval the UI never offers, then expire (~5 min); regression since app 1.28929.0 #86298
Bug report — desktop app (Windows): cross-session messages are silently dropped: held for an approval the UI never offers, then expire after ~5 minutes
Single bug report (the misleading receipts and the missing approval UI are facets of one delivery-pipeline defect; called out separately in "Suggested fixes").
Desktop app 1.28929.0 (current), bundled CLI 2.1.227; also reproduced with CLI 2.1.229.
Summary
Since the desktop app updated to 1.28929.0 (Aug 11), mcp__ccd_session_mgmt__send_message between two local desktop sessions:
returns a success receipt — Message sent to session … (idle target) or Message queued for session …; it will be processed after the in-flight turn finishes if that session stays healthy (busy target);
lands in the app's own per-session event store — list_events shows the <cross-session-message from="…" name="…" encoded="1"> user event, and the recipient's window renders a "Message from {title}" card;
never reaches the recipient CLI's transcript (JSONL), never starts or joins a turn, and is silently dropped after ~5 minutes. The card in the recipient's window offers no accept/approve/deny control of any kind (user-confirmed by inspection). No error or notification is emitted at any layer, on either side.
Before the app update this channel worked heavily on the same machine: ~200 delivered <cross-session-message> user turns across ~90 sessions and 10+ projects between Jul 25 and Aug 11, with idle recipients woken within seconds (Sending message to session … → Mapping internal session … to CLI session … → turn; fastest observed send→processed was ~119 ms). After the update, zero cross-session messages have reached any recipient's CLI transcript — verified by grepping unique marker strings across every sender/receiver JSONL involved (markers appear only in sender transcripts).
Environment
OS
Windows 10
Desktop app
1.28929.0 (MSIX; auto-updated Aug 11, which is the regression boundary)
Bundled CLI
2.1.227 (%APPDATA%\Claude\claude-code\2.1.227\claude.exe); failure identical with a 2.1.229 binary in the same slot, so the CLI generation is not the variable
Sessions
local desktop sessions, same user, same machine; no crossSessionInbound / dialogExpiry set in any settings scope (defaults apply)
Transport
MCP server ccd_session_mgmt (list_sessions / send_message / list_events)
Steps to reproduce
Open two desktop sessions A and B (different or same project folder — both reproduce).
Let B go idle.
From A, call mcp__ccd_session_mgmt__send_message with B's session id.
Receipt: Message sent to session <id> ("<title>").
Observe B: list_events shows the message as a user event; B's window renders the message card; B's CLI JSONL under ~/.claude/projects/... never receives it; no turn starts (app log: cycles end with hadFirstResponse=false; a later wake logs previous_message_not_found).
Wait ≥5 minutes; the message is gone for good — replying/waking B later does not surface it to the model. Sender receipt still says "sent".
Busy targets ("queued" receipt): the message is only ever processed if the in-flight turn ends within the ~5-minute window; long turns lose every queued message silently.
What we believe is happening (from the app's resource JS + CLI strings + settings schema)
The app's resources/app.asar (readable, minified JS) shows the pipeline:
The MCP handler wraps the body as <cross-session-message from="<host session id>" name="<sender title>" encoded="1">… and calls the session manager's sendMessage(target, envelope, undefined, {origin: {kind: 'peer', from, name}}) — note: no permission-mode class is asserted in origin.
sendMessage has three paths: idle target → cold-resume startSession({message}) (returns delivered:true → "Message sent…"); running target → deferredSends queue drained at the next turn boundary (→ "Message queued…"); user steers → holdSteer (this path still works — user steers deliver fine, peer messages do not).
The CLI/SDK settings schema (embedded in both the app and the CLI binaries) defines crossSessionInbound: 'accept' | 'hold' | 'refuse', default "mode parity": "a message auto-delivers only when the sending session's permission-mode class matches yours …; a sender that asserts no class is held only while this session bypasses permission prompts" — and dialogExpiry: "…how long a HELD cross-session message awaits approval, before … its safe no-action default (cancelled / dropped-with-denial). Defaults to 5m…".
The CLI contains a full TUI approval flow for held messages ("Released N held cross-session message(s) to Claude's queue", "That held message was already resolved before your approval/denial…", a peer-origin preview with verifiedPeerPid). The desktop app renders the held message but exposes none of these actions.
Putting it together: the desktop bridge sends class-less origin:{kind:'peer'} messages; desktop-managed recipients run in a bypass-class permission mode; mode parity therefore holds every message for an approval the desktop UI cannot grant; dialogExpiry (5m) then resolves them to dropped-with-denial. Every receipt, store append, and render still happens, which makes the loss invisible. The consent-gate strings are byte-identical across CLI 2.1.226/2.1.227/2.1.229, and 2.1.226 was the working-era CLI — so the gate itself predates the break; what changed at the app update is how the bridge's packets engage it (and/or the loss of the wake/approval surface). This also unifies #86212 (recipient in normal permission mode: classes match / weaker tier — messages park in the CLI queue unapproved-but-undropped and flush on the next human input) and #85888 (held-for-approval, no approval surface, macOS dashboard).
The send_message tool's own description still promises: "The message arrives in the target session as a user turn labelled 'From {this session's title}'" — currently not true for any idle desktop recipient.
Workaround experiment: setting crossSessionInbound: "accept" in the user-scope settings file ("an explicit value always wins" per the schema) did not restore delivery to an already-running recipient (marker still absent from its transcript past the expiry window; fresh-session/app-restart behavior not yet verified). One consistent explanation: on this lane the hold is enforced in the app's embedded SDK layer in front of the recipient CLI — which would also explain why the recipient CLI transcript shows nothing at all, and why the desktop sender receipt says "sent" where the CLI lane's sender is told "held for the recipient user's approval" (#85888).
[info] Sending message to session local_<target>
[info] Mapping internal session local_<target> to CLI session <uuid> <- seconds later, turn runs
Broken era (post-update): the send line still appears; no message-driven mapping line exists in the entire post-update log; instead:
[info] [CCD CycleHealth] healthy cycle for local_<target> (104s, hadFirstResponse=false)
[info] [CCD CycleHealth] unhealthy cycle for local_<target> ... (hadFirstResponse=false ... reason=no_response)
[info] [LocalSessionManager] flushed held steers (1 steer(s)) for local_<target> <- only user steers flush
Expected
A cross-session message either reaches the recipient model (as documented by the tool description), or the sender gets an honest receipt ("held for recipient approval; expires in N minutes"), or an error.
A held message is approvable somewhere on the surface that displays it.
Expiry produces a visible outcome (sender notification and/or a persistent "message dropped" event), not silence.
Trust same-user/same-machine/same-app-instance peer sends by default, or have the desktop bridge assert the sender's permission-mode class in origin so mode parity can evaluate honestly instead of falling into the class-less "held" branch.
Honest receipts: return "held for recipient approval (expires in Nm)" instead of "Message sent…" when the message is held; distinguish "queued" from "queued but will expire at HH:MM".
Notify the sender on expiry/denial (an error event or follow-up receipt) instead of dropping silently.
Don't expire held messages for idle/unattended recipients — park them until the next human focus (the recipient is precisely the session whose user isn't looking at it).
Evidence available on request
Full marker-probe matrix (10 sends across 9 sessions, sender/receiver JSONL sweeps), app-log excerpts for both eras, and the resource-JS excerpts quoted above. Filed after independent investigation by multiple sessions on this machine reached the same conclusion at file level.
Bug report — desktop app (Windows): cross-session messages are silently dropped: held for an approval the UI never offers, then expire after ~5 minutes
Preflight
Summary
Since the desktop app updated to 1.28929.0 (Aug 11),
mcp__ccd_session_mgmt__send_messagebetween two local desktop sessions:Message sent to session …(idle target) orMessage queued for session …; it will be processed after the in-flight turn finishes if that session stays healthy(busy target);list_eventsshows the<cross-session-message from="…" name="…" encoded="1">user event, and the recipient's window renders a "Message from {title}" card;Before the app update this channel worked heavily on the same machine: ~200 delivered
<cross-session-message>user turns across ~90 sessions and 10+ projects between Jul 25 and Aug 11, with idle recipients woken within seconds (Sending message to session …→Mapping internal session … to CLI session …→ turn; fastest observed send→processed was ~119 ms). After the update, zero cross-session messages have reached any recipient's CLI transcript — verified by grepping unique marker strings across every sender/receiver JSONL involved (markers appear only in sender transcripts).Environment
%APPDATA%\Claude\claude-code\2.1.227\claude.exe); failure identical with a 2.1.229 binary in the same slot, so the CLI generation is not the variablecrossSessionInbound/dialogExpiryset in any settings scope (defaults apply)ccd_session_mgmt(list_sessions/send_message/list_events)Steps to reproduce
mcp__ccd_session_mgmt__send_messagewith B's session id.Message sent to session <id> ("<title>").list_eventsshows the message as a user event; B's window renders the message card; B's CLI JSONL under~/.claude/projects/...never receives it; no turn starts (app log: cycles end withhadFirstResponse=false; a later wake logsprevious_message_not_found).Busy targets ("queued" receipt): the message is only ever processed if the in-flight turn ends within the ~5-minute window; long turns lose every queued message silently.
What we believe is happening (from the app's resource JS + CLI strings + settings schema)
The app's
resources/app.asar(readable, minified JS) shows the pipeline:<cross-session-message from="<host session id>" name="<sender title>" encoded="1">…and calls the session manager'ssendMessage(target, envelope, undefined, {origin: {kind: 'peer', from, name}})— note: no permission-mode class is asserted inorigin.sendMessagehas three paths: idle target → cold-resumestartSession({message})(returnsdelivered:true→ "Message sent…"); running target →deferredSendsqueue drained at the next turn boundary (→ "Message queued…"); user steers →holdSteer(this path still works — user steers deliver fine, peer messages do not).crossSessionInbound: 'accept' | 'hold' | 'refuse', default "mode parity": "a message auto-delivers only when the sending session's permission-mode class matches yours …; a sender that asserts no class is held only while this session bypasses permission prompts" — anddialogExpiry: "…how long a HELD cross-session message awaits approval, before … its safe no-action default (cancelled / dropped-with-denial). Defaults to 5m…".verifiedPeerPid). The desktop app renders the held message but exposes none of these actions.Putting it together: the desktop bridge sends class-less
origin:{kind:'peer'}messages; desktop-managed recipients run in a bypass-class permission mode; mode parity therefore holds every message for an approval the desktop UI cannot grant;dialogExpiry(5m) then resolves them to dropped-with-denial. Every receipt, store append, and render still happens, which makes the loss invisible. The consent-gate strings are byte-identical across CLI 2.1.226/2.1.227/2.1.229, and 2.1.226 was the working-era CLI — so the gate itself predates the break; what changed at the app update is how the bridge's packets engage it (and/or the loss of the wake/approval surface). This also unifies #86212 (recipient innormalpermission mode: classes match / weaker tier — messages park in the CLI queue unapproved-but-undropped and flush on the next human input) and #85888 (held-for-approval, no approval surface, macOS dashboard).The
send_messagetool's own description still promises: "The message arrives in the target session as a user turn labelled 'From {this session's title}'" — currently not true for any idle desktop recipient.Workaround experiment: setting
crossSessionInbound: "accept"in the user-scope settings file ("an explicit value always wins" per the schema) did not restore delivery to an already-running recipient (marker still absent from its transcript past the expiry window; fresh-session/app-restart behavior not yet verified). One consistent explanation: on this lane the hold is enforced in the app's embedded SDK layer in front of the recipient CLI — which would also explain why the recipient CLI transcript shows nothing at all, and why the desktop sender receipt says "sent" where the CLI lane's sender is told "held for the recipient user's approval" (#85888).Log signatures (app log,
%APPDATA%\Claude\logs\main.log)Working era (pre-update log, same machine):
Broken era (post-update): the send line still appears; no message-driven mapping line exists in the entire post-update log; instead:
Expected
Suggested fixes
originso mode parity can evaluate honestly instead of falling into the class-less "held" branch.Evidence available on request
Full marker-probe matrix (10 sends across 9 sessions, sender/receiver JSONL sweeps), app-log excerpts for both eras, and the resource-JS excerpts quoted above. Filed after independent investigation by multiple sessions on this machine reached the same conclusion at file level.