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:
- Add archive-state filtering to
MailboxService.countMessages() for inbox counts.
- Prefer an explicit
includeArchived argument on countMessages, defaulting to false, mirroring listMessages; callers that intentionally want archived counts can opt in.
- Update
MailboxService.getHealthcheckPreview() to keep default-excluding archived unread messages from unreadCount.
- Update
MemoryService.buildMailboxDelta() so add_memory.mailbox.unreadCount and latestPreview default-exclude archived messages on both direct DMs and per-recipient broadcasts.
- 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
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.
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_memoryreturnedmailbox.unreadCount = 28with latest previewMESSAGE:c953c039-a23e-47c1-9497-a05c609419c0.list_messages({box: 'inbox', status: 'unread', includeArchived: true})surfaced 28 archived-but-unread June 12 messages.[], and the nextadd_memoryresponse returnedmailbox.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:list_messages(status: 'unread')says no actionable unread work exists;add_memory.mailbox.unreadCountsays unread work still exists;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 withKnowledge 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 computesarchivedAtfrom the MESSAGE node or per-recipientDELIVERED_TOedge and skips archived messages unlessincludeArchived: trueis supplied. Source anchor:ai/services/memory-core/MailboxService.mjsaround lines 591-599 and 693-699.MailboxService.countMessages()does not expose or apply anincludeArchivedfilter. Its direct-SQL inboxUNIONmirrors read-state storage but not archive-state storage. Source anchor:ai/services/memory-core/MailboxService.mjsaround lines 1246-1392.MailboxService.getHealthcheckPreview()usescountMessages({box: 'inbox', status: 'unread'})forunreadCount, so it inherits the archived-unread count mismatch. Source anchor:ai/services/memory-core/MailboxService.mjsaround lines 1400-1418.MemoryService.buildMailboxDelta()uses its own direct-SQL unread query foradd_memory.mailbox, and that query also lacks archived filtering. Source anchor:ai/services/memory-core/MemoryService.mjsaround 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
listMessagesarchive semantics:MailboxService.countMessages()for inbox counts.includeArchivedargument oncountMessages, defaulting tofalse, mirroringlistMessages; callers that intentionally want archived counts can opt in.MailboxService.getHealthcheckPreview()to keep default-excluding archived unread messages fromunreadCount.MemoryService.buildMailboxDelta()soadd_memory.mailbox.unreadCountandlatestPreviewdefault-exclude archived messages on both direct DMs and per-recipient broadcasts.Contract Ledger Matrix
MailboxService.listMessages({includeArchived})includeArchived: truesurfaces them.MailboxService.countMessages({box: 'inbox', status, includeArchived})listMessages;includeArchived: trueincludes them.{count: 0}behavior.MailboxService.getHealthcheckPreview().unreadCount0visible unread.MemoryService.buildMailboxDelta()/add_memory.mailboxunreadCountandlatestPreviewdefault-exclude archived messages, matching defaultlist_messages(status:'unread').Decision Record impact
none.
Acceptance Criteria
MailboxService.countMessages({box: 'inbox', status: 'unread'})excludes an archived direct-DM unread message by default.MailboxService.countMessages({box: 'inbox', status: 'unread'})excludes a per-recipient archived broadcast unread message by default.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.MailboxService.getHealthcheckPreview().unreadCountmatches default-visible unread work when archived unread messages exist.MemoryService.buildMailboxDelta()/add_memory.mailboxexcludes archived unread messages from bothunreadCountandlatestPreviewby default.Out of Scope
archive_messagepermissions or storage location.Avoided Traps
includeArchived: true; that restores visibility but destroys the default archive contract and reintroduces stale backlog noise.Related
Origin Session ID: d2d31447-5009-47a8-992e-9ecc35b806c1 Retrieval Hint:
archived unread mailbox includeArchived countMessages add_memory unreadCount wake drainLive latest-open sweep: checked latest 20 open issues at 2026-06-13T11:16:46Z; no equivalent found. Targeted open search
includeArchived unreadCountreturned 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.