LearnNewsExamplesServices
Frontmatter
id16521
titleRESTORABLE answers two questions, and the safe answer to one is the unsafe answer to the other
stateClosed
labels
bugaiarchitecture
assignees[]
createdAtAug 4, 2026, 11:11 PM
updatedAtAug 25, 2026, 2:51 AM
githubUrlhttps://github.com/neomjs/neo/issues/16521
authorneo-opus-ada
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 25, 2026, 2:51 AM

RESTORABLE answers two questions, and the safe answer to one is the unsafe answer to the other

Closed Backlog/active-chunk-12 bugaiarchitecture
neo-opus-ada
neo-opus-ada commented on Aug 4, 2026, 11:11 PM

Context

Found while implementing #16510 (PR #16519's sibling, PR #16520). That ticket's own acceptance criterion — "a bundle with one populated collection and the rest empty must not return RESTORABLE" — turned out to create a data-loss path, and I withdrew it. This ticket is the honest version of the repair it was reaching for.

Nothing here is hypothetical: the failing sequence is derived from the shipped control flow in ai/scripts/maintenance/redeployPreflight.mjs, not inferred from a symptom.

The Problem

probeBundle produces one boolean, restorable, and evaluateRedeployPreconditions consumes it in two incompatible roles.

Role 1 — proof of prior state. redeployPreflight.mjs:127 pushes 'a verified restorable bundle' into priorEvidence, and :137 refuses --initialize when any prior evidence exists. :180 uses the same signal for PROCEED_MARKER_RECOVERED: a bundle proves a prior deployment even when the marker is gone, "otherwise a host that lost its marker independently of its bundles could never deploy again."

For this role, any populated collection is valid evidence. It answers "did a plane exist here?" — and the aggregate is exactly right.

Role 2 — authorization to proceed with a container-affecting redeploy. :168 returns PROCEED_VERIFIED with the reason "A verified, non-empty, restorable pre-transition bundle exists."

For this role, the aggregate is wrong. It answers "is there a usable recovery source if this redeploy goes bad?", and a bundle with a populated KB and five empty subsystems is not one.

One word, two questions. RESTORABLE currently means "at least one collection had rows", and role 2 reads it as "this plane can be recovered".

Why tightening the boolean is not the fix

The obvious repair — make partial bundles non-RESTORABLE — breaks role 1, and the failure is concrete:

host has a KB-only bundle and NO initialization marker
  -> restorable false, so PROCEED_MARKER_RECOVERED (:180) never fires
  -> falls to REFUSE_NO_VERIFIED_BUNDLE (:192), whose message reads
     "No prior deployment is recorded and no usable bundle exists ...
      If this is genuinely a first install, pass --initialize to say so."
  -> the operator follows that instruction
  -> priorEvidence is now EMPTY (the bundle no longer counts), so :159
     returns PROCEED_INITIALIZING
  -> a host holding a real KB backup is initialized over

Today restorable === true is precisely what blocks that path, and the refusal message is what would walk the operator into it. The aggregate is load-bearing as a safety interlock.

The Architectural Reality

Site Role Correct predicate
redeployPreflight.mjs:127 (priorEvidence) prior-state proof aggregate — any rows anywhere
redeployPreflight.mjs:180 (PROCEED_MARKER_RECOVERED) prior-state proof aggregate
redeployPreflight.mjs:168 (PROCEED_VERIFIED) recovery-source authorization completeness

PR #16520 already publishes the raw material — collectionCounts and emptyCollections land beside rowTotal on both verdict returns — deliberately without changing any predicate, because it was written while a live KB restore was in flight.

The Fix

Give role 2 its own field on the probe verdict rather than overloading restorable. Shape to settle during implementation, but the constraint set is fixed:

  • restorable keeps its current aggregate semantics — role 1 must not regress
  • a distinct completeness fact authorizes role 2, derived from emptyCollections
  • :168 reads the new field; :127 / :180 keep reading restorable
  • the REFUSE_NO_VERIFIED_BUNDLE message at :192 needs revisiting once role 2 can refuse independently — a refusal that says "no usable bundle exists" when one demonstrably does, and then recommends --initialize, is the sentence that makes the interlock break dangerous

Naming matters here and is part of the deliverable. restorable currently names role 2's question while implementing role 1's answer. Whatever the split lands on, neither field may keep a name that reads as the other's promise.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
probeBundle verdict this ticket gains a completeness field distinct from restorable absent ⇒ role 2 fails closed, never "fine" JSDoc spec: KB-only bundle authorizes role 1 and refuses role 2
restorable unchanged aggregate semantics preserved in-source rationale (landed in PR #16520) red witness: the --initialize sequence above
evaluateRedeployPreconditions this ticket :168 reads completeness; :127 / :180 read restorable JSDoc existing preflight specs must stay green
REFUSE_NO_VERIFIED_BUNDLE reason text this ticket must not claim absence when a partial bundle exists in-source operator-facing string review

Decision Record impact

none. No topology, backend posture, or config leaf. Reviewed ADR-0014 (cloud deployment topology / scheduler task taxonomy) as the nearest authority — this changes a maintenance probe's verdict shape, not deployment topology.

Acceptance Criteria

  • A KB-only bundle still satisfies role 1: --initialize is refused, and PROCEED_MARKER_RECOVERED still fires when the marker is missing. Red witness required — this is the regression the naive fix introduces, and a spec that does not fail before the fix has not tested it.
  • A KB-only bundle does not satisfy role 2: PROCEED_VERIFIED is refused, naming which subsystems are empty.
  • A complete bundle satisfies both. Positive control — without it, "refuse everything" passes the criterion above.
  • The all-empty case still returns BUNDLE_EMPTY, unregressed.
  • An absent or unreadable completeness fact fails role 2 closed; it may never be defaulted to complete.
  • The REFUSE_NO_VERIFIED_BUNDLE message no longer asserts that no bundle exists when a partial one does, and no longer recommends --initialize in that state.
  • Neither resulting field carries a name that reads as the other's promise.

Out of Scope

  • The per-collection reporting itself — landed in PR #16520.
  • Deciding a "required subsystem set" as a policy list. The set was never the question; the role overload is.
  • #16486's authorizeActivation. It consumes a verdict and does not produce one. It will inherit whichever field role 2 lands on, but its own logic is untouched.
  • Export-side completeness accounting — that is #16516 / PR #16519, the opposite end of the same pipeline.

Avoided Traps

  • Tightening the shared boolean. The obvious fix, and it converts a safety interlock into a data-loss path. This is the trap that cost #16510 its acceptance criterion.
  • Treating it as a policy question. My first framing, withdrawn: "which subsystems must be non-empty" sounds like taste and invites picking an answer, when the actual defect is that picking is the bug.
  • Reading emptyCollections as an authorization signal before the split. A consumer doing that recreates the interlock break from the outside; noted as post-merge validation on PR #16520.

Related

  • #16510 / PR #16520 — publishes the per-collection facts this builds on; its withdrawn AC is the origin of this ticket
  • #16516 / PR #16519 — export-side completeness accounting, same incident, opposite end
  • #16512 — @neo-opus-vega, healthcheck blessing an empty knowledge base
  • #16486 — the activation contract that consumes RESTORABLE

Live latest-open sweep: checked latest 20 open issues at 2026-08-04T21:10:28Z, plus a 200-issue title scan for restorab|redeploy|preflight|verdict; no equivalent found. A2A in-flight claim sweep: no overlapping [lane-claim].

Origin Session ID: eeacb603-97f1-4241-9b2f-3a542cab6d2c

Retrieval Hint: query_raw_memories("RESTORABLE two roles prior-state proof vs recovery authorization") · redeployPreflight priorEvidence --initialize interlock