Context
Leaf slice of #14228 (Prevent metadata-only rows on the Chroma auto-embed write paths). #14228's AC2 names two auto-embed persist surfaces — the embed-drain and SessionService:792/:932. This slice delivers the drain path (the highest-volume, plausibly-primary #13999 mechanism); the parent retains the empirical Chroma-atomicity V-B-A (AC1) + the SessionService direct-upsert sites.
The Problem
ai/daemons/embed/drainCycle.mjs embedBatch persists pending WAL rows via collection.add({ids, metadatas, documents}) — no explicit embeddings, relying on Chroma's collection embedding-function to auto-generate the vector. The cycle treats add-success as embed-success: it re-reads the WAL pending-state (purge-race compensation), not the persisted vector. So a non-atomic auto-embed (provider timeout / oversized input / model-not-resident) persists a metadata-only row AND marks it embedded (appends the WAL embed marker → removed from pending) — the #13999 corruption shape on the highest-volume write path.
The Architectural Reality
ai/daemons/embed/drainCycle.mjs embedBatch (collection.add, auto-embed) → drainWalOnce (marker write at the reconcile step).
- The atomic vector-write invariant
classifyRowVector / partitionRowsByVectorValidity (ai/services/memory-core/helpers/vectorWriteInvariant.mjs) already defines vector validity for the explicit-embedding gate (#14029) — reused here as the SSOT predicate, not a parallel check.
The Fix (delivered — branch agent/14228-prevent-metadata-only-drain @ 304007ca7)
- New
verifyEmbeddedVectors({collection, records, expectedDimension, log}) in drainCycle.mjs: after add, reads the persisted vectors back (get({ids, include:['embeddings']})) and classifies each with classifyRowVector.
- valid →
embedded (reconciled).
- confirmed metadata-only → deleted + retried (cooldown), never marked embedded (
add is create-not-upsert, so the metadata-only row is removed for a clean re-embed).
- unverifiable (read-back threw) → retried WITHOUT deletion (a transient read error must never destroy possibly-valid rows).
- opt-in on
expectedDimension; absent → verify skipped (logged), prior behavior preserved.
drainWalOnce integrates the verify between embed + reconcile; summary.metadataOnly added.
- Wired in both drain hosts: the embed daemon + the in-process memory-core server, passing
vectorDimension.
Acceptance Criteria
Out of Scope
- The empirical Chroma-atomicity V-B-A (
#14228 AC1) — the sandbox cannot force a real provider failure against live Chroma; deferred to the parent. The prevention is defensive-by-design (cheap no-op when auto-embed is atomic; catches the bug when it is not), warranted by the #13999 incident regardless.
SessionService:792/:932 direct-upsert sites (#14228 AC2) — different (no-WAL, best-effort, log-not-delete) handling shape; separate slice.
- The explicit-embedding write path (
#14029) + the heal actuator (#14134).
Related
#14228 (parent), #14029 (explicit-embed sibling gate), #14134 (heal actuator), #13999 (incident), #14039 (v13.1 epic).
Live latest-open sweep: checked latest 20 open issues at 2026-06-27T16:25Z; no equivalent drain-slice ticket found.
Authored by Vega (@neo-opus-vega · Claude Opus 4.8, Claude Code). Origin Session ID: 09af5f18-b64c-417b-be84-8cb3305005d2.
Handoff Retrieval Hints: query_raw_memories("drain auto-embed atomicity metadata-only verify-after-add"); anchors: ai/daemons/embed/drainCycle.mjs verifyEmbeddedVectors, commit 304007ca7.
Context
Leaf slice of #14228 (Prevent metadata-only rows on the Chroma auto-embed write paths).
#14228's AC2 names two auto-embed persist surfaces — the embed-drain andSessionService:792/:932. This slice delivers the drain path (the highest-volume, plausibly-primary#13999mechanism); the parent retains the empirical Chroma-atomicity V-B-A (AC1) + the SessionService direct-upsert sites.The Problem
ai/daemons/embed/drainCycle.mjsembedBatchpersists pending WAL rows viacollection.add({ids, metadatas, documents})— no explicitembeddings, relying on Chroma's collection embedding-function to auto-generate the vector. The cycle treats add-success as embed-success: it re-reads the WAL pending-state (purge-race compensation), not the persisted vector. So a non-atomic auto-embed (provider timeout / oversized input / model-not-resident) persists a metadata-only row AND marks it embedded (appends the WAL embed marker → removed from pending) — the#13999corruption shape on the highest-volume write path.The Architectural Reality
ai/daemons/embed/drainCycle.mjsembedBatch(collection.add, auto-embed) →drainWalOnce(marker write at the reconcile step).classifyRowVector/partitionRowsByVectorValidity(ai/services/memory-core/helpers/vectorWriteInvariant.mjs) already defines vector validity for the explicit-embedding gate (#14029) — reused here as the SSOT predicate, not a parallel check.The Fix (delivered — branch
agent/14228-prevent-metadata-only-drain@304007ca7)verifyEmbeddedVectors({collection, records, expectedDimension, log})indrainCycle.mjs: afteradd, reads the persisted vectors back (get({ids, include:['embeddings']})) and classifies each withclassifyRowVector.embedded(reconciled).addis create-not-upsert, so the metadata-only row is removed for a clean re-embed).expectedDimension; absent → verify skipped (logged), prior behavior preserved.drainWalOnceintegrates the verify between embed + reconcile;summary.metadataOnlyadded.vectorDimension.Acceptance Criteria
getper add-batch, not per-row); opt-in onexpectedDimension, wired in both drain hosts.Out of Scope
#14228AC1) — the sandbox cannot force a real provider failure against live Chroma; deferred to the parent. The prevention is defensive-by-design (cheap no-op when auto-embed is atomic; catches the bug when it is not), warranted by the#13999incident regardless.SessionService:792/:932direct-upsert sites (#14228AC2) — different (no-WAL, best-effort, log-not-delete) handling shape; separate slice.#14029) + the heal actuator (#14134).Related
#14228 (parent), #14029 (explicit-embed sibling gate), #14134 (heal actuator), #13999 (incident), #14039 (v13.1 epic).
Live latest-open sweep: checked latest 20 open issues at 2026-06-27T16:25Z; no equivalent drain-slice ticket found.
Authored by Vega (@neo-opus-vega · Claude Opus 4.8, Claude Code). Origin Session ID: 09af5f18-b64c-417b-be84-8cb3305005d2.
Handoff Retrieval Hints: query_raw_memories("drain auto-embed atomicity metadata-only verify-after-add"); anchors:
ai/daemons/embed/drainCycle.mjsverifyEmbeddedVectors, commit304007ca7.