[Bug] L1 extraction failures advance the L0→L1 cursor and silently drop conversations
Describe the bug | 问题描述
L1 extraction failures still advance the L0→L1 cursor, so the affected conversations are skipped forever — no retry, no warning, and the operator cannot see it.
When the L1 LLM call fails (429/quota, network error, timeout), or when the response cannot be parsed as a JSON array (e.g. output truncated by max_tokens, finishReason=length), the pipeline still calls markL1ExtractionComplete() with the processed slice's max recorded_at. Since the cursor is the only filter for the next L0 read, those rows are never re-read.
Two paths in MemoryCore (master):
(1) LLM error is swallowed — src/core/record/l1-extractor.ts:212-216
} catch (err) {
logger?.error(`${TAG} LLM extraction failed: ${err instanceof Error ? err.message : String(err)}`);
return { success: false, extractedCount: 0, storedCount: 0, records: [], sceneNames: [] };
}The L1 runner does not consume success for L1, and src/utils/pipeline-factory.ts:658 then advances the cursor unconditionally:
await checkpoint.markL1ExtractionComplete(sessionKey, totalStored, maxRecordedAtMs || undefined, lastSceneName);(2) An unparseable response is treated as a valid empty extraction
The parse path returns { scenes: [], emptyReason: "no_json" | "not_array" }. emptyReason currently has no consumer anywhere in MemoryCore/src, so a truncated/garbled response is indistinguishable from a genuine "nothing to remember" — and the cursor still advances.
To Reproduce | 复现步骤
- Standalone gateway (
deployMode: standalone), a single session, any OpenAI-compatible provider. - Force L1 failures: (a) use a provider that is over quota (429), or (b) point L1 at a reasoning model with a small
maxTokensso responses get truncated mid-JSON. - Capture a few conversation rounds, then wait for the L1 idle trigger (default 600s).
- In the gateway logs, observe the failure immediately followed by a cursor advance:
ERROR [l1-extractor] LLM extraction failed: Failed after 3 attempts. Last error: quota exhausted, will reset later
INFO [checkpoint] markL1ExtractionComplete session=<session>: extracted=0, cursor=1789633946176
WARN [l1-extractor] No JSON array found in extraction response
DEBUG [standalone-runner] step[0] text: 121 chars, finishReason=length
INFO [checkpoint] markL1ExtractionComplete session=<session>: extracted=0, cursor=1789633470553- Query the L1 store: there are no records for those conversations. The panel's "pending" count shows
0because it is cursor-based — a false negative.
Measured on a small standalone deployment (one day, one session): 8 truncated responses + 3 quota-exhausted calls → 11 batches / 72 L0 messages silently skipped. Manually rewinding runner_states[session].last_l1_cursor in .metadata/checkpoint.json re-ran L1 over exactly those rows and produced 39 L1 records — i.e. the content was not "unmemorable", it was lost.
Expected behavior | 预期行为
- A failed extraction must not advance the cursor: L0 accumulates, and the existing task-level retry (PipelineWorker retry / dead-letter) plus the idle/threshold triggers make it distill later — or the operator triggers it manually.
- A valid empty extraction must still advance the cursor (otherwise the cursor would stall forever).
- Truncated responses (
finishReason=length) should be treated as failures rather than as "no memories".
Error Logs / Screenshots | 报错日志
2026-09-17T16:42:32.882 ERROR [l1-extractor] LLM extraction failed: Failed after 3 attempts. Last error: quota exhausted
2026-09-17T16:42:32.890 INFO [checkpoint] markL1ExtractionComplete session=<session>: extracted=0, cursor=1789633946176
2026-09-17T16:26:05.539 DEBUG [standalone-runner] step[0] text: 121 chars, finishReason=length
2026-09-17T16:26:05.540 WARN [l1-extractor] No JSON array found in extraction response
2026-09-17T16:26:05.547 INFO [checkpoint] markL1ExtractionComplete session=<session>: extracted=0, cursor=1789633470553Additional context | 补充信息
Environment: Windows 11 · Node 22 · MemoryCore standalone gateway @ master · OpenAI-compatible provider · OpenClaw not used.
We patched this locally (make the extractor signal failure so the runner skips markL1ExtractionComplete, and consume emptyReason). Happy to open a PR following CONTRIBUTING (Conventional Commits + DCO sign-off) if the direction sounds right — or to adapt it to whatever shape you prefer.
(中文摘要)蒸馏失败(限额 / 网络 / 响应被 max_tokens 截断而不可解析)时,L1 仍会推进游标 → 该批 L0 被永久跳过,无重试、无告警;面板「未蒸馏」按游标计算,因此看不到这类丢失(假阴性)。当日实测 11 批 72 条被静默丢弃;回退游标重放后 39 条 L1 全部可提取,证明是丢失而非「无可记」。
Source: TencentCloud/TencentDB-Agent-Memory