Desktop app: session permanently wedged after manual /compact — deferred local-command send is echo-tracked by a uuid that never echoes

Open 💬 1 comment Opened Jul 3, 2026 by pgrmonteiro

Environment

  • Claude desktop app (macOS): 1.18286.0
  • Bundled Claude Code CLI: 2.1.197
  • macOS 15.6 (Darwin 24.6.0)
  • Reproduces on two separate profiles (work + personal) on the same machine, deterministically, on every manual /compact typed right after a turn ends.

Summary

After a manual /compact in the desktop app, the session becomes permanently wedged: messages typed afterwards render in the UI but are never delivered to the CLI process. Only restarting the app or archive/recovering the session un-wedges it (both rebuild the session manager, which flushes the queued sends).

Root cause (from main.log + the session JSONL): the app defers a send when the session is still "running", and drains it at the turn result. The drained send is echo-tracked by the uuid the app assigned. But when that send is a local slash command (/compact), the CLI writes the command message to the transcript with a different uuid, so the echo the app waits for never arrives. isRunning is then held forever, and every subsequent send is deferred into a queue that never drains.

Evidence

Session 2ba1c328-1435-427e-b9c6-61cd64943117, 2026-07-03.

~/Library/Logs/Claude/main.log (times local, UTC-3):

16:01:13 [info] [Stop hook] Query completed for session local_2ba1c328-…
16:01:13 [info] [LocalSessionManager] drained 1 deferred send(s) for local_2ba1c328-…
16:01:13 [info] [LocalSessionManager] isRunning held by unechoed input at result for local_2ba1c328-… { resultUuid: '5ee3dc02-…', pendingEchoUuids: [ '38c24a54-2212-417e-93a0-327da2eb74f9' ], nextCycleUuid: null, hasPendingCycle: true, inputStreamHasPending: false }
16:03:37 [info] [LocalSessionManager] isRunning held by unechoed input at result for local_2ba1c328-… { resultUuid: '100bfcbf-…', pendingEchoUuids: [ '38c24a54-2212-417e-93a0-327da2eb74f9' ], nextCycleUuid: null, hasPendingCycle: false, inputStreamHasPending: false }

The 16:01:13 → 16:03:37 cycle (143s, hadFirstResponse=false) is the compaction itself. At its result, pendingEchoUuids still holds the /compact send uuid and hasPendingCycle: false — nothing left that could ever clear isRunning.

Session JSONL at the same boundary: the compact completed normally (compact_boundary + summary written, SessionStart:compact hook recorded as hook_success), and the /compact command message was persisted with a fresh uuid:

<command-name>/compact</command-name>   uuid: 03b1ad6e-3b22-4251-99c3-4b811c1fbe20

The app's awaited uuid 38c24a54-2212-417e-93a0-327da2eb74f9 appears nowhere in the JSONL. After that: 86 minutes of zero entries while the user's follow-up messages sat rendered-but-undelivered in the UI, then on archive/recover a fresh enqueue/dequeue pair delivered them ("Continue from where you left off." + the queued message).

Why it's deterministic for this setup (and invisible for most users)

The buggy path requires the send to be deferred. This user has Stop hooks configured, which the app runs as a separate query after every turn ([Stop hook] Query completed). For a few seconds after each reply visibly finishes, the session is still "running" — so a /compact typed immediately after a reply is always deferred, always drained through the echo-tracked path, and always wedges. Every isRunning held by unechoed input occurrence in the log directly follows a Stop hook Query completed + drained N deferred send(s) pair. Users without Stop hooks send /compact into a genuinely idle session (normal path, no echo tracking) and never see it — but anyone who types /compact while a turn is still finishing should be able to hit this.

Repro

  1. Configure any Stop hook (e.g. a script that sleeps 2s and exits 0) so every turn ends with an app-side Stop-hook query.
  2. In a desktop-app session, send a message; as soon as the reply finishes streaming, type /compact (inside the Stop-hook window).
  3. Compaction completes (boundary + summary written to the JSONL) but the session stays "running" forever; subsequent messages render in the UI and are never delivered.
  4. main.log shows isRunning held by unechoed input at result … hasPendingCycle: false with the /compact send's uuid, which never appears in the transcript JSONL.

Expected

Either local-command sends drained from the deferred queue shouldn't be echo-tracked by uuid, or the echo matcher should recognize the local-command transcript entries (which get new uuids) as satisfying the pending send — and isRunning should have a fallback release when the result arrives with no pending cycle.

Workarounds found

  • Only send /compact after the session has been idle ~10-15s (past the Stop-hook query).
  • Un-wedge without restarting the app: archive + recover the session.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗