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):
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-syncand 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 sixrepoStates.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 selection — tenantRepoAccessContract.mjs:496; cannot matter while the lane never runs.
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
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 IDbelow.State, measured 2026-08-07T16:11:00Z
Tenant ingestion WORKS on the canonical plane. All three configured repos, one tenant (
tenantHash cf744f16ee7f,configTier: yaml):lastIngestedRevcheckpointStatusingestContractVersionlastSourceErrorCodeaa6366e46d50d8ae9ffa41acKB_REVISION_BOUNDARY_UNAVAILABLE45d352be11eeb5c9fe7d712dKB_REVISION_BOUNDARY_UNAVAILABLE663199047bd9a895dad15de7KB_REVISION_BOUNDARY_UNAVAILABLEconsecutiveFailures: 1,backoffMultiplier: 2,accessReadiness: KB_TENANT_REPO_ACCESS_EVIDENCE_EXPIRED(checked04:11:31Z).The two live problems
1. The lane is STARVED, not failing — and it is not alone
All three repos:
due: true,nextDueAtbetween05:13Zand05:17Z(~11h past),lastRunAttemptAt: 04:11:30Z(~12h ago) against a 60-second sweep.Cause is heavy-maintenance mutual exclusion. The
kbSyncre-embed took the slot at04:50:27Zand still holds it 13h later. The orchestrator log records the same starvation hitting REM consolidation: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_UNAVAILABLEis a post-first-ingest failure and is unstudiedEach 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_FAILEDthis ticket was filed for. Needs a decision: does it belong here or in a sibling?Facts that constrain any fix
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.useDefaultSources/rawRepoSource/sourcePathsgovern thekbSyncfull-corpus Source build.tenantRepoIngestEnvelopeBuilder.mjs:12-19builds envelopes straight from the git mirror and imports noSourceRegistry. Do not spend a cycle configuring them for this lane.host.docker.internal:1234); theirs via an in-compose ollama container. Their state is stillKB_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.Promise.all(...map(syncRepo)), not sequential — a yield belongs at the start ofsyncRepo, not in a loop body.tenant-repo-sync-lease.jsonis 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-06EMPTY_MATERIALIZATIONevidence is unrecoverable.kb-server-2026-08-06.logcovers13:43:39Z→23:37:55Z; the run was00: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-syncand REM consolidation, since one mechanism starves both.KB_REVISION_BOUNDARY_UNAVAILABLEis root-caused, or explicitly moved to a sibling ticket with its evidence. — root-caused, and the tenant-lane fix has already shipped. Verified againstorigin/devat7ef07a7ee3, 2026-08-10.Root cause, in two parts.
IngestionService.resolveRevisionTombstonesraises this code when a caller suppliesbaseRevisionwhilerevisionResolver.resolveDeletedPathsis unwired — andrevisionResolverhas no production implementation:revisionResolver: null(IngestionService.mjs:136) is the only assignment anywhere underai/, and everyresolveDeletedPathsin the tree is a test double. So the request could only ever fail. The second part is the caller: the tenant lane forwardedbaseRevision, asking that service to derive a deletion set it had already proven three lines earlier fromgitMirror.diffRevisions().Symptom, recorded at the fix site (
tenantRepoIngestEnvelopeBuilder.mjs:384-395): "every tenant repo past its first sync sat atconsecutiveFailures: 12with its cadence pinned at the 2h backoff cap, corpus frozen, because it kept asking for work it had already done." That is this epic'sneo-at-embed /create-app-at-materialization family, same shape.Fix in place:
baseRevisionis deliberately not forwarded; the authoritative delta travels indeleted, andheadRevisionstill 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
baseRevisionunderai/outsideIngestionServiceand the envelope builder returns onlygitMirror.diffRevisions— the git primitive, not an ingest caller. So no production caller suppliesbaseRevisiontoingestSourceFiles, 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.
isRepoDuecomputesbackoffCapped; the reported per-repo state omits it, so aeffectiveCadenceMs: 7200000reads as misconfiguration when it is the cap. TherepoStates.pushatTenantRepoSyncService.mjs:1162is a different path from the one assemblingjitterMs/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.pushsites 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 printsbackoffX=${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.
consecutiveFailuresis already on the record and the multiplier is2^failures, so a consumer derives it and can falsify the arithmetic instead of inheriting a number it cannot check. An earlier revision publisheduncappedCadenceMsand extendedisRepoDueto return it; both were dropped as accretion, andtenantRepoSync.mjsis untouched in the final diff. Both spec arms mutation-convicted (Received: undefinedin each direction), with an uncapped positive control so a hard-codedtruecannot pass.A repo whose ingest fails reports
failed, neveruninitialized, and the lane never reportsstatus: completedover a nulllastIngestedRev. (Reporting half overlaps #16551.)The underlying error is surfaced rather than wrapped as "an error-bearing summary".
KB_TENANT_REPO_SYNC_EMPTY_MATERIALIZATIONmust distinguish effect-without-receipt from a genuinely empty envelope —IngestionService.mjs:1016-1046documents that code firing withingested=50, embeddings=50, errors=0and no receipt, which is the opposite of what its message says.Proof artifact, plane-named:
lastIngestedRevadvances on a subsequent sync for a named repo on a named plane. A unit test does not close this.Out of scope
branchRefmain-vs-dev selection —tenantRepoAccessContract.mjs:496; cannot matter while the lane never runs.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 —
kbSyncandtenant-repo-syncboth stamp{neo-shared, neo})Origin Session ID:
4141258c-36d3-4788-b0c2-ab3ebe0867beRetrieval Hint:
query_raw_memories("tenant-repo-sync starved behind heavy maintenance while REM undigested grows")·TenantRepoSyncService.mjs:1162· the04:49:54Zdeferral line.