Skip to content

stream() async iterator does not yield exit chunks and can hang if process stdout stays open #155

@konard

Description

@konard

Problem

The stream() async iterator on ProcessRunner (v0.9.4) has two related issues:

1. No {type:'exit'} chunks yielded

The stream() method only yields {type:'stdout', data} and {type:'stderr', data} chunks. It does NOT yield {type:'exit', code} chunks, even though the documentation (issue-07-stream-output.mjs) and README examples show chunk.type === 'exit' handling.

The finish() method in $.process-runner-base.mjs does emit 'exit' as an EventEmitter event, but the stream() generator only listens to 'data' and 'end' events — it never captures or yields the exit information.

This means code like:

for await (const chunk of command.stream()) {
  if (chunk.type === 'exit') {
    exitCode = chunk.code; // ← This never executes
  }
}

...is dead code. The exit code can only be obtained from (await command).code or command.result.code after the loop.

2. stream() hangs if child process stdout stays open

The stream() generator only terminates when the 'end' event is emitted by ProcessRunner. However, finish() (which emits 'end') is called from executeChildProcess() only after:

const code = await exited;
await Promise.all([outPump, errPump, stdinPumpPromise]); // ← This can hang

The outPump is pumpReadable() which does for await (const chunk of readable) on the child's stdout. If the child process keeps stdout open (common with some CLI tools), pumpReadable() hangs → finish() never fires → stream() iterator never terminates → the for await loop in user code hangs forever.

Reproduction

const { $ } = await use('command-stream');

// Simulate a process that outputs result but keeps stdout open
// (Like Claude CLI which sends result JSON but doesn't close stdout immediately)
const cmd = $`sh -c 'echo "result"; sleep 999'`;

for await (const chunk of cmd.stream()) {
  console.log(chunk.type, chunk.data?.toString().trim());
  // Prints: stdout result
  // Then hangs forever waiting for stream to end
}
// Never reaches here

Expected Behavior

  1. stream() should yield {type:'exit', code} chunks when the process exits
  2. When stream() does not yield exit chunks, the documentation should clearly state this
  3. Consider adding a mechanism to detect when the process has exited even if pipes are still open

Workaround

We currently work around this by:

  1. Detecting the "result" event in the application-level JSON output
  2. Setting a timeout (30 seconds) after the result event
  3. Force-killing the process with SIGTERM/SIGKILL if the stream doesn't close
let resultTimeoutId = null;
for await (const chunk of cmd.stream()) {
  if (chunk.type === 'stdout') {
    const data = JSON.parse(chunk.data.toString());
    if (data.type === 'result') {
      resultTimeoutId = setTimeout(() => cmd.kill('SIGTERM'), 30000);
    }
  }
}
if (resultTimeoutId) clearTimeout(resultTimeoutId);

Environment

  • command-stream v0.9.4
  • Node.js v20.20.0
  • Linux x86_64

Related

  • hive-mind Issue #1280: --tool claude command execution stuck after success
  • The chunk.type === 'exit' dead code pattern exists in 6 source files across the hive-mind codebase

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions