{"record":{"id":"cf0ea6812bc5052c","repo":"headroomlabs-ai/headroom","slug":"join-cmd-returned-unparseable-output-fir","errorCode":null,"errorMessage":"`{' '.join(cmd)}` returned unparseable output. First {_MAX_SNIPPET_LEN} chars:\n{stdout_snippet}","messagePattern":"`(.+?)` returned unparseable output\\. First (.+?) chars:\n(.+?)","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"headroom/learn/analyzer.py","lineNumber":627,"sourceCode":"            \"Check network connectivity, raise HEADROOM_LEARN_CLI_TIMEOUT_SECS, \"\n            \"or try a different backend with --model <litellm-model-name>.\"\n        ) from None\n\n    if result.returncode != 0:\n        stderr_snippet = (result.stderr or \"\")[:_MAX_SNIPPET_LEN]\n        raise RuntimeError(\n            f\"`{' '.join(cmd)}` failed (exit {result.returncode}):\\n{stderr_snippet}\"\n        )\n\n    # Log stderr warnings even on success (auth refreshes, deprecation notices).\n    if result.stderr and result.stderr.strip():\n        logger.debug(\"CLI stderr (exit 0): %s\", result.stderr[:_MAX_SNIPPET_LEN])\n\n    try:\n        return _strip_fenced_json(result.stdout)\n    except json.JSONDecodeError as exc:\n        stdout_snippet = (result.stdout or \"\")[:_MAX_SNIPPET_LEN]\n        raise RuntimeError(\n            f\"`{' '.join(cmd)}` returned unparseable output. \"\n            f\"First {_MAX_SNIPPET_LEN} chars:\\n{stdout_snippet}\"\n        ) from exc\n\n\ndef _call_claude_cli_streaming(\n    cmd: list[str], prompt: str, *, hard_cap: int, idle_cap: int\n) -> dict:\n    \"\"\"Run claude-cli with stream-json output and an idle-timeout watchdog.\n\n    Each line of stdout is one JSON event from claude (system/assistant/user/\n    result). Any line resets the idle deadline. The process is killed if no\n    output arrives for *idle_cap* seconds, or if total elapsed exceeds\n    *hard_cap* seconds. The final ``type:\"result\"`` event carries the assistant\n    response, which is then parsed as JSON.\n\n    Threads (rather than ``select``) drain stdout/stderr so the watchdog works\n    on Windows too, where ``select`` does not support pipe handles.","sourceCodeStart":609,"sourceCodeEnd":645,"githubUrl":"https://github.com/headroomlabs-ai/headroom/blob/322425c43bffde1ed0b64fecf3cf5951565dd82b/headroom/learn/analyzer.py#L609-L645","documentation":"Raised when the CLI exited 0 but its stdout could not be parsed: _strip_fenced_json(result.stdout) raised json.JSONDecodeError. headroom expects the CLI to print JSON recommendations (optionally fenced in a ```json block); anything else — prose, disclaimers, partial output, empty stdout with the payload on stderr — triggers this RuntimeError including the first _MAX_SNIPPET_LEN chars of stdout for diagnosis.","triggerScenarios":"The CLI completes successfully but returns conversational text instead of strict JSON: model ignoring the JSON-only instruction, a preamble before/after the JSON that the fence-stripper can't handle, truncated output, or the CLI emitting its own log lines to stdout instead of stderr.","commonSituations":"Weaker CLI backends/models wrapping JSON in explanation; CLI version changes adding banners/telemetry to stdout; prompt-digest size pushing the model to answer in prose; non-English locales adding headers; ANSI color codes polluting captured stdout.","solutions":["Inspect the stdout snippet in the message to see what the CLI actually printed","Re-run — LLM output formatting is nondeterministic; often a retry yields clean JSON","Try a stronger/more compliant backend (claude over codex, or an API model via --model)","Update headroom — its fence-stripping may be hardened for your CLI's output quirks; report the snippet upstream if not"],"exampleFix":"# before\nheadroom learn\n# `codex ...` returned unparseable output. First 200 chars: \"Sure! Here is...\"\n\n# after\nheadroom learn  # retry; if persistent:\nexport HEADROOM_LEARN_CLI=claude  # stricter JSON compliance\nheadroom learn","handlingStrategy":"retry","validationCode":"null","typeGuard":null,"tryCatchPattern":"last = None\nfor attempt in range(3):\n    try:\n        return run_learn()\n    except RuntimeError as e:\n        if 'unparseable output' in str(e):\n            last = e\n            continue  # LLM formatting is nondeterministic — retry\n        raise\nraise last","preventionTips":["Retry once or twice — malformed JSON from an LLM CLI is often transient","Prefer backends with stricter JSON compliance (claude) for automated pipelines","Report persistent stdout pollution (banners, color codes) upstream with the embedded snippet"],"tags":["json-parsing","cli","llm-output","retryable"],"backgroundTag":null,"analyzedSha":"322425c43bffde1ed0b64fecf3cf5951565dd82b","analyzedAt":"2026-08-15T01:03:05.481Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}