Context
Parent epic: #13889
The #13889 incident is an acknowledged A2A message write that returned a MESSAGE:* id but never durably landed in Nodes, GraphLog, or WAL. The parent epic now has a local/cloud drain-topology child (#13890), but the core acceptance contract still needs its own leaf: add_message must not acknowledge a message until the message intent is durably captured.
Release classification: boardless - data-integrity hardening under an active epic, not currently attached to a release board.
Live latest-open sweep: checked the latest 20 open issues immediately before creation on 2026-06-22T23:10Z; no equivalent child ticket found. Scope-specific live sweep for add_message + WAL + durable returned only parent epic #13889, child #13890, and stale/misframed #13886. A2A in-flight sweep checked the latest 30 messages and found no competing lane-claim for this child scope.
The Problem
Today MailboxService.addMessage() can return success only after it has walked the in-request graph-write path. That leaves the durable acceptance boundary coupled to GraphService/cache/SQLite behavior instead of an append-only write-ahead surface. The real failure behind #13889 proves this is not theoretical: a caller saw a returned MESSAGE:* id, while the node never reached GraphLog, Nodes, or WAL.
A2A messages are continuity records. The write contract needs the same shape as add_memory: deliberate validation failures before acceptance, then durable append before any derived graph/vector work can fail or stall.
The Architectural Reality
ai/services/memory-core/MailboxService.mjs:495 starts addMessage() by requiring GraphService, resolving identity, validating the target, and generating the MESSAGE:* id. ai/services/memory-core/MailboxService.mjs:640 writes the MESSAGE node directly through GraphService.upsertNode(), then ai/services/memory-core/MailboxService.mjs:651 and following lines create delivery-critical graph edges before returning.
ai/services/memory-core/MemoryService.mjs:372 through ai/services/memory-core/MemoryService.mjs:386 documents the already-correct memory write order: validation gates, durable JSONL WAL append, asynchronous graph projection, and no embed on the request path. ai/services/memory-core/MemoryService.mjs:492 awaits appendWalMemory() before scheduling graph projection.
ai/services/memory-core/helpers/memoryWalStore.mjs:103 through ai/services/memory-core/helpers/memoryWalStore.mjs:119 is the existing durable append precedent. This ticket does not require reusing that helper directly if message semantics need a separate store; it does require the same acceptance-order invariant.
The Fix
Change the add_message acceptance contract so success means the canonical message intent is durably appended before acknowledgment:
- Preserve fail-loud pre-ack validation for identity, target resolution, reply permissions, task-state validation, wake-suppression policy, and other deliberate rejections.
- Generate a stable
MESSAGE:* id once validation passes, and write that id plus the full canonical message payload to a durable append-only message WAL before returning success.
- Ensure graph projection, delivery edges, wake pumping, concept extraction, vector/search population, or any model-dependent work is derived from the accepted WAL record and cannot erase or invalidate the returned id.
- Define return semantics clearly:
status: sent means accepted durably for mailbox delivery; if immediate graph projection remains best-effort in this leaf, it must not be the durability gate.
- Add restart-focused evidence that an accepted message id remains recoverable after process restart even if graph projection has not completed yet.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
add_message acknowledgment boundary |
Parent epic #13889 and MemoryService.addMemory() WAL-first precedent |
Return success only after durable message WAL append, or fail before ack |
Validation errors remain fail-loud and non-accepted |
JSDoc / PR body |
L2 unit tests for ack ordering |
MESSAGE:* id stability |
Current MailboxService.addMessage() id generation |
The id returned to caller is the id stored in WAL and later replayed into graph |
No regenerated ids during replay |
JSDoc / test names |
L2 id-stability test + L3 restart probe |
| Graph projection ownership |
Existing memory WAL projection pattern |
Graph node/edge projection derives from the accepted WAL record and cannot veto after durable append |
Failed projection remains pending/retryable |
PR body and follow-up drain ticket linkage |
L2 projection-failure test |
| Hot-path model isolation |
Parent epic avoided trap |
No chat/embed/model call on add_message request path |
Model-dependent work remains daemon/drain-owned |
PR body Evidence line |
L1 static audit + L2 stubbed provider test |
| Read-after-accept visibility |
Mailbox consumer expectations |
Ticket must state whether a pending-WAL overlay is included here or deferred to the drain/replay child |
If deferred, return contract must not overclaim immediate graph visibility |
PR body residual section |
L1 contract statement + L2 if implemented |
Decision Record impact
Aligned with the existing memory WAL durability substrate. No ADR change expected for this child.
Decision Record: Not needed.
Acceptance Criteria
Out of Scope
- Local/cloud drain topology; that is #13890.
- Full daemon replay/idempotency for every delivery edge type unless needed for this acceptance contract's tests.
- A2A message semantic-search population; defer or relate to
#10150 unless implemented by a later drain child.
- Fade/apoptosis MESSAGE exemption hardening.
Avoided Traps
- Do not preserve the current graph-write path as the acceptance gate.
- Do not return success after generating an id but before durable append.
- Do not make Chroma, GraphService cache, wake pumping, or concept extraction part of the success boundary.
- Do not silently fabricate fallback WAL directories; stale config overlays need actionable diagnostics.
Related
Parent: #13889
Related: #13890, #10150
Origin Session ID: db5b2ecf-db91-4b7d-9498-ccef00426a1c
Handoff Retrieval Hints: A2A add_message WAL-first acceptance contract durable message id survives restart; MailboxService.addMessage GraphService upsertNode appendWalMemory precedent
Context
Parent epic: #13889
The
#13889incident is an acknowledged A2A message write that returned aMESSAGE:*id but never durably landed in Nodes, GraphLog, or WAL. The parent epic now has a local/cloud drain-topology child (#13890), but the core acceptance contract still needs its own leaf:add_messagemust not acknowledge a message until the message intent is durably captured.Release classification: boardless - data-integrity hardening under an active epic, not currently attached to a release board.
Live latest-open sweep: checked the latest 20 open issues immediately before creation on 2026-06-22T23:10Z; no equivalent child ticket found. Scope-specific live sweep for
add_message+WAL+durablereturned only parent epic#13889, child#13890, and stale/misframed#13886. A2A in-flight sweep checked the latest 30 messages and found no competing lane-claim for this child scope.The Problem
Today
MailboxService.addMessage()can return success only after it has walked the in-request graph-write path. That leaves the durable acceptance boundary coupled to GraphService/cache/SQLite behavior instead of an append-only write-ahead surface. The real failure behind#13889proves this is not theoretical: a caller saw a returnedMESSAGE:*id, while the node never reached GraphLog, Nodes, or WAL.A2A messages are continuity records. The write contract needs the same shape as
add_memory: deliberate validation failures before acceptance, then durable append before any derived graph/vector work can fail or stall.The Architectural Reality
ai/services/memory-core/MailboxService.mjs:495startsaddMessage()by requiring GraphService, resolving identity, validating the target, and generating theMESSAGE:*id.ai/services/memory-core/MailboxService.mjs:640writes the MESSAGE node directly throughGraphService.upsertNode(), thenai/services/memory-core/MailboxService.mjs:651and following lines create delivery-critical graph edges before returning.ai/services/memory-core/MemoryService.mjs:372throughai/services/memory-core/MemoryService.mjs:386documents the already-correct memory write order: validation gates, durable JSONL WAL append, asynchronous graph projection, and no embed on the request path.ai/services/memory-core/MemoryService.mjs:492awaitsappendWalMemory()before scheduling graph projection.ai/services/memory-core/helpers/memoryWalStore.mjs:103throughai/services/memory-core/helpers/memoryWalStore.mjs:119is the existing durable append precedent. This ticket does not require reusing that helper directly if message semantics need a separate store; it does require the same acceptance-order invariant.The Fix
Change the
add_messageacceptance contract so success means the canonical message intent is durably appended before acknowledgment:MESSAGE:*id once validation passes, and write that id plus the full canonical message payload to a durable append-only message WAL before returning success.status: sentmeans accepted durably for mailbox delivery; if immediate graph projection remains best-effort in this leaf, it must not be the durability gate.Contract Ledger Matrix
add_messageacknowledgment boundaryMemoryService.addMemory()WAL-first precedentMESSAGE:*id stabilityMailboxService.addMessage()id generationadd_messagerequest pathDecision Record impact
Aligned with the existing memory WAL durability substrate. No ADR change expected for this child.
Decision Record: Not needed.
Acceptance Criteria
add_messagehas explicit pre-ack validation boundaries; deliberate validation/routing/policy failures do not create accepted WAL records.MESSAGE:*id is stable across WAL append, replay/projection, and process restart.add_messagerequest path.Out of Scope
#10150unless implemented by a later drain child.Avoided Traps
Related
Parent: #13889
Related: #13890, #10150
Origin Session ID: db5b2ecf-db91-4b7d-9498-ccef00426a1c
Handoff Retrieval Hints:
A2A add_message WAL-first acceptance contract durable message id survives restart;MailboxService.addMessage GraphService upsertNode appendWalMemory precedent