[BUG] macOS Desktop: user-typed messages silently delayed 5-10 min; failure rate scales with concurrent session count (3.4% at 1 session -> 21.1% at 4)
Summary
On macOS Desktop, messages typed by the user into the composer are sometimes accepted by the UI but never handed to the CLI process for several minutes. The UI shows no error and no spinner — the message simply appears to do nothing. It eventually goes through on its own, typically after 5–10 minutes.
This is measurable and the failure rate scales with the number of concurrently active sessions: 3.4% with one session, 21.1% with four.
This is distinct from #86012 / #86088 / #86069, which are all about the cross-session send_message MCP tool. Here there is no send_message involved — it is a human typing into the composer and pressing enter. The hadFirstResponse=false signature in the logs is the same, so it may share a root cause, but the trigger path is different and I did not find an existing issue covering it.
Environment
- Claude Desktop
1.40609.0(arm64) - Bundled Claude Code runtime
2.1.247 - macOS Darwin 27.0.0, Apple Silicon
- Typical usage: 2–5 Desktop sessions open across different repos, worked on in rotation
Ground truth used
The Desktop log alone is not sufficient to detect this, because Mapping internal session … to CLI session is only logged on spawn/resume, not on every send. The reliable signal is to compare the two sides:
- Sender side —
~/Library/Logs/Claude/main.log:
LocalSessions.sendMessage: sessionId=…, messageLength=N
- Receiver side — the CLI transcript
~/.claude/projects/<project>/<cli-session>.jsonl:
{"type":"queue-operation","operation":"enqueue","content":"…"}
The gap between those two timestamps is the true delivery delay. Matching is done on messageLength == len(content).
Measurements
358 matched sends over 24 days.
| Metric | Value |
|---|---|
| Median delivery delay | 0.6 s |
| p90 | 14 s |
| Max | 1732 s (29 min) |
| Delayed > 60 s | 32 / 358 (8.9%) |
| Delayed > 300 s | 29 / 358 (8.1%) |
Dose–response against concurrent session count
Concurrency = number of distinct sessions with any activity (sendMessage or Query completed) within ±300 s of the send.
| Concurrent sessions | Sends | Delayed > 60 s | Rate |
|---|---|---|---|
| 1 | 116 | 4 | 3.4% |
| 2 | 114 | 7 | 6.1% |
| 3 | 62 | 10 | 16.1% |
| 4 | 38 | 8 | 21.1% |
| 5 | 25 | 3 | 12.0% |
| 6 | 3 | 0 | — (n too small) |
Monotonic from 1 to 4, roughly 6x. A secondary factor: 22% of delayed sends happened while that session was still mid-query, vs 9% of normal sends — so "session busy" contributes but explains only 7 of 32 cases.
Log signature
The delayed sends line up with cycles where the CLI never emitted a first token, same signature as #86012:
[CCD CycleHealth] healthy cycle for local_8916e109-… (422s, hadFirstResponse=false)
That cycle ended at 14:55:20; 422 s earlier is 14:48:18, which is exactly the sendMessage timestamp of a delayed message. Ten such cycles are present in one log file, the longest 737 s.
Note these are labelled healthy. A cycle that ran 422 seconds and produced no first token is being counted as healthy, which is why this never surfaces as an error anywhere in the UI. unhealthy is only assigned for app_quit / api_error / cli_execution_error.
Reproduction
Not deterministic — it is a rate, not a trigger. To observe it:
- Open 4 Desktop sessions in different repos.
- Work through them in rotation, sending messages to each.
- Roughly 1 in 5 sends will sit there doing nothing for 5–10 minutes.
To measure it on an existing install, this script prints the delay distribution from logs that are already on disk:
import re, json, datetime, glob, statistics
enq = []
for f in glob.glob('~/.claude/projects/*/*.jsonl'.replace('~', __import__('os').path.expanduser('~'))):
for l in open(f, errors='replace'):
if '"enqueue"' not in l: continue
try: d = json.loads(l)
except Exception: continue
if d.get('operation') != 'enqueue' or d.get('content') is None: continue
t = datetime.datetime.fromisoformat(d['timestamp'].replace('Z', '+00:00'))
enq.append((t.astimezone().replace(tzinfo=None), len(d['content'])))
enq.sort()
TS = re.compile(r'^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})')
snd = []
for l in open(__import__('os').path.expanduser('~/Library/Logs/Claude/main.log'), errors='replace'):
m0, m = TS.match(l), re.search(r'sendMessage: sessionId=(\S+?), messageLength=(\d+)', l)
if m0 and m:
snd.append((datetime.datetime.strptime(m0.group(1), '%Y-%m-%d %H:%M:%S'), int(m.group(2))))
used, delays = set(), []
for t, ln in snd:
best = None
for i, (et, el) in enumerate(enq):
if i in used or el != ln: continue
d = (et - t).total_seconds()
if -5 <= d <= 1800 and (best is None or d < best[0]): best = (d, i)
if best: delays.append(best[0]); used.add(best[1])
delays.sort()
print(f"n={len(delays)} median={statistics.median(delays):.1f}s "
f"p90={delays[int(len(delays)*.9)]:.0f}s max={delays[-1]:.0f}s "
f">300s: {sum(1 for d in delays if d > 300)}")
Impact
The silent part is what makes it costly. There is no spinner, no error, no queued-message indicator — a message that will not be processed for 8 minutes looks identical to one that was never typed. In my case a reply sat undelivered for 9m44s while I assumed the session had died.
rewind does not help: the message never entered the transcript, so there is no rewind point ([Rewind] No rewind point … assistantUuid=none, hasText=true). Re-sending is the only recovery.
Suggested fixes
- Treat
hadFirstResponse=falsepast some threshold as unhealthy rather than healthy, and surface it. - Show a queued/pending state in the composer when a send has not reached the CLI within a few seconds.
- Investigate why concurrency drives this — it looks like contention in the spawn/resume or IPC path rather than anything session-specific.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗