fix(index): the flicker window dies — atomic in-place vector update; lazy open honors every provider's not-ready report; the Path Registry twin table
Some checks failed
CI / Node 22 (push) Successful in 12m10s
CI / Node 24 (push) Has been cancelled
CI / Bun (latest) (push) Has been cancelled

DP6/DP8 of the Path Registry (BRAINY-PROD-LATENCY-TRIAD, the proven flicker
mechanism): update paths staged RemoveFromVectorIndex then AddToVectorIndex
as two separately-awaited transaction ops — between them a live row was in
NEITHER index (dark to semantic recall, fine in metadata list). The native
pair widened that window to seconds in production before their side's
visibility-commit fix; the structural cure lands here:

- hnswIndex.updateItem: absent → add; SAME vector → pure no-op (the
  production shape — a type-only update re-indexed an unchanged vector,
  remove+add did pure damage); changed vector → the node NEVER leaves the
  index: synchronous vector swap first (every query from that instant sees
  correct distances), then unlink/relink at the node's existing level via
  shared internals (linkNode/unlinkNodeEdges refactored out of add/remove;
  entry point and maxLevel provably unchanged).
- ReplaceInVectorIndexOperation: ONE transaction leg; feature-detects
  provider updateItem (native seam flagged — their side ships updateItem,
  then the adjacent remove+add fallback is dead code). Both update staging
  sites swapped; delete sites untouched.
- LAZY-OPEN GATE (fleet adoption find, SELF-ENGINE-PAIR-STANDARD): under
  disableAutoRebuild, ensureIndexesLoaded assessed ONLY the vector index —
  a not-ready native METADATA provider never blocked the completion latch
  and every find() silently returned [] on a populated store. All three
  providers now vote; any not-ready report falls through to the rebuild.
- docs/path-registry.md: brainy's twin table for the 32 shared path IDs —
  service class, budgets, lifecycle, narration, and the cited pin per row;
  owed rows named (LC4 doors-open migration, MT4 yielding heals, LC7
  downgrade contract) per the lifecycle-sprint choreography.

Pins: update-item-atomic 9/9 (visibility-atomic swap, reverse-index parity
vs fresh rebuild, entry-point invariants) · lazy-notready-honor 2/2.
Gates: unit 1928/1928 (148 files) · integration 760 · conformance 27/27.
This commit is contained in:
David Snelling 2026-08-05 16:11:23 -07:00
parent 3236a01bef
commit ebe06cdf33
7 changed files with 937 additions and 57 deletions

View file

@ -97,6 +97,7 @@ import {
SaveVerbOperation,
AddToGraphIndexOperation,
RemoveFromVectorIndexOperation,
ReplaceInVectorIndexOperation,
RemoveFromMetadataIndexOperation,
RemoveFromGraphIndexOperation,
UpdateNounMetadataOperation,
@ -2949,11 +2950,16 @@ export class Brainy<T = any> implements BrainyInterface<T> {
level: 0
})
)
// ONE atomic vector-index leg: the historical Remove→Add pair was
// two separately-awaited operations — between them the row was in
// NEITHER index (dark to semantic recall, visible to metadata
// reads). ReplaceInVectorIndexOperation goes through the provider's
// in-place updateItem when available (row never absent; an
// element-wise UNCHANGED vector — the type-only-update shape that
// flickered in production — is a pure no-op), else remove+add
// adjacent within the single op.
tx.addOperation(
new RemoveFromVectorIndexOperation(this.index, params.id, existing.vector)
)
tx.addOperation(
new AddToVectorIndexOperation(this.index, params.id, vector)
new ReplaceInVectorIndexOperation(this.index, params.id, existing.vector, vector)
)
}
@ -9364,8 +9370,10 @@ export class Brainy<T = any> implements BrainyInterface<T> {
connections: new Map(),
level: 0
}),
new RemoveFromVectorIndexOperation(this.index, params.id, existing.vector),
new AddToVectorIndexOperation(this.index, params.id, vector)
// ONE atomic vector-index leg — same law as update(): the row must
// never be absent from vector search during an update (see
// ReplaceInVectorIndexOperation).
new ReplaceInVectorIndexOperation(this.index, params.id, existing.vector, vector)
)
}
plan.operations.push(
@ -14958,14 +14966,30 @@ export class Brainy<T = any> implements BrainyInterface<T> {
}
// If indexes already populated AND honestly serving, mark complete and skip.
// Honest gate: when the provider exposes isReady(), that REPLACES the size()>0
// Honest gate: when a provider exposes isReady(), that REPLACES the size()>0
// proxy (a native index can report a non-zero size while its serving structure
// is not loaded — the silent-empty cold-load class). A not-ready provider falls
// through so the rebuild path can load it; verifyVectorLive() is the query-time
// backstop either way. Providers without isReady() keep the size() heuristic
// (the JS index's size()>0 genuinely means loaded).
//
// ALL THREE providers vote (fleet-adoption find, SELF-ENGINE-PAIR-STANDARD):
// this gate used to assess ONLY the vector index, so a not-ready native
// METADATA provider (its strand report) never blocked the completion latch
// — under disableAutoRebuild the promised lazy first-query rebuild never
// fired and every find() silently returned [] on a populated store. A
// not-ready report from ANY provider now falls through to the rebuild.
const vectorReadiness = assessIndexReadiness(this.index)
if (vectorReadiness === 'ready' || (vectorReadiness === 'unknown' && this.index.size() > 0)) {
const metadataReadiness = assessIndexReadiness(this.metadataIndex)
const graphReadiness = assessIndexReadiness(this.graphIndex)
const anyProviderNotReady =
vectorReadiness === 'not-ready' ||
metadataReadiness === 'not-ready' ||
graphReadiness === 'not-ready'
if (
!anyProviderNotReady &&
(vectorReadiness === 'ready' || (vectorReadiness === 'unknown' && this.index.size() > 0))
) {
this.lazyRebuildCompleted = true
return
}