---
id: 12039
title: KB_REVISION_BOUNDARY_INVALID on first tenant sync (null baseRevision)
state: CLOSED
labels:
  - bug
  - ai
assignees:
  - neo-opus-ada
createdAt: '2026-05-26T19:20:56Z'
updatedAt: '2026-05-26T20:47:25Z'
githubUrl: 'https://github.com/neomjs/neo/issues/12039'
author: neo-opus-ada
commentsCount: 0
parentIssue: null
subIssues: []
subIssuesCompleted: 0
subIssuesTotal: 0
blockedBy: []
blocking: []
closedAt: '2026-05-26T20:47:25Z'
---
# KB_REVISION_BOUNDARY_INVALID on first tenant sync (null baseRevision)

## Context

Surfaced during the same first-real-world cloud-deployment exercise that produced [#12036](https://github.com/neomjs/neo/issues/12036) and [#12032](https://github.com/neomjs/neo/issues/12032). On the *first* tenant repo sync (no prior `lastIngestedRev`), the orchestrator-CLI reports `status: completed` and ingests chunks successfully, but the metrics row carries `event_type=error` with `error_code=KB_REVISION_BOUNDARY_INVALID`. The bookkeeping-vs-execution divergence is the bug, not the ingestion itself.

## The Problem

[`KnowledgeBaseIngestionService.resolveRevisionTombstones`](https://github.com/neomjs/neo/blob/dev/ai/services/knowledge-base/KnowledgeBaseIngestionService.mjs#L716-L724):

```javascript
async resolveRevisionTombstones({baseRevision, headRevision, tenantContext, summary}) {
    if (!baseRevision || !headRevision) {
        summary.errors.push(this.createError({
            code   : 'KB_REVISION_BOUNDARY_INVALID',
            message: '`baseRevision` and `headRevision` must be provided together.'
        }));
        return [];
    }
    // ...
}
```

The validator demands both-or-neither. On first sync, `baseRevision` is legitimately `null` (no prior state to diff against — `priorState?.lastIngestedRev || null`), but `headRevision` is set (the new HEAD just resolved). The validator pushes a spurious error onto `summary.errors`, which propagates to `kb_ingestion_metrics` as `event_type=error`.

### Empirical evidence

From R3 first-sync metrics row:
```json
{
  "event_type": "error",
  "chunks_total": 5,
  "chunks_embedded": 5,
  "chunks_deleted": 0,
  "error_code": "KB_REVISION_BOUNDARY_INVALID",
  "detail": "{\"errors\":[{\"code\":\"KB_REVISION_BOUNDARY_INVALID\",\"message\":\"`baseRevision` and `headRevision` must be provided together.\"}]}"
}
```

`chunks_total=5` and `chunks_embedded=5` — ingestion fully succeeded. But `event_type=error` because the tombstone-resolution validator flagged the boundary as "invalid" when it's actually just "first-sync = no boundary to resolve."

## The Architectural Reality

- [`KnowledgeBaseIngestionService.mjs:716-724`](https://github.com/neomjs/neo/blob/dev/ai/services/knowledge-base/KnowledgeBaseIngestionService.mjs#L716-L724) — `resolveRevisionTombstones` validator.
- Tombstone resolution is for finding *deletions* across a revision boundary. With no `baseRevision`, there's no boundary to traverse → no deletions to resolve → the correct behavior is "return empty tombstone list, no error."

## The Fix

Two options; PR author picks the cleaner one:

1. **Early-return for null-base**: when `baseRevision === null && headRevision != null`, return `[]` without pushing an error. The both-null case (no boundary at all) is also a clean no-op. Only the mixed case (`baseRevision != null && headRevision == null`) should error.

   ```javascript
   if (!baseRevision) {
       return []; // first sync — no boundary, no tombstones, no error
   }
   if (!headRevision) {
       summary.errors.push(this.createError({
           code   : 'KB_REVISION_BOUNDARY_INVALID',
           message: '`headRevision` required when `baseRevision` is provided.'
       }));
       return [];
   }
   ```

2. **Caller-side guard**: have the upstream caller (envelope builder or `ingestSourceFiles`) not invoke `resolveRevisionTombstones` when `baseRevision` is null. Cleaner separation of concerns, but pushes the first-sync awareness into a different layer.

Option (1) is preferred — keeps the boundary semantics localized to the method that owns them.

## Decision Record impact

`aligned-with ADR 0014` — narrow bug fix within existing substrate.

## Acceptance Criteria

- [ ] `resolveRevisionTombstones` returns `[]` cleanly when `baseRevision` is null (first-sync case), without pushing `KB_REVISION_BOUNDARY_INVALID` onto `summary.errors`.
- [ ] First-sync `kb_ingestion_metrics` row carries `event_type` consistent with the actual outcome (`completed` when chunks ingested successfully, not `error`).
- [ ] Existing both-non-null happy path remains unchanged.
- [ ] Existing mixed-null case (one set, one null where both expected) still errors loudly.
- [ ] Unit test: first-sync scenario (`baseRevision=null, headRevision=<hash>`) produces no `summary.errors` entries.

## Out of Scope

- Bug A / B / C from #12036 (separate substrate gaps).
- Architectural redesign of tombstone resolution (in-process vs MCP-call) — orthogonal to this fix.

## Avoided Traps

- **Treating the metric `event_type=error` as the cosmetic-only symptom**: that's the surface signal, but the underlying spurious-error propagation pollutes downstream consumers (health dashboards, observability, retry-decision logic). Fix the validator, not just the metric writer.

## Related

- Sibling: #12036 (3 substrate gaps from R3), #12032 (credentialRef `file:` scheme)
- Parent epic: #11731 (Server-side tenant-repo ingestion)
- Empirical anchor: first cloud-deployment tenant-ingestion attempt, 2026-05-26

## Timeline

- 2026-05-26T19:20:58Z @neo-opus-ada added the `bug` label
- 2026-05-26T19:20:58Z @neo-opus-ada added the `ai` label
- 2026-05-26T19:31:25Z @neo-opus-ada cross-referenced by PR #12041
- 2026-05-26T19:42:02Z @neo-opus-ada assigned to @neo-opus-ada
- 2026-05-26T19:44:36Z @neo-opus-ada cross-referenced by PR #12043
- 2026-05-26T19:53:07Z @neo-gpt cross-referenced by PR #12046
- 2026-05-26T20:47:25Z @tobiu referenced in commit `3feaf2f` - "fix(kb): treat null baseRevision as first-sync (#12039) (#12043)

resolveRevisionTombstones validated baseRevision + headRevision as
both-or-neither, but first tenant-repo sync has a legitimately null
baseRevision (no prior `lastIngestedRev`) and a set headRevision (the
newly-resolved HEAD). The old validator pushed a spurious
KB_REVISION_BOUNDARY_INVALID onto summary.errors, which propagated to
kb_ingestion_metrics as event_type=error even though the embedding
pipeline succeeded.

Early-return when baseRevision is null — no boundary to traverse,
no tombstones to resolve, no error. Mixed-null case (baseRevision
set, headRevision null) still errors loudly with a tightened message.

Empirical anchor: first real-world cloud tenant-ingestion smoke
recorded chunks_embedded=5 + event_type=error simultaneously in the
metrics row; the divergence is the bug.

Tests:
- new: null baseRevision → no error, no event_type=error
- new: baseRevision set with null headRevision → KB_REVISION_BOUNDARY_INVALID with new message
- preserved: both-set-unavailable-resolver, both-set-with-resolver

Co-authored-by: tobiu <tobiasuhlig78@gmail.com>"
- 2026-05-26T20:47:26Z @tobiu closed this issue
- 2026-05-26T21:06:13Z @neo-opus-ada cross-referenced by PR #12050

