sendRequest throws 'Stream closed' permanently after inputClosed flag flips (no reset path)

Resolved 💬 3 comments Opened Apr 22, 2026 by amart-builder Closed Jun 8, 2026

Summary

Running Claude Code 2.1.117 (native ARM64 macOS binary) under an ACP parent harness (OpenClaw 2026.4.15). After an initially-healthy period, sendRequest with subtype: "can_use_tool" begins throwing Error("Stream closed") synchronously on every subsequent non-allowlisted tool call, for the remainder of the process lifetime. The process continues to respond to notifications and behaves healthy from outside — it's permanently half-dead.

CLAUDE_CODE_STREAM_CLOSE_TIMEOUT=3600000 has no effect.

Evidence

Extracted via strings dump of the installed binary. The relevant control-request sender:

async sendRequest(H, _, q, K = randomUUID()) {
  let O = {type: "control_request", request_id: K, request: H};
  if (this.inputClosed) throw Error("Stream closed");  // ← the throw
  if (q?.aborted) throw Error("Request aborted");
  this.outbound.enqueue(O);
  if (H.subtype === "can_use_tool" && this.onControlRequestSent) { ... }
}

The EOF handler that flips the flag:

this.inputClosed = true;
for (let q of this.pendingRequests.values())
  q.reject(Error("Tool permission stream closed before response received"));

A binary-wide string search turned up no code path that resets inputClosed = false. Once set — possibly on a transient condition — the child is permanently broken for new can_use_tool roundtrips for the rest of its lifetime.

Why the parent can't observe it

The ACP ClientSideConnection.#receive() loop on the parent only closes the connection when the child's stdout reader ends or errors. Silent inputClosed flips on the child side emit no observable signal to the parent — no connection_close, no protocol-level notification. The parent keeps trying, every permission request throws, and the turn stalls forever until manual SIGTERM.

Expected behavior

One of:

  1. A recovery path that resets inputClosed after transient EOF-like conditions.
  2. An observable protocol-level signal (notification or stream error) when the flag flips, so the parent can decide to respawn.
  3. Clear documentation of what conditions flip the flag, so parents can avoid them.

Repro

Intermittent in real workloads. Triggers appear correlated with long-running prompts that hit backpressure on stdin, but we haven't been able to produce a deterministic repro — the bug only fires sporadically across ~dozens of Discord-driven sessions per day.

Workaround

Widening the parent's permissions.allow in settings.json to auto-approve all tools bypasses the broken code path (no can_use_tool roundtrip needed). Not a real fix — it just avoids the trigger for locally-configurable Claude Code sessions. Parent-harness-driven children (which govern permissions at the harness layer, not in settings.json) remain exposed.

Environment

  • Claude Code: 2.1.117 (native ARM64 macOS binary, ~200MB)
  • Platform: macOS 25.4.0 (Darwin)
  • Entrypoint: CLAUDE_CODE_ENTRYPOINT=sdk-ts (structuredIO NDJSON on stdin/stdout)
  • Parent harness: OpenClaw 2026.4.15 via @agentclientprotocol/sdk 1.x

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗