Context
Session 20781e6b (2026-07-18): after a mailbox readAt re-inflation (unread count jumped 5 → 52 mid-session — working hypothesis: a view-scoped repair rebuilt DELIVERED_TO cohorts and reset readAt on the rebuilt edges; the mechanism is a repair-lane question, see Out of Scope), draining the re-inflated mailbox took 51 sequential mark_read MCP calls — one full round-trip per message, identical payloads, one ID different each time. Operator-named fix: bulk marking.
The Problem
mark_read accepts exactly one messageId. Every bulk-clear flow — post-repair re-inflation, wake-flood triage, sunset-backlog draining — pays N round-trips for what is semantically one operation. Mailbox hygiene is a per-turn protocol obligation (AGENTS.md mailbox check + the unread-count discipline in add_memory responses), so the honest path should be cheap. A 51-call burst is pure transport overhead.
The Architectural Reality
- MCP surface:
ai/mcp/server/memory-core/openapi.yaml — the mark_read operation, messageId: string (verify exact operationId/path at implementation).
- Service:
ai/services/memory-core/MailboxService.mjs — markRead writes readAt onto the message's DELIVERED_TO edge (broadcasts) or the MESSAGE node (DMs), appending the WAL record per message.
- The per-message mark-write is the correct unit (per-message WAL records are the durability model). The BATCH belongs at the boundary — one MCP call fanning out to N service writes — not as a WAL/schema redesign.
The Fix
Extend mark_read to accept messageId: string | string[]. Arrays are processed in order, returning a per-element result list ({messageId, readAt, status}); per-element failures (unknown ID, unauthorized) isolate — no all-or-nothing. One code path: normalize the param to an array inside the service method. No new MCP tool (surface economy per the tool-description budget).
Optional second row (implementer's call, keep-or-drop at PR time): a mark_all_read convenience scoped to the caller's unread mailbox — sugar over list + bulk-mark, only if the wiring stays trivial.
Contract Ledger
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
MCP mark_read (messageId param) |
ai/mcp/server/memory-core/openapi.yaml operation + MailboxService.markRead |
`string |
string[]`; array → per-element mark + per-element result list |
single-string behavior byte-identical (current contract) |
openapi description gains the array form (single line) |
(optional) MCP mark_all_read |
same surface |
marks all caller-visible unread; returns the count |
absent if dropped at implementation |
new single-line description |
witness: 2 unread → both read, count 2 |
Acceptance Criteria
Out of Scope
- The readAt re-inflation mechanism itself (does cohort-rebuild repair preserve
readAt? — a repair-lane question adjacent to #15322 / #15369; named here as context, not bundled).
- Bulk variants of other mailbox tools (
archive_message, delete_message) — same pattern later if friction repeats.
- Any WAL batching/schema redesign.
Related
#15322, #15369 (the cohort-repair work whose rebuilt edges motivated this bulk-clear flow)
#15415 (session-2's other friction ticket — one-review codification)
Decision Record impact: none — MCP tool signature extension on an existing surface; no ADR surface.
Live latest-open sweep: checked latest 20 open issues at 2026-07-18T05:19Z; no equivalent found. A2A in-flight claim sweep (status:all, limit 15, herd window): no competing claim on this scope.
Origin Session ID: 20781e6b-ae74-48a6-ad1a-cfaba98d6590
Retrieval Hint: query_raw_memories("mark_read bulk array unread re-inflation readAt repair cohort 51 calls")
Context
Session
20781e6b(2026-07-18): after a mailbox readAt re-inflation (unread count jumped 5 → 52 mid-session — working hypothesis: a view-scoped repair rebuiltDELIVERED_TOcohorts and resetreadAton the rebuilt edges; the mechanism is a repair-lane question, see Out of Scope), draining the re-inflated mailbox took 51 sequentialmark_readMCP calls — one full round-trip per message, identical payloads, one ID different each time. Operator-named fix: bulk marking.The Problem
mark_readaccepts exactly onemessageId. Every bulk-clear flow — post-repair re-inflation, wake-flood triage, sunset-backlog draining — pays N round-trips for what is semantically one operation. Mailbox hygiene is a per-turn protocol obligation (AGENTS.md mailbox check + the unread-count discipline inadd_memoryresponses), so the honest path should be cheap. A 51-call burst is pure transport overhead.The Architectural Reality
ai/mcp/server/memory-core/openapi.yaml— themark_readoperation,messageId: string(verify exact operationId/path at implementation).ai/services/memory-core/MailboxService.mjs—markReadwritesreadAtonto the message'sDELIVERED_TOedge (broadcasts) or the MESSAGE node (DMs), appending the WAL record per message.The Fix
Extend
mark_readto acceptmessageId: string | string[]. Arrays are processed in order, returning a per-element result list ({messageId, readAt, status}); per-element failures (unknown ID, unauthorized) isolate — no all-or-nothing. One code path: normalize the param to an array inside the service method. No new MCP tool (surface economy per the tool-description budget).Optional second row (implementer's call, keep-or-drop at PR time): a
mark_all_readconvenience scoped to the caller's unread mailbox — sugar over list + bulk-mark, only if the wiring stays trivial.Contract Ledger
mark_read(messageIdparam)ai/mcp/server/memory-core/openapi.yamloperation +MailboxService.markReadmark_all_readAcceptance Criteria
mark_readaccepts an array of messageIds; every element is marked, per-element results returned.openapi.yamldescription updated within the tool-description budget (usage shape, no narrative).mark_all_readeither shipped (trivial wiring) or explicitly dropped with rationale in the PR body.Out of Scope
readAt? — a repair-lane question adjacent to#15322/#15369; named here as context, not bundled).archive_message,delete_message) — same pattern later if friction repeats.Related
#15322,#15369(the cohort-repair work whose rebuilt edges motivated this bulk-clear flow)#15415(session-2's other friction ticket — one-review codification)Decision Record impact: none — MCP tool signature extension on an existing surface; no ADR surface.
Live latest-open sweep: checked latest 20 open issues at 2026-07-18T05:19Z; no equivalent found. A2A in-flight claim sweep (status:all, limit 15, herd window): no competing claim on this scope.
Origin Session ID: 20781e6b-ae74-48a6-ad1a-cfaba98d6590
Retrieval Hint:
query_raw_memories("mark_read bulk array unread re-inflation readAt repair cohort 51 calls")