sendRequest throws 'Stream closed' permanently after inputClosed flag flips (no reset path)
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:
- A recovery path that resets
inputClosedafter transient EOF-like conditions. - An observable protocol-level signal (notification or stream error) when the flag flips, so the parent can decide to respawn.
- 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/sdk1.x
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗