Stop stops the machine: an interrupted turn's CLI is really gone #76
No reviewers
Labels
No labels
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
iterative-works/claude-code-query!76
Loading…
Reference in a new issue
No description provided.
Delete branch "cc-chat"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Goal
I press Stop on a runaway turn, the CLI is gone, and my next message continues the conversation.
Fit test (the close gate, passed on Michal's read)
Ask Gladys for something long on the chat page, press Stop part-way, confirm from outside the page
that the CLI is gone, then send the next message and get an answer with the conversation intact.
Taken on the
proceduresoffline harness (profiles/gladys-offline/start-offline.sh, an in-memorygraph, no Telegram, no email, driving the real CLI).
What was wrong
Not one fault but three, at the same seam:
finalizer ever ran.
ZStream.fromInputStreamreads throughZIO.attemptBlockingInterrupt, whose interrupt path spinsthread.interrupt()until the readreturns — and a pipe read ignores thread interruption. Only the process dying ends it, so
waiting for a reader before the kill is a deadlock.
Command.runandZIO.addFinalizer(kill)left a spawned CLI with nothingregistered to end it.
And, found in review: the kill ended the CLI but not the tools it had started, so a Stop during
a tool call orphaned the tool and left it working.
What changed
ProcessManager.spawnUnderKill— spawn and arm the kill as one uninterruptible step, and kill thewhole process tree.
ProcessManager.forkPipeReader— read each pipe on a daemon fiber whose teardown only signals.ProcessManager.executeProcessandSessionProcess.start: the card settled onemechanism for both call sites, and it is now literally one.
ClaudeCodeTranscriptreadsAssistantLogEntry.model: a reply Claude Code wrote itself(
<synthetic>) renders as the activity row "Injected reply" instead of as the agent's words.Tests
ProcessTeardownIntegrationTest— interrupting a query, interrupting a session, closing a sessionscope normally, and interrupting a query that is running a tool. Every process is resolved by the
exact pid the stand-in CLI recorded for itself, never by a name pattern.
StopAndKeepTalkingE2ETest— against the real CLI: kill a turn mid tool call, resume, get anon-error result.
ClaudeCodeTranscriptTest— a<synthetic>reply is activity, never the agent's.Measured: the kill lands 130–220 ms after the interrupt, 5 runs of 5. Before, the process outlived
the interrupt entirely.
One limit, stated rather than glossed: neither session test fails when the reader fork is
reverted (six runs). The mechanism is pinned on the query path — red before the fix, green after —
and both call sites now run that same code.
Review
review-slice-20260908-205148.md, six skills, one round: 1 critical, 4 warnings, 5 suggestions.Critical and all warnings fixed in-loop; one suggestion rejected with reasoning (wrapping a fork in
ZIO.uninterruptiblewould make the forked fiber inherit it); two parked.Follow-ups
proceduresrecords the CLI session id only from the end-of-turn result, so a Stop on the firstturn of a fresh conversation loses it. Does not affect an established conversation.
following turn. Nothing this SDK writes; nothing it can prevent.
project-management/efforts/cc-chat/FOLLOW-UPS.md.Not in this PR
Cutting
0.7.0and movingproceduresonto it, which happens after merge on Michal's approval.🤖 Generated with Claude Code
https://claude.ai/code/session_01XTrxcxBQiNjXh79dN8Sh5x