From 21dfa4b23bc7bcf39263c2324be7f6e4251e40c1 Mon Sep 17 00:00:00 2001 From: kays0x Date: Sat, 20 Jun 2026 02:07:22 -0400 Subject: [PATCH] fix(haiku): send prompt on stdin, not argv (avoids E2BIG on long sessions) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit call_haiku passed the full prompt as a single argv string (claude -p ). Linux caps one argument at MAX_ARG_STRLEN (131072 bytes = 128KB), so a prompt at or above that size fails at exec() with OSError [Errno 7] "Argument list too long" — surfaced as RuntimeError and the save is lost. The failure is silent (log only) and permanent for that session, since the extract only grows. It hits exactly the sessions worth capturing: long ones, recovery of a missed session, and any accumulated delta over 128KB. claude -p reads the prompt from stdin when no positional prompt is given, so pass it via subprocess input= instead. stdin has no argv size limit; no behavior change for normal-size prompts. Adds a regression test asserting the prompt arrives via input= and is absent from the command argv (it fails against the old argv form). Repro: subprocess.run(["/bin/true", "x"*131072]) raises E2BIG; the same payload via input= on stdin runs fine. --- pipeline/haiku.py | 7 ++++++- tests/test_haiku.py | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/pipeline/haiku.py b/pipeline/haiku.py index b0793d7..1f41c5c 100644 --- a/pipeline/haiku.py +++ b/pipeline/haiku.py @@ -97,9 +97,13 @@ def call_haiku( RuntimeError: If the subprocess times out or exits with a non-zero return code, or if the JSON response cannot be parsed. """ + # Prompt goes on STDIN, not argv: a session extract can exceed Linux's + # MAX_ARG_STRLEN (128KB per single argument), which raises E2BIG ("Argument + # list too long") at exec time and silently kills saves of long sessions. + # `claude -p` with no positional prompt reads the prompt from stdin. cmd = [ "claude", - "-p", prompt, + "-p", "--output-format", "json", "--no-session-persistence", "--exclude-dynamic-system-prompt-sections", @@ -116,6 +120,7 @@ def call_haiku( try: result = subprocess.run( cmd, + input=prompt, capture_output=True, text=True, # claude emits UTF-8; without this, text=True decodes with the diff --git a/tests/test_haiku.py b/tests/test_haiku.py index 76134da..d41f856 100644 --- a/tests/test_haiku.py +++ b/tests/test_haiku.py @@ -140,6 +140,26 @@ def test_call_haiku_success(mock_run): assert "CLAUDECODE" not in env +@patch("pipeline.haiku.subprocess.run") +def test_call_haiku_sends_prompt_on_stdin_not_argv(mock_run): + """The prompt is delivered on STDIN, never as an argv string. + + A session extract can exceed Linux's MAX_ARG_STRLEN (131072 bytes / 128KB + per single argument); the old ``claude -p `` form fails at exec() + with OSError E2BIG ("Argument list too long"), silently losing the save. + Guard both halves so the regression can't silently return: the prompt must + arrive via ``input=`` and must NOT appear in the command argv (``-p`` is a + bare flag, immediately followed by the next option).""" + mock_run.return_value = MagicMock( + returncode=0, stdout=_mock_claude_response("ok"), stderr="") + call_haiku("the full prompt text") + args = mock_run.call_args + cmd = args[0][0] + assert args[1]["input"] == "the full prompt text" + assert "the full prompt text" not in cmd + assert cmd[cmd.index("-p") + 1] == "--output-format" + + @patch("pipeline.haiku.subprocess.run") def test_call_haiku_strips_parent_session_env(mock_run, monkeypatch): """The nested claude -p must not inherit the PARENT Claude Code session