Context
Leaf slice of #14228 (Prevent metadata-only rows on the Chroma auto-embed write paths) — the SessionService half of AC2. The drain-path half shipped as #14254 (PR #14255). This covers the two direct-upsert auto-embed sites that bypass the WAL drain entirely.
The Problem
SessionService persists via collection.upsert({documents}) with no explicit embeddings at two sites, relying on Chroma's collection embedding-function to auto-generate the vector:
summarizeSession → this.sessionsCollection.upsert(...) (the session summary).
ingestAntigravityArtifacts → this.memoryCollection.upsert(...) (the implementation plan).
If that auto-embed is non-atomic (provider timeout / oversized input / model-not-resident), the document/metadata persist while the vector does not — a metadata-only row (the #13999 corruption shape) on a path with no WAL retry queue behind it.
The Architectural Reality
ai/services/memory-core/SessionService.mjs — summarizeSession (sessionsCollection.upsert) + ingestAntigravityArtifacts (memoryCollection.upsert).
- The atomic vector-write invariant
classifyRowVector (ai/services/memory-core/helpers/vectorWriteInvariant.mjs) — reused as the SSOT predicate.
The Fix (delivered — branch agent/14228-sessionservice-verify)
- New
ai/services/memory-core/helpers/verifyPersistedVector.mjs: reads the just-upserted vector back (get({ids, include:['embeddings']})) and classifies it with classifyRowVector. Disposition differs from the drain (no WAL behind these single-shot persists of expected data):
- metadata-only → logged loud for the autonomous recovery actuator to re-embed;
- never deletes (the document is real data the caller expects);
- never throws (a verify failure, including a read-back error, must not break the persist path);
- opt-in on
expectedDimension (no-ops without it).
- Wired after both
SessionService upserts.
Acceptance Criteria
Out of Scope
- The drain-path verify — #14254 (PR #14255).
- The empirical Chroma-atomicity V-B-A (
#14228 AC1) — sandbox-unreachable; deferred to the parent.
- The explicit-embedding write path (
#14029) + the heal actuator (#14134).
V-B-A note (refines the parent's AC3)
#14228 AC3 assumed SessionService is an interactive-hot path needing a sample/gate. V-B-A: both sites are background-ish (per-session summarization + per-artifact ingestion, infrequent) — not a per-request query path — so an unconditional per-upsert verify (one get) is affordable; no config-gate / Contract-Ledger row is required for this surface.
Related
#14228 (parent), #14254 (drain-slice sibling, PR #14255), #14029 (explicit-embed sibling gate), #14134 (heal actuator), #13999 (incident), #14039 (v13.1 epic).
Live latest-open sweep: checked latest open issues at 2026-06-27T16:38Z; no equivalent SessionService-verify 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("SessionService direct-upsert auto-embed verify metadata-only"); anchors: ai/services/memory-core/helpers/verifyPersistedVector.mjs, SessionService.mjs summarizeSession + ingestAntigravityArtifacts.
Context
Leaf slice of #14228 (Prevent metadata-only rows on the Chroma auto-embed write paths) — the SessionService half of AC2. The drain-path half shipped as #14254 (PR #14255). This covers the two direct-upsert auto-embed sites that bypass the WAL drain entirely.
The Problem
SessionServicepersists viacollection.upsert({documents})with no explicitembeddingsat two sites, relying on Chroma's collection embedding-function to auto-generate the vector:summarizeSession→this.sessionsCollection.upsert(...)(the session summary).ingestAntigravityArtifacts→this.memoryCollection.upsert(...)(the implementation plan).If that auto-embed is non-atomic (provider timeout / oversized input / model-not-resident), the document/metadata persist while the vector does not — a metadata-only row (the
#13999corruption shape) on a path with no WAL retry queue behind it.The Architectural Reality
ai/services/memory-core/SessionService.mjs—summarizeSession(sessionsCollection.upsert) +ingestAntigravityArtifacts(memoryCollection.upsert).classifyRowVector(ai/services/memory-core/helpers/vectorWriteInvariant.mjs) — reused as the SSOT predicate.The Fix (delivered — branch
agent/14228-sessionservice-verify)ai/services/memory-core/helpers/verifyPersistedVector.mjs: reads the just-upserted vector back (get({ids, include:['embeddings']})) and classifies it withclassifyRowVector. Disposition differs from the drain (no WAL behind these single-shot persists of expected data):expectedDimension(no-ops without it).SessionServiceupserts.Acceptance Criteria
classifyRowVector.expectedDimension.Out of Scope
#14228AC1) — sandbox-unreachable; deferred to the parent.#14029) + the heal actuator (#14134).V-B-A note (refines the parent's AC3)
#14228AC3 assumed SessionService is an interactive-hot path needing a sample/gate. V-B-A: both sites are background-ish (per-session summarization + per-artifact ingestion, infrequent) — not a per-request query path — so an unconditional per-upsert verify (oneget) is affordable; no config-gate / Contract-Ledger row is required for this surface.Related
#14228 (parent), #14254 (drain-slice sibling, PR #14255), #14029 (explicit-embed sibling gate), #14134 (heal actuator), #13999 (incident), #14039 (v13.1 epic).
Live latest-open sweep: checked latest open issues at 2026-06-27T16:38Z; no equivalent SessionService-verify 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("SessionService direct-upsert auto-embed verify metadata-only"); anchors:
ai/services/memory-core/helpers/verifyPersistedVector.mjs,SessionService.mjssummarizeSession + ingestAntigravityArtifacts.