Skip to content

extract: unparseable LLM JSON is silently swallowed — raw response discarded, no retry, no top-level warning #812

Description

@itlackey

Summary

When the extract LLM returns something parseExtractPayload() cannot parse, the run reports success with zero candidates and the only diagnostic is the fixed string "LLM response was not parseable JSON". The raw response is discarded, so there is no way to tell whether the model emitted prose, a truncated object, malformed JSON, or an error body — and there is no retry. Session extraction then silently produces nothing, run after run.

Environment

  • akm CLI 0.9.1, Linux
  • Invoked via the akm Claude Code plugin's SessionEndextract-session hook
  • defaults.llmEngine = "default" → local vLLM, supportsJsonSchema: false
  • Endpoint verified reachable (GET /v1/models → 200), so the call is reaching the model

Actual

~/.local/state/akm-claude/extract.log:

{
  "schemaVersion": 1,
  "ok": true,
  "shape": "extract-result",
  "type": "claude-code",
  "sessionsProcessed": 1,
  "sessionsSkipped": 0,
  "candidatesCreated": 0,
  "proposals": [],
  "sessions": [
    {
      "sessionId": "d517f795-92f0-4cc5-b40b-495fa5627ea6",
      "harness": "claude-code",
      "candidateCount": 0,
      "proposalIds": [],
      "rationaleIfEmpty": "LLM response was not parseable JSON",
      "preFilter": { "inputCount": 12, "outputCount": 9, "truncatedCount": 0 },
      "warnings": [],
      "contentHash": "825fbb182199d8df4a68bc90524ba7cb10801798f0bbf779b4664ec3b8123169"
    }
  ],
  "warnings": [],
  "durationMs": 9885
}

The session was read and pre-filtered correctly (12 in, 9 out), so everything up to the LLM call worked. ok: true and an empty warnings array mean nothing upstream — a health check, a nightly improve pass — registers this as a failure.

Cause

src/commands/improve/extract-prompt.ts, parseExtractPayload(). The function already tolerates prose preamble/postamble by slicing the first balanced top-level object, which is good, but both failure paths drop stdout on the floor:

const start = stdout.indexOf("{");
const end = stdout.lastIndexOf("}");
if (start === -1 || end <= start) {
  return { candidates: [], rationale_if_empty: `LLM response was not parseable JSON` };
}
try {
  parsed = JSON.parse(stdout.slice(start, end + 1));
} catch {
  return { candidates: [], rationale_if_empty: `LLM response was not parseable JSON` };
}

Nothing distinguishes "no brace found at all" from "braces found but the slice did not parse", and the response body is never retained, logged, or surfaced.

Impact

On an engine with supportsJsonSchema: false, free-form output is the expected failure mode rather than an exceptional one, so this is not a rare path — it is the steady state until someone happens to read extract.log. Durable knowledge capture stops working with no visible signal.

Suggested fix

  1. Retain evidence: include a truncated preview (say the first 500 chars) and the response length in rationale_if_empty, or log the full body behind --verbose / a debug env var. Without the raw text the failure is not diagnosable from the outside.
  2. Distinguish the two branches — "no JSON object found in response" vs "extracted object failed to parse" point at different problems.
  3. Consider one repair retry when the engine has supportsJsonSchema: false, re-prompting for JSON only. That is where the failure concentrates.
  4. Surface the parse failure in the run's top-level warnings so ok: true with zero candidates is not indistinguishable from a genuinely empty session.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions