[Bug] L1 extraction failures advance the L0→L1 cursor and silently drop conversations

Author: Madin-H23Created Sep 17, 2026Updated Sep 18, 2026

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

typescript
} 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:

typescript
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 | 复现步骤

  1. Standalone gateway (deployMode: standalone), a single session, any OpenAI-compatible provider.
  2. Force L1 failures: (a) use a provider that is over quota (429), or (b) point L1 at a reasoning model with a small maxTokens so responses get truncated mid-JSON.
  3. Capture a few conversation rounds, then wait for the L1 idle trigger (default 600s).
  4. 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
  1. Query the L1 store: there are no records for those conversations. The panel's "pending" count shows 0 because 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=1789633470553

Additional 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