LearnNewsExamplesServices
Frontmatter
id16377
titleminiSummary backfill counters cannot distinguish the two failure branches
stateClosed
labels
enhancementai
assigneesneo-opus-vega
createdAtAug 2, 2026, 6:01 PM
updatedAtAug 2, 2026, 6:57 PM
githubUrlhttps://github.com/neomjs/neo/issues/16377
authorneo-opus-vega
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 2, 2026, 6:57 PM

miniSummary backfill counters cannot distinguish the two failure branches

Closed Backlog/active-chunk-12 enhancementai
neo-opus-vega
neo-opus-vega commented on Aug 2, 2026, 6:01 PM

Context

Carved out of #16223 as the deliverable instrumentation slice, so a complete change has an honest close-target rather than riding a parent with six open ACs. Prerequisite for #14418's diagnosis half.

The Problem

MemoryService.backfillMiniSummaries has two failure paths that increment the same counters, so a consumer of the pass result cannot tell them apart:

if (!miniSummary) { ... exhausted++ : deferred++ }   // falsy return
} catch (error)   { ... exhausted++ : deferred++ }   // thrown

Those two paths are not interchangeable — they correspond to the two nested timeouts:

leaf value fires lands on
generateMiniSummaryTimeoutMs 20000 inside buildMiniSummary, whose try/catch swallows it falsy return
miniSummaryTimeoutMs 30000 wraps summarize() from outside, rejection escapes the sweep's catch

So which branch a failure takes is not a property of the code — it is a function of the generation window. Widen the inner leaf past the outer one and every failure migrates from one branch to the other while deferred and exhausted report an identical shape.

That is the defect: a consumer reading only the totals sees no change at the moment the failure mode changed. An adaptive controller (#14418) would read "still starved, keep widening" and keep actuating toward a window where its own signal has already gone blind. A detector whose blind spot is correlated with the actuator's activity is worse than no detector, because it reads healthy under load.

The Architectural Reality

  • ai/services/memory-core/MemoryService.mjs — the backfill loop's falsy branch and catch branch, plus three early returns that must carry any added field for shape consistency.
  • ai/configBase.mjs — the two window leaves whose ordering creates the branch boundary.
  • test/playwright/unit/ai/services/memory-core/QueryRecentTurns.spec.mjs — four existing exact-shape toEqual assertions on the return; an added field is a deliberate contract change they will catch.

The Fix

Add failedInner / failedOuter alongside the existing totals, incremented on the falsy and thrown branches respectively, present on every exit. Existing keys unchanged; no behaviour change.

The content-store-unreachable early return reports 0 for both: no summarize() is attempted there, so attributing its deferred rows to a branch would report generation timeouts that never happened.

Contract Ledger

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
backfillMiniSummaries return this ticket adds failedInner / failedOuter; existing keys unchanged consumers reading only totals are unaffected inline comment on the counter declaration stating the branch↔leaf mapping spec: one falsy + one thrown row produce an indistinguishable deferred but distinct branch counters

Decision Record impact

none — an additive field on an internal service return; no ADR authority touched.

Acceptance Criteria

  • A falsy-returning summarizer increments failedInner; a throwing one increments failedOuter.
  • Both are present on every return path, including the zero-row, no-SQLite, and content-store-unreachable early exits.
  • The content-store-unreachable path reports 0/0 — deferred rows there are not attributed to a generation branch.
  • failedInner + failedOuter equals deferred + exhausted for any pass where generation was attempted.
  • The existing exact-shape assertions are updated, not relaxed — no toEqualtoMatchObject weakening.
  • The regression test is order-independent: it pins the split without pinning which rows the sweep selects.

Out of Scope

  • The detector/diagnosis that consumes this (#14418).
  • The actuation that moves the window (#16374 / #14418).
  • Everything else on #16223 — provider-side cancellation, restore-on-widen, thought inclusion.

Avoided Traps

  • Do not relax the existing toEqual assertions. Their exactness is what forces an added field to be acknowledged rather than silently absorbed; swapping to toMatchObject would remove the guard at the moment it did its job.
  • Do not attribute non-generation failures to a branch. The store-unreachable path defers rows without attempting generation; counting them would fabricate timeouts and mislead an adaptive consumer.
  • Do not pin the population in the regression test. Sibling specs leave pending rows in the shared store, so an exact-count assertion passes in isolation and fails in file order.

Related

  • #16223 — parent; this is its instrumentation slice.
  • #14418 — the homeostatic controller whose diagnosis half consumes these counters.
  • #16313 — shipped the attempt budget these counters sit beside.

Live latest-open sweep: latest 10 open issues at 2026-08-02T16:00:35Z; no equivalent. A2A claim sweep: no [lane-claim] on the miniSummary counters.

Origin Session ID: 1bf32e47-868c-43ce-9e9c-537eeeee5ea1

Retrieval Hint: query_raw_memories("miniSummary backfill failedInner failedOuter branch flip nested timeout")

Authored by Vega (Claude Opus 5, Claude Code).

tobiu closed this issue on Aug 2, 2026, 6:57 PM