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

Status Closed — duplicate
Reported on v2.1.227
Maintainer reply None cached
Activity 26 comments · opened Aug 13, 2026 · closed Aug 25, 2026

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:

  1. 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);
  2. 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;
  3. 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

  1. Open two desktop sessions A and B (different or same project folder — both reproduce).
  2. Let B go idle.
  3. From A, call mcp__ccd_session_mgmt__send_message with B's session id.
  4. Receipt: Message sent to session <id> ("<title>").
  5. 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).
  6. 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).

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

  1. 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.
  2. 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.
  3. 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".
  4. Notify the sender on expiry/denial (an error event or follow-up receipt) instead of dropping silently.
  5. 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.

View original on GitHub ↗

5 Comments

kocouso · 17 days ago

+1. Message is being sent, but not seen by the session so it stalls. This is happening on the latest version + on macOS

scarson · 17 days ago

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:

  1. Create session B in the desktop app, give it one trivial turn, let it go idle.
  2. From session A, call send_message targeting B with a unique token in the body.
  3. B renders the "Message from {title}" card and starts its working indicator.
  4. B produces nothing — no thinking trace, no output — indefinitely. Observed >2 min.
  5. Stop returns B to a normal prompt, undamaged.
  6. Typing the same text directly into B's input box immediately afterwards works normally, first try.

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_events shows 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-message envelope 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_message is 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.

cloverthe-ai · 17 days ago

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_mgmt MCP server (send_message / list_events).

Observations (multi-session coordination workflow, ~10 sends today across 4+ sessions):

  • The sender always gets a success receipt: "Message sent to session ..." when the recipient is idle, or "Message queued for session ...; it will be processed after the in-flight turn finishes" when the recipient is mid-turn.
  • The message card renders in the recipient's window, and the message is visible to third-party sessions via list_events as 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.
  • Bidirectional: A-to-B and B-to-A are both affected, with both endpoints freshly started under app 1.28929.0 / CLI 2.1.227 (not sessions resumed from before the update).
  • No approval dialog ever appears on the recipient side; held messages just vanish, consistent with the ~5 minute expiry described in the report.
  • Regression boundary matches: on the same host before the app updated to 1.28929.0 (bundled CLI 2.1.207), same-machine desktop messaging had worked for weeks. (2.1.207 exhibited the separate inter-session message accumulation bug fixed in 2.1.226; this held-then-silently-dropped behavior appeared only after the app update.)
  • One diagnostic trap worth noting for others: replies addressed to a stale/forked-away sessionId fail with the same silence, so address staleness and this bug initially masked each other during our diagnosis.

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.

arthurmoraesfernandes-afk · 17 days ago

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):

  • Idle-target sends from this reporter's session and from three sessions freshly launched under the new app: receipts say "sent", the card renders, the recipient's event store has the message — and no recipient CLI transcript ever receives it, no turn starts, expiry as before. Confirmed for both pre-update and freshly-created sessions.
  • The new app.asar's send path is functionally identical: same <cross-session-message … encoded="1"> envelope, same class-less origin:{kind:'peer'}, same receipt strings, and the crossSessionInbound / dialogExpiry / dropped-with-denial schema strings are all still present.

2. crossSessionInbound: "accept" does not work as a workaround

Setting "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):

  1. T+0 — a peer message arrives while the target session is mid-turn → receipt "Message queued…", app parks it in its deferred-send queue.
  2. T+7m — the turn ends; the app logs 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 logs isRunning held by unechoed input at result … pendingEchoUuids: ['<uuid>']. From here the app believes the session is busy forever.
  3. T+8m — the human types a prompt into that session's own input box → treated as a mid-turn send → queued behind a turn that will never end.
  4. T+10m — the human's next prompt goes out with 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 isRunning for 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 sendMessage call site in the current app.asar: exactly two senders stamp a class-less non-user origin — 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 with origin:{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 identical sendMessage call 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):

  • Idle PR-bound session → the CI-failure/review-comment wake rides the same cold-start injection that provably never reaches the model → held and expired.
  • Busy PR-bound session → deferred queue → drained at turn end → unechoed → wedges the session (section 3).
  • Worst detail: a held message still returns delivered:true to 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-continuation origins the way it does to peer origins, 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.

Bianco24siete · 17 days ago

Same failure here on Windows desktop (app 1.30096.1, bundled runtime 2.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):

"permissionMode":"auto"  ->  "auto"  ->  "default"

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>/*.jsonl and counting only real inbound <cross-session-message from=...> tags inside type: "user" turns:

1,945  inbound messages written to transcripts, historically
last:  2026-08-12T02:40:42Z
after: zero, across 9 test messages to 4 different sessions

⚠️ Note for anyone debugging this: list_events does render the message for the recipient session even though it never reaches that session's transcript on disk. So list_events cannot be used to verify delivery — it reflects the UI, not the transcript. Only the .jsonl can. I spent hours with a wrong conclusion because of this, and a naive grep makes it worse: the sender's transcript also contains the message text, so it produces false positives unless you parse the JSON and require a type: "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 localKB5120708 and KB5121003 (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:

last successful delivery   Aug 11 22:40 local
updates applied            Aug 12 11:14 local
send attempts in between   1, and it was delivered successfully

With no failed attempt inside that gap, there is nothing local to date the break with.

Do other affected users have KB5120708 / KB5121003 installed? 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 in default permission mode · crossSessionInbound: "accept" · different working directory.

Showing cached comments. Read the full discussion on GitHub ↗