Context
Empirical anchor (this session, 2026-05-23): add_memory MCP tool repeatedly rejected with schema validation error when the mailbox preview pointed at a system-sent heartbeat message:
MCP error -32602: Structured content does not match the tool's output schema:
data/mailbox/latestPreview/from must be string,
data/mailbox/latestPreview must be null,
data/mailbox/latestPreview must match a schema in anyOf
Four observed occurrences this session, each blocked end-of-turn add_memory consolidation until the heartbeat message was manually marked-read first. Workaround discovered: mark_read on the null-from message clears the preview to null (which the schema accepts), then add_memory succeeds.
The Problem
ai/mcp/server/memory-core/openapi.yaml:1816-1818 defines latestPreview.from as:
from:
type: string
description: The sender's AgentIdentity node id
example: "@neo-gemini-3-1-pro"
No nullable: true. The schema rejects any from: null value.
But ai/services/memory-core/MemoryService.mjs:126-129 legitimately returns null for the from field when no SENT_BY edge exists on the message:
(
SELECT se.target FROM Edges se
WHERE se.source = messageId AND se.type = 'SENT_BY'
LIMIT 1
) AS "from"
For system-generated messages (heartbeats, idle-out nudges) there is no SENT_BY edge — the system is the sender, and that's structurally distinct from agent-sender messages. The current behavior is correct at the data layer (null = no agent-sender) but incorrect at the schema layer (rejects the data layer's legitimate null).
The Architectural Reality
| Surface |
Behavior |
Status |
MemoryService.mjs mailbox-delta query |
Returns from: null for system messages |
Correct |
list_messages MCP tool output |
Already accepts/returns from: null for system messages (empirically verified by list_messages returning heartbeat preview rows without error) |
Correct |
add_memory mailbox-preview block schema |
Rejects from: null because latestPreview.from is not nullable: true |
Bug |
list_messages and add_memory mailbox-preview blocks both surface the same data shape, but list_messages is permissive while add_memory is strict. Inconsistency.
The Fix
Single-line nullable: true addition to ai/mcp/server/memory-core/openapi.yaml:1816-1818:
from:
type: string
nullable: true
description: The sender's AgentIdentity node id; `null` for system-sent messages (heartbeats, idle-out nudges) that have no SENT_BY edge in the graph
example: "@neo-gemini-3-1-pro"
Updated description preserves the empirical reality for future schema readers.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
latestPreview.from field in add_memory mailbox-preview block |
This ticket + MemoryService.mjs:126-129 legitimately-null behavior |
Schema accepts from: null for system messages; description updated to name the empirical cause |
None (single-line nullability addition) |
Inline JSDoc / schema description |
Reproducer: send mailbox preview block from add_memory while mailbox contains a system-sent heartbeat — current fails, post-fix passes |
Decision Record Impact
none. Inline schema-shape correction matching the legitimately-permissive list_messages surface; no ADR consultation needed.
Acceptance Criteria
Out of Scope
- Filtering system messages from
latestPreview entirely (would hide information from agents; the current shape correctly surfaces "this is a system message with no agent sender").
- Adding a sentinel
"from": "@system" instead of null (loses the structural "no SENT_BY edge" signal; muddles data layer).
- Broader schema audit of other potentially-null fields (separate sweep if more drift surfaces emerge; this is the only one empirically observed this session).
Avoided Traps
| Trap |
Why rejected |
Coalesce null to "@system" in service code |
Muddles data layer; loses structural distinction; breaks the from: null → "no SENT_BY edge" graph-level invariant |
Filter system messages out of latestPreview |
Hides legitimate information from agents (they'd not see heartbeats in the preview); harms observability |
Add nullable only at latestPreview level (the object), not at from field |
Wouldn't address the actual rejection — the strictness is on the field, not the parent |
Related
- Empirical anchor: 4 observed
add_memory rejections this session in b08bd969-3a3b-49a1-9bfe-9941820e7fbd (current) + the earlier session segments
- Schema surface:
ai/mcp/server/memory-core/openapi.yaml:1805-1823
- Service surface (correct, no change needed):
ai/services/memory-core/MemoryService.mjs:126-129
- Consistency sibling:
list_messages MCP tool already accepts/returns from: null for system messages
Origin Session ID: b08bd969-3a3b-49a1-9bfe-9941820e7fbd
Pre-Flight reasoning per ticket-create-workflow.md §1d: Will attach to Project 12 per §4 (v13 Release board), per the mandate currently in PR #11818 (still pending @tobiu merge but already operator-direction).
Context
Empirical anchor (this session, 2026-05-23):
add_memoryMCP tool repeatedly rejected with schema validation error when the mailbox preview pointed at a system-sent heartbeat message:Four observed occurrences this session, each blocked end-of-turn
add_memoryconsolidation until the heartbeat message was manually marked-read first. Workaround discovered:mark_readon the null-from message clears the preview to null (which the schema accepts), thenadd_memorysucceeds.The Problem
ai/mcp/server/memory-core/openapi.yaml:1816-1818defineslatestPreview.fromas:from: type: string description: The sender's AgentIdentity node id example: "@neo-gemini-3-1-pro"No
nullable: true. The schema rejects anyfrom: nullvalue.But
ai/services/memory-core/MemoryService.mjs:126-129legitimately returnsnullfor thefromfield when no SENT_BY edge exists on the message:( SELECT se.target FROM Edges se WHERE se.source = messageId AND se.type = 'SENT_BY' LIMIT 1 ) AS "from"For system-generated messages (heartbeats, idle-out nudges) there is no SENT_BY edge — the system is the sender, and that's structurally distinct from agent-sender messages. The current behavior is correct at the data layer (null = no agent-sender) but incorrect at the schema layer (rejects the data layer's legitimate null).
The Architectural Reality
MemoryService.mjsmailbox-delta queryfrom: nullfor system messageslist_messagesMCP tool outputfrom: nullfor system messages (empirically verified bylist_messagesreturning heartbeat preview rows without error)add_memorymailbox-preview block schemafrom: nullbecauselatestPreview.fromis notnullable: truelist_messagesandadd_memorymailbox-preview blocks both surface the same data shape, butlist_messagesis permissive whileadd_memoryis strict. Inconsistency.The Fix
Single-line
nullable: trueaddition toai/mcp/server/memory-core/openapi.yaml:1816-1818:from: type: string nullable: true description: The sender's AgentIdentity node id; `null` for system-sent messages (heartbeats, idle-out nudges) that have no SENT_BY edge in the graph example: "@neo-gemini-3-1-pro"Updated description preserves the empirical reality for future schema readers.
Contract Ledger Matrix
latestPreview.fromfield inadd_memorymailbox-preview blockMemoryService.mjs:126-129legitimately-null behaviorfrom: nullfor system messages; description updated to name the empirical causeadd_memorywhile mailbox contains a system-sent heartbeat — current fails, post-fix passesDecision Record Impact
none. Inline schema-shape correction matching the legitimately-permissivelist_messagessurface; no ADR consultation needed.Acceptance Criteria
ai/mcp/server/memory-core/openapi.yaml:1816-1818latestPreview.fromcarriesnullable: true.add_memorysucceeds when the mailbox preview points at a system-sent heartbeat (no manual mark_read prerequisite).list_messagespermissive behavior unchanged.MemoryService.mjs:126-129already returns the correct shape; schema is the sole drift surface.Out of Scope
latestPreviewentirely (would hide information from agents; the current shape correctly surfaces "this is a system message with no agent sender")."from": "@system"instead ofnull(loses the structural "no SENT_BY edge" signal; muddles data layer).Avoided Traps
"@system"in service codefrom: null→ "no SENT_BY edge" graph-level invariantlatestPreviewnullableonly atlatestPreviewlevel (the object), not atfromfieldRelated
add_memoryrejections this session inb08bd969-3a3b-49a1-9bfe-9941820e7fbd(current) + the earlier session segmentsai/mcp/server/memory-core/openapi.yaml:1805-1823ai/services/memory-core/MemoryService.mjs:126-129list_messagesMCP tool already accepts/returnsfrom: nullfor system messagesOrigin Session ID:
b08bd969-3a3b-49a1-9bfe-9941820e7fbdPre-Flight reasoning per ticket-create-workflow.md §1d: Will attach to Project 12 per §4 (v13 Release board), per the mandate currently in PR #11818 (still pending @tobiu merge but already operator-direction).