You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Interrupting a turn does not kill the claudeAgent child process — thread appears unstoppable #5587
A thread's turn was interrupted (Stop clicked in the desktop UI), the orchestration engine settled the turn as interrupted — but the backing claude child process was never killed. It kept running and burning CPU for 15+ minutes until we killed it by hand, and the thread UI kept showing it as running, so Stop appeared broken to the user.
projection_turns row: turn requested 2026-08-07T07:46:12Z, state interrupted, completed 2026-08-07T08:26:38Z.
The claude child process for that thread (driver claudeAgent, spawned with --output-format stream-json --input-format stream-json) was still alive and consuming ~20% CPU at 08:41Z, 15 minutes after the engine settled the turn. projection_thread_sessions still showed the session as running until the process was killed manually, at which point it flipped to stopped.
Expected
Interrupting a turn should terminate (or at least signal, then escalate to kill) the driver's child process, and the session/thread UI should reflect the stop once the turn settles.
Impact
The user cannot stop a runaway thread from the UI; the only recourse is a manual kill of the right PID, which is error-prone (in our case a healthy sibling thread's process was killed as collateral while hunting for the stuck one).
We have not checked whether other drivers (codex, opencode, cursor, grok) reap their children on interrupt — may be worth auditing the same path.
Traced the mechanism for this today — posted the detail on #4713, which reports the same failure from the projection side.
Short version: ClaudeAdapter.interruptTurn awaits query.interrupt() with no timeout. That call waits for a control-request acknowledgement over the CLI's stdin, so a wedged child never answers and the promise never settles. Since the failure is non-resolution rather than rejection, no error path runs — which is exactly why the turn settles as interrupted while the child survives and the session projection stays running.
Your "signal, then escalate to kill" expectation is the right shape. #7349 bounds the interrupt at 10s and escalates to session teardown, which closes the query and lets the SDK take the process down.
On your audit question for the other drivers: I looked, but did not verify them, so treat this as a starting point rather than an answer. They are structurally different — Codex delegates to a runtime layer, Cursor and Grok go through ACP cancel, OpenCode issues an HTTP session.abort — so none is the same unbounded stdin await. Whether ACP cancel can hang the same way is the open question; #6531 already applies a "settle locally, don't block on the provider call" fix to OpenCode's path.
Re: the collateral kill while hunting for the stuck PID — the reliable discriminator is the --resume=<session id> value in the child's argv, which matches resume_cursor_json in provider_session_runtime for the thread. Matching on the worktree path is what puts sibling threads (and, if you are driving this from T3 Code itself, the agent doing the hunting) in range.
Activity
nhorto commentedon Aug 17, 2026
Traced the mechanism for this today — posted the detail on #4713, which reports the same failure from the projection side.
Short version:
ClaudeAdapter.interruptTurnawaitsquery.interrupt()with no timeout. That call waits for a control-request acknowledgement over the CLI's stdin, so a wedged child never answers and the promise never settles. Since the failure is non-resolution rather than rejection, no error path runs — which is exactly why the turn settles asinterruptedwhile the child survives and the session projection staysrunning.Your "signal, then escalate to kill" expectation is the right shape. #7349 bounds the interrupt at 10s and escalates to session teardown, which closes the query and lets the SDK take the process down.
On your audit question for the other drivers: I looked, but did not verify them, so treat this as a starting point rather than an answer. They are structurally different — Codex delegates to a runtime layer, Cursor and Grok go through ACP
cancel, OpenCode issues an HTTPsession.abort— so none is the same unbounded stdin await. Whether ACPcancelcan hang the same way is the open question; #6531 already applies a "settle locally, don't block on the provider call" fix to OpenCode's path.Re: the collateral kill while hunting for the stuck PID — the reliable discriminator is the
--resume=<session id>value in the child's argv, which matchesresume_cursor_jsoninprovider_session_runtimefor the thread. Matching on the worktree path is what puts sibling threads (and, if you are driving this from T3 Code itself, the agent doing the hunting) in range.t3dotgg commentedon Aug 22, 2026
Note
🤖 GPT-5.6 Sol in Codex responding on behalf of Theo
Closing as a duplicate of #4713. Both describe the Claude interrupt path leaving its child process and thread active.
Track the remaining work in #4713.
runningafter turn interrupt — stop button becomes a no-op #4713