LearnNewsExamplesServices
Frontmatter
id16566
titleTenant ingestion fails at TWO different stages: neo at embed, create-app at materialization
stateOpen
labels
bugepicai
assigneesneo-opus-vega
createdAtAug 6, 2026, 12:48 AM
updatedAtAug 24, 2026, 9:28 PM
githubUrlhttps://github.com/neomjs/neo/issues/16566
authorneo-opus-vega
commentsCount21
parentIssuenull
subIssues
16577 A zero-chunk materialization is rejected, then backs off forever
16580 A tenant-sync failure states what it materialized, above both guards
16581 An unnameable ingest failure: the generic code discards the only thing that identified it
16587 In-process ingest params are absent from the contract that gates them
16557 Blobless tenant mirror turns first ingestion into 23,931 network round trips
16584 Stale-data deletion is the default, and the MCP gate never counts it
16591 The corpus-wipe refusal is proven below the surface agents call
16592 The neo tenant entry collides with kbSync and duplicates it untyped
16799 A capability that was never wired fails every ingest run it touches
16863 EMPTY_MATERIALIZATION means both "rows landed" and "nothing arrived"
16890 A capped tenant-sync cadence is indistinguishable from a configured one
17017 A poisoned first embedding batch masquerades as provider outage
subIssuesCompleted11
subIssuesTotal12
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]

Tenant ingestion fails at TWO different stages: neo at embed, create-app at materialization

neo-opus-vega
neo-opus-vega commented on Aug 6, 2026, 12:48 AM

Rewritten 2026-08-07T18:05Z — current facts only. The prior body carried its own superseded history and had become a context-window cost. Provenance is in git history, the PR trail, and Origin Session ID below.

State, measured 2026-08-07T16:11:00Z

Tenant ingestion WORKS on the canonical plane. All three configured repos, one tenant (tenantHash cf744f16ee7f, configTier: yaml):

repoHash lastIngestedRev checkpointStatus ingestContractVersion lastSourceErrorCode
aa6366e46d50 d8ae9ffa41ac complete 2 KB_REVISION_BOUNDARY_UNAVAILABLE
45d352be11ee b5c9fe7d712d complete 2 KB_REVISION_BOUNDARY_UNAVAILABLE
663199047bd9 a895dad15de7 complete 2 KB_REVISION_BOUNDARY_UNAVAILABLE

consecutiveFailures: 1, backoffMultiplier: 2, accessReadiness: KB_TENANT_REPO_ACCESS_EVIDENCE_EXPIRED (checked 04:11:31Z).

The two live problems

1. The lane is STARVED, not failing — and it is not alone

All three repos: due: true, nextDueAt between 05:13Z and 05:17Z (~11h past), lastRunAttemptAt: 04:11:30Z (~12h ago) against a 60-second sweep.

Cause is heavy-maintenance mutual exclusion. The kbSync re-embed took the slot at 04:50:27Z and still holds it 13h later. The orchestrator log records the same starvation hitting REM consolidation:

04:11:36Z  Deferring REM sleep graph extraction; memory miniSummary backfill is active
04:20:05Z  Deferring REM sleep graph extraction; session summarization is active
04:49:54Z  Deferring REM sleep graph extraction; knowledge base sync is active

REM is now at 766 undigested / recentCycles: []. One cause, at least two starved lanes — so this is a scheduling-fairness question, not a tenant-sync defect. That framing is new and is the most actionable thing in this ticket.

2. KB_REVISION_BOUNDARY_UNAVAILABLE is a post-first-ingest failure and is unstudied

Each repo has a revision, and the incremental path cannot establish a diff boundary from it. This is a different defect from the KB_VECTOR_EMBED_FAILED this ticket was filed for. Needs a decision: does it belong here or in a sibling?

Facts that constrain any fix

  • The lane belongs on this plane. taskAuthority.mjs:109'tenant-repo-sync': ORCHESTRATOR_AUTHORITY_CLASS.containerPlane, and the dockerized Agent OS is the container plane. ADR-0014's 2026-05-23 (#11740) amendment reads otherwise and is superseded by its 2026-07-30 (#16166) projection. Cite the latest amendment plus the runtime authority map, never the original decision table.
  • Source/Parser config is INERT on the pull path. useDefaultSources / rawRepoSource / sourcePaths govern the kbSync full-corpus Source build. tenantRepoIngestEnvelopeBuilder.mjs:12-19 builds envelopes straight from the git mirror and imports no SourceRegistry. Do not spend a cycle configuring them for this lane.
  • This plane is not a proxy for the external deployment. Ours embeds via LM Studio on the host (host.docker.internal:1234); theirs via an in-compose ollama container. Their state is still KB_VECTOR_EMBED_FAILED / lastIngestedRev: null — the pre-first-ingest stage this ticket was filed from. Different stage, different embedding path. Any AC asserting "a tenant repo ingests end to end" must name its plane.
  • Per-repo concurrency is Promise.all(...map(syncRepo)), not sequential — a yield belongs at the start of syncRepo, not in a loop body.
  • Two leases exist. The service's own tenant-repo-sync-lease.json is distinct from the cross-daemon heavy-maintenance gate; yielding on the wrong one is a live trap (it invalidated a first attempt at #16561 OQ1).
  • create-app's 2026-08-06 EMPTY_MATERIALIZATION evidence is unrecoverable. kb-server-2026-08-06.log covers 13:43:39Z→23:37:55Z; the run was 00:48–00:55Z. Container stdout retains nothing.

Acceptance criteria

  • Scheduling fairness: a due lane cannot be starved indefinitely by a long-running heavy-maintenance holder. Asserted with a fixture where a holder outlives a due lane's cadence by a multiple, and the due lane still runs. Covers tenant-repo-sync and REM consolidation, since one mechanism starves both.

  • KB_REVISION_BOUNDARY_UNAVAILABLE is root-caused, or explicitly moved to a sibling ticket with its evidence. — root-caused, and the tenant-lane fix has already shipped. Verified against origin/dev at 7ef07a7ee3, 2026-08-10.

    Root cause, in two parts. IngestionService.resolveRevisionTombstones raises this code when a caller supplies baseRevision while revisionResolver.resolveDeletedPaths is unwired — and revisionResolver has no production implementation: revisionResolver: null (IngestionService.mjs:136) is the only assignment anywhere under ai/, and every resolveDeletedPaths in the tree is a test double. So the request could only ever fail. The second part is the caller: the tenant lane forwarded baseRevision, asking that service to derive a deletion set it had already proven three lines earlier from gitMirror.diffRevisions().

    Symptom, recorded at the fix site (tenantRepoIngestEnvelopeBuilder.mjs:384-395): "every tenant repo past its first sync sat at consecutiveFailures: 12 with its cadence pinned at the 2h backoff cap, corpus frozen, because it kept asking for work it had already done." That is this epic's neo-at-embed / create-app-at-materialization family, same shape.

    Fix in place: baseRevision is deliberately not forwarded; the authoritative delta travels in deleted, and headRevision still travels because the materializer reads content at that revision. Demoting the guard globally was considered and rejected in the source — it would have bought one caller a fix at the price of every other caller's deletion guarantee.

    Unreachable in production today, checked rather than assumed: grepping every baseRevision under ai/ outside IngestionService and the envelope builder returns only gitMirror.diffRevisions — the git primitive, not an ingest caller. So no production caller supplies baseRevision to ingestSourceFiles, and this code cannot fire.

    Named residual, not a defect: the capability gap stands — nobody has built a production revisionResolver, so a future caller that legitimately needs derived tombstones still gets a fail-closed refusal. The source already distinguishes that from a genuine resolver failure (KB_REVISION_BOUNDARY_RESOLVER_FAILED), so the vocabulary is ready if it is ever wired. Deliberately not filed as a ticket: there is no caller wanting it, and a ticket for an unrequested capability is a roadmap promise in tracker form — the same trap the message's own comment records, where a stale pointer told operators to wait for a phase that had already shipped.

  • Reported state distinguishes a derived cadence from a capped one. isRepoDue computes backoffCapped; the reported per-repo state omits it, so a effectiveCadenceMs: 7200000 reads as misconfiguration when it is the cap. The repoStates.push at TenantRepoSyncService.mjs:1162 is a different path from the one assembling jitterMs/backoffMultiplier — trace both. — split to the leaf #16890 and delivered by PR #16891, so this epic's remaining criteria are not closed over by one PR.

    The "trace both" warning paid off, and the answer was not the one implied. All six repoStates.push sites traced: only the not-due/backoff-suppressed site publishes a cadence at all; the other five (revalidation-deferred, recovery-receipt-deferred, deferred, active, aborted-lease-lost) carry a status and no numbers, so they need no discriminator — checked and listed rather than assumed. The cadence-assembling path turned out to be the LOG LINE, which already prints backoffX=${dueState.backoffMultiplier}. So a human tailing logs could tell a capped repo from a configured one and a consumer reading the structured record could not; that asymmetry was the defect.

    The magnitude the cap hides is deliberately not republished. consecutiveFailures is already on the record and the multiplier is 2^failures, so a consumer derives it and can falsify the arithmetic instead of inheriting a number it cannot check. An earlier revision published uncappedCadenceMs and extended isRepoDue to return it; both were dropped as accretion, and tenantRepoSync.mjs is untouched in the final diff. Both spec arms mutation-convicted (Received: undefined in each direction), with an uncapped positive control so a hard-coded true cannot pass.

  • A repo whose ingest fails reports failed, never uninitialized, and the lane never reports status: completed over a null lastIngestedRev. (Reporting half overlaps #16551.)

  • The underlying error is surfaced rather than wrapped as "an error-bearing summary". KB_TENANT_REPO_SYNC_EMPTY_MATERIALIZATION must distinguish effect-without-receipt from a genuinely empty envelope — IngestionService.mjs:1016-1046 documents that code firing with ingested=50, embeddings=50, errors=0 and no receipt, which is the opposite of what its message says.

  • Proof artifact, plane-named: lastIngestedRev advances on a subsequent sync for a named repo on a named plane. A unit test does not close this.

Out of scope

  • Tenant Source/Parser configuration — inert on this lane (above).
  • branchRef main-vs-dev selectiontenantRepoAccessContract.mjs:496; cannot matter while the lane never runs.
  • Cold-mirror first-ingest cost#16557.
  • Backoff/counter freezing#16551.
  • The external deployment's ingestion state — same symptom, different stage and embedding path.

Related

#16551 (reporting overlap) · #16557 (mirror cost) · #16630 / #16642 (the heap-ceiling incident that surfaced the starvation) · #11790 / #11788 / #11789 (the lane, mirror primitive, envelope) · ADR-0014 (#16166 projection is current) · D#15605 (acquisition-vs-extraction hub — kbSync and tenant-repo-sync both stamp {neo-shared, neo})

Origin Session ID: 4141258c-36d3-4788-b0c2-ab3ebe0867be

Retrieval Hint: query_raw_memories("tenant-repo-sync starved behind heavy maintenance while REM undigested grows") · TenantRepoSyncService.mjs:1162 · the 04:49:54Z deferral line.

tobiu referenced in commit 8f9e804 - "An unreachable store is named, not flattened into a generic ingest failure (#16581) (#16579) on Aug 6, 2026, 9:43 AM
tobiu referenced in commit c40003d - "fix(knowledge-base): consult the lease yield predicate per provider chunk (#16822) (#16823) on Aug 9, 2026, 8:02 PM
tobiu referenced in commit 7be8583 - "fix(ai): a capped cadence says so, instead of reading as a misconfiguration (#16890) (#16891) on Aug 10, 2026, 8:47 PM
tobiu referenced in commit 1f0b7f5 - "fix(knowledge-base): consult the lease yield predicate per provider chunk (#16822) on Aug 21, 2026, 2:31 AM
tobiu referenced in commit ca19327 - "test(orchestrator): prove the fairness bound cannot preempt a stuck holder (#16817) on Aug 21, 2026, 2:31 AM