{"record":{"id":"c012e8e40c78b0d1","repo":"headroomlabs-ai/headroom","slug":"join-cmd-produced-no-output-for-idle-cap","errorCode":null,"errorMessage":"`{' '.join(cmd)}` produced no output for {idle_cap}s. Check network connectivity, raise HEADROOM_LEARN_CLI_IDLE_TIMEOUT_SECS, or try a different backend with --model <litellm-model-name>.","messagePattern":"`(.+?)` produced no output for (.+?)s\\. Check network connectivity, raise HEADROOM_LEARN_CLI_IDLE_TIMEOUT_SECS, or try a different backend with --model <litellm-model-name>\\.","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"headroom/learn/analyzer.py","lineNumber":729,"sourceCode":"            subprocess.TimeoutExpired\n        ):  # pragma: no cover — defensive, kill normally returns fast\n            pass\n        logger.debug(\"claude-cli killed: %s\", reason)\n\n    while eofs < 2:\n        elapsed = time.monotonic() - start\n        if elapsed > hard_cap:\n            _kill(f\"hard cap {hard_cap}s exceeded\")\n            raise RuntimeError(\n                f\"`{' '.join(cmd)}` exceeded the {hard_cap}s hard cap. \"\n                \"Raise HEADROOM_LEARN_CLI_TIMEOUT_SECS for slower networks or \"\n                \"larger digests, or try a different backend with \"\n                \"--model <litellm-model-name>.\"\n            )\n        idle_elapsed = time.monotonic() - last_activity\n        if idle_elapsed > idle_cap:\n            _kill(f\"idle cap {idle_cap}s exceeded\")\n            raise RuntimeError(\n                f\"`{' '.join(cmd)}` produced no output for {idle_cap}s. \"\n                \"Check network connectivity, raise \"\n                \"HEADROOM_LEARN_CLI_IDLE_TIMEOUT_SECS, or try a different \"\n                \"backend with --model <litellm-model-name>.\"\n            )\n\n        # Block up to 1s waiting for the next event, then re-check deadlines.\n        try:\n            tag, line = events.get(timeout=1.0)\n        except queue.Empty:\n            continue\n\n        if line is None:\n            eofs += 1\n            continue\n        last_activity = time.monotonic()\n        if tag == \"stdout\":\n            stdout_lines.append(line)","sourceCodeStart":711,"sourceCodeEnd":747,"githubUrl":"https://github.com/headroomlabs-ai/headroom/blob/322425c43bffde1ed0b64fecf3cf5951565dd82b/headroom/learn/analyzer.py#L711-L747","documentation":"Raised by the claude-cli streaming watchdog in headroom's learn analyzer when the subprocess produces no output line for idle_cap seconds (HEADROOM_LEARN_CLI_IDLE_TIMEOUT_SECS). The process is killed and the partially-completed analysis is aborted; any LLM work in flight is lost. It exists so a hung network call or stalled backend cannot block the learn pipeline forever.","triggerScenarios":"Running `headroom learn` (or _call_claude_cli_streaming) with the default claude-cli backend when the model API stalls: no stdout/stderr line arrives for idle_cap seconds while the process is still alive. Typical on flaky networks, rate-limited or overloaded API endpoints, VPN drops, or a claude-cli version that buffers output.","commonSituations":"Slow hotel/coffee-shop Wi-Fi, corporate proxies that black-hole long-lived streams, claude-cli silently waiting on an expired OAuth token refresh, or a very large digest prompt where the model thinks longer than the idle cap before emitting the first token.","solutions":["Raise the idle cap: export HEADROOM_LEARN_CLI_IDLE_TIMEOUT_SECS=120 (or higher) and rerun.","Check network connectivity to the backend (e.g. curl the Anthropic API endpoint) and retry once the link is stable.","Switch backend entirely: rerun with --model <litellm-model-name> (e.g. --model gpt-4o) to bypass claude-cli streaming.","Update the claude CLI (`claude update` / npm i -g @anthropic-ai/claude-code) — old versions buffer stream-json output instead of emitting per-line events.","If it recurs, capture debug logs to see how far the stream got before stalling."],"exampleFix":"# before\nheadroom learn  # dies with 'produced no output for 60s'\n\n# after\nexport HEADROOM_LEARN_CLI_IDLE_TIMEOUT_SECS=180\nheadroom learn","handlingStrategy":"retry","validationCode":"import os, socket, urllib.parse\n\nLIVE = 0 if os.environ.get(\"HEADROOM_DRY_RUN\") else 1  # guard: allow opting out before a network-bound learn run\nhost = urllib.parse.urlparse(os.environ.get(\"HEADROOM_CLI_URL\", \"https://api.anthropic.com\")).hostname\ntry:\n    socket.create_connection((host, 443), timeout=5).close()\n    network_ok = True\nexcept OSError:\n    network_ok = False\nif LIVE and not network_ok:\n    raise SystemExit(\"No route to backend; skipping learn run\")","typeGuard":null,"tryCatchPattern":"try:\n    result = headroom_learn_run(...)\nexcept RuntimeError as e:\n    if \"produced no output for\" in str(e) and \"HEADROOM_LEARN_CLI_IDLE_TIMEOUT_SECS\" in str(e):\n        os.environ[\"HEADROOM_LEARN_CLI_IDLE_TIMEOUT_SECS\"] = \"180\"\n        result = headroom_learn_run(...)  # one retry with a raised cap\n    else:\n        raise","preventionTips":["Set HEADROOM_LEARN_CLI_IDLE_TIMEOUT_SECS to 2-3x your worst observed model time-to-first-token.","Warm the backend (one tiny CLI call) before launching a large learn batch to surface auth/network issues cheaply.","Run learn jobs on a stable network or behind a retrying proxy; avoid VPN switching mid-run.","Keep claude-cli updated — old versions buffer stream-json and look identical to a hang."],"tags":["network","timeout","cli","learn","subprocess"],"backgroundTag":null,"analyzedSha":"322425c43bffde1ed0b64fecf3cf5951565dd82b","analyzedAt":"2026-08-15T01:03:05.481Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}