LearnNewsExamplesServices
Frontmatter
id16563
titleThe KB export reports "Export complete" on zero rows, and the bundle looks successful
stateClosed
labels
bugai
assigneesneo-opus-grace
createdAtAug 5, 2026, 8:55 PM
updatedAtAug 8, 2026, 2:40 PM
githubUrlhttps://github.com/neomjs/neo/issues/16563
authorneo-opus-vega
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 8, 2026, 2:40 PM

The KB export reports "Export complete" on zero rows, and the bundle looks successful

neo-opus-vega
neo-opus-vega commented on Aug 5, 2026, 8:55 PM

Context

Surfaced by @tobiu as a V-B-A on PR #16562: "last (broken) backup flagged as a success." Verified on the live plane.

The 2026-08-05T09:39 daily backup ran while neo-knowledge-base held 0 documents (the three-day corpus outage). Its bundle-meta.json:

"kb": {
  "message": "Export complete. Exported 0 knowledge base chunks.",
  "count": 0,
  "collectionId": "ab75f86b-1651-4865-96f4-0287acd42ea7"
}

And on disk:

backup-2026-08-05T09-39-56.157Z/kb/     ← empty directory, no .jsonl at all
backup-2026-08-05T09-39-56.157Z/        completedAt: 2026-08-05T09:40:30.836Z

"Export complete." · count: 0 · a completedAt stamp · no degradation of any kind. Total bundle 2.0 GB, so it is a substantial, plausible-looking artifact.

Observation vs inference: the meta contents, the empty directory, the counts and the collectionId are measured. What a consumer then does with this bundle is reasoned from shipped control flow and marked as such below.

Scale update 2026-08-07 — it is 6 of 10 bundles, not one

Full inventory of the real backup root (/Users/tobiasuhlig/.neo-ai/backups), wc -l on each bundle's kb/*.jsonl:

bundle kb bytes kb rows bundle-meta.json
2026-07-31T04-57 0 0 yes
2026-08-01T05-01 0 0 yes
2026-08-01T12-13 3,368,227,090 61,206 yes
2026-08-03T08-09 107,196,660 2,001 NO
2026-08-03T21-37 3,287,574,636 59,754 yes
2026-08-04T08-38 0 0 yes
2026-08-04T08-55 0 0 yes
2026-08-05T09-39 0 0 yes
2026-08-05T19-05 0 0 yes
2026-08-06T13-09 (NEWEST) 900,214,445 16,550 yes

Six of ten bundles carry zero KB rows, and every one of the six carries a valid bundle-meta.json. Only two hold a real corpus. This ticket's original evidence was a single bundle; the defect has produced a majority of the retained set, so it is the steady state rather than an edge case.

Two things the wider sample adds:

  • The newest bundle is degraded, not empty — 16,550 rows where ~60,000 is correct. So the failure mode is not binary: a bundle can look substantial at 900 MB and still be a trap. Only the row count discriminates, and no artifact surfaces it.
  • 08-03T08-09 has no bundle-meta.json at all yet sits in the retention set beside complete captures, at 2,001 rows. Retention discovery is bundle-shaped-directory, and a partial capture is bundle-shaped.

The policy consequence is filed separately as #16614 — retention's keepMinimum: 3 counts directories rather than restorable bundles, so the three newest bundles here contain no recoverable KB corpus, and the 59,754-row bundle is protected only by being 4 days inside a 30-day age bound. The two are complements: a correct receipt stops producing empty bundles, a correct policy stops them from displacing real ones.

The Problem

A subsystem that exported nothing reports the same shape as one that exported everything.

Compare the sibling subsystems in the same file — they are more honest than the KB:

subsystem reported
mc expected: 32387, exported: 32387, skipped: 0
graph expected: 271280, exported: 271280, skipped: 0
trajectories copied: 0, note: "source not present: …"
kb count: 0, message "Export complete.", no note, no expectation

trajectories explicitly records why zero is fine. The KB records zero with a completion message and nothing to distinguish "the corpus is empty" from "the corpus was captured". There is no expected field to compare against, so a zero-row KB export cannot fail its own contract — it has no contract.

The collectionId is the sharpest part: ab75f86b-… is the same id that now holds 62,486 documents. The bundle names the right collection and captured none of it.

Why this is worse than a missing backup

A missing backup is visible. This is a success-shaped artifact that becomes the newest restore source. Reasoned from shipped control flow, not observed: probeBundle / evaluateRedeployPreconditions (redeployPreflight.mjs) consume bundle state as both proof of prior state and authorization to proceed — the overload #16521 documents. An mc-populated, kb-empty bundle satisfies "non-empty" in aggregate.

The near-miss is real. @neo-opus-grace restored the Aug-3 bundle (59,754 docs), not this newer one. Had the newest bundle been chosen — the obvious choice — a --mode replace KB restore from it would have written an empty collection over a populated one. That restore was correct; nothing in the artifacts made it obviously correct.

This is the third surface of one pattern. #16512 fixed it on the health path (an empty collection degrades health). #16521 owns the verdict path. This is the producer: the exporter that mints the artifact both of those then reason about.

The Architectural Reality

  • ai/services/knowledge-base/DatabaseService.mjsexportDatabase emits "Export complete. Exported N knowledge base chunks." with no non-empty expectation. Its importDatabase sibling does carry an atomic vector-write invariant that rejects rows fail-loud; the export side has no equivalent floor.
  • ai/scripts/maintenance/backup.mjs — assembles bundle-meta.json per subsystem and stamps completedAt. Records what each subsystem returned; does not adjudicate whether a return is degenerate.
  • The mc / graph exporters already carry expected vs exported vs skipped. The shape to copy exists in the same file.
  • ai/services/knowledge-base/HealthService.mjs already treats an empty KB as health-degrading (#16512). The two surfaces disagree today: health says degraded, backup says complete.

The Fix

  1. The KB export declares an expectation. Read the collection count first and report expected / exported alongside count, matching mc and graph. A zero-row export against a non-zero expectation is then a failure, not a message.
  2. Zero rows with a zero expectation is degraded, never complete — an empty corpus is a real state and the bundle must say so in a field a consumer can branch on, not only in prose. Follow trajectories' precedent: zero is acceptable with a recorded reason.
  3. bundle-meta.json carries a per-subsystem status so a consumer can distinguish captured-nothing from captured-everything without string-matching a message.

Not proposed: failing the whole backup when the KB is empty. The mc + graph halves of that 2.0 GB bundle are genuinely valuable and refusing to write them would trade a silent gap for real data loss — which is the same trade #16510's withdrawn AC made and #16521 exists to undo.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback / Error Semantics Docs Evidence
KB exportDatabase return this ticket, over the existing {message, count, collectionId} gains expected; zero-vs-expectation becomes a failure absent expectation ⇒ degraded, never complete KB service JSDoc live bundle-meta.json, quoted above
bundle-meta.json subsystems.kb backup.mjs gains a status a consumer can branch on unknown status is never treated as success RestorationRunbook the 09:39 bundle
mc / graph export shape existing unchanged — already carry expected/exported/skipped same bundle, same file
probeBundle / RESTORABLE #16521 unchanged here — that ticket owns the verdict; this one stops minting the misleading input scope boundary

Decision Record impact

none. No ADR governs export completeness semantics; this adds a floor to an existing return shape.

Acceptance Criteria

  • A KB export of 0 rows against a populated collection fails rather than reporting complete.
  • A KB export of 0 rows against a genuinely empty collection is recorded as degraded with a reason, not as complete.
  • bundle-meta.json exposes a per-subsystem status that a consumer can branch on without parsing the human message.
  • Asserted in both directions: a populated export still reports success, and an empty one does not. A one-sided test passes for an always-fail implementation.
  • The 09:39 bundle is usable as a fixture — its exact meta shape is the regression case.
  • The mc + graph halves of a KB-empty bundle still get written.

Out of Scope

  • RESTORABLE's dual role#16521 owns it. This ticket stops producing the misleading input; it does not change how the verdict reads it.
  • Health-surface treatment of an empty KB — already shipped (#16512).
  • Why the corpus was empty#16549, still unexplained.
  • Backup scheduling / lease starvation#16561 and PR #16562.
  • Retroactively marking existing bundles. The 09:39 bundle stays as-is and becomes the fixture.

Avoided Traps

  • Failing the whole bundle on an empty KB. Trades a silent gap for real data loss, and repeats #16510's withdrawn AC.
  • Fixing only the message. "Export complete""Export complete (empty)" still leaves consumers string-matching prose. The missing thing is a branchable field and an expectation.
  • Assuming zero is always wrong. A genuinely empty corpus must remain expressible — trajectories already models this correctly with copied: 0 plus a note.

Related

  • #16521 — the verdict-side overload this ticket's output feeds.
  • #16512 — the same "empty ≠ fine" fix on the health surface; the precedent for the disposition.
  • #16549 — why the corpus was empty, still open.
  • #16561 / PR #16562 — backup scheduling; disjoint from backup content.
  • #16510 — the withdrawn AC whose failure mode the Out of Scope section preserves.

Live latest-open sweep: latest 30 open issues checked immediately before filing; nearest neighbours #16521 (verdict, not producer) and #16561 (scheduling, not content) — neither covers an exporter reporting completion on zero rows. A2A in-flight claim sweep: recent messages across all read-states, no competing claim.

Origin Session ID: 11695cce-9854-4be2-80c3-8ea4322298bf

Retrieval Hint: query_raw_memories("KB export complete zero rows bundle-meta success-shaped empty kb directory expected field")

tobiu referenced in commit a4bbb79 - "fix(ai): an empty KB export is degraded, not complete (#16563) (#16668) on Aug 8, 2026, 2:40 PM
tobiu closed this issue on Aug 8, 2026, 2:40 PM