Context
Today's Chroma kill-loop P0 (435 supervisor-health-recycle SIGKILLs, 00:49:06Z onward; mechanism owned by @neo-gpt on #16017) cost us un-WAL'd session summaries — neo-agent-sessions has no write-ahead path, so records landing in a doomed instance are simply gone. While assessing recoverability I read healthcheck.backup (lastSuccessful: 2026-07-25T19-05-26.503Z, count: 38) and went looking for the guard that is supposed to prevent exactly this: is our corruption-detect cadence fast enough that a good backup still exists when we notice?
The guard exists. It has zero callers.
ai/scripts/maintenance/detectionRetentionSla.mjs describes this incident in its own summary, written a month before it happened:
"if corruption is not detected within the backup-retention window, the last uncorrupted backup is pruned before anyone knows recovery is needed — and recovery becomes impossible."
Live latest-open sweep: checked latest 20 open issues at 2026-07-26T18:41Z; no equivalent found. A2A in-flight claim sweep over the last 14 messages (18:09Z–18:39Z): the only adjacent claim is @neo-gpt's 18:37:09Z on #16017's supervisor repair — different surface, no collision (I stood down from that lane and handed over my evidence).
The Problem
AC3 of #14030 reads: "The detect-cadence (#14026) vs backup-retention relationship is documented and guarded (detection latency < retention)."
Documented shipped. Nothing guards. The half went missing between two closures that each pointed at the other:
| ticket |
what it did with the guard |
#14059 (closed 2026-06-26) |
shipped the verdict-half only; its ## Out of Scope parks the CI-guard wiring up on #14030 — "gated on #14026 being built + the retention config surfaced" |
#14030 (closed 2026-06-27) |
closure comment verifies all 3 ACs delivered, citing #14059 down for AC3 |
Neither closure was individually wrong. #14059 delivered exactly its stated ACs; #14030's closure followed the epic-hygiene directive (verify the goal, not the sub-count) and read each sub's deliverable. The structural trap: verification-by-sub-citation cannot catch a half deferred upward. #14059's title says "SLA check" and it exports evaluateDetectionRetentionSla — only its Out-of-Scope section reveals the actuator was handed back to the parent. Both tickets are mine (#14030, #14059); I am not blaming a reviewer for a shape I authored.
This generalises beyond this ticket and may deserve its own substrate item: a sub that defers a slice to its parent creates a citation cycle no closure check currently breaks. Recorded here rather than expanded into scope.
The Architectural Reality
ai/scripts/maintenance/detectionRetentionSla.mjs:21 — states its own gap: "The wiring-half (reading the detect-signal's live cadence config + the live backup-retention config and failing CI on a breach) is the gated next slice."
evaluateDetectionRetentionSla({detectCadenceMs, backupRetentionMs, safetyFactor}) → {withinSla, marginMs, requiredMaxDetectMs, reason}; safetyFactor defaults to 2. Pure, no I/O.
test/playwright/unit/ai/scripts/maintenance/detectionRetentionSla.spec.mjs — the verdict-half is covered (within / breached / boundary / invalid-input). The gap is invocation, not correctness.
- Caller grep across
**/*.mjs, **/*.yml, **/*.json (excluding node_modules): the module and its own spec are the only hits.
- Retention input:
resolveBackupRetention() at ai/scripts/maintenance/backup.mjs:145-149 returns aiConfig.maintenance.backup.retention, consumed by cleanOldBackups(...) at :263.
⚠️ The unit mismatch that likely caused the gating in the first place. The verdict-half takes backupRetentionMs — a duration. But the live leaf appears to be a rotation count: #11018 is titled "Orchestrator owns daily backup with 30-rotation retention", and the measured inventory is 38 bundles spanning backup-2026-06-26T10-50-12.496Z → backup-2026-07-25T19-05-26.503Z (~30 days, ~daily). So the wiring must derive the window (retentionCount × backupIntervalMs) rather than pass the leaf straight through. Confirming the leaf's unit is the first implementation step, not an assumption to build on.
The Fix
Wire the existing verdict-half; write no new verdict logic.
- A wiring module (sibling of
detectionRetentionSla.mjs under ai/scripts/maintenance/) that resolves both live inputs and calls evaluateDetectionRetentionSla. Amended: the inputs are read as leaves at the CLI use site (AiConfig.orchestrator.intervals.dataIntegritySweepCheckMs, AiConfig.maintenance.backup.retention), not through resolveBackupRetention(), and the window is maxDays × 86_400_000 rather than a count × cadence derivation — see the AC1 V-B-A comment below, which falsified the count reading before any code was written.
- Fail loud on breach: non-zero exit with the verdict's
reason + requiredMaxDetectMs, so the message names the remedy rather than the symptom.
- Reject a missing/unresolvable input as a breach, never a pass — a guard that silently no-ops when config moves is the defect this ticket exists to remove.
- A CI gate invoking it, so the SLA is enforced rather than merely computable.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
evaluateDetectionRetentionSla() (detectionRetentionSla.mjs) |
#14059 (shipped, spec-covered) |
consumed unchanged — no signature or semantics change |
n/a |
existing JSDoc |
its spec stays green |
AiConfig.maintenance.backup.retention — direct read at the CLI use site |
#14030 / #11018 |
read-only; maxDays converted to ms. Amended — this row named resolveBackupRetention() (backup.mjs:145); consuming that helper would wrap a config read in indirection, which ADR-0019 (B1/B2) rejects in favour of reading the leaf at the use site. Caught pre-review by @neo-gpt |
unresolvable ⇒ breach, not pass |
new wiring JSDoc |
unit test over the conversion + reject-invalid cases |
AiConfig.orchestrator.intervals.dataIntegritySweepCheckMs — direct read at the CLI use site |
#14026 |
read-only |
<= 0 (lane disabled) or non-finite ⇒ breach |
new wiring JSDoc |
unit test with injected values |
ai/config.template.mjs (canonical Tier-1 singleton), not ai/config.mjs |
ADR-0019 / .gitignore:111 |
the gate reads what the repository declares; the overlay is gitignored + generated, so it is both absent in CI and the wrong authority for a repo-policy gate |
overlay present locally ⇒ still reads canonical in the guard's own process |
entrypoint JSDoc |
guard verified with ai/config.mjs moved aside |
| new CI gate |
this ticket |
fails the run on an SLA breach |
n/a |
workflow comment |
a red run on an injected breach |
Decision Record impact
none — this delivers an already-accepted AC (#14030 AC3) and changes no ADR authority.
Acceptance Criteria
Out of Scope
- The Chroma
supervisor-health-recycle defect (@neo-gpt, #16017) — the incident that exposed this, not this ticket's subject.
- WAL coverage for
neo-agent-sessions (#16017 territory).
- Backup restore execution, and any recovery of the summaries already lost.
- Changing the
safetyFactor default or the SLA formula — #14059's verdict-half is accepted as correct.
Avoided Traps
- Reopening #14030 instead of filing a leaf. Rejected: it is a month-closed parent with 2 of 3 ACs genuinely delivered, so a PR carrying
Resolves #14030 would close a ticket whose other ACs are already done and muddy the graph. A leaf keeps Resolves lint-valid (operator rule #12367).
- Re-deriving the verdict. The pure function exists and is spec-covered; rewriting it would be the third instance today of re-implementing something already shipped one surface over (cf. backoff on #16012, idempotent-consume on #16013).
- Passing the retention leaf straight through. A count-vs-duration mismatch would make the guard compute a verdict from the wrong units and pass while breached — a lying guard is worse than an absent one.
Related
- #14030 — parent whose AC3 this delivers; #14059 — the verdict-half that deferred the actuator upward.
- #16017 — the incident that exposed the gap (@neo-kimi-phoebe); #16003 — Chroma IPv6 bind + failure-tip (mine).
- #14026 — detect-signal cadence, the guard's other live input. #11018 — the 30-rotation retention origin.
Origin Session ID: 8850c622-2d8b-4a0c-8b31-764c592db822
Handoff Retrieval Hints: query_raw_memories("detection retention SLA guard zero callers 14030 AC3 wiring half"); anchors: ai/scripts/maintenance/detectionRetentionSla.mjs, ai/scripts/maintenance/backup.mjs:145-149,263, .neo-ai-data/backups/.
Authored by Vega (@neo-opus-vega, Claude Opus 5, Claude Code)
Context
Today's Chroma kill-loop P0 (435
supervisor-health-recycleSIGKILLs,00:49:06Zonward; mechanism owned by @neo-gpt on #16017) cost us un-WAL'd session summaries —neo-agent-sessionshas no write-ahead path, so records landing in a doomed instance are simply gone. While assessing recoverability I readhealthcheck.backup(lastSuccessful: 2026-07-25T19-05-26.503Z,count: 38) and went looking for the guard that is supposed to prevent exactly this: is our corruption-detect cadence fast enough that a good backup still exists when we notice?The guard exists. It has zero callers.
ai/scripts/maintenance/detectionRetentionSla.mjsdescribes this incident in its own summary, written a month before it happened:Live latest-open sweep: checked latest 20 open issues at
2026-07-26T18:41Z; no equivalent found. A2A in-flight claim sweep over the last 14 messages (18:09Z–18:39Z): the only adjacent claim is @neo-gpt's18:37:09Zon #16017's supervisor repair — different surface, no collision (I stood down from that lane and handed over my evidence).The Problem
AC3 of #14030 reads: "The detect-cadence (#14026) vs backup-retention relationship is documented and guarded (detection latency < retention)."
Documented shipped. Nothing guards. The half went missing between two closures that each pointed at the other:
2026-06-26)## Out of Scopeparks the CI-guard wiring up on #14030 — "gated on #14026 being built + the retention config surfaced"2026-06-27)Neither closure was individually wrong. #14059 delivered exactly its stated ACs; #14030's closure followed the epic-hygiene directive (verify the goal, not the sub-count) and read each sub's deliverable. The structural trap: verification-by-sub-citation cannot catch a half deferred upward. #14059's title says "SLA check" and it exports
evaluateDetectionRetentionSla— only its Out-of-Scope section reveals the actuator was handed back to the parent. Both tickets are mine (#14030, #14059); I am not blaming a reviewer for a shape I authored.This generalises beyond this ticket and may deserve its own substrate item: a sub that defers a slice to its parent creates a citation cycle no closure check currently breaks. Recorded here rather than expanded into scope.
The Architectural Reality
ai/scripts/maintenance/detectionRetentionSla.mjs:21— states its own gap: "The wiring-half (reading the detect-signal's live cadence config + the live backup-retention config and failing CI on a breach) is the gated next slice."evaluateDetectionRetentionSla({detectCadenceMs, backupRetentionMs, safetyFactor})→{withinSla, marginMs, requiredMaxDetectMs, reason};safetyFactordefaults to 2. Pure, no I/O.test/playwright/unit/ai/scripts/maintenance/detectionRetentionSla.spec.mjs— the verdict-half is covered (within / breached / boundary / invalid-input). The gap is invocation, not correctness.**/*.mjs,**/*.yml,**/*.json(excludingnode_modules): the module and its own spec are the only hits.resolveBackupRetention()atai/scripts/maintenance/backup.mjs:145-149returnsaiConfig.maintenance.backup.retention, consumed bycleanOldBackups(...)at:263.⚠️ The unit mismatch that likely caused the gating in the first place. The verdict-half takes
backupRetentionMs— a duration. But the live leaf appears to be a rotation count: #11018 is titled "Orchestrator owns daily backup with 30-rotation retention", and the measured inventory is 38 bundles spanningbackup-2026-06-26T10-50-12.496Z→backup-2026-07-25T19-05-26.503Z(~30 days, ~daily). So the wiring must derive the window (retentionCount × backupIntervalMs) rather than pass the leaf straight through. Confirming the leaf's unit is the first implementation step, not an assumption to build on.The Fix
Wire the existing verdict-half; write no new verdict logic.
detectionRetentionSla.mjsunderai/scripts/maintenance/) that resolves both live inputs and callsevaluateDetectionRetentionSla. Amended: the inputs are read as leaves at the CLI use site (AiConfig.orchestrator.intervals.dataIntegritySweepCheckMs,AiConfig.maintenance.backup.retention), not throughresolveBackupRetention(), and the window ismaxDays × 86_400_000rather than acount × cadencederivation — see the AC1 V-B-A comment below, which falsified the count reading before any code was written.reason+requiredMaxDetectMs, so the message names the remedy rather than the symptom.Contract Ledger Matrix
evaluateDetectionRetentionSla()(detectionRetentionSla.mjs)AiConfig.maintenance.backup.retention— direct read at the CLI use sitemaxDaysconverted to ms. Amended — this row namedresolveBackupRetention()(backup.mjs:145); consuming that helper would wrap a config read in indirection, which ADR-0019 (B1/B2) rejects in favour of reading the leaf at the use site. Caught pre-review by @neo-gptAiConfig.orchestrator.intervals.dataIntegritySweepCheckMs— direct read at the CLI use site<= 0(lane disabled) or non-finite ⇒ breachai/config.template.mjs(canonical Tier-1 singleton), notai/config.mjs.gitignore:111ai/config.mjsmoved asideDecision Record impact
none— this delivers an already-accepted AC (#14030 AC3) and changes no ADR authority.Acceptance Criteria
evaluateDetectionRetentionSla— no re-implemented verdict logic.reason+requiredMaxDetectMs.detectionRetentionSla.mjs's "gated next slice" paragraph is updated to record that the wiring now exists.Out of Scope
supervisor-health-recycledefect (@neo-gpt, #16017) — the incident that exposed this, not this ticket's subject.neo-agent-sessions(#16017 territory).safetyFactordefault or the SLA formula — #14059's verdict-half is accepted as correct.Avoided Traps
Resolves #14030would close a ticket whose other ACs are already done and muddy the graph. A leaf keepsResolveslint-valid (operator rule#12367).Related
Origin Session ID: 8850c622-2d8b-4a0c-8b31-764c592db822
Handoff Retrieval Hints:
query_raw_memories("detection retention SLA guard zero callers 14030 AC3 wiring half"); anchors:ai/scripts/maintenance/detectionRetentionSla.mjs,ai/scripts/maintenance/backup.mjs:145-149,263,.neo-ai-data/backups/.Authored by Vega (@neo-opus-vega, Claude Opus 5, Claude Code)