LearnNewsExamplesServices
Frontmatter
id13891
titleMake add_message WAL-first before ack
stateClosed
labels
bugaiarchitecturemodel-experience
assigneesneo-gpt
createdAtJun 23, 2026, 1:10 AM
updatedAtJun 23, 2026, 2:36 AM
githubUrlhttps://github.com/neomjs/neo/issues/13891
authorneo-gpt
commentsCount0
parentIssue13889
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJun 23, 2026, 2:36 AM

Make add_message WAL-first before ack

Closed v13.1.0/archive-v13-1-0-chunk-5 bugaiarchitecturemodel-experience
neo-gpt
neo-gpt commented on Jun 23, 2026, 1:10 AM

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

  • add_message has explicit pre-ack validation boundaries; deliberate validation/routing/policy failures do not create accepted WAL records.
  • After validation passes, the canonical message payload is durably appended before the tool returns success.
  • The returned MESSAGE:* id is stable across WAL append, replay/projection, and process restart.
  • Graph projection failure after WAL append does not turn an accepted message into an acknowledged-but-lost message.
  • No synchronous chat/embed/model call exists on the add_message request path.
  • The PR declares whether pending-WAL mailbox read visibility is implemented here or deferred to the replay/drain child.
  • Tests cover the restart/durability invariant at the highest practical level: at minimum L2 unit proof plus an L3 smoke or clearly declared residual if process restart is outside the test harness.

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

tobiu referenced in commit 26577ae - "fix(memory-core): make add_message WAL-first (#13891) (#13898)" on Jun 23, 2026, 2:36 AM
tobiu closed this issue on Jun 23, 2026, 2:36 AM