LearnNewsExamplesServices
Frontmatter
id16965
titleMailbox index loss must not masquerade as an empty inbox
stateClosed
labels
bugaiperformanceagent-os
assigneesneo-gpt-emmy
createdAtAug 11, 2026, 2:14 PM
updatedAtAug 21, 2026, 6:13 PM
githubUrlhttps://github.com/neomjs/neo/issues/16965
authorneo-gpt-emmy
commentsCount1
parentIssue16677
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 21, 2026, 6:13 PM

Mailbox index loss must not masquerade as an empty inbox

Closed Backlog/active-chunk-15 bugaiperformanceagent-os
neo-gpt-emmy
neo-gpt-emmy commented on Aug 11, 2026, 2:14 PM

Context

PR #16962 replaces MailboxService.listMessages() cache-wide edge enumeration with the Native Edge Graph Store's existing source and target secondary indexes. That repair is correct at exact head e25f972812: Database.beforeSetEdges() constructs both indexes, the focused mailbox/Fleet slice passes 168/168, and all hosted checks are green.

The cross-family review identified a changed failure direction worth carrying separately rather than returning the performance repair for another cycle. An exact-object probe demonstrated it:

store shape                   raw matching edges   target-index result
unindexed Graph Store         1                    0
source+target indexed Store   1                    1

Store.getByIndex() returns [] when the index set, property map, or value set is absent. A missing configured index is therefore indistinguishable from an honestly empty lookup. Before #16962, the list path was slow but still walked the raw edge store; after #16962, loss of its index precondition can manufacture an empty mailbox.

This is not a blocker for #16962. Production constructs the required indexes today. It is a follow-up contract-hardening leaf under #16677.

The Problem

The indexed mailbox path depends on two facts that are currently positional rather than asserted:

  1. the edge Store was constructed with both source and target indexes; and
  2. callers can distinguish an unavailable index from a valid index with no matching value.

Today getByIndex() collapses both states to []. A future alternate Store construction, test double, or configuration regression can therefore turn a lookup failure into a plausible empty inbox. That is a more dangerous failure mode than the superseded full scan because callers cannot distinguish "there are no messages" from "the mailbox could not look."

One sibling remains on the old path: getBroadcastDeliveryEdges() still filters GraphService.db.edges.items and is reached by single-message authorization, mark-read, and archive paths. Those calls are not the quadratic hot path fixed by #16962, but they share the same delivery-edge projection and can now use the established source-index idiom. Keeping one scan-based and one indexed projection also leaves two failure semantics for the same graph fact.

The Architectural Reality

  • ai/graph/Store.mjs#getByIndex() owns secondary-index lookup. Its JSDoc says the property is assumed to exist, but its implementation silently returns [] when that assumption is false.
  • ai/graph/Database.mjs#beforeSetEdges() is the production construction authority and configures [{property:'source'}, {property:'target'}].
  • ai/services/memory-core/MailboxService.mjs#listMessages() at PR #16962 consumes both indexes for candidate discovery and per-message projection.
  • MailboxService.mjs#getBroadcastDeliveryEdges() still enumerates db.edges.items for single-message delivery-state consumers.
  • The targeted Agent OS structure maps place the contract in the existing ai/graph/Store.mjs / Database.mjs owners and the consumer assertion/projection in the existing MailboxService.mjs; no new file or service is warranted. The whole-ai/ structure-map command currently exceeds Node's maximum string length, while targeted ai/graph and ai/services/memory-core maps complete.

The Fix

  1. Give the Graph Store public lookup surface an explicit distinction between index unavailable and indexed value absent. The implementation may expose an assertion/query helper or fail loudly inside getByIndex() for an unknown property, but MailboxService must not reach into indexMaps directly.
  2. Assert the edge Store's source and target index contract before mailbox routing can return a result.
  3. Convert getBroadcastDeliveryEdges(messageId) to the existing source index and preserve its DELIVERED_TO filtering and first-match identity semantics at every current caller.
  4. Bind the changed failure mode with mutation-sensitive controls: a matching raw edge plus a removed/missing index must produce an explicit bounded failure, while a present index with no matching value must still return [].

Contract Ledger

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
Store.getByIndex(property, value) Store.indices / secondary-index maps Unknown or unavailable index is explicit; valid index with no value returns [] No raw-store scan JSDoc on lookup contract Indexed/unindexed exact-object pair
Database.edges construction Database.beforeSetEdges() source and target remain mandatory edge indexes Construction/config failure is loud Existing method JSDoc Production-construction control
MailboxService.listMessages() Graph Store lookup contract Cannot report an empty mailbox when either required index is unavailable Bounded tool error; never manufactured empty success Local Anchor and Echo Delete-index mutation with a matching message
getBroadcastDeliveryEdges() and its callers Message-source edge index Source-index projection preserves delivery authorization/read/archive semantics No cache-wide scan Helper JSDoc Poisoned full-store iterator plus behavior controls

Decision Record Impact

none. This hardens the existing Native Edge Graph secondary-index contract and MailboxService ownership; it does not introduce a new architecture or public mailbox schema.

Acceptance Criteria

  • A Graph Store with a matching raw edge but no requested secondary index cannot return a successful empty lookup for that property.
  • A configured secondary index with no matching value continues to return [].
  • Production Database.edges construction proves both source and target indexes are present.
  • MailboxService.listMessages() fails explicitly when either required edge index is unavailable; it never returns an empty mailbox for that state.
  • A mutation-sensitive mailbox control removes or disables each required index with a matching message present and observes the explicit failure.
  • getBroadcastDeliveryEdges() uses the source index rather than enumerating db.edges.items.
  • Authorization, mark-read, archive, canonical/legacy recipient identity, and first-equivalent-receipt precedence remain unchanged across the indexed helper conversion.
  • A full-store-iteration poison witness stays green across the converted single-message delivery paths.
  • Focused Graph Store and MailboxService suites pass on current dev.

Out of Scope

  • Adding a new secondary index or changing edge persistence.
  • Falling back to a full edge-store scan when an index is unavailable.
  • Changing mailbox request/response schemas, pagination, or read-state semantics.
  • Refactoring unrelated graph-index consumers.
  • A timing benchmark; structural mutation witnesses remain the evidence shape.

Avoided Traps

  • Fallback scan for safety. It hides the broken precondition and reintroduces the cost #16960 removed.
  • Mailbox reaches into indexMaps. That duplicates Store internals in a consumer and makes later index implementation changes unsafe.
  • Treat [] as one universal state. Genuine no-match and unavailable instrumentation need distinct outcomes.
  • Return #16962 for scope expansion. The shipped performance leaf is correct; this changed failure mode is a successor contract.
  • Split two one-file-adjacent observations into micro-tickets. The remaining delivery scan and index assertion share the same Store/Mailbox boundary and fit one PR.

Related

Parent: #16677

Predecessor: #16960

Source review: https://github.com/neomjs/neo/pull/16962#pullrequestreview-4905950248

Adjacent historical cache-coherence repair: #10257

Duplicate and Collision Sweep

  • Exact GitHub search for mailbox index silent empty getByIndex found no existing issue.
  • Knowledge Base ticket retrieval was degraded at synthesis but returned only unrelated candidates; direct repository history surfaced #10257, which repaired cross-process cache freshness rather than secondary-index availability.
  • The #16960 predecessor owns only the repeated list-projection scan and closes with PR #16962; reopening it would violate the resolved-ticket successor rule.
  • Live latest-open created-descending and recent all-state A2A claim sweeps immediately before creation found no equivalent ticket or competing lane claim.

Origin Session ID: 019fe5e8-b963-7e93-8762-c8e4af16bdec

Retrieval Hint: "MailboxService secondary index unavailable silent empty inbox getByIndex source target broadcast delivery"

tobiu referenced in commit b99334f - "feat(ai): make graph index availability explicit (#16965) (#17483)" on Aug 21, 2026, 6:13 PM
tobiu closed this issue on Aug 21, 2026, 6:13 PM