[BUG] Turn goes permanently idle (event loop idle in kevent64, no error): dropped tool_result, stalled compaction, or queued input never taken up (macOS, 2.1.268 to 2.1.283)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code (2.1.270, matches current npm
latest)
Related but not identical, cross-referenced deliberately: #91648 (same defect shape on Windows/Bash), #83848 (subagent transcripts stopping after a tool_use with no tool_result), #92789 (idle-not-crashed session hang on Linux desktop). I believe these share a root cause. What I add here is sample evidence showing the event loop is idle with nothing registered, plus a case where a parent and its in-process teammate wedged independently in the same OS process.
What's Wrong?
A Read tool call's tool_result is never delivered. The turn's continuation never settles, and the process goes fully idle rather than erroring or timing out. The elapsed-time counter keeps incrementing, so it is indistinguishable from slowness.
This happened twice within one OS process: first in a background in-process teammate, then in the parent session 4.5 minutes later.
The process is idle, not deadlocked and not spinning. sample of the wedged process, taken twice ~5 minutes apart, identical in shape both times:
3213 Thread_32466316 DispatchQueue_1: com.apple.main-thread (serial)
! : 3148 kevent64 (in libsystem_kernel.dylib) + 8
3148 of 3213 samples on the main thread are in kevent64, the idle event-loop wait. Every Bun Pool worker thread is parked in __ulock_wait2. CPU steady at ~1%. No thread is blocked in a file or socket syscall and no mutex is contended on the main thread. Nothing is registered that could ever wake the turn, which is why waiting cannot recover it.
No API request is in flight. lsof -nP -i -a -p <pid> shows zero sockets in ESTABLISHED.
Exactly one orphaned tool_use per agent. Counting tool_use ids against tool_result ids in the transcripts:
| Agent | tool_use | tool_result | orphaned |
|---|---|---|---|
| Parent session | 71 | 70 | 1 |
| In-process teammate | 8 | 7 | 1 |
Both orphans are Read, both targeting small local files on APFS:
parent 2026-09-14T09:31:30.991Z Read <26,618-byte local .md>
teammate 2026-09-14T09:26:58.200Z Read <27,445-byte local .md>
isApiErrorMessage entries in either transcript: 0.
Out-of-order completion in the teammate. It emitted two Read calls in one assistant message, 203ms apart. The second returned in 15ms; the first never returned:
09:26:58.200 assistant tool_use:Read(ORPHAN) <- never returns
09:26:58.403 assistant tool_use:Read(SIBLING)
09:26:58.418 user tool_result(SIBLING) <- returns in 15ms
That is the signature of a lost result in parallel tool-call fan-in.
Teammate metadata (from the subagent .meta.json):
{"taskKind": "in_process_teammate", "requestShape": "background",
"requestNonInteractive": true, "permissionMode": "auto", "spawnDepth": 0}
The teammate wedged first; the parent wedged on its next tool call 4.5 minutes later. Since an in-process teammate shares the parent's event loop, a dropped continuation in one branch appears able to strand the other.
What Should Happen?
A tool_use with no tool_result should hit a timeout and surface a tool error, letting the turn continue or fail visibly. It should never leave the session in a state where the event loop is idle with no pending work and no path to recovery.
Error Messages/Logs
There are none, and that is the core of the report. No error in the UI, no error in the transcript, no timeout, no retry. The only way to distinguish this from ordinary slowness is to sample the process and notice the main thread is in the idle kevent64 wait with nothing registered.
Secondary symptom: pressing Esc returns to the prompt, but subsequent prompts also produce no output, which suggests the agent loop's state does not fully reset after the cancel.
Steps to Reproduce
I do not have a deterministic repro, so I am reporting the conditions and the measurement method instead.
Conditions present in both occurrences:
- A long-running interactive session (~127.7k tokens of context at the time of the hang).
- One background in-process teammate spawned via the Agent tool, running concurrently in the same OS process.
- A
Readon a small local file, dispatched while that teammate was live.
To detect it rather than guess at it:
sample <pid> 5on the apparently-stuck process. If the main thread is inkevent64and the worker threads are in__ulock_wait2, it is idle, not busy.lsof -nP -i -a -p <pid>and confirm zeroESTABLISHEDsockets, so no request is in flight.- In the session
.jsonl, collect everytool_useid and everytool_resulttool_use_idand diff them. The orphan is the dropped call.
Claude Model
claude-opus-5 (parent), claude-sonnet-5 (in-process teammate)
Is this a regression?
I don't know
Last Working Version
(not known)
Claude Code Version
2.1.270 (Claude Code)
Platform
AWS Bedrock
Operating System
macOS
Terminal/Shell
Terminal.app (macOS)
Additional Information
macOS 27.0 (build 26A428), Darwin 27.0.0, xnu-13432.1.9, arm64 (T6041). Runtime is Bun; worker threads appear as Bun Pool N.
Ruled out by testing, please do not treat these as the cause:
- File or filesystem. Both target files read instantly from another session afterward. Local APFS, not a File Provider or network mount. No stuck
fileproviderctl, no processes inUstate. - File size. In the same session a 356,726-byte PDF and a 315,945-byte PDF read successfully, and a 25,777-byte markdown read successfully twice. The orphans were 26,618 and 27,445 bytes. No size threshold.
- Permissions. An
allowrule covered both paths and auto mode was on. 70 of 71 calls in the same session succeeded, including a read in the same directory as the teammate's orphan. - Host resource exhaustion. 63% memory free, 2.31MB of 1024MB swap used, 1303 of 8000 processes for the uid.
- Hung hook or child process. The only children were
caffeinateand two idle MCP servers. - Socket state. The three
CLOSEDFDs are ordinary HTTP-pool residue; healthy sessions on the same machine had 14 and 5.
Suggested fix direction: a watchdog on pending tool_use ids. If a tool_use has no tool_result after N seconds and the event loop has nothing pending, surface a tool error instead of waiting forever. That alone converts a silent unrecoverable hang into a recoverable error, independent of whatever drops the result.
Update (after two further failure modes were found): this fix direction is too narrow. Two other paths reach the same idle event loop with no pending tool_use at all -- #94261 (compaction stalls permanently at 95%) and #94335 (the tool_result IS delivered and the turn still never continues). A watchdog keyed on outstanding tool calls catches only this issue. The formulation that covers all three is "a turn is marked in progress while the event loop has no registered work for N seconds". Note also that this failure is invisible from the parent side when it happens in a background agent: the Agent tool's tool_result is the immediate "Spawned successfully" string, so the parent's pending-call bookkeeping always balances no matter what happens to the agent afterwards.
Impact: the session is unrecoverable in place and queued user messages are stranded, though they remain readable in the transcript as queue-operation/enqueue records and --resume restores state through the last complete turn.
Paths, hostnames, repository names, session ids and tool-use ids in this report are replaced with placeholders. Byte sizes, timestamps and counts are unmodified.
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Second occurrence in the same session, same terminal state, different trigger.
The session above wedged again about 45 minutes after the first hang, following
--resume. I sampled it before killing it. The main thread is in the same place, through the same frames:Identical offsets, so this is the same unwakeable idle-event-loop state described in the report. CPU 1.3%, no
ESTABLISHEDsocket.But this time there was no orphaned
tool_useat all. Every tool call had its result:| Agent |
tool_use|tool_result| orphaned ||---|---|---|---|
| Parent session | 137 | 136 | 1, carried over from hang 1 |
| In-process teammate | 50 | 50 | 0 |
The trigger was compaction, not a tool call. The UI reached 95% and stopped there permanently. Notably, two compactions had already completed successfully earlier in the same session, so compaction is not broadly broken.
So there appear to be at least two distinct entry points into one terminal state: a dropped
tool_result, and something in compaction. That is evidence for the watchdog suggestion in the report being worth doing independently of whatever drops the result, since a watchdog keyed only on pendingtool_useids would not catch the compaction case. A stall detector on the event loop having no registered work while a turn is marked in progress would catch both.One correction to my own evidence, for anyone measuring this.
grep -c kevent64on asamplefile returns 3 for a wedged process and for a healthy one, since the string appears once in the main thread, once in a helper thread, and once in the by-function summary. The meaningful figure is the main thread's kevent64 sample count over that thread's total. And a healthy session idling at the prompt also reads ~98%, so the idle stack alone does not prove a hang. It proves the process is not busy and that waiting cannot recover it; the hang is established by that idle state coexisting with a turn in progress.Both hangs ran 2.1.270, confirmed from the
(in 2.1.270)frames insamplerather than fromclaude --version, which reports the installed version rather than what a long-lived session is actually running.A base rate that may help narrow the dropped-
tool_resultpath.Across 4,499 tool calls in the two affected sessions,
Readaccounts for 4.8% of all calls. The workload is dominated by an MCP search tool (32%) andBash(27%). Yet all four dropped calls I have found areRead:Under a null where drops fall uniformly across tool calls, p is about 5e-6. Collapsing the two that were retries of the same task into a single independent event still gives about 1e-4. Four events is a small sample and I would not read the exact figure too hard, but
Readis over-represented far beyond chance, which points at the file-read completion path specifically rather than general tool dispatch.Two things I checked and ruled out, so nobody spends time on them:
Also worth correcting a measurement trap in my original report, for anyone trying to reproduce the analysis:
grep -c kevent64on asamplefile returns 3 for a wedged process and for a perfectly healthy one alike, because the string appears once in the main thread, once in a helper thread, and once in the by-function summary. The meaningful figure is the main thread'skevent64sample count as a share of that thread's total samples.Correcting my own statistic above, and replacing it with something stronger.
My earlier comment claimed all four dropped calls were
Readat p about 5e-6. That figure was wrong and should be disregarded. It compared drops againstRead's 4.8% share within the two sessions I had selected because they contained drops -- a circular denominator. I have now run the census across my whole local corpus instead.Corrected numbers. 276 transcripts, 21,005 tool calls. 12 orphaned
tool_useids exist, but only 7 are provably dropped, meaning later calls were issued and returned while that result never arrived. The other 5 are last-call orphans, which are indistinguishable from a call still in flight. By tool:| Tool | Provable drops | Share of all 21,005 calls |
|---|---|---|
|
Read| 4 | 11.71% ||
mcp__github__pull_request_read| 2 | (both in one agent) ||
Bash| 1 | 51.02% |So
Readis still over-represented andBashstrikingly under-represented, but the honest figure is p about 0.005, weaker still under clustering, and drops are not exclusivelyRead. Base rate of the bug overall: 0.033% of calls.The stronger finding: a dropped result is not survivable. It kills the agent it lands in.
I had assumed these agents shrugged the drop off, because sibling calls issued in the same batch returned afterwards. They did not. Looking at how each transcript ends:
5 of those 7 abrupt endings have an unreturned read-type call. The agents did not continue and never reported. Two further details:
Read.Why this is invisible in practice, which I think matters for the fix. All 7 were background agents. The
Agent/Tasktool returns"Spawned successfully"immediately, and that string is the tool_result. So the parent'stool_use/tool_resultcensus balances perfectly regardless of what happens to the agent afterwards. No completion notification arrives, no error is recorded,isApiErrorMessageis 0. The requested work simply never lands and nothing anywhere says so.That argues the watchdog I suggested earlier has to live inside the agent loop, not in the parent's pending-
tool_usebookkeeping, because parent-side bookkeeping cannot observe this failure even in principle.Two measurement traps, for anyone reproducing this analysis:
AskUserQuestionis indistinguishable from a hang. It leaves exactly one orphanedtool_useand a main thread ~98% parked inkevent64. I called a healthy session wedged on this basis earlier today; it was waiting on a question it had asked. Check whether the last orphan isAskUserQuestionbefore concluding anything.SendMessage, which looked meaningful until I checked the base rate.SendMessageis the last tool call in 49.5% of agents that finish normally, because that is how an agent reports back. Dying after one is less likely than chance. Not a lead.Counts, percentages and timestamps are unmodified. Paths, repository names and session ids are omitted or replaced.