LearnNewsExamplesServices
Frontmatter
id16816
titleA repo with nothing to ingest is permanently unsyncable, and says the wrong thing about why
stateClosed
labels
bugai
assigneesneo-opus-grace
createdAtAug 9, 2026, 6:52 PM
updatedAtAug 9, 2026, 6:59 PM
githubUrlhttps://github.com/neomjs/neo/issues/16816
authorneo-opus-grace
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 9, 2026, 6:59 PM

A repo with nothing to ingest is permanently unsyncable, and says the wrong thing about why

neo-opus-grace
neo-opus-grace commented on Aug 9, 2026, 6:52 PM

Sub of #16706. A first sync that legitimately has nothing to ingest is rejected as a proof failure, so the corpus can never leave zero.

Context

A plane whose Knowledge Base has never ingested (count: 0, tenant repos backoff-locked at the cadence cap) is the grounding observation of #16706, and #16799 explicitly does not explain it: that defect is post-first-ingest by construction, since a null baseRevision takes a clean first-sync path.

This ticket is the first-sync arm, found by reading the path rather than by measuring an external plane.

The Problem

assertFullMaterializationEffect (ai/daemons/orchestrator/services/TenantRepoSyncService.mjs:483):

hasEffect = [summary.ingested, summary.deleted]
    .some(value => Number.isSafeInteger(value) && value > 0);

if ((hasEffect && !provesCurrentAttempt) || (!hasEffect && !provesUncommittedRetry)) {
    throw new TenantRepoSyncError(KB_TENANT_REPO_SYNC_EMPTY_MATERIALIZATION, ...)
}

Zero effect is treated as missing proof. But a full materialization can have zero effect for an entirely ordinary reason: the repo contains no ingestable paths. buildFullEnvelope returns files: [] with manifestSnapshot.pathsAfterPush: [], ingestion reports ingested: 0, deleted: 0, errors: [], and the run is thrown.

The consequences compound:

  1. The run fails, so lastIngestedRev is never persisted.
  2. consecutiveFailures climbs to the 2-hour cadence cap.
  3. Every subsequent cycle repeats it — the input never changes, so the outcome never changes. It cannot self-clear.
  4. The corpus stays at count: 0 indefinitely.

And the message is wrong about the cause. "Tenant-repo full materialization produced no durable positive-effect proof" describes a receipt/proof problem. The actual condition is "this repo had nothing to ingest" — a fact about content. An operator reading that message investigates receipts, attempt ids and contract versions; none of them are involved.

ingested: 0 is reachable ordinarily: an empty repo, a repo whose files are all filtered by rootKind/parser selection, or one whose every chunk was skipped as oversized (summary.skippedOversized — counted, and not part of hasEffect).

The guard itself is correct and must stay. It exists because a real effect without proof of it is a genuine defect, observed live at ingested=50, embeddings=50, errors=0 with no receipt. The bug is that it cannot distinguish "no effect because nothing was expected" from "no effect because the proof is missing".

The Architectural Reality

surface file:line today
the assertion TenantRepoSyncService.mjs:483 zero effect ⇒ throw unless it proves an uncommitted retry
the discriminator, already present and unused envelope.manifestSnapshot.pathsAfterPush the paths the materialization claimed would exist
envelope producer tenantRepoIngestEnvelopeBuilder.buildFullEnvelope emits pathsAfterPush: paths, empty when the repo has none

The evidence needed is already in the envelope: pathsAfterPush is the claim, ingested is the outcome. Zero against an empty claim is agreement; zero against a non-empty claim is the failure the guard was built for.

The Fix

Discriminate on the claim, not on the effect alone.

  • pathsAfterPush.length === 0 and no errors ⇒ a legitimately empty materialization. Complete the run, persist the checkpoint, do not charge a failure.
  • pathsAfterPush.length > 0 with zero effect ⇒ unchanged, still throws. The guard keeps its teeth for the case it exists for.
  • hasEffect without proof ⇒ unchanged.

Contract Ledger

Target surface Source of authority Proposed behavior Fallback Docs Evidence
assertFullMaterializationEffect TenantRepoSyncService.mjs:483 admit zero-effect only when pathsAfterPush is empty and no errors non-empty claim ⇒ throw as today verified: hasEffect reads only ingested/deleted
pathsAfterPush buildFullEnvelope read as the expectation absent manifest ⇒ returns null early, unchanged verified present on every full envelope

Decision Record impact: none.

Acceptance Criteria

  • A full materialization with pathsAfterPush: [], ingested: 0, deleted: 0, no errors completes and returns without throwing.
  • The checkpoint advances for that repo, so the next cycle is a normal incremental sync rather than a repeated first sync.
  • consecutiveFailures is not incremented for it.
  • Unchanged: pathsAfterPush: ['a.md'] with ingested: 0 still throws KB_TENANT_REPO_SYNC_EMPTY_MATERIALIZATION. This is the load-bearing AC — a change that admits every zero-effect run would silently retire the guard, which exists for an observed live defect.
  • Unchanged: a real effect without a matching receipt still throws.
  • An empty materialization is distinguishable in the outcome from an ingesting one, so "this repo is empty" is not silently identical to "this repo synced content".
  • Mutation-convicted both ways: admitting all zero-effect runs reddens the non-empty-claim test; reverting the fix reddens the empty-claim test.

Out of Scope

  • Whether a given external plane exhibits this. It is unmeasured there, and this ticket does not claim it.
  • #16799 (the post-first-ingest revision-boundary arm) and #16780 (embedding work bounds).
  • Changing what counts as ingestable — parser selection and oversize skipping are unchanged.

Avoided Traps

  • Admitting every zero-effect materialization. The obvious one-line "fix", and it retires a guard built for a live defect where 50 ingested rows had no proof. The discriminator has to be the claim, not the effect.
  • Treating skippedOversized > 0 as effect. Skipping is not ingesting; a repo whose every file is too large has genuinely produced nothing and should say so rather than report success.
  • Fixing the message only. A clearer error still leaves the repo permanently unsyncable and the corpus at zero.

Related

  • #16706 (parent) · #16799 (the other arm of an empty corpus, post-first-ingest) · #16780

Origin Session ID: d8332b13-5d97-4839-ac11-d2de4602a989

Retrieval Hint: query_raw_memories("EMPTY_MATERIALIZATION zero effect empty pathsAfterPush first sync corpus stays zero")

🖖 Grace (Claude Opus 5, Claude Code)