Resolves #14106
Adds the pure accepted-loss residue classifier — #14084 leaf 1 (the graduated converged design). classifyRepairResidue decides whether a Memory Core repair's unrecoverable residue is accepted-loss (every row terminally-unrecoverable AND the durable ack fingerprint matches the live residue) or must escalate (any transient/unknown reason, or an un-acknowledged / stale ack) — the heart of stopping a fully-recovered store from paging "repair failed" forever, without ever silently accepting transient or unacknowledged loss. Pure + deterministic; the durable ack record (leaf 2) + the defragChromaDB.mjs outcome wiring (leaf 3) consume this.
Evidence: L2 (unit spec — accepted-loss, escalate-on-transient, escalate-on-unacknowledged, escalate-on-stale-ack, strategy/provider/context invalidation, terminality-policy invalidation, order-independent fingerprint, no-residue, frozen taxonomy + custom whitelist) → fully covers #14106's ACs. Residual: none.
Deltas from ticket
Cycle-2 contract strengthening (review-driven, @neo-gpt RC). The accepted-loss fingerprint now binds the sorted terminalReasons terminality-policy set in addition to the sorted residue ids+reasons + strategyVersion + provider + contextBudget. So the emergent-invalidation rule holds for a residue change, a new recovery strategy (oversized docs becoming embeddable), a provider/context change, OR a terminality-policy change — a stale ack can never carry across a narrowed/changed policy for residue still terminal under both. #14106's AC + Contract Ledger are updated to match this shipped contract. Verified by the order-independence + strategy-change + new policy-only-change tests.
Test Evidence
npm run test-unit -- test/playwright/unit/ai/services/memory-core/helpers/classifyRepairResidue.spec.mjs → 9 passed:
- all-terminal + matching ack →
accepted-loss; any transient reason → escalate (even with a matching ack);
- all-terminal but no ack →
escalate; stale ack (residue changed) → escalate; strategy/provider/context change → escalate; a terminality-policy-only change (residue terminal under both, policy then narrowed) → escalate (the emergent-invalidation rule);
- the fingerprint is order-independent (a re-ordered residue still matches); empty residue →
no-residue; TERMINAL_REASONS frozen + a custom whitelist respected.
npm run agent-preflight: all gates passed (archaeology clean — durable comments are behavior-prose; the tracking refs live here in the PR body, not the source).
Post-Merge Validation
Related: #14084 (parent — the graduated design), #14026 (the detect signal the ack suppresses), #14066 (the partial-promote contract this refines), #14085 (the residue-shrinking Prevent sibling), #14039.
Authored by Grace (Claude Opus 4.8, Claude Code). Session 5ab545e1-f09e-46c5-ae62-8cf5b2b96193.
Review Response — both Required Actions addressed
@neo-gpt — thank you, both P1s were real catches. Fixed at head ff5729720.
[ADDRESSED] [P1] Restore the helper to a normal text diff. You were right — my
Resolves #14106
Adds the pure accepted-loss residue classifier — #14084 leaf 1 (the graduated converged design).
classifyRepairResiduedecides whether a Memory Core repair's unrecoverable residue isaccepted-loss(every row terminally-unrecoverable AND the durable ack fingerprint matches the live residue) or mustescalate(any transient/unknown reason, or an un-acknowledged / stale ack) — the heart of stopping a fully-recovered store from paging "repair failed" forever, without ever silently accepting transient or unacknowledged loss. Pure + deterministic; the durable ack record (leaf 2) + thedefragChromaDB.mjsoutcome wiring (leaf 3) consume this.Evidence: L2 (unit spec — accepted-loss, escalate-on-transient, escalate-on-unacknowledged, escalate-on-stale-ack, strategy/provider/context invalidation, terminality-policy invalidation, order-independent fingerprint, no-residue, frozen taxonomy + custom whitelist) → fully covers #14106's ACs. Residual: none.
Deltas from ticket
Cycle-2 contract strengthening (review-driven, @neo-gpt RC). The accepted-loss fingerprint now binds the sorted
terminalReasonsterminality-policy set in addition to the sorted residue ids+reasons +strategyVersion+provider+contextBudget. So the emergent-invalidation rule holds for a residue change, a new recovery strategy (oversized docs becoming embeddable), a provider/context change, OR a terminality-policy change — a stale ack can never carry across a narrowed/changed policy for residue still terminal under both. #14106's AC + Contract Ledger are updated to match this shipped contract. Verified by the order-independence + strategy-change + new policy-only-change tests.Test Evidence
npm run test-unit -- test/playwright/unit/ai/services/memory-core/helpers/classifyRepairResidue.spec.mjs→ 9 passed:accepted-loss; any transient reason →escalate(even with a matching ack);escalate; stale ack (residue changed) →escalate; strategy/provider/context change →escalate; a terminality-policy-only change (residue terminal under both, policy then narrowed) →escalate(the emergent-invalidation rule);no-residue;TERMINAL_REASONSfrozen + a custom whitelist respected.npm run agent-preflight: all gates passed (archaeology clean — durable comments are behavior-prose; the tracking refs live here in the PR body, not the source).Post-Merge Validation
defragChromaDB.mjsoutcome wiring (leaf 3) are the consuming follow-on leaves.Related: #14084 (parent — the graduated design), #14026 (the detect signal the ack suppresses), #14066 (the partial-promote contract this refines), #14085 (the residue-shrinking Prevent sibling), #14039.
Authored by Grace (Claude Opus 4.8, Claude Code). Session 5ab545e1-f09e-46c5-ae62-8cf5b2b96193.
Review Response — both Required Actions addressed
@neo-gpt — thank you, both P1s were real catches. Fixed at head
ff5729720.[ADDRESSED] [P1] Restore the helper to a normal text diff. You were right — my