LearnNewsExamplesServices
Frontmatter
id15369
titleMailboxService read gate is blind to lost DELIVERED_TO cohorts: list/count never self-heal a cohort-damaged broadcast (the read-path half of #15322)
stateClosed
labels
bugaitesting
assigneesneo-opus-grace
createdAtJul 17, 2026, 5:52 PM
updatedAtJul 18, 2026, 7:25 AM
githubUrlhttps://github.com/neomjs/neo/issues/15369
authorneo-opus-grace
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJul 18, 2026, 7:21 AM

MailboxService read gate is blind to lost DELIVERED_TO cohorts: list/count never self-heal a cohort-damaged broadcast (the read-path half of #15322)

Closed Backlog/active-chunk-7 bugaitesting
neo-opus-grace
neo-opus-grace commented on Jul 17, 2026, 5:52 PM

Premise

Scoped out of #15322 (mark-path) at PR #15357 review RA-3, with @neo-opus-ada's root-cause analysis. This is the read-path half: listMessages / countMessages do not repair a broadcast whose entire DELIVERED_TO cohort was lost, because the gate that triggers view-scoped repair cannot see the loss.

The gap (live-verified on dev)

ai/services/memory-core/MailboxService.mjs:850hasMailboxGraphProjectionGap():

return (row?.messageCount ?? 0) < projectedCount ||
    (row?.sentByCount   ?? 0) < projectedCount ||
    (row?.sentToCount   ?? 0) < projectedCount;

It compares messageCount, sentByCount, sentToCount against the WAL projectedCount. There is no DELIVERED_TO term. A broadcast that lost its whole per-recipient delivery cohort still has its MESSAGE node, its SENT_BY, and its SENT_TO → AGENT:* — so all three counts match projectedCount, the gate returns false, and:

MailboxService.mjs:1658if (!idFilter && !await hasMailboxGraphProjectionGap()) return summary; — the view-scoped repair returns early at scanned: 0. Every listMessages / countMessages over that view leaves the cohort unrepaired.

Consequence: the pure-read case — a recipient lists a cohort-damaged broadcast with no prior mark — never self-heals. (#15322's mark-path fix covers the case where someone marks: repairMessageGraphIntegrity({ids}) rebuilds the whole cohort. It does not cover a read with no mark, because reads route through this blind gate.)

The fix — and the trap in the obvious version

Add a DELIVERED_TO-aware term. But not deliveredToCount < projectedCount: projectedCount counts all messages while DISTINCT DELIVERED_TO source counts broadcasts only, so a single DM makes deliveredToCount < projectedCount permanently true → a full WAL scan on every list. (Flagged derived-not-measured by @neo-opus-ada; do not ship it.)

The precise predicate is a broadcast with zero delivery rows:

SELECT COUNT(*) FROM Edges b
 WHERE b.source LIKE 'MESSAGE:%' AND b.type = 'SENT_TO' AND b.target = 'AGENT:*'
   AND NOT EXISTS (SELECT 1 FROM Edges d WHERE d.source = b.source AND d.type = 'DELIVERED_TO')

Scope

In: the total cohort-loss (silent) case — a broadcast with zero delivery rows makes hasMailboxGraphProjectionGap true, so list/count repair it.

Out (surface, don't bundle): partial cohort loss (some recipients' edges present, one missing). That currently throws Unauthorized on the missing recipient — loud, not silent — and is a different disposition. @neo-opus-ada scoped it out; keep it out unless a witness shows it's silent too.

Acceptance Criteria

  • hasMailboxGraphProjectionGap returns true when a broadcast has SENT_TO → AGENT:* but zero DELIVERED_TO rows.
  • The predicate does not false-positive on direct messages (a DB with only DMs and intact broadcasts returns no gap — no per-list WAL scan).
  • Red-proven: a recipient LISTS a total-cohort-damaged broadcast with no prior mark → currently the message is missing/wrong; after the fix it is repaired and correctly present. Red against the unfixed gate, green after.
  • The setup does not heal itself before the probe (storage-level damage, no preceding read — the #15322 instrument discipline).
  • CI is the oracle (local MailboxService specs die on the Neo.ai.Config collision, #15364).
  • No regression to markRead (the #15322 mark-path fix) or DM read/count.

Avoided Traps

  • Do not use deliveredToCount < projectedCount — false-positives on any DM; forces a full WAL scan every list.
  • Do not bundle partial-cohort loss — different symptom (loud Unauthorized), different disposition.
  • Do not assert green from a local run — this spec class can't execute locally (#15364); a red-proof that was never observed red is not a proof (CI is the oracle).

Retrieval Hint: "MailboxService hasMailboxGraphProjectionGap DELIVERED_TO read gate broadcast list countMessages self-heal cohort loss zero delivery rows 15322 read-path half"