Context
#14084 leaf 2 (after leaf 1 #14106 — the classifyRepairResidue decider). The classifier consumes a durable ack and returns accepted-loss only when ack.fingerprint matches the live terminal residue. This leaf builds the producer of that ack — the durable operator-acknowledgement record — so the ack the operator stores is exactly the one the classifier accepts (the loop closes), and a later residue/strategy/provider change makes the stored ack stop matching by construction (the emergent-invalidation contract).
The Architectural Reality
ai/services/memory-core/helpers/classifyRepairResidue.mjs (#14106) exports computeResidueFingerprint — the stable hash the classifier uses. The ack MUST reuse it (so the fingerprints are identical).
ai/services/memory-core/helpers/recoveryRunStateStore.mjs is the typed-record-constructor pattern to mirror (createRecoveryDiagnosisEvent / createRecoveryRunStateEntry: pure constructor + schemaVersion + a typed type).
- The durable PERSISTENCE (store/retrieve-by-fingerprint) + the operator-invocation surface + the defrag wiring are leaf 3 — this leaf is the pure record SSOT.
The Fix
A pure createAcceptedLossAckEntry({residue, operatorId, acknowledgedAt, strategyVersion, provider, contextBudget, terminalReasons, recoveryRunId}) in a ai/services/memory-core/helpers/acceptedLossAck.mjs sibling:
- Computes
fingerprint via the shared computeResidueFingerprint (identical to the classifier's), packaging it + acknowledgedIds (sorted) + residueCount + the strategy/provider/context + the sorted terminality-policy set (terminalReasons) + operatorId + acknowledgedAt into a typed accepted-loss-ack record (schemaVersion: 1). The fingerprint binds terminalReasons (PR #14108 review), so a policy change staleness-invalidates the ack.
- Pure — no I/O, no time/randomness (the caller supplies
acknowledgedAt).
- Throws on missing
operatorId / non-finite acknowledgedAt (mirrors the store's validation).
Acceptance Criteria
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
createAcceptedLossAckEntry({residue, operatorId, acknowledgedAt, strategyVersion, provider, contextBudget, terminalReasons, recoveryRunId}) |
#14084 graduated design + computeResidueFingerprint (#14106) |
typed accepted-loss-ack record with the shared fingerprint binding the sorted terminalReasons policy set |
bad operatorId/acknowledgedAt → TypeError |
module + fn JSDoc |
unit spec (round-trip with the classifier) |
Out of Scope
- The durable persistence + retrieve-by-fingerprint + the operator-invocation surface (leaf 3).
- The
defragChromaDB.mjs outcome wiring (leaf 3).
Related
#14084 (parent — graduated design), #14106 (leaf 1 — the classifier this pairs with), #14026 (the detect signal the ack suppresses), #14039.
Live latest-open sweep: checked latest open + the #14084 leaf set at 2026-06-26T14:34Z; no ack-constructor leaf exists. A2A: #14084 graduation comment posted.
Origin Session ID: 5ab545e1-f09e-46c5-ae62-8cf5b2b96193
Context
#14084 leaf 2 (after leaf 1 #14106 — the
classifyRepairResiduedecider). The classifier consumes a durableackand returnsaccepted-lossonly whenack.fingerprintmatches the live terminal residue. This leaf builds the producer of that ack — the durable operator-acknowledgement record — so the ack the operator stores is exactly the one the classifier accepts (the loop closes), and a later residue/strategy/provider change makes the stored ack stop matching by construction (the emergent-invalidation contract).The Architectural Reality
ai/services/memory-core/helpers/classifyRepairResidue.mjs(#14106) exportscomputeResidueFingerprint— the stable hash the classifier uses. The ack MUST reuse it (so the fingerprints are identical).ai/services/memory-core/helpers/recoveryRunStateStore.mjsis the typed-record-constructor pattern to mirror (createRecoveryDiagnosisEvent/createRecoveryRunStateEntry: pure constructor + schemaVersion + a typedtype).The Fix
A pure
createAcceptedLossAckEntry({residue, operatorId, acknowledgedAt, strategyVersion, provider, contextBudget, terminalReasons, recoveryRunId})in aai/services/memory-core/helpers/acceptedLossAck.mjssibling:fingerprintvia the sharedcomputeResidueFingerprint(identical to the classifier's), packaging it +acknowledgedIds(sorted) +residueCount+ the strategy/provider/context + the sorted terminality-policy set (terminalReasons) +operatorId+acknowledgedAtinto a typedaccepted-loss-ackrecord (schemaVersion: 1). The fingerprint bindsterminalReasons(PR #14108 review), so a policy change staleness-invalidates the ack.acknowledgedAt).operatorId/ non-finiteacknowledgedAt(mirrors the store's validation).Acceptance Criteria
createAcceptedLossAckEntryreturns a typedaccepted-loss-ackrecord whosefingerprintequalscomputeResidueFingerprintover the same residue + context + sorted terminality-policy set; the record carries the sortedterminalReasonsfor provenance.classifyRepairResidue({residue, ack: createAcceptedLossAckEntry({residue, …})})→accepted-loss(the ack the constructor builds is accepted by the classifier).operatorId/ non-finiteacknowledgedAt.Contract Ledger Matrix
createAcceptedLossAckEntry({residue, operatorId, acknowledgedAt, strategyVersion, provider, contextBudget, terminalReasons, recoveryRunId})computeResidueFingerprint(#14106)accepted-loss-ackrecord with the shared fingerprint binding the sortedterminalReasonspolicy setoperatorId/acknowledgedAt→TypeErrorOut of Scope
defragChromaDB.mjsoutcome wiring (leaf 3).Related
#14084 (parent — graduated design), #14106 (leaf 1 — the classifier this pairs with), #14026 (the detect signal the ack suppresses), #14039.
Live latest-open sweep: checked latest open + the #14084 leaf set at 2026-06-26T14:34Z; no ack-constructor leaf exists. A2A: #14084 graduation comment posted.
Origin Session ID: 5ab545e1-f09e-46c5-ae62-8cf5b2b96193