{"record":{"id":"658769a82d7275fb","repo":"headroomlabs-ai/headroom","slug":"join-cmd-returned-unparseable-output-fir-658769","errorCode":null,"errorMessage":"`{' '.join(cmd)}` returned unparseable output. First {_MAX_SNIPPET_LEN} chars:\n{snippet}","messagePattern":"`(.+?)` returned unparseable output\\. First (.+?) chars:\n(.+?)","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"headroom/learn/analyzer.py","lineNumber":778,"sourceCode":"        stderr_blob = \"\".join(stderr_lines)[:_MAX_SNIPPET_LEN]\n        raise RuntimeError(f\"`{' '.join(cmd)}` failed (exit {proc.returncode}):\\n{stderr_blob}\")\n\n    stderr_blob = \"\".join(stderr_lines)\n    if stderr_blob.strip():\n        logger.debug(\"CLI stderr (exit 0): %s\", stderr_blob[:_MAX_SNIPPET_LEN])\n\n    if final_result is None:\n        stdout_snippet = \"\".join(stdout_lines)[:_MAX_SNIPPET_LEN]\n        raise RuntimeError(\n            f\"`{' '.join(cmd)}` did not emit a final `result` event. \"\n            f\"First {_MAX_SNIPPET_LEN} chars of stdout:\\n{stdout_snippet}\"\n        )\n\n    try:\n        return _strip_fenced_json(final_result)\n    except json.JSONDecodeError as exc:\n        snippet = final_result[:_MAX_SNIPPET_LEN]\n        raise RuntimeError(\n            f\"`{' '.join(cmd)}` returned unparseable output. \"\n            f\"First {_MAX_SNIPPET_LEN} chars:\\n{snippet}\"\n        ) from exc\n\n\ndef _parse_stream_event(line: str) -> dict | None:\n    \"\"\"Parse one line of claude-cli stream-json output, returning None on junk.\"\"\"\n    line = line.strip()\n    if not line:\n        return None\n    try:\n        parsed = json.loads(line)\n    except json.JSONDecodeError:\n        return None\n    return parsed if isinstance(parsed, dict) else None\n\n\ndef _call_llm(digest: str, model: str) -> dict:","sourceCodeStart":760,"sourceCodeEnd":796,"githubUrl":"https://github.com/headroomlabs-ai/headroom/blob/322425c43bffde1ed0b64fecf3cf5951565dd82b/headroom/learn/analyzer.py#L760-L796","documentation":"Raised when the claude-cli result event was received and its text extracted, but _strip_fenced_json + json.loads could not parse it into the expected JSON structure. The analyzer asks the model to answer with JSON (optionally fenced); any model chatter, apology text, or truncated JSON triggers this. The original JSONDecodeError is chained and the first _MAX_SNIPPET_LEN chars of the raw output are shown.","triggerScenarios":"The model replies with prose instead of JSON, wraps JSON with commentary, emits fenced markdown the stripper doesn't recognize, or truncates the JSON mid-object because the response hit a token limit. Occurs in both the non-streaming path (analyzer.py:627) and streaming path (analyzer.py:778).","commonSituations":"Smaller/weaker models via --model that ignore JSON output instructions; very large digests pushing the answer past max output tokens so JSON is cut off; prompt-format changes between headroom and CLI versions; non-English models adding preamble text.","solutions":["Retry — single-shot non-JSON replies are often transient model behavior.","Use a stronger or more instruction-following backend: headroom learn --model <litellm-model-name> (e.g. claude-3-5-sonnet, gpt-4o).","Reduce input size: analyze fewer conversations per run so the model has output budget to complete valid JSON.","Update headroom — the prompt/parse contract (fence stripping) is tightened over time.","If persistent, copy the snippet from the error and check whether the model is returning an apology/refusal."],"exampleFix":"# before\nheadroom learn  # 'returned unparseable output. First 500 chars: Sure! Here is...'\n\n# after\nheadroom learn --model gpt-4o  # model that follows the JSON contract","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"for model in (\"claude\", \"gpt-4o\", \"gemini/gemini-1.5-pro\"):\n    try:\n        return run_learn(model=model)\n    except RuntimeError as e:\n        if \"returned unparseable output\" not in str(e):\n            raise\n# all backends produced non-JSON output\nraise RuntimeError(\"no backend produced parseable JSON\")","preventionTips":["Prefer stronger instruction-following models for the analyze step via --model.","Split very large conversation sets into smaller learn batches so JSON answers fit output limits.","Treat single parse failures as transient: retry once before changing anything."],"tags":["llm","json","parsing","learn","model-output"],"backgroundTag":null,"analyzedSha":"322425c43bffde1ed0b64fecf3cf5951565dd82b","analyzedAt":"2026-08-15T01:03:05.481Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}