Context
Spun out of the #12830 corrupted-memory investigation (AC#3 write-source trace by @neo-opus-ada — the findings comment on #12830, 2026-06-10). The Memory Core holds a meaningful population of empty-content AGENT_MEMORY rows (prompt/thought/response empty / whitespace). They are not cosmetic: they inflate session-summary input (a contributor to the 30-min summarization stalls) and distort backlog/cost accounting. The investigation traced the write source: MemoryService.addMemory has no non-empty validation anywhere on its write path, so an add_memory call with empty content silently persists a corrupted memory + a meaningless vector. This ticket is the prevention half (stop new corruption). The gated cleanup of the existing set is a separate ticket, blocked on #12830's AC#2 measurement.
The Problem
add_memory is the never-fail durable write — but "never-fail" currently also means "never validates": empty / whitespace prompt+thought+response are accepted and embedded. Three trigger paths were code-evidenced (#12830 AC#3):
- Boot-heartbeat (strongest, by design).
ai/scripts/lifecycle/resumeHarness.mjs:307 instructs every resumed agent to "call add_memory once as a boot heartbeat" — a designed low-content save on every resume cycle, harness-agnostic. Likely the dominant source, and the reason a naive empty-reject would regress a legitimate path.
- Agent-loop placeholder.
ai/agent/Loop.mjs:454 writes thought/response with placeholder fallbacks but prompt: event.data with no fallback — a gray contributor on low-content turns.
- Direct MCP
add_memory. ai/mcp/server/memory-core/toolService.mjs:18 binds the tool with no validation wrapper — the path external harnesses (incl. Codex) call; the operator-flagged recent @neo-gpt corruption skew points here.
The Architectural Reality
- Write-gap (fix locus):
ai/services/memory-core/MemoryService.mjs addMemory — L318 (combinedText embed-doc build), L327-334 (metadata), L353-357 (collection.add). No mandatory / min-length guard on prompt/thought/response.
- (Note: the embed doc is always label-non-empty —
"User Prompt: \nAgent Thought: \nAgent Response:" — so a document === '' check is useless; the guard MUST inspect the raw prompt/thought/response inputs.)
- Raw content lives in the Chroma
neo-agent-memory document/metadata, keyed by semanticVectorId; the SQLite Nodes row holds only metadata.
- The load-bearing constraint: the guard cannot blanket-reject empty content — that would break the legitimate boot-heartbeat (path 1).
The Fix
- Add a non-empty / min-length guard to
MemoryService.addMemory over the combined prompt+thought+response (reject or fail-soft a genuinely-empty save) at the write-path locus above; covers the direct-MCP (3) and agent-loop (2) paths uniformly.
- Fix the boot-heartbeat to write a meaningful-minimal liveness memory (
resumeHarness.mjs:307) — a structured boot-marker with non-empty content — so the guard can be strict without regressing liveness.
Contract Ledger
| Surface |
Source of Authority |
Proposed |
Fallback |
Consumer |
add_memory empty-content input |
MemoryService.addMemory (MemoryService.mjs:318) |
Guard: reject / fail-soft a genuinely-empty (prompt+thought+response) save |
Documented error to caller; never a silent empty persist |
agents (all harnesses) |
| Boot-heartbeat write |
resumeHarness.mjs:307 |
Write a meaningful-minimal liveness memory (non-empty) |
n/a |
orchestrator / resumed agents |
Decision Record impact
none directly. Embodies the general "no silent empty/placeholder write" integrity principle #12830 names.
Out of Scope
- Measuring the existing corrupted set (
#12830 AC#2 — the Chroma metadata scan; embedder-gated).
- Deleting the existing corrupted set (separate gated-cleanup ticket; blocked on
#12830 AC#2 + this prevention).
- The session-summary synthesis abort-guard (the OTHER half of the 30-min stall —
SessionService.mjs:576 has no timeout; routed to the session-summary-stall lane, sibling to #12831 / #12805).
Avoided Traps
document === '' check — useless; addMemory always builds a label-non-empty embed doc. Inspect the raw inputs.
- Blanket empty-reject — regresses the designed boot-heartbeat (path 1); the heartbeat must be redesigned to write meaningful content first.
- Dedicated off-
AGENT_MEMORY liveness channel as the primary fix — heavier wire/schema change; the meaningful-minimal heartbeat memory closes the gap with far less blast radius. Revisit only if heartbeat volume itself becomes a cost concern.
Related
#12830 — Root-cause & characterize corrupted memories (parent investigation; this is its AC#3-handoff prevention spinout). Implementation offered to @neo-opus-grace per the AC#3 handoff (memory-integrity domain + the #12450 sibling).
#12450 — query_summaries empty: corrupt sessions vectors (sibling memory-integrity defect, different collection).
#10063 — Claude Code hook auto-persist turn memories (a candidate write path).
- Gated-cleanup ticket — TO FOLLOW, blocked on
#12830 AC#2.
Release classification: post-v13 (prevention follow-up; the corrupted-memory family is post-v13 per the v13 board triage in #12696).
Origin Session ID: fa0b47bc-e066-4e8b-8aec-a9de8fc69a09
Handoff Retrieval Hints:
- Retrieval Hint: "add_memory empty content validation guard MemoryService addMemory boot-heartbeat resumeHarness corrupted memory prevention"
- Fix locus:
MemoryService.mjs addMemory L318 / L327-334 / L353-357; boot-heartbeat resumeHarness.mjs:307.
Context
Spun out of the
#12830corrupted-memory investigation (AC#3 write-source trace by @neo-opus-ada — the findings comment on#12830, 2026-06-10). The Memory Core holds a meaningful population of empty-contentAGENT_MEMORYrows (prompt/thought/responseempty / whitespace). They are not cosmetic: they inflate session-summary input (a contributor to the 30-min summarization stalls) and distort backlog/cost accounting. The investigation traced the write source:MemoryService.addMemoryhas no non-empty validation anywhere on its write path, so anadd_memorycall with empty content silently persists a corrupted memory + a meaningless vector. This ticket is the prevention half (stop new corruption). The gated cleanup of the existing set is a separate ticket, blocked on#12830's AC#2 measurement.The Problem
add_memoryis the never-fail durable write — but "never-fail" currently also means "never validates": empty / whitespaceprompt+thought+responseare accepted and embedded. Three trigger paths were code-evidenced (#12830AC#3):ai/scripts/lifecycle/resumeHarness.mjs:307instructs every resumed agent to "calladd_memoryonce as a boot heartbeat" — a designed low-content save on every resume cycle, harness-agnostic. Likely the dominant source, and the reason a naive empty-reject would regress a legitimate path.ai/agent/Loop.mjs:454writesthought/responsewith placeholder fallbacks butprompt: event.datawith no fallback — a gray contributor on low-content turns.add_memory.ai/mcp/server/memory-core/toolService.mjs:18binds the tool with no validation wrapper — the path external harnesses (incl. Codex) call; the operator-flagged recent@neo-gptcorruption skew points here.The Architectural Reality
ai/services/memory-core/MemoryService.mjsaddMemory— L318 (combinedTextembed-doc build), L327-334 (metadata), L353-357 (collection.add). No mandatory / min-length guard onprompt/thought/response."User Prompt: \nAgent Thought: \nAgent Response:"— so adocument === ''check is useless; the guard MUST inspect the rawprompt/thought/responseinputs.)neo-agent-memorydocument/metadata, keyed bysemanticVectorId; the SQLiteNodesrow holds only metadata.The Fix
MemoryService.addMemoryover the combinedprompt+thought+response(reject or fail-soft a genuinely-empty save) at the write-path locus above; covers the direct-MCP (3) and agent-loop (2) paths uniformly.resumeHarness.mjs:307) — a structured boot-marker with non-empty content — so the guard can be strict without regressing liveness.Contract Ledger
add_memoryempty-content inputMemoryService.addMemory(MemoryService.mjs:318)prompt+thought+response) saveresumeHarness.mjs:307Decision Record impact
nonedirectly. Embodies the general "no silent empty/placeholder write" integrity principle#12830names.Out of Scope
#12830AC#2 — the Chroma metadata scan; embedder-gated).#12830AC#2 + this prevention).SessionService.mjs:576has no timeout; routed to the session-summary-stall lane, sibling to#12831/#12805).Avoided Traps
document === ''check — useless;addMemoryalways builds a label-non-empty embed doc. Inspect the raw inputs.AGENT_MEMORYliveness channel as the primary fix — heavier wire/schema change; the meaningful-minimal heartbeat memory closes the gap with far less blast radius. Revisit only if heartbeat volume itself becomes a cost concern.Related
#12830— Root-cause & characterize corrupted memories (parent investigation; this is its AC#3-handoff prevention spinout). Implementation offered to @neo-opus-grace per the AC#3 handoff (memory-integrity domain + the#12450sibling).#12450— query_summaries empty: corrupt sessions vectors (sibling memory-integrity defect, different collection).#10063— Claude Code hook auto-persist turn memories (a candidate write path).#12830AC#2.Release classification: post-v13 (prevention follow-up; the corrupted-memory family is post-v13 per the v13 board triage in
#12696).Origin Session ID: fa0b47bc-e066-4e8b-8aec-a9de8fc69a09
Handoff Retrieval Hints:
MemoryService.mjsaddMemoryL318 / L327-334 / L353-357; boot-heartbeatresumeHarness.mjs:307.