headroomlabs-ai/headroom · error · RuntimeError

`{' '.join(cmd)}` did not emit a final `result` event. First

Error message

`{' '.join(cmd)}` did not emit a final `result` event. First {_MAX_SNIPPET_LEN} chars of stdout:
{stdout_snippet}

What it means

Raised by the streaming analyzer when claude-cli exited cleanly (exit 0) but no JSON event with type=="result" was ever seen on stdout. The result event is the only place the assistant's answer lives, so without it there is nothing to parse; the message includes the first _MAX_SNIPPET_LEN chars of captured stdout for diagnosis.

Source

Thrown at headroom/learn/analyzer.py:769

                result_text = event.get("result")
                if isinstance(result_text, str):
                    final_result = result_text
        else:
            stderr_lines.append(line)

    proc.wait()

    if proc.returncode != 0:
        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:

View on GitHub (pinned to 322425c43b)

Solutions

  1. Inspect the stdout snippet in the message — if it shows assistant text but no result frame, update claude-cli (`claude update`) to pick up stream fixes.
  2. Retry the run; transient empty responses from the backend are a known shape.
  3. Shrink the digest being analyzed (learn fewer conversations at once) so the response is shorter and the result event is not truncated.
  4. Switch to a different backend with --model <litellm-model-name> to rule out claude-cli-specific behavior.
  5. Report with debug logs if it persists on the latest CLI version.

Example fix

# before
headroom learn  # exit 0 but 'did not emit a final `result` event'

# after
claude update  # or:
headroom learn --model gpt-4o  # different backend
Defensive patterns

Strategy: retry

Try / catch

last_err = None
for attempt in range(2):
    try:
        result = run_learn(...)
        break
    except RuntimeError as e:
        if "did not emit a final `result` event" in str(e):
            last_err = e
            continue
        raise
else:
    raise SystemExit(f"backend dropped result event twice; switch --model: {last_err}")

Prevention

When it happens

Trigger: Calling the claude-cli with --output-format stream-json where the model returns an empty response, the CLI emits only system/assistant events and drops the final result event, the stream is truncated by a kill, or an unexpected claude-cli version changes the event schema so _parse_stream_event never matches type 'result'.

Common situations: claude-cli version drift changing stream-json event shapes; empty model responses (empty conversation or prompt filtered); upstream CLI bug losing the result event on long outputs; running against a litellm-proxied model that ends the stream without a result frame.

Related errors


AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15). Data as JSON: /api/errors/5e9cccc654cd1d91. Report an issue: GitHub.