Channel notifications enqueue forever into an abandoned session after /fork (13 enqueues, 0 dequeues) — inbound messages silently never surface

Status Open
Reported on v2.1.218
Maintainer reply None cached
Activity 0 comments · opened Jul 23, 2026

Summary

After a session runs /fork, the conversation continues under a new session id, but the original session's process keeps running with its MCP channel servers attached — and every subsequent notifications/claude/channel is enqueued into the old session's queue and never dequeued.

The queue is not rejected, not errored, and not empty — it just grows forever. In my case an inbound customer email sat in that orphaned queue for over an hour while the user, working in the new session id, had no idea it had arrived. They eventually asked "did we get that email?" and it had to be retrieved by querying the upstream API by hand.

From the MCP server's perspective everything looks healthy: the notification is written to the transport and acked. There is no signal anywhere that the message will never be seen.

Environment

  • Claude Code 2.1.218 (also present on 2.1.216/2.1.217), Linux (Ubuntu 26.04)
  • Three in-house stdio MCP channel servers (an email bridge, a chat bridge, and a task-tracker bridge), each declaring the experimental claude/channel capability and delivering notifications/claude/channel
  • Launched interactively with --dangerously-load-development-channels server:<name> … --continue
  • Session hosted via the daemon / pty-host architecture (claude bg-spare, claude bg-pty-host)

Evidence

Session ids below are truncated; S1 = the original long-running session, S2 = the session id the conversation continued under. Both live in the same project dir. All content is redacted — only the queue mechanics matter.

From ~/.claude/projects/<project>/<S1>.jsonl, the queue-operation records:

19:04:03  enqueue → remove     (channel event, drained in ~1s)   ← healthy
19:06:50  enqueue → remove     (channel event, drained in ~10s)  ← healthy
19:10:50  enqueue → remove     (channel event, drained in ~16s)  ← healthy
19:15:26  dequeue              ← S1 dequeues a queued "/fork …" prompt. LAST DEQUEUE EVER.
19:22:11  enqueue              (channel event) ─┐
19:23:50  enqueue              (redelivery)     │
19:26:50  enqueue              (redelivery)     │
19:30:50  enqueue              (redelivery)     │  13 enqueues
19:38:50  enqueue              (redelivery)     │  0 dequeues
19:54:50  enqueue              (redelivery)     │  0 removes
20:24:50  enqueue              (redelivery)     │
20:54:50  enqueue              (redelivery)     │  still queued
21:07:43  enqueue              (chat event)     │  as of writing
21:12:44  enqueue              (redelivery)     │
21:22:44  enqueue              (redelivery)     │
21:42:44  enqueue              (redelivery)     │
22:03:56  enqueue              (chat event)    ─┘

Counted programmatically over that window: enqueued: 13 · dequeued/removed: 0 · still queued: 13.

Meanwhile:

  • S1's last assistant message is at 19:15:26 — the exact moment it dequeued the /fork prompt. It has emitted nothing since, but its OS process is still alive (Sl+) with its three MCP shim subprocesses still connected and still receiving events.
  • The human's conversation continued in S2, a fork of the same parent transcript snapshot, with normal turns at 20:21, 20:35, 20:40 and tool calls against the very same MCP channel servers. S2's own queue drains normally (enqueue at 20:27:58 → remove at 20:27:59).
  • So both sessions are channel-connected and alive; inbound events go to the one nobody is watching.

This is not #78028

#78028 reports the --channels allowlist not propagating on daemon fork-resume, where notifications are rejected with Channel notifications skipped: server <name> not in --channels list for this session.

I see that too — three sessions spawned at 19:15:28, 19:15:51 and 19:17:09 logged exactly that message in ~/.cache/claude-cli-nodejs/<project>/mcp-logs-<server>/*.jsonl. But it is a different failure: those notifications were refused outright, whereas S1 accepted and enqueued all 13 (the <channel …> payloads are present verbatim in its queue records) and then never drained them. Fixing the allowlist propagation would not fix the orphaned queue.

The two do compound each other, though, which is what made this so hard to see: the freshly spawned sessions still connect their MCP shims and still ack the notifications they are about to discard, so a server that tracks delivery sees healthy acks from several subscribers while no session surfaces anything.

Impact

For any MCP server used as an inbound message channel (email, chat, ticketing, on-call), this is silent message loss:

  • The server has no way to detect it. It wrote to the transport, got an ack, and is told nothing further.
  • The user has no way to detect it. The old session looks idle rather than broken, and the new session simply never mentions the message.
  • It persists indefinitely. Nothing drains the orphaned queue, and the process stays alive holding its channel subscriptions.

Redelivery on the server side does not help either, since every retry lands in the same orphaned queue.

Expected

Any one of these would have prevented the loss:

  1. Channel subscriptions follow the conversation. When a session forks/migrates to a new id, its MCP channel servers should be re-pointed at the live session, so notifications enqueue where the user actually is.
  2. The abandoned session tears down its channel attachments when it stops being the active conversation, so servers observe a disconnect and can route or retry elsewhere.
  3. At minimum, surface the drop. An enqueue into a session with no consumer should log a warning to the MCP log (like the --channels skip message already does) rather than silently accumulating.

Repro sketch

  1. Register a stdio MCP server that emits notifications/claude/channel on demand.
  2. Start an interactive session with --dangerously-load-development-channels server:<name>; confirm events surface as <channel> turns and drain from the queue.
  3. Run /fork (in my case the /fork … prompt was itself queued and dequeued at the moment of the failure).
  4. Continue the conversation and note the session id it now writes to.
  5. Emit further notifications from the MCP server.
  6. Inspect ~/.claude/projects/<project>/<original-session>.jsonl — the queue-operation records show enqueue with no matching dequeue/remove, and nothing surfaces in the UI.

Happy to supply further detail from the transcripts if useful.

View original on GitHub ↗