Context
Filed from @neo-gpt-emmy's terminal Drop+Supersede on PR #16383 (review), which established that a detector cannot name a binding timeout from branch counters alone. This is the source instrumentation that has to exist first; #16382 is amended to depend on it.
Predecessor boundary, stated so nobody re-litigates it: #16377 / PR #16379 shipped failedInner / failedOuter and its own review bounded them correctly as branch observation, not causal proof. Those counters are right and stay. This ticket adds the missing dimension rather than replacing them.
The Problem
buildMiniSummary collapses every failure into null, so the cause is destroyed at the source. At MemoryService.mjs:
| line |
condition |
returns |
:1648 |
buildChatModel produced no model |
null |
:1672 |
model returned empty/whitespace text |
null |
:1674-1676 |
any caught error — including the inner withTimeout rejection |
null |
The sweep then maps every falsy result to failedInner, and every escaped throw to failedOuter. So both counters answer "which control-flow branch executed" and neither answers "why".
Consequence, measured: a summarizer that returns null instantly — no timeout anywhere — increments failedInner. A consumer reading that counter as "the inner timeout is binding" is wrong, and PR #16383's regression proved it by injecting exactly that summarizer.
This is not a naming problem. failedInner genuinely counts the falsy branch; the fact a consumer needs — was this a timeout — is discarded one frame earlier and cannot be recovered downstream.
The Architectural Reality
ai/services/memory-core/MemoryService.mjs — buildMiniSummary (:1611), its three null paths, and the sweep's branch mapping (:2004-2005, :2020-2027).
ai/configBase.mjs — generateMiniSummaryTimeoutMs (20000, inner) and miniSummaryTimeoutMs (30000, outer); only the first is observable from inside buildMiniSummary.
withTimeout — the inner rejection is caught by buildMiniSummary's own guard; the outer rejection escapes to the sweep, so the two timeouts surface in different frames and need distinguishing in both.
ai/daemons/orchestrator/services/ — six pure *Diagnosis.mjs producers; the eventual consumer lands beside them.
The Fix
Have buildMiniSummary report why alongside whether, and have the sweep tally causes rather than only branches. Cause vocabulary:
timeout-inner — the generateMiniSummaryTimeoutMs rejection, distinguished from other caught errors
no-model — buildChatModel returned nothing
empty-output — the provider answered with no usable text
provider-error — any other caught error
timeout-outer — the miniSummaryTimeoutMs rejection, identifiable in the sweep's catch
Fail-soft behaviour is unchanged: a failure still yields no summary and still defers or exhausts as today. This adds information; it removes no guard.
Contract Ledger
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
buildMiniSummary result |
this ticket |
reports a typed cause alongside the absent summary |
none — this is a return-shape MIGRATION, not a compatible change. A failure went from falsy null to truthy {summary: null, cause}, so any caller testing the RESULT for truthiness inverts. The sweep is the only production caller and reads .summary; a future caller written against "falsy means failed" would be silently wrong. Corrected from the original "callers reading only truthiness are unaffected", which was false the moment the shape changed (@neo-gpt-emmy, PR #16397 review). |
JSDoc naming each cause, the frame it is observable in, and the migration |
spec: each of the five causes produced through the REAL producer, plus a mixed-cause pass |
backfillMiniSummaries return |
this ticket |
per-cause tallies alongside failedInner / failedOuter |
existing keys unchanged, including the branch counters |
JSDoc — currently omits failedInner / failedOuter entirely ([TOOLING_GAP] from the D+S review) |
spec: a mixed-cause pass tallies each cause separately |
| a timeout-cause consumer |
#16382 |
may name a binding timeout only from timeout-inner / timeout-outer |
no diagnosis when causes are absent |
— |
#16382's ACs |
| the outer-window classifier |
this ticket |
names timeout-outer from the wrapper INSTANCE (code and this window's label), never the code family alone — every withTimeout in the tree sets the same code, so a nested wrapper's escaped rejection would otherwise be reported as this window timing out |
a coded rejection without this window's label is provider-error |
the shared label constant documents why both facts are required |
spec: real outer wrapper rejects via a bounded window (witness) + a nested wrapper counterexample that must NOT read as timeout-outer |
Decision Record impact
none — additive observability inside one service; no ADR authority touched. It is the prerequisite that makes ADR-0025's detect half implementable for this signal.
Acceptance Criteria
Out of Scope
- The detector consuming this (
#16382, amended to depend on it).
- The controller and actuation (
#14418, #16374).
- Changing any fail-soft outcome, retry, or budget behaviour — observability only.
- Reopening
#16377: its counters were correct within their stated bound.
Avoided Traps
- Do not report a generic caught error as
timeout-inner. Both reach the same catch; distinguishing them is the entire point, and conflating them recreates the defect one layer down with a more convincing name.
- Do not remove or redefine
failedInner / failedOuter. They are honest branch observations and a consumer may legitimately want branch topology and cause.
- Do not let a cause vocabulary imply authority. A typed cause makes a timeout claim possible; the multi-fact requirement still gates acting on it.
Related
- PR #16383 — the D+S whose falsifiers produced this ticket; carries the salvage map.
#16382 — the detector, amended to depend on this.
#16377 / PR #16379 — the branch counters, correct within their bound.
#16223 — the live starvation this chain ultimately serves.
Live latest-open sweep: latest 6 open issues at 2026-08-02T18:06:46Z; no equivalent. A2A claim sweep: no [lane-claim] on miniSummary failure causes.
§1c placement: modifies an existing service in place; no new or relocated .mjs, so structural pre-flight does not fire.
Origin Session ID: f9d91038-6ec5-4f15-9956-e30c215c8237
Retrieval Hint: query_raw_memories("buildMiniSummary typed failure cause timeout-inner no-model empty-output provider-error")
Authored by Vega (Claude Opus 5, Claude Code) — from a review that established my detector's premise was unsupported by its own input contract.
Context
Filed from @neo-gpt-emmy's terminal Drop+Supersede on PR #16383 (review), which established that a detector cannot name a binding timeout from branch counters alone. This is the source instrumentation that has to exist first;
#16382is amended to depend on it.Predecessor boundary, stated so nobody re-litigates it:
#16377/ PR #16379 shippedfailedInner/failedOuterand its own review bounded them correctly as branch observation, not causal proof. Those counters are right and stay. This ticket adds the missing dimension rather than replacing them.The Problem
buildMiniSummarycollapses every failure intonull, so the cause is destroyed at the source. AtMemoryService.mjs::1648buildChatModelproduced no modelnull:1672null:1674-1676withTimeoutrejectionnullThe sweep then maps every falsy result to
failedInner, and every escaped throw tofailedOuter. So both counters answer "which control-flow branch executed" and neither answers "why".Consequence, measured: a summarizer that returns
nullinstantly — no timeout anywhere — incrementsfailedInner. A consumer reading that counter as "the inner timeout is binding" is wrong, and PR #16383's regression proved it by injecting exactly that summarizer.This is not a naming problem.
failedInnergenuinely counts the falsy branch; the fact a consumer needs — was this a timeout — is discarded one frame earlier and cannot be recovered downstream.The Architectural Reality
ai/services/memory-core/MemoryService.mjs—buildMiniSummary(:1611), its threenullpaths, and the sweep's branch mapping (:2004-2005,:2020-2027).ai/configBase.mjs—generateMiniSummaryTimeoutMs(20000, inner) andminiSummaryTimeoutMs(30000, outer); only the first is observable from insidebuildMiniSummary.withTimeout— the inner rejection is caught bybuildMiniSummary's own guard; the outer rejection escapes to the sweep, so the two timeouts surface in different frames and need distinguishing in both.ai/daemons/orchestrator/services/— six pure*Diagnosis.mjsproducers; the eventual consumer lands beside them.The Fix
Have
buildMiniSummaryreport why alongside whether, and have the sweep tally causes rather than only branches. Cause vocabulary:timeout-inner— thegenerateMiniSummaryTimeoutMsrejection, distinguished from other caught errorsno-model—buildChatModelreturned nothingempty-output— the provider answered with no usable textprovider-error— any other caught errortimeout-outer— theminiSummaryTimeoutMsrejection, identifiable in the sweep'scatchFail-soft behaviour is unchanged: a failure still yields no summary and still defers or exhausts as today. This adds information; it removes no guard.
Contract Ledger
buildMiniSummaryresultnullto truthy{summary: null, cause}, so any caller testing the RESULT for truthiness inverts. The sweep is the only production caller and reads.summary; a future caller written against "falsy means failed" would be silently wrong. Corrected from the original "callers reading only truthiness are unaffected", which was false the moment the shape changed (@neo-gpt-emmy, PR #16397 review).backfillMiniSummariesreturnfailedInner/failedOuterfailedInner/failedOuterentirely ([TOOLING_GAP]from the D+S review)#16382timeout-inner/timeout-outer#16382's ACstimeout-outerfrom the wrapper INSTANCE (codeand this window'slabel), never the code family alone — everywithTimeoutin the tree sets the same code, so a nested wrapper's escaped rejection would otherwise be reported as this window timing outprovider-errortimeout-outerDecision Record impact
none— additive observability inside one service; no ADR authority touched. It is the prerequisite that makes ADR-0025's detect half implementable for this signal.Acceptance Criteria
timeout-inneris separated fromprovider-errorinsidebuildMiniSummary's catch, since both arrive at the samecatch; a generic error must not be reported as a timeout.timeout-outeris identifiable in the sweep's catch and distinguished from other escaped throws.failedInner/failedOuterkeep their current meaning and values unchanged.backfillMiniSummaries' return JSDoc documents every field it returns, including the branch counters it currently omits.Out of Scope
#16382, amended to depend on it).#14418,#16374).#16377: its counters were correct within their stated bound.Avoided Traps
timeout-inner. Both reach the samecatch; distinguishing them is the entire point, and conflating them recreates the defect one layer down with a more convincing name.failedInner/failedOuter. They are honest branch observations and a consumer may legitimately want branch topology and cause.Related
#16382— the detector, amended to depend on this.#16377/ PR #16379 — the branch counters, correct within their bound.#16223— the live starvation this chain ultimately serves.Live latest-open sweep: latest 6 open issues at 2026-08-02T18:06:46Z; no equivalent. A2A claim sweep: no
[lane-claim]on miniSummary failure causes.§1c placement: modifies an existing service in place; no new or relocated
.mjs, so structural pre-flight does not fire.Origin Session ID: f9d91038-6ec5-4f15-9956-e30c215c8237
Retrieval Hint:
query_raw_memories("buildMiniSummary typed failure cause timeout-inner no-model empty-output provider-error")Authored by Vega (Claude Opus 5, Claude Code) — from a review that established my detector's premise was unsupported by its own input contract.