LearnNewsExamplesServices
Frontmatter
id13091
titleMailbox unread counts include archived messages
stateClosed
labels
bugaiarchitecturemodel-experience
assigneesneo-gpt
createdAtJun 13, 2026, 1:17 PM
updatedAtJun 13, 2026, 4:58 PM
githubUrlhttps://github.com/neomjs/neo/issues/13091
authorneo-gpt
commentsCount0
parentIssue13012
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 13, 2026, 4:58 PM

Mailbox unread counts include archived messages

Closed v13.1.0/archive-v13-1-0-chunk-2 bugaiarchitecturemodel-experience
neo-gpt
neo-gpt commented on Jun 13, 2026, 1:17 PM

Context

During the 2026-06-13 wake-drain cycle for PR #13090, the normal mailbox drain check returned clear while the final memory-save hook still reported unread work:

  • list_messages({box: 'inbox', status: 'unread'}) returned [].
  • add_memory returned mailbox.unreadCount = 28 with latest preview MESSAGE:c953c039-a23e-47c1-9497-a05c609419c0.
  • list_messages({box: 'inbox', status: 'unread', includeArchived: true}) surfaced 28 archived-but-unread June 12 messages.
  • After marking those archived messages read, both default unread and include-archived unread views returned [], and the next add_memory response returned mailbox.unreadCount = 0.

This is a live wake-drain correctness bug: one substrate says the mailbox is clear, another says it is not. The default-visible mailbox was empty, but archived unread messages still contaminated the count surface that agents see at turn end.

Release classification: boardless for v13; belongs to Agent Harness Project 13 because wake-drain correctness gates autonomous harness operation.

The Problem

Archive semantics deliberately hide messages from the default list surface without deleting them. That means archive != unread work. If archived unread messages still count as unread in mailbox-delta or preview surfaces, agents receive contradictory signals:

  • default list_messages(status: 'unread') says no actionable unread work exists;
  • add_memory.mailbox.unreadCount says unread work still exists;
  • wake-drain automation or humans may treat the count as proof that wakes remain unprocessed.

This directly matches the PR #11535 out-of-scope warning: count-style mailbox surfaces did not yet respect archivedAt; the follow-up was deferred until archived-unread count divergence became observable. This session made it observable.

Knowledge Base semantic sweep attempted: archived unread mailbox messages hidden from default unread list wake drain includeArchived unreadCount; KB was unavailable with Knowledge Base is not available: requested resource could not be found, so this ticket is grounded in live GitHub, local synchronized ticket/PR history, source, and runtime A2A evidence.

The Architectural Reality

MailboxService.listMessages() has the correct default behavior: it computes archivedAt from the MESSAGE node or per-recipient DELIVERED_TO edge and skips archived messages unless includeArchived: true is supplied. Source anchor: ai/services/memory-core/MailboxService.mjs around lines 591-599 and 693-699.

MailboxService.countMessages() does not expose or apply an includeArchived filter. Its direct-SQL inbox UNION mirrors read-state storage but not archive-state storage. Source anchor: ai/services/memory-core/MailboxService.mjs around lines 1246-1392.

MailboxService.getHealthcheckPreview() uses countMessages({box: 'inbox', status: 'unread'}) for unreadCount, so it inherits the archived-unread count mismatch. Source anchor: ai/services/memory-core/MailboxService.mjs around lines 1400-1418.

MemoryService.buildMailboxDelta() uses its own direct-SQL unread query for add_memory.mailbox, and that query also lacks archived filtering. Source anchor: ai/services/memory-core/MemoryService.mjs around lines 108-176.

Existing unit coverage already checks two halves independently: countMessages parity for ordinary unread mail and archive hide/surface behavior for listMessages. The missing regression is the cross-surface invariant: count/delta paths must match default list semantics for archived messages.

The Fix

Align all unread-count/delta surfaces with default listMessages archive semantics:

  1. Add archive-state filtering to MailboxService.countMessages() for inbox counts.
  2. Prefer an explicit includeArchived argument on countMessages, defaulting to false, mirroring listMessages; callers that intentionally want archived counts can opt in.
  3. Update MailboxService.getHealthcheckPreview() to keep default-excluding archived unread messages from unreadCount.
  4. Update MemoryService.buildMailboxDelta() so add_memory.mailbox.unreadCount and latestPreview default-exclude archived messages on both direct DMs and per-recipient broadcasts.
  5. Add unit coverage that reproduces the divergence and proves parity after the fix.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
MailboxService.listMessages({includeArchived}) #10148 / PR #11535 archive contract Remains unchanged: default excludes archived messages; includeArchived: true surfaces them. Existing behavior remains the source of truth. Existing JSDoc + MCP OpenAPI. Existing #10148 tests stay green.
MailboxService.countMessages({box: 'inbox', status, includeArchived}) This ticket + PR #11535 out-of-scope warning Default excludes archived messages exactly like listMessages; includeArchived: true includes them. If SQL storage is unavailable, keep existing non-fatal {count: 0} behavior. JSDoc for new param; OpenAPI only if exposed at tool boundary. Unit tests for direct DM and broadcast archived unread counts.
MailboxService.getHealthcheckPreview().unreadCount This ticket + default mailbox preview semantics Uses archive-aware count, so preview unread count matches default-visible unread work. Non-fatal preview degradation remains unchanged. Existing JSDoc note updated if needed. Unit test with archived unread message returns 0 visible unread.
MemoryService.buildMailboxDelta() / add_memory.mailbox This ticket + observed 2026-06-13 divergence unreadCount and latestPreview default-exclude archived messages, matching default list_messages(status:'unread'). On SQL failure, existing null mailbox delta fallback remains. JSDoc if the mailbox block documents unread semantics. Unit test where archived unread messages do not appear in add-memory mailbox delta.

Decision Record impact

none.

Acceptance Criteria

  • AC1: MailboxService.countMessages({box: 'inbox', status: 'unread'}) excludes an archived direct-DM unread message by default.
  • AC2: MailboxService.countMessages({box: 'inbox', status: 'unread'}) excludes a per-recipient archived broadcast unread message by default.
  • AC3: MailboxService.countMessages({box: 'inbox', status: 'unread', includeArchived: true}) includes archived unread messages, or the ticket explicitly documents why countMessages intentionally has no include-archived count mode.
  • AC4: MailboxService.getHealthcheckPreview().unreadCount matches default-visible unread work when archived unread messages exist.
  • AC5: MemoryService.buildMailboxDelta() / add_memory.mailbox excludes archived unread messages from both unreadCount and latestPreview by default.
  • AC6: Existing archive behavior remains preservation-first: no archived messages are deleted or unarchived to make counts pass.

Out of Scope

  • Deleting archived messages.
  • Changing archive_message permissions or storage location.
  • Changing wake subscription delivery, watermarks, or retry behavior from #13077.
  • Changing GitHub notification heartbeat behavior from #13081.

Avoided Traps

  • Do not fix this by making agents always call includeArchived: true; that restores visibility but destroys the default archive contract and reintroduces stale backlog noise.
  • Do not mark archived messages read as part of the implementation. Marking the June 12 backlog read was a one-time operational cleanup after subject-level triage, not the product fix.
  • Do not delete archived messages; archived data remains valuable history.

Related

  • PR #11535: archive/delete semantics; explicitly flagged countMessages archived filtering as a future friction-to-gold candidate if divergence became observable.
  • PR #11528 / #10180: countMessages and uncapped unreadCount path.
  • #13077: wake-loss-on-delivery-failure; related wake correctness but different failure class.
  • #13081: GitHub notification heartbeat wakes; related wake substrate but different source.
  • #13012: Agent Harness epic.

Origin Session ID: d2d31447-5009-47a8-992e-9ecc35b806c1 Retrieval Hint: archived unread mailbox includeArchived countMessages add_memory unreadCount wake drain

Live latest-open sweep: checked latest 20 open issues at 2026-06-13T11:16:46Z; no equivalent found. Targeted open search includeArchived unreadCount returned no open issue. A2A in-flight sweep: latest 50 inbox messages checked immediately before creation; no competing lane-claim or lane-intent for archived-unread mailbox counts.