LearnNewsExamplesServices
Frontmatter
id16384
titleBackup restorability selection stops at the newest bundle, hiding recoverable history behind it
stateClosed
labels
bugai
assigneesneo-opus-ada
createdAtAug 2, 2026, 7:25 PM
updatedAtAug 2, 2026, 9:17 PM
githubUrlhttps://github.com/neomjs/neo/issues/16384
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 2, 2026, 9:17 PM

Backup restorability selection stops at the newest bundle, hiding recoverable history behind it

neo-opus-ada
neo-opus-ada commented on Aug 2, 2026, 7:25 PM

Context

Narrow leaf split out of #16348 (A backup that captured nothing must not report success) so the delivered half carries an honest close target. #16348's body itself separates this concern: "Independent of which is chosen: selection must fall back past an unusable newest bundle, or a single bad run keeps hiding good history." That independence is why this is its own leaf rather than a fragment — the capture-side verdict work stays on #16348.

Live latest-open sweep: checked the latest 20 open issues (created-descending) at 2026-08-02T17:22:55Z; no equivalent found. Adjacent, not duplicate: #16344 (redeployPreflight: --initialize proceeds on evidence it never consults) — a different defect in the probe's consumer, claimed by @neo-gpt at 17:22:39Z. A2A in-flight claim sweep at 17:23Z: no claim on backup-restorability selection.

Owning folder: ai/scripts/maintenance/ — the backup/restore pair. Sibling precedent: this is a change to restore.mjs, beside backup.mjs and redeployPreflight.mjs.

The Problem

verifyLatestBackupRestorable (ai/scripts/maintenance/restore.mjs) sorts bundle names reverse-lexically and inspects only bundleNames[0]:

const bundleRoot = path.join(backupRoot, bundleNames[0]);

One unusable newest bundle therefore reports the whole backup root unrecoverable.

Live incident, 2026-08-02: a backup run died mid-write and left a bundle-shaped directory (7 empty subdirectories plus a 0-byte JSONL, no bundle-meta.json). The probe read that directory, returned a non-RESTORABLE verdict, and the deploy guard refused — while a complete bundle carrying 94,325 recoverable rows sat directly beside it, never examined:

before  →  (guard blocked by the 0-byte artifact)
after   →  restorable: true   code: RESTORABLE   rowTotal: 94325
           bundleRoot: backup-2026-08-01T12-13-23.398Z

The repair was rm -rf on a directory inside the backup root. Deleting evidence to unblock a guard is the standing remedy this ticket exists to remove.

Why this is not rare

Measured against the live bundle store on #16348: 7 of 36 bundles (19%) are unusable — 3 with no bundle-meta.json, 4 with a receipt attesting a zero-row capture — and the zero-capture class recurs across four separate dates, not one cutover. With a 19% unusable rate, the probability that the newest bundle is unusable is not a corner case.

Bound on that number, stated rather than implied: it is one store. A second bundle location exists off-docker and was not counted; 19% is the rate where it was measured, not a global claim.

The Fix

Walk backup-* newest-first until one validates, with three properties that keep the fallback from trading a false refusal for an invisible one:

  1. Report, never hide. Every bundle passed over travels in skipped with its own code and reason, and a warning names them. A fallback that succeeded silently would remove the pressure to notice bundles are being produced broken at all — which is the #16348 half.
  2. Preserve the failure contract. When nothing validates, the verdict is the newest bundle's own result, spread whole rather than rebuilt as a projection. A single-bundle root answers byte-identically to today, and redeployPreflight still prints the most recent failure as its refusal cause.
  3. Bound the walk. Full validation streams every row of every JSONL, so an unbounded walk over multi-GB bundles could turn a deploy preflight into an arbitrarily long scan. Exhausting the bound warns which candidates went unexamined — a silent cap reads exactly like an exhaustive search that found nothing.

Acceptance Criteria

  • A valid bundle with a newer invalid bundle above it yields RESTORABLE against the valid one.
  • The fallback crosses every unusable class, not only the one that caused the incident (aborted-mid-write, zero-capture, torn JSONL).
  • Bundles passed over are reported by name and code, not silently skipped.
  • When nothing is restorable, the verdict still describes the newest bundle, with rowTotal and embeddingAdvisories intact — a single-bundle root is unchanged.
  • The scan is bounded, the bound is a config leaf rather than a module constant, and exhausting it is announced.
  • A legacy bundle carrying no bundle-meta.json but real rows stays restorable — meta-absence is the documented legacy contract, not the abort signature.

Out of Scope

  • The capture-side verdict#16348. Whether a capture that found nothing may report success, and the empty vs unavailable distinction, stay there. The mechanism for that half is not the one #16348's body proposes; see the correction comment on it.
  • redeployPreflight's --initialize evidence gap — #16344, claimed by @neo-gpt.
  • Retention policy and the cadence slot of a failed run.

Avoided Traps

  • Treating meta-absence as the abort signature. validateBundle documents an absent bundle-meta.json as the LEGACY bundle contract, returning {legacy: true}. Rejecting meta-less bundles up front would make every pre-meta bundle permanently unrecoverable. The abort-mid-write and legacy shapes are distinguishable only by whether recoverable rows exist — which the probe already decides correctly.
  • A silent fallback. Succeeding quietly against an older bundle would fix the guard and hide the defect producing broken bundles.
  • An aggregate failure code. redeployPreflight branches on RESTORABLE and prints the code as its refusal cause; replacing the newest bundle's precise reason with a summary tells an operator less than the old single-bundle probe did.

Related

  • #16348 — parent; the capture-side half stays there
  • #16344 — the preflight's --initialize evidence gap (same consumer, different defect)
  • #16055 — the survivability incident whose shape this reproduces

Origin Session ID: 56105163-6e66-44b6-8c6f-9e81bc1be08c

Retrieval Hint: query_raw_memories("verifyLatestBackupRestorable newest-only bundle selection hides recoverable history fallback")

tobiu referenced in commit 886f21f - "fix(ai): the restorability probe walks past an unusable newest bundle (#16384) (#16385) on Aug 2, 2026, 9:17 PM
tobiu closed this issue on Aug 2, 2026, 9:17 PM