---
id: 11819
title: Allow null `from` in mailbox latestPreview schema for system messages
state: CLOSED
labels:
  - bug
  - ai
  - model-experience
assignees:
  - neo-opus-ada
createdAt: '2026-05-23T04:40:49Z'
updatedAt: '2026-06-07T17:14:38Z'
githubUrl: 'https://github.com/neomjs/neo/issues/11819'
author: neo-opus-ada
commentsCount: 0
parentIssue: null
subIssues: []
subIssuesCompleted: 0
subIssuesTotal: 0
blockedBy: []
blocking: []
closedAt: '2026-05-23T08:19:24Z'
---
# Allow null `from` in mailbox latestPreview schema for system messages

## 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:

```yaml
from:
  type: string
  description: The sender's AgentIdentity node id
  example: "@neo-gemini-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:

```sql
(
    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`:

```yaml
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-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

- [ ] **AC1**: `ai/mcp/server/memory-core/openapi.yaml:1816-1818` `latestPreview.from` carries `nullable: true`.
- [ ] **AC2**: Description updated to document the system-message null case.
- [ ] **AC3**: `add_memory` succeeds when the mailbox preview points at a system-sent heartbeat (no manual mark_read prerequisite).
- [ ] **AC4**: `list_messages` permissive behavior unchanged.
- [ ] **AC5**: No service-side code change required — `MemoryService.mjs:126-129` already returns the correct shape; schema is the sole drift surface.

## 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).

## Timeline

- 2026-05-23T04:40:50Z @neo-opus-ada added the `bug` label
- 2026-05-23T04:40:50Z @neo-opus-ada assigned to @neo-opus-ada
- 2026-05-23T04:40:50Z @neo-opus-ada added the `ai` label
- 2026-05-23T04:40:51Z @neo-opus-ada added the `model-experience` label
- 2026-05-23T04:42:56Z @neo-opus-ada cross-referenced by PR #11820
- 2026-05-23T08:19:25Z @tobiu referenced in commit `3a44d5f` - "fix(mcp): allow null from in mailbox latestPreview schema for system messages (#11819) (#11820)

OpenAPI schema rejected legitimate `from: null` in the latestPreview block of
add_memory responses when the mailbox pointed at a system-sent message
(heartbeats, idle-out nudges). MemoryService.mjs:126-129 correctly returns
null when no SENT_BY edge exists on the message (system messages have no
agent sender), but the strict schema rejected this at the MCP boundary —
blocking add_memory end-of-turn consolidation until the heartbeat was
manually marked-read first.

Empirical anchor: 4 observed `MCP error -32602` rejections this session.
Workaround was mark_read on the null-from message, then retry add_memory.

Fix: add `nullable: true` to latestPreview.from in
ai/mcp/server/memory-core/openapi.yaml + update description to document the
system-message null case.

Matches the permissive shape `list_messages` already uses (heartbeat preview
rows return without error). No service-side code change needed —
MemoryService is already shape-correct; schema was the sole drift surface.

Co-authored-by: tobiu <tobiasuhlig78@gmail.com>"
- 2026-05-23T08:19:25Z @tobiu closed this issue
- 2026-05-23T10:48:52Z @neo-opus-ada cross-referenced by #11829

