LearnNewsExamplesServices
Frontmatter
id17443
titleStale-marked rows are counted but never re-embedded, so the census has no second number
stateClosed
labels
bugaiagent-os
assigneesneo-opus-ada
createdAtAug 21, 2026, 1:34 AM
updatedAtAug 25, 2026, 8:43 PM
githubUrlhttps://github.com/neomjs/neo/issues/17443
authorneo-opus-vega
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 25, 2026, 8:43 PM

Stale-marked rows are counted but never re-embedded, so the census has no second number

Closed Backlog/active-chunk-18 bugaiagent-os
neo-opus-vega
neo-opus-vega commented on Aug 21, 2026, 1:34 AM

Context

Split out of #17428 at delivery time, not at filing time. That ticket's detector half shipped — rows now carry the provider-input format's identity, and npm run ai:stale-embedding-census reports the affected population per tenant. What it cannot do is fix anything: nothing in the repository re-embeds a row whose marker is absent or superseded.

Live latest-open sweep at 2026-08-20T23:1xZ: checked the latest 20 open issues; #17439 (resumed repo slices forget previously settled embeddings) is the nearest neighbour and is a different subject — resume bookkeeping, keyed on slice state rather than on format identity. No equivalent found.

Why this is a separate ticket rather than a second commit on #17428. A read-only census is a leaf. Re-embedding is a plane operation: on an external tenant deployment a full corpus rebuild costs days of provider compute, and when to spend that is an operator decision informed by the census, not a property of the code. The two halves have different delivery authority, which is the axis this repository splits on.

The Problem

The census on this deployment reports 68,039 rows scanned, 68,039 pre-marker, 0 current. Every one of those rows holds a vector built from a string nobody stored, and the only mechanism that would rebuild them is a parserVersion advance — which re-mints every chunk id in the repository, re-embeds the whole corpus rather than the affected subset, and which nothing schedules.

So the state after #17428 is: the population is measurable and unrepairable. That is strictly better than invisible, and it is not done.

The Architectural Reality

Carried forward from #17428, verified there rather than assumed here:

mechanism re-embeds a marked-stale row? why
parserVersion advance yes, and far too much parserVersion IS a hashInputs member, so ids change and the whole corpus re-embeds. Nothing schedules it.
EMBEDDING_POISON_STRATEGY_FAMILY bump no it scopes poison/suppression evidence only — resolveEmbeddingPoisonGeneration's own docblock, and the falsifier is one grep for a call site
ordinary re-ingestion no the provider input is derived and not a hashInputs member, so ids are unchanged, the row classifies as present, and incremental selection skips it
the census from #17428 no — it is read-only by design it names the rows; it does not touch them

The repair therefore needs to delete-and-re-embed by id, driven off the census, rather than to invalidate a generation and hope.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
the repair runner (new) the census from #17428 takes stale ids, re-embeds them, and re-upserts with the current marker dry-run by default; --apply commits its own module docblock ai/scripts/migrations/backfillChromaSharedUserId.mjs is the runner precedent: dry-run default, explicit apply, per-collection summary
helpers/staleEmbeddingCensus unchanged already exposes the ids and the cause split the runner consumes shipped in #17428; classifyRowFormat returning null for a current row is the idempotence primitive
the before/after count the census the runner reports both numbers, from the same instrument a run that repairs nothing reports zero repaired rather than succeeding silently "repaired" must be a measurement, which is the AC #17428 could only half-close
provider compute the operator the runner is never scheduled automatically days of compute on a live plane is not a leaf's decision

Acceptance Criteria

Four of these are carried verbatim from #17428, where they could not be closed by a detector. They keep their original wording so nothing is quietly weakened in transit.

  • RED-PROOF, and it must not be the mistake #17428 was filed with. The arm must observe a row actually being re-embedded — not that an id or a generation hash changed. A hash-difference assertion is what let a docblock read as a wired mechanism in the first place; the falsifier is "does a present, stale row get a new vector", and it must fail against today's tree.
  • The absence is asserted too. An arm confirms that bumping EMBEDDING_POISON_STRATEGY_FAMILY re-embeds nothing, so #17428's corrected finding is pinned as behaviour and cannot be re-mis-read by the next reader.
  • No silent partial repair. If a run repairs some rows and stops, the remainder is reported with its count; a partial rebuild that reads as complete is the failure mode this whole lane keeps hitting.
  • The affected population is counted before and after, per tenant, so "repaired" is a measurement rather than an inference.
  • The repaired row carries the CURRENT marker, and a second run skips it. Idempotence end to end, not only at the classifier: re-selecting its own output turns a bounded repair into a loop, which on a plane paying days of compute is worse than not repairing.
  • Dry-run is the default. A runner that spends provider compute on an accidental invocation is not shippable; --apply is the only path that writes.
  • A repair that fails mid-row leaves no half-written row. Either the new vector and the new marker land together, or neither does — a row carrying a current marker over an old vector is undetectable by construction, which is worse than the state this lane started in.

Out of Scope

  • The marker and the census — shipped in #17428.
  • Adding the provider input to hashInputs. It would make future format changes self-repairing and re-mint every chunk id in existence; noted there as the general fix with its own blast radius.
  • Scheduling. The runner is invoked by an operator; wiring it into the orchestrator is a separate decision with its own cost argument.

Avoided Traps

  • Treating a generation bump as a repair. #17428 shipped exactly that error in its most load-bearing section, and the falsifier was one grep for a call site. Nothing here may assert re-embedding by observing a hash.
  • A green run that re-embedded nothing. The census returning zero stale rows is a valid outcome AND the signature of a broken selector; the before/after pair is what distinguishes them.
  • Claiming a corpus is repaired from a completed run. Completion is not coverage — the remainder count is the claim, not the exit code.

Related

  • #17428 — the detector half: the marker, the census, and the measured 68,039.
  • Parent epic: #17411. Sibling leaves: #17412 (throughput), #17413 (the lane guide).
  • #17439 — resumed slices forget settled embeddings; adjacent, keyed on slice state rather than format identity.

Origin Session ID: 046f993e-13ba-47dd-827d-d786428e318b

Retrieval Hint: query_raw_memories("stale embedding rows are counted by the census but nothing re-embeds them; delete-and-re-embed by id driven off the marker absence; dry-run default and before/after counts")

tobiu referenced in commit 089411c - "feat(kb): repair stale embeddings by id, dry-run by default (#17443) (#17768) on Aug 25, 2026, 8:43 PM
tobiu closed this issue on Aug 25, 2026, 8:43 PM