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
SessionEnd → extract-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
- 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.
- Distinguish the two branches — "no JSON object found in response" vs "extracted object failed to parse" point at different problems.
- Consider one repair retry when the engine has
supportsJsonSchema: false, re-prompting for JSON only. That is where the failure concentrates.
- 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.
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
SessionEnd→extract-sessionhookdefaults.llmEngine = "default"→ local vLLM,supportsJsonSchema: falseGET /v1/models→ 200), so the call is reaching the modelActual
~/.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: trueand an emptywarningsarray mean nothing upstream — a health check, a nightlyimprovepass — 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 dropstdouton the floor: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 readextract.log. Durable knowledge capture stops working with no visible signal.Suggested fix
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.supportsJsonSchema: false, re-prompting for JSON only. That is where the failure concentrates.warningssook: truewith zero candidates is not indistinguishable from a genuinely empty session.