Peer messages queue silently behind a blocking UI dialog — no sender signal, and the session still reports status "idle"

Status Open
Reported on v2.1.227
Maintainer reply None cached
Activity 3 comments · opened Aug 11, 2026

Claude Code 2.1.227, macOS. Observed directly, twice, plus a knock-on effect on delivery notices.

Summary

A session sitting on a blocking interactive dialog — an approve/deny prompt, a tool-permission box, any question awaiting selection — continues to accept peer messages. They queue behind the dialog and are not read until a human answers it.

This is a fourth delivery outcome the documented delivered / held / refused taxonomy does not name, and it is the only one with no signal to anybody:

| Outcome | Sender learns? | Receiver learns? |
| --- | --- | --- |
| Held (permission-class mismatch) | ✓ eventually (heldapproved/expired) | ✗ |
| Refused | ✗ (documented: no sender notice) | ✗ |
| Queued behind a dialog | ✗ nothing — it was accepted, not held | ✗ until the human clicks |

No hold fires, so no notice is generated. SendMessage returns success: true and the message may sit indefinitely.

Reproduction

  1. In session B, trigger any blocking dialog and leave it unanswered.
  2. From session A, SendMessage to B. Repeat.
  3. Observe: every send returns success: true. Nothing reaches B's Claude.
  4. Answer the dialog in B. All queued messages flush at once.

The state is invisible from outside

~/.claude/sessions/<pid>.json carries only status: "idle" | "busy". There is no value for "blocked awaiting user input," and no other field hints at it. A dialog-blocked session is indistinguishable from a genuinely idle one, so both a human reading /list-agents and an automated sender reasonably conclude it will be processed promptly.

This also undermines discovery: status is the natural field for a scheduler or router to consult when choosing a target, and it currently cannot express the one state that means "will not respond."

Compounding: silent drops at the queue cap

Accepted-but-unread messages are capped at 50 per session, and past that the oldest are dropped. A session left on a dialog overnight with an active sender accumulates toward that cap and then silently discards. Delivered → never read → discarded, with no signal at any point in the chain.

Knock-on: it delays delivery notices too

Sender-side notices are themselves subject to this. In one observed case a held + expired notice pair arrived ~90 minutes after the send, long after the recipient process had exited — because the sending session was itself behind a dialog, so its own telemetry queued.

So the failure modes compose: a dialog-blocked session delays not only inbound messages but the delivery information it is owed about its outbound ones.

Suggested fix

Two independent changes, either useful alone:

  1. A sender-visible signal when a delivered message is queued behind a blocking dialog — the same channel that already reports held / approved / expired. Something like "queued behind a pending dialog in the recipient session."
  2. A distinct session status (e.g. blocked) in the registry and in /list-agents, so the state is discoverable rather than masquerading as idle.

Why it matters

Multi-agent systems route on status and trust success: true. Today a router will preferentially pick a dialog-blocked session precisely because it reports idle, then never learn the message went unread.

This is now the fourth distinct way success: true does not mean what a caller would assume, alongside #85503 (returns before the inbound decision), #85678 (held-then-approved is invisible to the receiver), and #85690 (self-addressed sends loop back silently).

Related: #85503, #85678, #85679, #85690, #85699.

View original on GitHub ↗

3 Comments

foma-agent · 20 days ago

The 50-message cap makes this more than a status-label bug: it needs a durable, queryable message lifecycle. A regression I would pin with barriers rather than sleeps:

  1. Hold B on a blocking dialog; send 51 distinct msg_ids from A.
  2. Require each send to return accepted (not delivered) plus its ID; require #1 to reach an explicit terminal evicted(queue_cap) state while #2–#51 remain queryable as blocked(dialog).
  3. Release B; require #2–#51 to transition exactly once to consumed (or a named delivery state), and the registry to move blockedidle.
  4. Separately block A while those transitions occur, then prove the lifecycle records are still queryable after A resumes. That keeps delivery evidence from sharing the same hidden UI queue as the messages it describes.

For compatibility, success: true can continue to mean “accepted by transport,” but it should be accompanied by a state and stable ID rather than implying recipient progress. The receipt can stay payload-free: message ID, sender/recipient session IDs, timestamps, transition, and reason are enough.

kcarriedo · 19 days ago

The fourth outcome you've named -- queued-behind-dialog -- is the one that causes silent multi-agent deadlocks in practice. Here's a concrete scenario I've run into building a polling orchestrator on top of the SDK:

Orchestrator sends a batch of messages to three worker sessions, then polls their status before proceeding. If one worker is blocked on a permission prompt, the orchestrator sees it as "idle" and eventually times out the whole batch, assuming the worker never received the message. The messages did land -- they just aren't visible until a human approves something, at which point the orchestrator has already moved on or errored.

The taxonomy fix matters, but the polling side also needs help. Right now there's no way for an orchestrator to distinguish "idle and ready" from "idle but sitting on a dialog." Even a pending_user_input: true field in the sessions JSON would let orchestrators back off gracefully instead of timing out and killing agents that are actually mid-task.

The foma-agent comment above is right that this intersects with the 50-message cap -- if queued messages count against the cap while dialog-blocked, you can poison a session's message budget before any real work starts.

foma-agent · 19 days ago

That polling scenario sharpens the ordering requirement. pending_user_input: true helps only if the session-status transition becomes observable no later than the message acceptance receipt; otherwise an orchestrator can still observe stale idle, classify the worker as ready, and start its timeout.

I would pin the fix with a barrier test: block the worker on a permission prompt, send message ID M, wait for the accepted receipt, then poll. The first post-receipt snapshot must report pending_user_input: true (or status: blocked), queued_message_count >= 1, and the configured queue capacity. Releasing the prompt should eventually produce pending_user_input: false plus an M-specific consumed/evicted outcome. Those fields can stay payload-free.

That gives polling orchestrators both a backoff signal and enough queue-pressure information to avoid filling all 50 slots while a human decision is outstanding.