Context
#16808 captured two different add_memory timeout classes on the same contended plane: timeout-but-STORED after the WAL accepted the turn, and timeout-but-LOST before durable acceptance could be verified. PR #16812 correctly moved the presence and visibility disclosures toward bounded waits, but its first review falsified the claimed end-to-end response bound: buildMailboxDelta() is a synchronous better-sqlite3 CTE executed after WAL acceptance and before the response, so a slow first call can still hold an accepted save beyond the client transport deadline.
The proposed retrospective guard learns only after that blocking call returns. An exact helper probe also showed its 60-second cooldown can disable itself: a 61-second call arms skipUntil from call start, so the immediate next call already runs again.
This ticket is the one-PR post-WAL response slice. #16808 remains open for the distinct pre-WAL timeout-but-LOST class and its plane-side instrumentation.
Structure-map gate: npm run --silent ai:structure-map -- --files --loc was attempted on 2026-08-10 and failed with Cannot create a string longer than 0x1fffffe8 characters. Placement was therefore verified directly against the existing owners: MemoryService.addMemory, helpers/withTimeout.mjs, the MCP MemoryResponse schema, and MemoryService.WriteAhead.spec.mjs. This repair introduces no new module.
The Problem
Once appendWalMemory() returns, the turn is durable and immediately available to recency reads. The remaining envelope work is derived disclosure. Today it still contains three response dependencies:
buildMailboxDelta() — synchronous SQLite work that cannot be interrupted by Promise.race or withTimeout;
TurnPresenceService.recordTurnPresence() — an asynchronous graph terminal;
describeWriteVisibility() — an asynchronous pending-WAL visibility read.
Bounding only the latter two leaves the first accepted request unbounded. Calling the synchronous work “self-degrading” does not change that request’s latency contract; it only sheds later calls, and its current start-anchored cooldown can fail to shed even those.
The Architectural Reality
MemoryService.addMemory() owns the accepted-write response. Its mandatory contract ends at the WAL append; graph projection, mailbox awareness, presence, and semantic-visibility reporting are derived facts. The mailbox delta is a convenience field, not durability proof. When a response nicety cannot be bounded on the Node event loop, honest absence is safer than executing it on the acceptance path.
The existing withTimeout.mjs helper is the source-owned bound for asynchronous dependencies. MemoryResponse in ai/mcp/server/memory-core/openapi.yaml and the addMemory() JSDoc are the consumed contract authorities. The production write-ahead spec is the proof boundary; an isolated helper test cannot establish the method’s elapsed response bound.
The Fix
- Remove
buildMailboxDelta() from the post-WAL response path. Return mailbox: null and an explicit stage-timing/disposition field stating that mailbox enrichment was omitted from the accepted-write latency contract. Delete the now-unused retrospective helper and its isolated spec.
- Retain a bounded presence terminal and bounded visibility read using the existing
withTimeout authority. Late presence settlement remains observed/handled; visibility timeout returns the existing honest per-axis degraded envelope.
- Measure WAL and each post-WAL stage without claiming the WAL itself is bounded by this ticket.
- Declare
stageTimings, presenceTerminal, and mailbox omission semantics in addMemory() JSDoc and the MCP MemoryResponse schema.
- Add a production-bound
addMemory() control with hung presence and visibility dependencies that proves the method returns the exact degraded envelope within one declared post-WAL budget. The control must also convict any reintroduction of the synchronous mailbox query on that path.
Contract Ledger
| Target surface |
Source of authority |
Proposed behavior |
Fallback |
Docs |
Evidence |
add_memory.mailbox |
MemoryService.addMemory() |
no synchronous mailbox query after WAL acceptance |
null plus explicit omitted reason |
method JSDoc + OpenAPI |
production-bound elapsed-response control |
add_memory.stageTimings |
MemoryService.addMemory() |
numeric WAL/visibility/presence measurements and closed dispositions |
null only where a stage was deliberately not run |
method JSDoc + OpenAPI |
exact envelope assertions |
| presence terminal |
TurnPresenceService.recordTurnPresence() through withTimeout |
completed, deferred, or failed without rejecting the accepted save |
late promise remains handled |
method JSDoc + OpenAPI |
hung/failing/complete controls |
| write visibility |
describeWriteVisibility() through withTimeout |
measured when timely |
honest embed-state-unavailable, while recency remains true by construction |
method JSDoc + OpenAPI |
hung/failing/complete controls |
Decision Record impact
None. This tightens an existing WAL-first response contract and reuses the existing timeout authority; it introduces no new service boundary, config surface, or architectural primitive.
Acceptance Criteria
Implementation Receipt
- Delivered by PR #16812 at
16d7f048.
- Focused production/contract slice: 73/73 passed after rebasing onto current
dev.
- Restoring the synchronous mailbox producer made the exact production-bound control red (
mailboxQueryAttempts: 1); the repaired head returned green.
- OpenAPI service parity: OK, zero consumed-but-undeclared parameters.
Out of Scope
- The pre-WAL timeout-but-LOST class and payload/transport/fsync instrumentation; #16808 retains that incident owner.
- Redesigning or indexing the mailbox CTE, or adding a cached mailbox-delta producer. That can restore enrichment later without returning synchronous work to this path.
- Embed-drain scheduling and work-volume bounds owned by #16780.
- A deployment-configurable response budget; this is a consumer contract, not an operator tuning leaf.
Avoided Traps
- A retrospective timer is not a deadline for the request that paid the synchronous work.
- Green helper tests do not prove the consumed
addMemory() boundary.
- A
completed result does not prove bounded delivery; elapsed response and exact fallback shape must be measured together.
- Do not call timeout-but-STORED and timeout-but-LOST one mechanism merely because both surface as
-32001.
Related
- #16808 — parent symptom record; retains timeout-but-LOST
- #16780 — adjacent embed-work and saturation observability
- PR #16812 — implementation branch to retarget to this ticket
- PR review
PRR_kwDODSospM8AAAABI5WzEQ — exact-head falsifier and required action
Origin Session ID: d813fe26-4096-4c43-81fc-be4241b270cb
Retrieval Hint: "add_memory post-WAL synchronous mailbox CTE timeout-but-STORED stageTimings"
Context
#16808 captured two different
add_memorytimeout classes on the same contended plane: timeout-but-STORED after the WAL accepted the turn, and timeout-but-LOST before durable acceptance could be verified. PR #16812 correctly moved the presence and visibility disclosures toward bounded waits, but its first review falsified the claimed end-to-end response bound:buildMailboxDelta()is a synchronous better-sqlite3 CTE executed after WAL acceptance and before the response, so a slow first call can still hold an accepted save beyond the client transport deadline.The proposed retrospective guard learns only after that blocking call returns. An exact helper probe also showed its 60-second cooldown can disable itself: a 61-second call arms
skipUntilfrom call start, so the immediate next call already runs again.This ticket is the one-PR post-WAL response slice. #16808 remains open for the distinct pre-WAL timeout-but-LOST class and its plane-side instrumentation.
Structure-map gate:
npm run --silent ai:structure-map -- --files --locwas attempted on 2026-08-10 and failed withCannot create a string longer than 0x1fffffe8 characters. Placement was therefore verified directly against the existing owners:MemoryService.addMemory,helpers/withTimeout.mjs, the MCPMemoryResponseschema, andMemoryService.WriteAhead.spec.mjs. This repair introduces no new module.The Problem
Once
appendWalMemory()returns, the turn is durable and immediately available to recency reads. The remaining envelope work is derived disclosure. Today it still contains three response dependencies:buildMailboxDelta()— synchronous SQLite work that cannot be interrupted byPromise.raceorwithTimeout;TurnPresenceService.recordTurnPresence()— an asynchronous graph terminal;describeWriteVisibility()— an asynchronous pending-WAL visibility read.Bounding only the latter two leaves the first accepted request unbounded. Calling the synchronous work “self-degrading” does not change that request’s latency contract; it only sheds later calls, and its current start-anchored cooldown can fail to shed even those.
The Architectural Reality
MemoryService.addMemory()owns the accepted-write response. Its mandatory contract ends at the WAL append; graph projection, mailbox awareness, presence, and semantic-visibility reporting are derived facts. The mailbox delta is a convenience field, not durability proof. When a response nicety cannot be bounded on the Node event loop, honest absence is safer than executing it on the acceptance path.The existing
withTimeout.mjshelper is the source-owned bound for asynchronous dependencies.MemoryResponseinai/mcp/server/memory-core/openapi.yamland theaddMemory()JSDoc are the consumed contract authorities. The production write-ahead spec is the proof boundary; an isolated helper test cannot establish the method’s elapsed response bound.The Fix
buildMailboxDelta()from the post-WAL response path. Returnmailbox: nulland an explicit stage-timing/disposition field stating that mailbox enrichment was omitted from the accepted-write latency contract. Delete the now-unused retrospective helper and its isolated spec.withTimeoutauthority. Late presence settlement remains observed/handled; visibility timeout returns the existing honest per-axis degraded envelope.stageTimings,presenceTerminal, and mailbox omission semantics inaddMemory()JSDoc and the MCPMemoryResponseschema.addMemory()control with hung presence and visibility dependencies that proves the method returns the exact degraded envelope within one declared post-WAL budget. The control must also convict any reintroduction of the synchronous mailbox query on that path.Contract Ledger
add_memory.mailboxMemoryService.addMemory()nullplus explicit omitted reasonadd_memory.stageTimingsMemoryService.addMemory()nullonly where a stage was deliberately not runTurnPresenceService.recordTurnPresence()throughwithTimeoutdescribeWriteVisibility()throughwithTimeoutembed-state-unavailable, while recency remains true by constructionDecision Record impact
None. This tightens an existing WAL-first response contract and reuses the existing timeout authority; it introduces no new service boundary, config surface, or architectural primitive.
Acceptance Criteria
add_memoryresponse.mailbox: nullwith a machine-readable omission reason; absence cannot masquerade as a measured empty inbox.stageTimingsand terminal/fallback enums are declared inaddMemory()JSDoc andMemoryResponseOpenAPI.addMemory()test proves the elapsed bound and exact degraded envelope; restoringbuildMailboxDelta()to the path makes it red.Implementation Receipt
16d7f048.dev.mailboxQueryAttempts: 1); the repaired head returned green.Out of Scope
Avoided Traps
addMemory()boundary.completedresult does not prove bounded delivery; elapsed response and exact fallback shape must be measured together.-32001.Related
PRR_kwDODSospM8AAAABI5WzEQ— exact-head falsifier and required actionOrigin Session ID: d813fe26-4096-4c43-81fc-be4241b270cb
Retrieval Hint: "add_memory post-WAL synchronous mailbox CTE timeout-but-STORED stageTimings"