LearnNewsExamplesServices
Frontmatter
id16767
titleMailbox list repeats full repair for a non-converging gap
stateClosed
labels
bugairegressionperformanceagent-os
assigneesneo-gpt-emmy
createdAtAug 9, 2026, 3:43 AM
updatedAtAug 9, 2026, 12:22 PM
githubUrlhttps://github.com/neomjs/neo/issues/16767
authorneo-gpt-emmy
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 9, 2026, 12:22 PM

Mailbox list repeats full repair for a non-converging gap

Closed Backlog/active-chunk-14 bugairegressionperformanceagent-os
neo-gpt-emmy
neo-gpt-emmy commented on Aug 9, 2026, 3:43 AM

Context

After the canonical Memory Core plane was rebuilt at exact dev revision 92c0a49fdaa5b1ced9b261e138164eee648f643e, the multi-minute wedge class disappeared but list_messages retained a clean-plane latency floor.

Same-plane receipts:

  • healthcheck: 65 ms;
  • get_message on a related-ticket-bearing message: 10 ms;
  • list_messages filtered to an identity with zero matching rows, limit: 1: 6,806 ms;
  • the next list_messages returning five rows: 7,144 ms.

The empty-result control removes response size, sorting the returned page, and pull-request-state enrichment as explanations for the floor.

To assert which layer owns the remaining cost, I traced and measured the exact deployed path:

  • the compact message-WAL marker read took 10 ms;
  • the global SQLite gap-count query took 105 ms;
  • reading all 3,185 accepted message-WAL records took 50 ms;
  • the same deployed store contained 186,643 nodes, 91,732 edges, 3,184 projected message ids, 2,848 distinct SENT_BY sources, and 85 broadcasts with SENT_TO -> AGENT:* but zero DELIVERED_TO rows.

The global repair predicate is therefore truthy before every list, while the bounded view repair does not make that global predicate converge.

This is the measured production form of the follow-up already identified after closed #15369: a legitimate zero-recipient broadcast is byte-indistinguishable from total delivery-cohort loss, so the then-new zero-cohort predicate can remain true and force a WAL scan on every list.

The Problem

MailboxService.listMessages() pays a global integrity-repair tax before it can answer any caller-scoped mailbox query.

A single non-converging graph/WAL discrepancy anywhere in the plane keeps hasMailboxGraphProjectionGap() true. Every list then:

  1. reads the accepted message WAL;
  2. scans until 250 records matching the requested mailbox view have been inspected;
  3. hydrates each message vicinity;
  4. runs repeated in-memory edge searches and storage fallbacks;
  5. only afterwards performs the actual mailbox list traversal.

The repair call is bounded, but it is not progress-bearing. The same caller-visible read can repeatedly inspect the same healthy or non-repairable prefix while the global gate remains open for unrelated historical records. Increasing the SDK timeout would only hide this work amplification.

This floor is distinct from the live server-wide wedge tracked by #16677. It contaminates incident timing and spends several seconds on every ordinary mailbox poll, but it does not explain the historical cross-tool wait cohorts by itself.

The Architectural Reality

The owner is ai/services/memory-core/MailboxService.mjs:

  • lines 970-1000: hasMailboxGraphProjectionGap() reduces several global graph/WAL comparisons to one Boolean;
  • lines 1997-2044: repairMessageGraphIntegrity() opens the full accepted WAL when that Boolean is true and stops after a view-scoped limit;
  • lines 2069-2095: every listMessages() call invokes that repair before listing;
  • lines 1009-1052 and 627-655: each inspected record hydrates a graph vicinity and checks delivery edges;
  • lines 2119-2250: the list path then traverses the graph again to build the caller result.

get_message is message-id scoped and does not pay the global list repair unless that exact cached projection is damaged. The observed 10 ms versus 6,806 ms control matches that source boundary.

No ChromaDB or embedding call exists in the list_messages path. A slow list_messages row therefore cannot be used as evidence that an embedding-provider wait released the call.

The scoped Agent OS structure map confirms ai/services/memory-core/ as the owning folder; MailboxService.mjs owns the read/repair contract and helpers/messageWalStore.mjs owns accepted-record and projection-marker reads.

The Fix

Replace the non-progress-bearing global-Boolean/read-hot-path composition with a targetable, convergent repair contract:

  1. Classify graph/WAL discrepancies into exact candidate message ids and reasons, rather than only gap: true.
  2. Distinguish a genuinely damaged positive delivery cohort from a legitimate zero-audience or legacy/unknown broadcast. The accepted WAL already carries the send-time audience; an immutable intended-cohort fact may be projected for new records, while historical records need an explicit compatibility disposition.
  3. Repair exact candidate ids and record progress/residual disposition. Do not rescan the same healthy/non-repairable 250-record prefix on every list.
  4. Keep caller-scoped listMessages() bounded. If global reconciliation requires a sweep, schedule/own it outside the ordinary list hot path or consume a bounded candidate index whose work advances.
  5. Preserve #15369's real self-heal: deleting every DELIVERED_TO edge from a broadcast with a known positive audience must still be detected and repaired.
  6. Preserve the current list_messages request/response schema and authorization boundary.

Contract Ledger

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
Graph/WAL gap classification accepted message WAL routing + persisted MESSAGE/SENT_BY/SENT_TO/DELIVERED_TO rows Return exact candidate ids, reason, and intended-cohort disposition; no unscoped Boolean that stays permanently true Legacy/unknown rows are surfaced for bounded reconciliation, never assumed damaged on every list method JSDoc positive-damage, zero-audience, legacy/unknown matrix
repairMessageGraphIntegrity candidate classifier + immutable accepted record Repair exact ids and advance a cursor/state; repeated healthy reads do not re-inspect the same prefix residual failures remain observable and retryable without blocking all mailbox reads method JSDoc call-count/progress witnesses
listMessages hot path caller identity, box, filters, pagination Bounded caller-scoped read; no full-WAL parse or 250-record repair when no target-relevant candidate exists explicit bounded failure if caller-relevant integrity cannot be established existing MCP docs unchanged empty-result and populated-result latency/operation-count controls
Intended broadcast cohort send-time audience snapshot in message WAL Preserve enough immutable intent to distinguish audience-size zero from lost positive cohort historical record derives from WAL or receives an explicit unknown disposition Anchor-and-Echo JSDoc zero, one, multi-recipient, and historical fixtures
Public MCP contract openapi.yaml list_messages operation Request and response remain shape-compatible none no public doc delta unless behavior note is warranted schema smoke + existing mailbox suites

Decision Record impact

none — this enforces the existing durable mailbox and per-recipient delivery-cohort contract. It does not change service ownership, transport, authorization, or public MCP shape.

Acceptance Criteria

  • A red-first fixture proves that a legitimate zero-audience broadcast does not keep the global repair gate open or trigger a full WAL-backed repair on repeated listMessages() calls.
  • A historical/legacy broadcast without sufficient intended-cohort evidence receives an explicit compatibility disposition and does not force the same non-progressing scan on every list.
  • A known-positive broadcast whose full DELIVERED_TO cohort is deleted still triggers exact-id repair and reappears correctly for every intended recipient.
  • A global discrepancy unrelated to the bound caller does not make an empty-result caller-scoped list inspect 250 WAL records.
  • Repeated list calls either close candidate work or advance a measurable cursor; a fixed healthy/non-repairable prefix cannot be rescanned indefinitely.
  • Deterministic instrumentation proves the compact marker/count checks remain bounded and the expensive WAL/edge traversal is absent from the healthy list path.
  • Production-shaped before/after receipts include p50, p95, and max for empty and populated lists on the same store; no timeout is raised.
  • get_message, mark_read, archive/retraction, broadcast authorization, and the list_messages OpenAPI schema remain unchanged.
  • The canonical Neo unit command targets the affected mailbox specs explicitly; no default npx playwright test invocation is used.

Out of Scope

  • The server-wide live-process wedge and shared wait cohorts in #16677.
  • Read-state loss / stale-wake reconciliation in #16541.
  • Raising MCP or healthcheck timeouts.
  • Removing WAL-backed repair or weakening genuine total-cohort-loss recovery.
  • ChromaDB, embedding-provider, or model-server tuning.
  • A new MCP diagnostic tool.

Avoided Traps

  • Every zero cohort is corruption. False: a single-resident deployment can broadcast to an empty audience legitimately.
  • Remove the zero-cohort term. That reintroduces the silent total-cohort-loss defect fixed by #15369.
  • Make the global scan larger. More repeated work does not create convergence.
  • Blame PR-state enrichment. The zero-result control still took 6,806 ms.
  • Blame embeddings. The exact list call graph contains no Chroma/model operation.
  • Treat a faster rebuild as repair. Process replacement cleared the wedge class, not this list-path cost.

Related

Successor to #15369

Related: #16677

Related: #16541

Related: #14797

Duplicate and Collision Sweep

  • Live latest-open sweep: checked the latest 20 open GitHub issues created-descending immediately before filing; no equivalent ticket existed.
  • Recent A2A sweep: checked the latest 30 all-state messages; no overlapping lane claim or intent existed.
  • Semantic KB sweep returned adjacent broadcast/read-state/WAL tickets but no ticket owning the non-converging list repair floor.
  • Exact repository/GitHub searches for repairMessageGraphIntegrity, MESSAGE_GRAPH_REPAIR_LIMIT, zero-recipient broadcasts, and full-WAL-per-list behavior found the closed predecessors above, not an open successor.

Origin Session ID: b93c021e-d387-4c4f-8ae5-4d7d2d007303

Retrieval Hint: "Memory Core list_messages empty result 6806ms non-converging global graph gap repair 250 WAL records zero delivery cohort"

tobiu referenced in commit ed5b8bb - "fix(memory-core): converge mailbox repair candidates (#16767) (#16773) on Aug 9, 2026, 12:22 PM
tobiu closed this issue on Aug 9, 2026, 12:22 PM