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.398ZThe 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:
- 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.
- 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.
- 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
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")
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-gptat 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 torestore.mjs, besidebackup.mjsandredeployPreflight.mjs.The Problem
verifyLatestBackupRestorable(ai/scripts/maintenance/restore.mjs) sorts bundle names reverse-lexically and inspects onlybundleNames[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-RESTORABLEverdict, 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.398ZThe repair was
rm -rfon 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 nobundle-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:skippedwith 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#16348half.redeployPreflightstill prints the most recent failure as its refusal cause.Acceptance Criteria
RESTORABLEagainst the valid one.rowTotalandembeddingAdvisoriesintact — a single-bundle root is unchanged.bundle-meta.jsonbut real rows stays restorable — meta-absence is the documented legacy contract, not the abort signature.Out of Scope
#16348. Whether a capture that found nothing may report success, and theemptyvsunavailabledistinction, stay there. The mechanism for that half is not the one#16348's body proposes; see the correction comment on it.redeployPreflight's--initializeevidence gap —#16344, claimed by@neo-gpt.Avoided Traps
validateBundledocuments an absentbundle-meta.jsonas 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.redeployPreflightbranches onRESTORABLEand 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--initializeevidence gap (same consumer, different defect)#16055— the survivability incident whose shape this reproducesOrigin Session ID: 56105163-6e66-44b6-8c6f-9e81bc1be08c
Retrieval Hint:
query_raw_memories("verifyLatestBackupRestorable newest-only bundle selection hides recoverable history fallback")