[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)

Status Open
Reported on v2.1.270
Maintainer reply None cached
Activity 7 comments · opened Sep 14, 2026

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:

  1. A long-running interactive session (~127.7k tokens of context at the time of the hang).
  2. One background in-process teammate spawned via the Agent tool, running concurrently in the same OS process.
  3. A Read on a small local file, dispatched while that teammate was live.

To detect it rather than guess at it:

  1. sample <pid> 5 on the apparently-stuck process. If the main thread is in kevent64 and the worker threads are in __ulock_wait2, it is idle, not busy.
  2. lsof -nP -i -a -p <pid> and confirm zero ESTABLISHED sockets, so no request is in flight.
  3. In the session .jsonl, collect every tool_use id and every tool_result tool_use_id and 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 in U state.
  • 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 allow rule 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 caffeinate and two idle MCP servers.
  • Socket state. The three CLOSED FDs 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.

View original on GitHub ↗

3 Comments

SnoElement · 1 month ago

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:

hang 1   3148 / 3213  (98.0%)   +0x7ac3a8 -> +0x7abd14 -> kevent64
hang 2   3934 / 4010  (98.1%)   +0x7ac3a8 -> +0x7abd14 -> kevent64

Identical offsets, so this is the same unwakeable idle-event-loop state described in the report. CPU 1.3%, no ESTABLISHED socket.

But this time there was no orphaned tool_use at 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 pending tool_use ids 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 kevent64 on a sample file 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 in sample rather than from claude --version, which reports the installed version rather than what a long-lived session is actually running.

SnoElement · 1 month ago

A base rate that may help narrow the dropped-tool_result path.

Across 4,499 tool calls in the two affected sessions, Read accounts for 4.8% of all calls. The workload is dominated by an MCP search tool (32%) and Bash (27%). Yet all four dropped calls I have found are Read:

dropped call #1 of 2      Read    subagent
dropped call #1 of 1      Read    subagent, first call of its life
dropped call #7 of 8      Read    in-process teammate
dropped call #71 of 181   Read    parent session

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 Read is 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:

  • Not the file. Two of the drops targeted the same 54,515-byte JSON file. That same file was read successfully twice, two days earlier, in the same session. Ordinary JSON, longest line 309 characters, and siblings up to 3.1MB read fine throughout.
  • Not position in the session. The drops occurred at call #1, #1, #7 and #71 of their respective agents, so this is neither a startup nor a long-session effect.

Also worth correcting a measurement trap in my original report, for anyone trying to reproduce the analysis: grep -c kevent64 on a sample file 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's kevent64 sample count as a share of that thread's total samples.

SnoElement · 1 month ago

Correcting my own statistic above, and replacing it with something stronger.

My earlier comment claimed all four dropped calls were Read at p about 5e-6. That figure was wrong and should be disregarded. It compared drops against Read'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_use ids 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 Read is still over-represented and Bash strikingly under-represented, but the honest figure is p about 0.005, weaker still under clustering, and drops are not exclusively Read. 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:

196 subagent transcripts
189  (96.4%)  end with an assistant text message   <- the agent's finished report
  7  ( 3.6%)  end abruptly on a tool_result or an unreturned tool_use

5 of those 7 abrupt endings have an unreturned read-type call. The agents did not continue and never reported. Two further details:

  • Controlled for the obvious confound. An abrupt tail would also be produced by killing the process. It was not that: each affected agent's parent session went on working for 1778 to 2032 further turns, in two cases across two more days. The agent died alone while its parent lived on.
  • One of the 7 is the retry of another. The same task was attempted twice and both attempts died on an unreturned Read.

Why this is invisible in practice, which I think matters for the fix. All 7 were background agents. The Agent/Task tool returns "Spawned successfully" immediately, and that string is the tool_result. So the parent's tool_use/tool_result census balances perfectly regardless of what happens to the agent afterwards. No completion notification arrives, no error is recorded, isApiErrorMessage is 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_use bookkeeping, because parent-side bookkeeping cannot observe this failure even in principle.

Two measurement traps, for anyone reproducing this analysis:

  • A pending AskUserQuestion is indistinguishable from a hang. It leaves exactly one orphaned tool_use and a main thread ~98% parked in kevent64. 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 is AskUserQuestion before concluding anything.
  • A correlation I chased and had to discard: two of the abrupt agents died immediately after SendMessage, which looked meaningful until I checked the base rate. SendMessage is 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.

Showing cached comments. Read the full discussion on GitHub ↗