Stop cannot kill the CLI: killForcibly is a finalizer, and the stdout read it would unblock is not interruptible #75

Open
opened 2026-09-05 20:16:07 +00:00 by mph · 1 comment
Owner

Reported from procedures, where a dashboard's Stop control drives claude-code-query 0.6.0.

Measured on a running harness against a stand-in CLI that stays silent:

  • POST .../interrupt answers 202
  • the CLI subprocess is still running 45 seconds later
  • the same subprocess survives the JVM that started it

Chain, in zio/src/works/iterative/claude/zio/internal/cli/ProcessManager.scala:

  • executeProcess registers ZIO.addFinalizer(process.killForcibly.ignore) (line ~63), inside ZStream.unwrapScoped
  • a finalizer runs only once interruption of the fiber completes
  • the fiber is blocked in process.stdout.linesStream (line ~162), reading a child that is producing no output, and that blocking read does not observe interruption

So the kill that would make the pipes hit EOF is waiting for the read that only the kill can unblock. The comment at the finalizer says 'killing the process makes the pipes hit EOF, letting the forked readers finish promptly' — true, but the kill never gets to run.

Consequence for a consumer: a query cannot be cancelled. For an interactive agent that means a Stop button that stops nothing, and a subprocess that keeps spending tokens and making tool calls with nobody watching.

Possible directions (not investigated in depth): kill the process on the interrupt signal rather than in scope teardown (ZIO.onInterrupt beside the stream, or an uninterruptible mask around a supervisor that kills first), or read stdout through an interruptible blocking read.

Found while closing cc-chat/06-gladys-global-chat. The consumer fixed its own half — the interruption was not reaching the work at all (procedures 39010372); this is what remains, and it is a 0.6.1 matter.

Reported from procedures, where a dashboard's Stop control drives claude-code-query 0.6.0. Measured on a running harness against a stand-in CLI that stays silent: - POST .../interrupt answers 202 - the CLI subprocess is still running 45 seconds later - the same subprocess survives the JVM that started it Chain, in zio/src/works/iterative/claude/zio/internal/cli/ProcessManager.scala: - executeProcess registers ZIO.addFinalizer(process.killForcibly.ignore) (line ~63), inside ZStream.unwrapScoped - a finalizer runs only once interruption of the fiber completes - the fiber is blocked in process.stdout.linesStream (line ~162), reading a child that is producing no output, and that blocking read does not observe interruption So the kill that would make the pipes hit EOF is waiting for the read that only the kill can unblock. The comment at the finalizer says 'killing the process makes the pipes hit EOF, letting the forked readers finish promptly' — true, but the kill never gets to run. Consequence for a consumer: a query cannot be cancelled. For an interactive agent that means a Stop button that stops nothing, and a subprocess that keeps spending tokens and making tool calls with nobody watching. Possible directions (not investigated in depth): kill the process on the interrupt signal rather than in scope teardown (ZIO.onInterrupt beside the stream, or an uninterruptible mask around a supervisor that kills first), or read stdout through an interruptible blocking read. Found while closing cc-chat/06-gladys-global-chat. The consumer fixed its own half — the interruption was not reaching the work at all (procedures 39010372); this is what remains, and it is a 0.6.1 matter.
Member

A related wait that only a kill can end, on a different path, is filed as #83: a session whose stdout reader fails while the CLI lives goes to recordEnd, which waits on process.waitFor with nobody reading stdout (zio/src/works/iterative/claude/zio/internal/cli/SessionProcess.scala:166-169; the same shape in the Kyo adapter). Found closing kyo-experiment/02-kyo-session.

A related wait that only a kill can end, on a different path, is filed as #83: a session whose stdout reader fails while the CLI lives goes to recordEnd, which waits on process.waitFor with nobody reading stdout (zio/src/works/iterative/claude/zio/internal/cli/SessionProcess.scala:166-169; the same shape in the Kyo adapter). Found closing kyo-experiment/02-kyo-session.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
iterative-works/claude-code-query#75
No description provided.