LearnNewsExamplesServices
Frontmatter
id16567
titleOne boolean answers two questions: the redeploy verdict must split prior-state evidence from recovery authorization
stateClosed
labels
bugai
assigneesneo-gpt
createdAtAug 6, 2026, 1:02 AM
updatedAtAug 25, 2026, 10:26 AM
githubUrlhttps://github.com/neomjs/neo/issues/16567
authorneo-opus-vega
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 25, 2026, 10:26 AM

One boolean answers two questions: the redeploy verdict must split prior-state evidence from recovery authorization

neo-opus-vega
neo-opus-vega commented on Aug 6, 2026, 1:02 AM

Problem

redeployPreflight.mjs authorizes a container-affecting deploy against a bundle that cannot restore the Knowledge Base.

Reproduction (read-only, ~40s):

node ai/scripts/maintenance/redeployPreflight.mjs --compose-project <project> --json

Observed:

decision    : PROCEED_VERIFIED
proceed     : true
reason      : "A verified, non-empty, restorable pre-transition bundle exists."
verdictCode : RESTORABLE
rowTotal    : 34239

Ground truth for the bundle it selected:

bundle-meta.json → {"kb":0,"memories":32462,"summaries":1777}
kb/ payload      → 0B, 0 entries

rowTotal 34239 is memories + summaries alone. The KB contributes zero, on a plane whose KB corpus is the recovery target. A deploy that went wrong would have had nothing to roll back to.

The obvious fix is WRONG — it breaks a documented safety interlock

⚠️ Do not implement "refuse when any substrate is empty". restore.mjs:1043-1061 records that the aggregate is deliberate:

restorable deliberately still keys on the aggregate, and NOT because the question is merely unsettled: making a partially-empty bundle non-RESTORABLE would break a safety interlock. evaluateRedeployPreconditions reads this verdict in TWO incompatible roles — as PROOF OF PRIOR STATE (any populated collection shows the plane existed; it is one of the priorEvidence items that REFUSE --initialize), and as AUTHORIZATION TO PROCEED with a container-affecting redeploy (which does want completeness).

And the concrete regression it names:

a host with a KB-only bundle and no marker would fall through to REFUSE_NO_VERIFIED_BUNDLE, whose message instructs the operator to pass --initialize — and --initialize would then PROCEED, because the bundle no longer counts as prior evidence, discarding a plane that had a real backup.

Tightening the boolean trades a false PROCEED for a false --initialize authorization. That is strictly worse: the second destroys data.

The comment also pre-empts the consumer-side workaround:

emptyCollections is a REPORTING fact only: a consumer that treats it as an authorization signal recreates the same interlock break from the outside.

So collectionCounts and emptyCollections are already published (#16510 / #16520). The defect is not missing information. It is that one boolean answers two different questions.

The real repair — split the verdict's two roles

Named by the code itself:

Splitting the verdict's two roles is the real repair — it changes a consumed surface and does not belong under an incident-time additive change.

Restated as scope:

  1. evaluateRedeployPreconditions asks two questions instead of reading one boolean twice:
    • Prior-state evidence — "did a plane exist here?" Satisfied by ANY populated collection. Keeps refusing --initialize exactly as today. Must not tighten.
    • Recovery-source authorization — "is this bundle a complete rollback target?" Requires per-substrate completeness. This is the question that should refuse a kb:0 bundle.
  2. restorable splits into two named fields, or gains an explicit role parameter. This is a consumed-surface change — every caller of verifyLatestBackupRestorable needs auditing, not just the preflight.
  3. The refusal for an incomplete-but-real bundle must NOT emit the --initialize instruction. That instruction is what makes today's wrong answer dangerous rather than merely wrong.

The class worth fixing, not just the instance

Any aggregate-over-substrates verdict can hide a fully-empty substrate. Same family as #16569 (a plane-id default valid for exactly one deployment) and #16568 (runtimeFreshness reporting "current" while far behind): each returns a well-formed pass about the wrong subject.

Acceptance criteria

  • Prior-state evidence and recovery authorization are separate questions, with a test pinning that the KB-only-bundle-plus-no-marker case still refuses --initialize — the interlock proven unchanged.
  • A kb:0 bundle fails the authorization question, and the refusal names the empty substrate.
  • The incomplete-bundle refusal does not instruct --initialize.
  • Every caller of verifyLatestBackupRestorable audited for which role it wants; recorded even where unchanged.
  • emptyCollections stays reporting-only until the split lands, per the existing comment.
  • Coverage for the exact live shape: manifest {"kb":0,...} with healthy siblings, which returns PROCEED_VERIFIED today.

Out of scope

  • The producer side — #16563 (KB export reports success on zero rows). Even once producers are fixed, the gate must not bless an empty substrate.
  • Any change to restorable's prior-evidence semantics.

Related

  • #16563 — producer side: success-shaped bundles with zero KB rows (5 consecutive observed).
  • #16510 / #16520 — per-collection restorability facts; already available, deliberately reporting-only.
  • #16568, #16569 — same failure family.

Note on this ticket's history

Filed from the symptom with an AC that was the interlock break, because I read the observed output and not the decision recorded at the decision site. Corrected here. The live observation stands; only the prescription was wrong.

Evidence

L1 — live tool invocation plus the on-disk manifest of the selected bundle, 2026-08-06.

Authored by @neo-opus-vega (Claude Opus 5).

tobiu referenced in commit af69182 - "fix(ai): split redeploy backup verdicts (#16567) (#17748) on Aug 25, 2026, 10:26 AM
tobiu closed this issue on Aug 25, 2026, 10:26 AM