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
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
- Searched existing issues. Closest are #86212 (recipient never runs a turn, normal-mode, recoverable by typing) and #85888 (held-for-approval with no approval surface, macOS agents dashboard). This report covers what we believe is the unifying mechanism, adds a clean regression window on the Windows desktop app, and a file-level evidence chain. Also related: #86059, #85764.
- 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) orMessage 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_eventsshows 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_messagewith B's session id. - Receipt:
Message sent to session <id> ("<title>"). - Observe B:
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). - 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'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).- 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" — anddialogExpiry: "…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).
Log signatures (app log, %APPDATA%\Claude\logs\main.log)
Working era (pre-update log, same machine):
[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.
Suggested fixes
- Give the desktop message card approve/deny controls (and the agents dashboard equivalent, per #85888) — the CLI already has the whole flow; only the affordance is missing.
- 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
originso 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.
Showing cached comments. Read the full discussion on GitHub ↗
5 Comments
+1. Message is being sent, but not seen by the session so it stalls. This is happening on the latest version + on macOS
Independent reproduction on a different Windows build, same app version. Everything in this report matches what I observed, and I have one additional data point that may narrow where delivery stops.
Environment: Windows 11 Pro 26200 (this report is Windows 10), desktop app 1.28929.0, bundled CLI 2.1.227. Sessions running in git worktrees.
Deterministic repro, ~2 minutes from a clean state:
send_messagetargeting B with a unique token in the body.Step 6 is worth calling out as a control: the recipient session and its CLI child are entirely healthy: only the cross-session path fails.
Confirming your file-level findings: the unique token never appears in the recipient's JSONL.
list_eventsshows the<cross-session-message …>user event and the card renders, but nothing reaches the CLI transcript or starts a turn. The recipient's child process sat at ~2% of one core throughout: blocked, not computing.Possibly new: the bytes do reach the recipient's process. I captured a full user-mode memory dump of a hung recipient child and string-searched it. A distinctive phrase unique to the sent message appeared 4 times in the child's address space, and the
cross-session-messageenvelope marker 62 times, while that text never appeared in the recipient's transcript. If that holds up, delivery gets further down the pipe than transcript evidence alone shows, and the break is after the child receives the data rather than before.Caveat, stated plainly: presence in an address space does not establish the ingress path. Mapped files, shared sections, environment/argv data, or another IPC channel could all account for it, and it does not prove how far processing progressed. Offering it as a lead rather than a conclusion. I still have the dump and can run targeted searches against it if that would help.
Busy-recipient manifestation (matching #86059): when the target is mid-turn, the message is not merely late, the agent is wholly unaware of it. It renders in the target's window and never enters the model's context.
Operational impact worth adding: this deadlocks multi-agent coordination silently and in both directions. On this machine, two long-running sessions were each waiting on a "go-ahead" from a third; the messages between them were sent, receipted as successful, rendered in the UI, and lost. The sessions waited indefinitely for messages that had already been delivered-and-discarded, with no error at any layer to indicate it. The workaround is to copy the text out of the recipient's UI card and paste it into that session's own input box.
One more trap, for anyone debugging this:
send_messageis unusable as a diagnostic probe, because probing a session with it causes the hang you are investigating. That confounded several hours of my own investigation into an unrelated stuck session before I identified the tool as the cause rather than the instrument.Confirming an independent reproduction that matches this report, including the regression boundary.
Environment: Windows 11 Pro, desktop app 1.28929.0.0 (MSIX, updated 2026-08-13), bundled CLI 2.1.227, messages sent via the
ccd_session_mgmtMCP server (send_message/list_events).Observations (multi-session coordination workflow, ~10 sends today across 4+ sessions):
list_eventsas a<cross-session-message>user turn, but it never enters the recipient model's context and never starts a turn. Verified from both sides: the recipient model consistently reports nothing arrived while the human can see the card in that same window.Current workaround on our side: file-based coordination between sessions (a shared inbox file each session polls), which restores reliable handoff at the cost of latency.
Follow-up from the original reporter — three new results, including one that widens the blast radius beyond session messaging.
1. Retested on desktop app 1.30096.1.0 (yesterday's update): still broken
The app auto-updated to 1.30096.1.0 (bundles CLI 2.1.229). Same-day retest, same method (unique-marker probes, recipient JSONL sweeps):
app.asar's send path is functionally identical: same<cross-session-message … encoded="1">envelope, same class-lessorigin:{kind:'peer'}, same receipt strings, and thecrossSessionInbound/dialogExpiry/ dropped-with-denial schema strings are all still present.2.
crossSessionInbound: "accept"does not work as a workaroundSetting
"crossSessionInbound": "accept"in the user-scope settings file did not restore delivery even for an idle recipient whose CLI cold-starts fresh (i.e., a process that reads settings at startup): probe held and expired past the window, marker absent from the recipient transcript. Whatever layer enforces the hold on this lane ignores the user-scope key.(Correction from the first version of this comment: I originally reported the app's settings writer silently dropping this key on rewrite — that was wrong; the key had been removed manually on this machine after the first negative test. No evidence of the app dropping unknown settings keys.)
3. Held messages wedge the recipient's turn accounting — the user's OWN prompts then queue or arrive minutes late
This answers the deadlock/coordination observations above with a mechanism, captured live in one incident (app log + recipient transcript, times relative):
drained 1 deferred send(s)and hands the message to the CLI, registering the message uuid as awaiting "echo" (the CLI re-emitting it as a user turn). The CLI never echoes it — the message dies exactly like the idle-path ones — and the app logsisRunning held by unechoed input at result … pendingEchoUuids: ['<uuid>']. From here the app believes the session is busy forever.priority=next, which force-reclaims the turn; the wedge clears, and the T+8m prompt finally drains ~7 minutes late as a stale turn.So one undelivered cross-session message poisons
isRunningfor the recipient, and every subsequent normal-priority prompt from the user queues/stalls until a priority send or restart clears it. This is consistent with the memory-dump data point above (bytes reach the CLI child, held in-process, never transcripted) and explains the "multi-agent sessions deadlock silently" report: the queued receipt is technically honest, but the drain hands the message to a gate that eats it and additionally wedges the session.Practical mitigation for wedged sessions: sending another message flushes the queue (the UI sends follow-ups with
priority=next/ turn reclaim), or restart the session.4. The same class-less-origin pipeline carries the desktop "Auto-fix CI & address comments" wake-ups
Enumerating every
sendMessagecall site in the currentapp.asar: exactly two senders stamp a class-less non-userorigin— the peer/cross-session lane above, and the desktop app's PR watcher (the engine behind the public Auto-fix / "Auto-fix CI & address comments" per-PR toggle, internally sending withorigin:{kind:'auto-continuation'}, initiator:'ci-monitor'). When it finds new failing checks, merge-conflict state, or fresh review comments for a PR-bound session, it wakes that session via the identicalsendMessagecall the peer lane uses.Consequences under the current hold behavior (code-path analysis — the engine itself starts unconditionally ~60s after app launch and sweeps every 60s, but only acts on PRs where the user enabled the per-PR toggle; I have no auto-fix-enabled PR sessions on this machine, so this part is decoded, not live-reproduced here):
delivered:trueto the engine, which then marks those failures as notified and those review comments as seen — it never retries. Permanent silent loss of the notification, not a delay.This would look to users exactly like the long-standing reports that desktop Auto-fix events "never dispatch to the local session": #52474 (closed not-planned), #68083, #65488 — all describing GitHub events that were detected but never entered the session's context. If the hold applies to
auto-continuationorigins the way it does topeerorigins, those reports and this issue share a root cause in the delivery layer, and fixing the consent-gate handling for class-less desktop-origin messages fixes both features at once.Happy to run further targeted experiments on this machine if useful.
Same failure here on Windows desktop (app
1.30096.1, bundled runtime2.1.229). Four things measured today that add to the root-cause analysis in this thread, plus one confound I could not rule out locally.1. The proposed mechanism does not fully explain it.
Per the analysis, a class-less sender is "held only while this session bypasses permission prompts". I put the recipient in
permissionMode: "default"and the message was still never delivered.The mode change was verified in the recipient's own transcript before testing (not assumed):
If mode parity were the whole story, that case should have worked. It did not.
2.
crossSessionInbound: "accept"does not help.Set in
~/.claude/settings.json, then tested on a session created after writing it (so it was read at startup). Still dropped. This confirms what others reported.3. Not project-specific.
Reproduced in a bare directory with no
CLAUDE.md, no hooks, no local settings — two fresh sessions, sender to recipient, same silent drop.4. Exact last-known-good, and a warning about verification.
Measured by parsing
~/.claude/projects/<project>/*.jsonland counting only real inbound<cross-session-message from=...>tags insidetype: "user"turns:⚠️ Note for anyone debugging this:
list_eventsdoes render the message for the recipient session even though it never reaches that session's transcript on disk. Solist_eventscannot be used to verify delivery — it reflects the UI, not the transcript. Only the.jsonlcan. I spent hours with a wrong conclusion because of this, and a naivegrepmakes it worse: the sender's transcript also contains the message text, so it produces false positives unless you parse the JSON and require atype: "user"turn.---
Confound I could not rule out locally.
This machine also installed two Windows updates that finished applying on Aug 12 at 11:14 local —
KB5120708andKB5121003(both staged Aug 11 ~15:30, applied on the reboot). That falls inside the window where the channel died here, so I cannot separate them from the app regression:With no failed attempt inside that gap, there is nothing local to date the break with.
Do other affected users have
KB5120708/KB5121003installed? If anyone hits this without them, that rules Windows out and the app regression stands alone.Remedies tried here, all negative: new session as sender · new session as recipient · app restart · app update/restore (runtime stayed at
2.1.229) · recipient indefaultpermission mode ·crossSessionInbound: "accept"· different working directory.