headroomlabs-ai/headroom · error · RuntimeError
`{' '.join(cmd)}` returned unparseable output. First {_MAX_S
Error message
`{' '.join(cmd)}` returned unparseable output. First {_MAX_SNIPPET_LEN} chars:
{snippet} What it means
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.
Source
Thrown at headroom/learn/analyzer.py:778
stderr_blob = "".join(stderr_lines)[:_MAX_SNIPPET_LEN]
raise RuntimeError(f"`{' '.join(cmd)}` failed (exit {proc.returncode}):\n{stderr_blob}")
stderr_blob = "".join(stderr_lines)
if stderr_blob.strip():
logger.debug("CLI stderr (exit 0): %s", stderr_blob[:_MAX_SNIPPET_LEN])
if final_result is None:
stdout_snippet = "".join(stdout_lines)[:_MAX_SNIPPET_LEN]
raise RuntimeError(
f"`{' '.join(cmd)}` did not emit a final `result` event. "
f"First {_MAX_SNIPPET_LEN} chars of stdout:\n{stdout_snippet}"
)
try:
return _strip_fenced_json(final_result)
except json.JSONDecodeError as exc:
snippet = final_result[:_MAX_SNIPPET_LEN]
raise RuntimeError(
f"`{' '.join(cmd)}` returned unparseable output. "
f"First {_MAX_SNIPPET_LEN} chars:\n{snippet}"
) from exc
def _parse_stream_event(line: str) -> dict | None:
"""Parse one line of claude-cli stream-json output, returning None on junk."""
line = line.strip()
if not line:
return None
try:
parsed = json.loads(line)
except json.JSONDecodeError:
return None
return parsed if isinstance(parsed, dict) else None
def _call_llm(digest: str, model: str) -> dict:View on GitHub (pinned to 322425c43b)
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.
Example fix
# before headroom learn # 'returned unparseable output. First 500 chars: Sure! Here is...' # after headroom learn --model gpt-4o # model that follows the JSON contract
Defensive patterns
Strategy: retry
Try / catch
for model in ("claude", "gpt-4o", "gemini/gemini-1.5-pro"):
try:
return run_learn(model=model)
except RuntimeError as e:
if "returned unparseable output" not in str(e):
raise
# all backends produced non-JSON output
raise RuntimeError("no backend produced parseable JSON") Prevention
- 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.
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- `{' '.join(cmd)}` did not emit a final `result` event. First
- invalid pipeline config TOML: {0}
- recommendations file not found: {0}
- items_json must be JSON: {e}
- items_json must be a JSON array, got {}
AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15).
Data as JSON: /api/errors/658769a82d7275fb.
Report an issue: GitHub.