LearnNewsExamplesServices
Frontmatter
id17785
titleThe backup maintenance scorer ignores the success receipt in its own snapshot
stateClosed
labels
bugai
assigneesneo-opus-ada
createdAtAug 25, 2026, 11:42 PM
updatedAtAug 26, 2026, 10:11 AM
githubUrlhttps://github.com/neomjs/neo/issues/17785
authorneo-opus-vega
commentsCount4
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 26, 2026, 10:11 AM

The backup maintenance scorer ignores the success receipt in its own snapshot

neo-opus-vega
neo-opus-vega commented on Aug 25, 2026, 11:42 PM

Context

Retargeted 2026-08-25 (twice) after reopen — the original body prescribed the census/receipt fix that already shipped (#17495 / PR #17676, verified on-pin); the first retarget draft misread the payload as self-contradicting. @neo-gpt's source archaeology (comment 5417754310) settled the true mechanism, and this body follows his five-point retarget. Reopen lifecycle validated there; falsification history in the reopen comment chain and D#17782 rev-14.

Freeze status: sanctioned — Wave-0 gate-(d) scope, exception class 1. Pooled; @neo-opus-ada first-claim.

The Problem

backup.observationStatus: "unavailable" (census: MC root unreadable from this service) and maintenance.backup.observationStatus: "observed" (orchestrator retry state: read fine) are disjoint subjects by declared design (backup.mjs:285-288) — no payload self-contradiction there, and details[] is not a second producer (it renders maintenance.backup.reasonCodes).

The real contradiction sits inside the orchestrator maintenance snapshot itself, live on pin 467fd122f3:

"retry":      { "phase": "exhausted", "lastSuccessAt": null,
                "streakStartedAtMs": 1785831744707 }          // streak began 2026-08-04
"lastBackup": { "backup": { "status": "success" },
                "finishedAt": "2026-08-25T13:48:03.060Z" }    // success, 21 days NEWER

describeBackupMaintenanceHealth() emits backup-never-succeeded solely from retry state, ignoring the successful receipt it holds in the same snapshot. The scorer has the owner's truth in hand and reads only the stale derived ledger — which then renders as the exact prose (details[]) that consumed a four-maintainer planning cycle on 2026-08-25 and is live again with fresher timestamps as false corroboration.

Ground truth at measurement: ~/.neo-ai/backups = 28 bundles / 125 GB, newest same-day, receipt success · restorable: true.

The Architectural Reality

  • Census producer: fixed (#17495; tri-state observed|unavailable|unreadable). Out of scope here — and the nested maintenance verdict must NOT be made to consume census state (crosses planes/subjects; @neo-gpt's point 4).
  • The scorer: describeBackupMaintenanceHealth() reads orchestrator retry task state; the receipt (lastBackup) rides the same snapshot unread by the verdict.
  • The stale ledger: retry.phase: "exhausted" with lastSuccessAt: null since 2026-08-04 while daily backups succeed — the retry lane never reconciled with the succeeding main lane; that non-reconciliation is a co-defect, not background.
  • Root-cause lineage (@neo-opus-ada's self-catch): #17338's AC guarded the false positive (never healthy from an absent observation) and licensed the false negative. The symmetric principle governs the fix: a verdict may not fabricate a definite negative that its own snapshot's newer evidence contradicts.

The Fix (per the five-point retarget)

  1. Reconcile before publishing: a successful receipt newer than the failure streak cannot coexist with backup-never-succeeded. Either the retry task state is repaired at source, or the scorer emits a receipt-authoritative verdict / an explicit conflict code (backup-state-conflict) — never the fabricated negative.
  2. details[] follows the corrected verdict — no independent fix arm.
  3. Preserve the true negatives: off-host-durability-unmet stays while off-host sync is disabled; a genuinely receipt-less exhausted state still emits backup-never-succeeded. This defect must not flip the block healthy.
  4. No census-consumer coupling (point 4 upheld).
  5. kb-server specimen REMOVED — re-measured post-bump: service available, Docker healthy; the original degraded/healthy pair is not reproducible (pin-lag family, as the runway's class section suspected).

Contract Ledger

Backfilled 2026-08-26 (@neo-opus-ada, assignee) — the reopen retarget replaced the original matrix without adding one for the consumed surface this lane changes. Rows cover the scorer's reason-code contract, the details[] projection, every receipt fallback, docs, and executable evidence.

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
maintenance.backup.reasonCodes — whole-history claim describeBackupMaintenanceHealth() backup-never-succeeded only when no success receipt exists; a success receipt of any age beside retryState.lastSuccessAt: null emits backup-state-conflict instead Existing codes unchanged; block stays degraded either way Scorer JSDoc (symmetric-verdict contract) backup.spec.mjs — newer-receipt, older-receipt, receipt-less arms
maintenance.backup.reasonCodes — current-streak claim same backup-retry-exhausted is untouched by the reconciliation; it claims the recovery window is spent, not that the lane never succeeded n/a — never traded for the whole-history code same JSDoc paragraph backup.spec.mjs — every conflict arm asserts it survives
Receipt status discrimination lastBackup.backup.status Only success falsifies the whole-history negative; failed leaves it standing and additionally emits backup-last-run-failed A receipt whose status is absent is not success, so the negative stands Inline reconciliation note backup.spec.mjs — a failed receipt does not suppress the negative
Unreadable receipt same backup-receipt-unreadable continues to fire from lastBackup.status === 'unreadable'; the reconciliation does not consume it Pre-existing behavior preserved Numbered contract rule 4 Pre-existing arm in maintenance health describe block
Off-host durability durability.posture off-host-durability-unmet is independent of the reconciliation and survives a conflict verdict Unchanged Numbered contract backup.spec.mjs — off-host-durability-unmet survives the conflict verdict
details[] prose composeMemoryCoreHealthcheck() Pure rendering of maintenance.backup.reasonCodes; no independent emission path Empty code list renders see maintenance.backup rather than a bare accusation Helper docblock HealthcheckBackupDetails.spec.mjs — four arms incl. appears nowhere in details
Census coupling ADR-adjacent, #17495 Explicitly none. The orchestrator verdict never consults the Memory Core census tri-state n/a Inline note in the scorer Absence is structural — no import or parameter exists
Co-defect ownership TaskStateService The stale ledger is reported, not repaired here; backup-state-conflict is its observer and its first live emission is the repair trigger n/a Inline THE CO-DEFECT note AC-2

Acceptance Criteria

  • AC-1: describeBackupMaintenanceHealth() reconciles lastBackup receipt vs retry state; with a success receipt newer than the streak, the verdict is receipt-authoritative (or explicit backup-state-conflict) and backup-never-succeeded is absent from reasonCodes and details[].
  • AC-2: the stale retry task state (exhausted-since-2026-08-04 against daily successes) is root-caused and repaired at source, or its non-reconciliation is documented with a named follow-up trigger.
  • AC-3 (negative mutations, three arms): (i) success-receipt-newer-than-streak → no definitive negative anywhere in the payload; (ii) genuinely no receipt + exhausted retry → backup-never-succeeded STILL fires; (iii) off-host sync disabled → off-host-durability-unmet persists. Each arm names the mutation that reddens it.
  • AC-4: a spec asserts details[] derives from the corrected verdict (mutating reasonCodes mutates the prose; no independent emission path).
  • AC-5: the symmetric-verdict contract lands in the scorer's JSDoc — no fabricated negative against newer same-snapshot evidence; absent/conflicting resolves to unknown/conflict, never healthy, never an invented negative — citing #17338's asymmetry and this incident.

Out of Scope

The census producer (#17495, shipped) · coupling the maintenance verdict to census state · offHostSync enablement (operator decision; #16516/#17338 adjacent) · the kb-server heap-observation advisory (non-authoritative, post-bump healthy) · backup mechanism changes.

Decision Record impact

aligned-with #17495's tri-state contract; amends #17338's AC wording at the JSDoc level (asymmetric → symmetric).

Related

Related: #17495 · #17338 · #16516 · D#17782 · #17500

Origin Session ID: fa8ebb22-864a-4f04-b9fd-8b6f2c22bcc4

Retrieval Hint: "scorer ignores success receipt retry exhausted backup-never-succeeded reconcile receipt-authoritative symmetric verdict"

tobiu referenced in commit 3947383 - "fix(orchestrator): reconcile the backup receipt against its retry ledger (#17785) (#17795) on Aug 26, 2026, 10:11 AM
tobiu closed this issue on Aug 26, 2026, 10:11 AM