Problem scope
Empirical anchor (this is a real, reproduced data-loss): a session-sunset self-DM continuity handover (MESSAGE:de1d9f0b-c98d-49e9-ae0b-a7a586b5b5fc, authored @neo-opus-grace 2026-06-22T15:21 — the session's final write, STEP 8 of the sunset) was created (add_message returned its id; the durable Sandman memory 68f06bb7 references it) but never durably landed in the graph:
- absent from the
Nodes table,
- absent from
GraphLog (a SQLite insert-trigger appends a GraphLog row on every node write — so no row = the node-insert never hit SQLite),
- absent from the
memory-wal.
Other messages (8,199 MESSAGE nodes) and other sunset self-DMs persist fine — only this last-write-before-restart was lost.
Ruled out (data-backed, no shortcuts):
- Not orphan-apoptosis —
GraphMaintenanceService apoptosis would delete all orphaned non-exempt nodes; 2,291 orphaned MESSAGE nodes survive, which falsifies it.
- Not fade — fade is a ~3-month half-life; de1d9f0b died the same day.
- Not a discoverability filter — the node was never durably there to surface.
Architectural root (operator): add_message historically hit the chat/embedding model synchronously at write time, which contends with the shared model during heavy maintenance (REM / KB-sync / golden-path). The intended direction is daemon-based async ingestion — but unlike memories (which write durably to memory-wal and are drained by ai/daemons/embed/), the A2A-message path persists in-request and lacks a durable write-ahead surface, so a message can be acknowledged to the caller yet lost before it is durably ingested.
This warrants an Epic: it spans a durable write-ahead surface for messages, the ingestion daemon, replay/idempotency, and removing the synchronous model hit — bound by one hard durability invariant.
Intended solution shape
Mirror the memory ingestion pattern for A2A messages: add_message writes durably first (append-only WAL) and returns; a load-aware daemon drains → graph-persists → embeds, off both the request path and the shared maintenance model.
Durability invariant (load-bearing): an add_message that returns an id MUST survive a process restart — no acknowledged-but-lost messages. A2A messages are durable continuity records (sunset handovers, lifecycle coordination, lane claims); losing one is a continuity-integrity failure, not an acceptable cache miss.
Out of scope / related
- Memory ingestion already uses this pattern (
ai/daemons/embed/ + memory-wal) — the model to mirror, not rebuild.
- #13755 — heavy-lease "items never reach the graph" (embedding-stall for kb-sync/REM/golden-path): the memory/golden-path twin of this message-path gap.
- #10349 (CLOSED) — the sunset self-DM handover mechanism this gap silently broke.
- #10150 — A2A-message Chroma semantic-search collection: complementary; the ingestion daemon is the natural place to populate it.
- The fade/apoptosis MESSAGE-exemption gap — a separate hardening (the orphan-sweep should never apoptose A2A messages); real but ~3-month-horizon, not this incident.
Avoided traps
- "It's a deletion" — falsified (no GraphLog/Nodes trace; 2,291 orphaned messages survive).
- "Discoverability filter" — falsified (never durably persisted).
- Re-introducing a synchronous embed in
add_message — the original maintenance-interference this Epic removes.
Problem scope
Empirical anchor (this is a real, reproduced data-loss): a session-sunset self-DM continuity handover (
MESSAGE:de1d9f0b-c98d-49e9-ae0b-a7a586b5b5fc, authored@neo-opus-grace2026-06-22T15:21 — the session's final write, STEP 8 of the sunset) was created (add_messagereturned its id; the durable Sandman memory68f06bb7references it) but never durably landed in the graph:Nodestable,GraphLog(a SQLite insert-trigger appends a GraphLog row on every node write — so no row = the node-insert never hit SQLite),memory-wal.Other messages (8,199 MESSAGE nodes) and other sunset self-DMs persist fine — only this last-write-before-restart was lost.
Ruled out (data-backed, no shortcuts):
GraphMaintenanceServiceapoptosis would delete all orphaned non-exempt nodes; 2,291 orphaned MESSAGE nodes survive, which falsifies it.Architectural root (operator):
add_messagehistorically hit the chat/embedding model synchronously at write time, which contends with the shared model during heavy maintenance (REM / KB-sync / golden-path). The intended direction is daemon-based async ingestion — but unlike memories (which write durably tomemory-waland are drained byai/daemons/embed/), the A2A-message path persists in-request and lacks a durable write-ahead surface, so a message can be acknowledged to the caller yet lost before it is durably ingested.This warrants an Epic: it spans a durable write-ahead surface for messages, the ingestion daemon, replay/idempotency, and removing the synchronous model hit — bound by one hard durability invariant.
Intended solution shape
Mirror the memory ingestion pattern for A2A messages:
add_messagewrites durably first (append-only WAL) and returns; a load-aware daemon drains → graph-persists → embeds, off both the request path and the shared maintenance model.Durability invariant (load-bearing): an
add_messagethat returns an id MUST survive a process restart — no acknowledged-but-lost messages. A2A messages are durable continuity records (sunset handovers, lifecycle coordination, lane claims); losing one is a continuity-integrity failure, not an acceptable cache miss.Out of scope / related
ai/daemons/embed/+memory-wal) — the model to mirror, not rebuild.Avoided traps
add_message— the original maintenance-interference this Epic removes.