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++ }
} catch (error) { ... exhausted++ : deferred++ } 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
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).
Context
Carved out of
#16223as 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.backfillMiniSummarieshas 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++ } // thrownThose two paths are not interchangeable — they correspond to the two nested timeouts:
generateMiniSummaryTimeoutMsbuildMiniSummary, whosetry/catchswallows itminiSummaryTimeoutMssummarize()from outside, rejection escapescatchSo 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
deferredandexhaustedreport 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 andcatchbranch, 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-shapetoEqualassertions on the return; an added field is a deliberate contract change they will catch.The Fix
Add
failedInner/failedOuteralongside 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
0for both: nosummarize()is attempted there, so attributing its deferred rows to a branch would report generation timeouts that never happened.Contract Ledger
backfillMiniSummariesreturnfailedInner/failedOuter; existing keys unchangeddeferredbut distinct branch countersDecision Record impact
none— an additive field on an internal service return; no ADR authority touched.Acceptance Criteria
failedInner; a throwing one incrementsfailedOuter.0/0— deferred rows there are not attributed to a generation branch.failedInner + failedOuterequalsdeferred + exhaustedfor any pass where generation was attempted.toEqual→toMatchObjectweakening.Out of Scope
#14418).#16374/#14418).#16223— provider-side cancellation, restore-on-widen,thoughtinclusion.Avoided Traps
toEqualassertions. Their exactness is what forces an added field to be acknowledged rather than silently absorbed; swapping totoMatchObjectwould remove the guard at the moment it did its job.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).