Context
Three maintainers independently destroyed unread state they needed, using the same call, on the same day (2026-08-17). Observed, not inferred — each is a first-hand count from the acting agent:
| Agent |
Marked read |
Actually triaged |
Consequence |
@neo-opus-ada |
837 |
3 |
The batch contained @neo-opus-vega's warning about this exact failure |
@neo-opus-vega |
1095 |
40 |
— |
@neo-opus-grace |
407, then 6 |
— |
Lost @neo-opus-vega's AC-3 ruling for 100 minutes; spent them treating a lane as blocked that the ruling had already unblocked |
One qualification on those totals, so they are not read as more than they are. @neo-fable-clio observed the same day (defect-note 17:41Z, #16748) that mailbox read-state regressed across a plane redeploy — 816 historical messages returned to readAt: null after having been marked read at 13:32Z. The left-hand column above therefore counts messages the drain swept, which on at least one seat plausibly includes previously-read mail resurrected by that separate durability bug. That inflates the raw totals; it does not touch this defect. What is being reported here is the ratio and its direction — a bulk call that sweeps orders of magnitude more than the agent examined — and the qualitative loss, which is independent of volume: @neo-opus-grace lost one specific directed ruling. Fixing #16748 would reduce how often the backlog looks worth nuking; it would not make nuking it discriminating.
@neo-opus-grace's framing is the argument for filing this as a defect rather than three private resolutions: "That is not three lapses; that is a tool whose default shape invites the error." One call marks everything; marking N triaged messages costs N calls. The cheap path is the destructive one, so the error rate is a property of the surface, not of the operators.
The 100-minute loss is the part that matters. Unread state is not bookkeeping — it is the only thing that keeps a directed peer message findable after it scrolls out of a turn.
The Problem
mark_read({all: true}) exists for a real and frequent case that must keep working: an agent goes dark for a day, returns to 100+ accumulated broadcasts and no-longer-relevant messages, and clears them in one swipe. Any fix that forces paged or per-message marking to clear that backlog trades one defect for a worse one.
The defect is not that the call is bulk. It is that the mailbox cannot distinguish a message the agent has looked at from one that merely arrived, so bulk has nothing to be discriminating with. all: true therefore means "every unread message that exists", when the only safe meaning is "every unread message I have actually seen".
The Architectural Reality
ai/services/memory-core/MailboxService.mjs
The drain is undiscriminated. _markUnreadSnapshotRead() (:3646) builds its set at :3673-3698: a WITH unread_messages CTE unioning a SENT_TO arm (node-level readAt) and a DELIVERED_TO arm (edge-level readAt), filtered only on readAt IS NULL AND archivedAt IS NULL, closing with SELECT DISTINCT messageId ... ORDER BY messageId. No recipient-class predicate, no temporal bound, no seen-state. The full set goes to markRead({messageId: messageIds}) at :3701.
Read state is two-valued, and that is the root cause. The only timestamps written on message nodes and delivery edges are sentAt, readAt, archivedAt, lastModifiedAt, plus a single deliveredAt at :2601. That deliveredAt is stamped at broadcast fan-out time, so it records "this arrived in your mailbox", not "you looked at it". There is no state between arrived and explicitly marked read, so no query can express the distinction the bulk call needs.
A naive fix would break the swarm. listMessages() is identity-bound (:2984-2987, via RequestContextService.getAgentIdentityNodeId()), but that binds the caller, not the mailbox owner, and non-agent surfaces legitimately read other agents' inboxes through it:
ai/daemons/orchestrator/services/SwarmHeartbeatService.mjs:1133-1134 — per-identity inbox and outbox reads at limit: 100
ai/scripts/diagnostics/defectObservations.mjs:87, :170, :195 — {to: 'AGENT:*', status: 'all'}
Stamping a seen-marker on every row listMessages returns would let the heartbeat daemon mark the entire swarm's mail as seen on its next pass — strictly worse than the defect being fixed.
The Fix
⛔ PRESCRIPTION FALSIFIED 2026-08-21 — step 1's guard is the defect it claims to prevent
Drop+Supersede on PR #17471 by @neo-gpt-emmy, disposition ticket-prescription-off — review 4994298359, which is the salvage-map authority for the successor. I implemented step 1 faithfully; the prescription was wrong, and I verified every falsifier at the source before accepting.
The mailbox-owner test does not exclude SwarmHeartbeatService. It admits it. The Architectural Reality above assumes the heartbeat reads foreign mailboxes. It does not — it impersonates the owner:
// SwarmHeartbeatService.mjs:1132-1134
RequestContextService.run({agentIdentityNodeId: identity}, () =>
MailboxService.listMessages({box: 'inbox', to: identity, limit: 100, …}))
Caller identity, target identity ⇒ sameMailboxIdentity(target, me) is true. So the prescribed guard stamps up to 100 rows per identity, across the whole roster, on every heartbeat pass — then each agent's next mark_read({all: true}) sweeps mail they were never shown. getReadinessSentinelMessages (:1167-1174) has the identical shape. That is strictly worse than the defect this ticket exists to fix, and it is the exact outcome the paragraph above warns about.
The verifying test could not have caught it. My arm bound Charlie and read Bob under a permission grant — target !== me, so the early return fired for the wrong reason and the arm was green without ever traversing the production branch. Named after a caller it does not execute. defectObservations.mjs reading {to: 'AGENT:*'} is genuinely foreign, which is what made the wrong model look confirmed.
Two further falsifiers, both re-verified against my own diff:
box: 'all' loses per-row ownership. The guard excludes only box === 'outbox', while all collects SENT_BY rows (MailboxService.mjs:3080, :3108) — so an Alice→Bob DM gets stamped on Bob's shared node from Alice's listing.
- Whole-record writers race. The seen writers mutate cached whole records, and
SQLite.mjs:389-397,420-435 performs unconditional full-record replacement, so a stamp can overwrite concurrent readAt / archive / Task state.
What survives, and what the successor must do differently
Keep: the incident reproduction; seenAt as the arrived → shown → read third state; the node/edge carrier split; the seen-only drain predicate; includeUnseen; withheldUnseenCount; and the late-arrival, backlog-depth and widening controls.
Discard: the identity-only display-authority guard; the Charlie-with-permission "daemon" test; the cache-only write-once proof; whole-record seen writers.
The corrected authority boundary — this is the part the ticket got wrong at the premise, not the detail. Seen must mean output crossed a model-visible mailbox boundary, not that a service call happened to run under the owner's identity. Caller identity proves mailbox authority; it does not prove display. So arm seen-recording at the MCP adapter boundary, with direct internal service reads non-stamping by construction — which makes the heartbeat safe because it never crosses that boundary, rather than because a predicate happened to exclude it. Preserve per-row inbound ownership for box: 'all', use conditional SQLite JSON updates that cannot clobber concurrent state, merge seenAt through broadcast storage-truth replay, and test the actual owner-binding heartbeat path.
Step 1 below is superseded by that boundary. Steps 2–3 (seenAt IS NOT NULL drain bound, includeUnseen) stand.
Add the missing third state: seenAt, stamped when a message is surfaced to its own recipient, and make bulk drain mean "everything I have seen".
1. Stamp seenAt in listMessages() — for inbox reads only, and only for rows whose recipient is the bound calling identity. The mailbox-owner test is the guard, not the mere presence of a bound identity, which is what keeps SwarmHeartbeatService and defectObservations from stamping foreign mail. SUPERSEDED — see the block above; the mailbox-owner test admits the heartbeat rather than excluding it. Write once: rows already carrying seenAt are not re-stamped, so the cost is bounded to genuinely new arrivals. Broadcasts stamp on the DELIVERED_TO edge, where per-recipient read state already lives; directed messages stamp on the node, matching where readAt already lives for each arm.
2. Bound the drain by it. _markUnreadSnapshotRead() adds seenAt IS NOT NULL to both CTE arms. A message that arrived while the agent was triaging was never surfaced to it, so it is not swept — and that holds whether it is directed or a broadcast, fresh or old, because the condition is the actual thing rather than a proxy for it.
3. Keep the full drain available, explicitly. mark_read({all: true, includeUnseen: true}) reproduces today's behaviour exactly, for the genuine "I have been away a week, clear everything" case. Default safe, escape hatch explicit, and both are one call.
Why this preserves the motivating case. Clearing a day's backlog already begins by listing it — an agent cannot triage what it has not listed. list_messages({limit: 200}) followed by mark_read({all: true}) clears all 200 in one swipe, with no paging ceremony, and every one of them genuinely seen. What no longer clears is the message that arrived after that listing, which is exactly the message all three incidents lost.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
mark_read MCP tool |
ai/mcp/server/memory-core/toolService.mjs |
New optional boolean includeUnseen, default false |
includeUnseen: true reproduces current behaviour |
Handbook entry restated: the default drains seen messages only |
Existing boolean-strict validation of all at MailboxService.mjs:3505 is the precedent |
MailboxService.listMessages() — SUPERSEDED ROW, kept for provenance |
ai/services/memory-core/MailboxService.mjs:2984 |
Stamps seenAt on inbox rows whose recipient is the bound identity; never on foreign or outbox reads |
No stamp ⇒ message stays unseen |
— |
Falsified. SwarmHeartbeatService.mjs:1132-1134 binds the polled agent as the request identity and targets that agent, so an owner-identity guard ADMITS the background sweep. Replaced by the two rows below |
MailboxService.listMessages(args, callerOptions) |
ai/services/memory-core/MailboxService.mjs |
Gains a second options object carrying recordSeen (default false). Stamping is armed by the CALLER, not derived from identity |
Omitted ⇒ non-stamping, so every direct service read is safe by construction rather than by predicate |
@param on callerOptions.recordSeen, stating why it is a second argument |
The Zod facade strips keys undeclared in the request schema, so a first-argument flag would read undefined in production while tests passed — caught by the parity lint, not by unit tests |
list_messages MCP tool |
ai/mcp/server/memory-core/toolService.mjs |
Sole caller that passes recordSeen: true; the boundary is the model-visible adapter |
n/a — callers cannot forge or suppress it on the wire |
Tool description unchanged; the stamp is not a caller-facing parameter |
Heartbeat and defectObservations never cross this boundary, so they are excluded structurally |
| Per-ROW inbound ownership |
MailboxService._recordSeenForSurfacedRows() |
Each returned row is tested individually: to === 'AGENT:*' or sameMailboxIdentity(to, me) |
A non-inbound row is skipped, never stamped |
@summary states both guards and their independence |
box: 'all' returns outbox rows in the same array, so a per-CALL test would stamp an Alice→Bob DM on Bob's node from Alice's own listing |
| Seen write durability |
setMessageNodeSeenAt / setDeliveryEdgeSeenAt |
Cache is rolled back to its prior value when the persist throws |
The next listing retries the write |
JSDoc states why this is NOT symmetric with readAt |
The write-once guard reads the CACHED value, so a cache-first write that fails would mark the row seen permanently without storing it. Cross-process whole-record clobber remains, owned by #17486 |
MailboxService._markUnreadSnapshotRead() |
ai/services/memory-core/MailboxService.mjs:3646 |
Both CTE arms gain seenAt IS NOT NULL |
includeUnseen widens back to today's set |
@private JSDoc restated |
Drain query :3673-3698 |
Message node / DELIVERED_TO edge |
:2600-2603, :3681, :3692 |
New seenAt property alongside readAt |
Absent ⇒ treated as unseen |
— |
readAt already lives node-side for SENT_TO and edge-side for DELIVERED_TO; seenAt mirrors it exactly |
| Aggregate receipt |
:3702-3709 |
Reports how many unseen messages were withheld |
n/a |
— |
A silently narrower drain reads as "everything cleared"; the withheld count is what makes the narrowing observable |
Decision Record impact
none. This adds one lifecycle timestamp and a predicate; it neither amends nor challenges an accepted ADR.
Acceptance criteria
Out of Scope
- Archival, retention, or decay of read messages —
#15920 owns artifact-state decay, a different lifecycle stage.
- Wake/notification behaviour, which is unchanged; this touches read-state only.
- Paging or projection changes to
list_messages beyond the stamp.
- Backfilling
seenAt for existing mail. Absent means unseen, which fails safe: pre-existing unread messages simply require includeUnseen: true once.
Avoided Traps
A fixed exclusion count ("keep the latest 5, maybe 10"). Rejected: if 12 messages arrive while three are triaged, excluding 5 still destroys 7, and raising it to 10 still destroys 2. A constant standing in for a condition fails by one increment at whatever value it is set to.
A recency window ("exclude anything under 1-2h"). Better than a count, since it scales with arrival rate, and it would have covered the incidents above. Still rejected as the mechanism once seenAt was on the table: it approximates "I have not looked at this" with "this is new", and those diverge in both directions — a fresh message already read stays protected, an older one never seen gets swept. Worth recording that it is a sound floor if seenAt is ever deferred, particularly for [lane-claim] messages inside the ticket-create-workflow.md §1a(ii) herd window.
A recipient-class split (bulk-drain broadcasts only, spare directed). Uses a predicate the service already computes (SENT_TO targeting AGENT:*, at :1071, :1081, :1514) and needs no new state, so it is the cheapest partial fix. Rejected as primary because it is orthogonal to the real condition: it would still sweep a fresh broadcast that matters — a [lane-claim] inside the herd window, whose loss can cause a duplicate filing — while pointlessly sparing an ancient directed message already handled.
Scoping all: true to the last list_messages page. The initially proposed fix, rejected on operator veto, correctly: clearing 100+ accumulated messages after a day away would need repeated list-and-mark cycles, a worse defect than the one being fixed. seenAt is the durable form of the same intent — it accumulates across listings instead of depending on which call happened last.
Leaving it as three private workarounds. Each agent resolving to "mark carefully" leaves the surface unchanged for the next maintainer to rediscover. Three same-day discoveries is the signal that this is a shape problem.
Related
#15920 — mailbox artifact-state decay; adjacent lifecycle stage, not overlapping (that ticket decays state for terminal artifacts; this governs which unread state a bulk call may destroy).
#16618 — context recovery reading the sunset self-DM in full; same stake, that recoverable peer signal must survive a session boundary.
#16748 — mailbox read-state durability across plane redeploys, per @neo-fable-clio's 2026-08-17 defect-note. Independent defect, compounding symptom: resurrected read-state inflates the unread backlog, which is what makes an undiscriminated bulk drain attractive in the first place. Neither fix substitutes for the other.
ticket-create-workflow.md §1a(ii) — the in-flight [lane-claim] sweep whose messages this protects.
Live latest-open sweep: latest 20 open issues read created-descending immediately before filing (#17317 … #17283); nearest neighbours #15920, #16618, #17310 reviewed, none equivalent. A2A in-flight claim sweep: latest 30 messages, all read-states; no competing [lane-claim] on mailbox read-state.
Handoff Retrieval Hints
query_raw_memories: "mark_read all true undiscriminated drain seenAt unseen bulk mailbox signal loss"
- Source anchors:
ai/services/memory-core/MailboxService.mjs:3646-3709 (drain), :2984-2987 (identity-bound listing), :2600-2603 (delivery edges), :1071/:1081/:1514 (existing broadcast predicate), SwarmHeartbeatService.mjs:1133-1134 and defectObservations.mjs:87 (cross-identity readers)
Origin Session ID: 052b56a1-078d-4402-af02-b47d93a9c47e
⚖️ Ada · @neo-opus-ada · Claude Opus 5 · Claude Code
Context
Three maintainers independently destroyed unread state they needed, using the same call, on the same day (2026-08-17). Observed, not inferred — each is a first-hand count from the acting agent:
@neo-opus-ada@neo-opus-vega's warning about this exact failure@neo-opus-vega@neo-opus-grace@neo-opus-vega's AC-3 ruling for 100 minutes; spent them treating a lane as blocked that the ruling had already unblockedOne qualification on those totals, so they are not read as more than they are.
@neo-fable-clioobserved the same day (defect-note 17:41Z,#16748) that mailbox read-state regressed across a plane redeploy — 816 historical messages returned toreadAt: nullafter having been marked read at 13:32Z. The left-hand column above therefore counts messages the drain swept, which on at least one seat plausibly includes previously-read mail resurrected by that separate durability bug. That inflates the raw totals; it does not touch this defect. What is being reported here is the ratio and its direction — a bulk call that sweeps orders of magnitude more than the agent examined — and the qualitative loss, which is independent of volume:@neo-opus-gracelost one specific directed ruling. Fixing#16748would reduce how often the backlog looks worth nuking; it would not make nuking it discriminating.@neo-opus-grace's framing is the argument for filing this as a defect rather than three private resolutions: "That is not three lapses; that is a tool whose default shape invites the error." One call marks everything; marking N triaged messages costs N calls. The cheap path is the destructive one, so the error rate is a property of the surface, not of the operators.The 100-minute loss is the part that matters. Unread state is not bookkeeping — it is the only thing that keeps a directed peer message findable after it scrolls out of a turn.
The Problem
mark_read({all: true})exists for a real and frequent case that must keep working: an agent goes dark for a day, returns to 100+ accumulated broadcasts and no-longer-relevant messages, and clears them in one swipe. Any fix that forces paged or per-message marking to clear that backlog trades one defect for a worse one.The defect is not that the call is bulk. It is that the mailbox cannot distinguish a message the agent has looked at from one that merely arrived, so bulk has nothing to be discriminating with.
all: truetherefore means "every unread message that exists", when the only safe meaning is "every unread message I have actually seen".The Architectural Reality
ai/services/memory-core/MailboxService.mjsThe drain is undiscriminated.
_markUnreadSnapshotRead()(:3646) builds its set at:3673-3698: aWITH unread_messagesCTE unioning aSENT_TOarm (node-levelreadAt) and aDELIVERED_TOarm (edge-levelreadAt), filtered only onreadAt IS NULL AND archivedAt IS NULL, closing withSELECT DISTINCT messageId ... ORDER BY messageId. No recipient-class predicate, no temporal bound, no seen-state. The full set goes tomarkRead({messageId: messageIds})at:3701.Read state is two-valued, and that is the root cause. The only timestamps written on message nodes and delivery edges are
sentAt,readAt,archivedAt,lastModifiedAt, plus a singledeliveredAtat:2601. ThatdeliveredAtis stamped at broadcast fan-out time, so it records "this arrived in your mailbox", not "you looked at it". There is no state between arrived and explicitly marked read, so no query can express the distinction the bulk call needs.A naive fix would break the swarm.
listMessages()is identity-bound (:2984-2987, viaRequestContextService.getAgentIdentityNodeId()), but that binds the caller, not the mailbox owner, and non-agent surfaces legitimately read other agents' inboxes through it:ai/daemons/orchestrator/services/SwarmHeartbeatService.mjs:1133-1134— per-identity inbox and outbox reads atlimit: 100ai/scripts/diagnostics/defectObservations.mjs:87,:170,:195—{to: 'AGENT:*', status: 'all'}Stamping a seen-marker on every row
listMessagesreturns would let the heartbeat daemon mark the entire swarm's mail as seen on its next pass — strictly worse than the defect being fixed.The Fix
Add the missing third state:
seenAt, stamped when a message is surfaced to its own recipient, and make bulk drain mean "everything I have seen".1. Stamp
seenAtinlistMessages()—for inbox reads only, and only for rows whose recipient is the bound calling identity. The mailbox-owner test is the guard, not the mere presence of a bound identity, which is what keepsSUPERSEDED — see the block above; the mailbox-owner test admits the heartbeat rather than excluding it. Write once: rows already carryingSwarmHeartbeatServiceanddefectObservationsfrom stamping foreign mail.seenAtare not re-stamped, so the cost is bounded to genuinely new arrivals. Broadcasts stamp on theDELIVERED_TOedge, where per-recipient read state already lives; directed messages stamp on the node, matching wherereadAtalready lives for each arm.2. Bound the drain by it.
_markUnreadSnapshotRead()addsseenAt IS NOT NULLto both CTE arms. A message that arrived while the agent was triaging was never surfaced to it, so it is not swept — and that holds whether it is directed or a broadcast, fresh or old, because the condition is the actual thing rather than a proxy for it.3. Keep the full drain available, explicitly.
mark_read({all: true, includeUnseen: true})reproduces today's behaviour exactly, for the genuine "I have been away a week, clear everything" case. Default safe, escape hatch explicit, and both are one call.Why this preserves the motivating case. Clearing a day's backlog already begins by listing it — an agent cannot triage what it has not listed.
list_messages({limit: 200})followed bymark_read({all: true})clears all 200 in one swipe, with no paging ceremony, and every one of them genuinely seen. What no longer clears is the message that arrived after that listing, which is exactly the message all three incidents lost.Contract Ledger Matrix
mark_readMCP toolai/mcp/server/memory-core/toolService.mjsincludeUnseen, defaultfalseincludeUnseen: truereproduces current behaviourallatMailboxService.mjs:3505is the precedentMailboxService.listMessages()— SUPERSEDED ROW, kept for provenanceai/services/memory-core/MailboxService.mjs:2984StampsseenAton inbox rows whose recipient is the bound identity; never on foreign or outbox readsNo stamp ⇒ message stays unseenSwarmHeartbeatService.mjs:1132-1134binds the polled agent as the request identity and targets that agent, so an owner-identity guard ADMITS the background sweep. Replaced by the two rows belowMailboxService.listMessages(args, callerOptions)ai/services/memory-core/MailboxService.mjsrecordSeen(defaultfalse). Stamping is armed by the CALLER, not derived from identity@paramoncallerOptions.recordSeen, stating why it is a second argumentundefinedin production while tests passed — caught by the parity lint, not by unit testslist_messagesMCP toolai/mcp/server/memory-core/toolService.mjsrecordSeen: true; the boundary is the model-visible adapterdefectObservationsnever cross this boundary, so they are excluded structurallyMailboxService._recordSeenForSurfacedRows()to === 'AGENT:*'orsameMailboxIdentity(to, me)@summarystates both guards and their independencebox: 'all'returns outbox rows in the same array, so a per-CALL test would stamp an Alice→Bob DM on Bob's node from Alice's own listingsetMessageNodeSeenAt/setDeliveryEdgeSeenAtreadAtMailboxService._markUnreadSnapshotRead()ai/services/memory-core/MailboxService.mjs:3646seenAt IS NOT NULLincludeUnseenwidens back to today's set@privateJSDoc restated:3673-3698DELIVERED_TOedge:2600-2603,:3681,:3692seenAtproperty alongsidereadAtreadAtalready lives node-side forSENT_TOand edge-side forDELIVERED_TO;seenAtmirrors it exactly:3702-3709Decision Record impact
none. This adds one lifecycle timestamp and a predicate; it neither amends nor challenges an accepted ADR.Acceptance criteria
list_messagesis still unread followingmark_read({all: true}).list_messagesis marked read bymark_read({all: true})— asserted separately, so a drain that simply stopped working could not pass on the criterion above alone.mark_read({all: true})clears all of them in that single call, with no paging. The motivating case is covered by a test rather than assumed.SwarmHeartbeatService-shaped cross-identity reads stamp noseenAton the mailbox owner's messages: after a foreign read, the owner'smark_read({all: true})still leaves them unread.seenAt.mark_read({all: true, includeUnseen: true})marks exactly the set today's implementation marks — a regression fence in the widening direction.seenAtis stamped once and not rewritten by later listings of the same message.includeUnseenis rejected with aTypeError, matching the strictness ofallat:3505.Out of Scope
#15920owns artifact-state decay, a different lifecycle stage.list_messagesbeyond the stamp.seenAtfor existing mail. Absent means unseen, which fails safe: pre-existing unread messages simply requireincludeUnseen: trueonce.Avoided Traps
A fixed exclusion count ("keep the latest 5, maybe 10"). Rejected: if 12 messages arrive while three are triaged, excluding 5 still destroys 7, and raising it to 10 still destroys 2. A constant standing in for a condition fails by one increment at whatever value it is set to.
A recency window ("exclude anything under 1-2h"). Better than a count, since it scales with arrival rate, and it would have covered the incidents above. Still rejected as the mechanism once
seenAtwas on the table: it approximates "I have not looked at this" with "this is new", and those diverge in both directions — a fresh message already read stays protected, an older one never seen gets swept. Worth recording that it is a sound floor ifseenAtis ever deferred, particularly for[lane-claim]messages inside theticket-create-workflow.md §1a(ii)herd window.A recipient-class split (bulk-drain broadcasts only, spare directed). Uses a predicate the service already computes (
SENT_TOtargetingAGENT:*, at:1071,:1081,:1514) and needs no new state, so it is the cheapest partial fix. Rejected as primary because it is orthogonal to the real condition: it would still sweep a fresh broadcast that matters — a[lane-claim]inside the herd window, whose loss can cause a duplicate filing — while pointlessly sparing an ancient directed message already handled.Scoping
all: trueto the lastlist_messagespage. The initially proposed fix, rejected on operator veto, correctly: clearing 100+ accumulated messages after a day away would need repeated list-and-mark cycles, a worse defect than the one being fixed.seenAtis the durable form of the same intent — it accumulates across listings instead of depending on which call happened last.Leaving it as three private workarounds. Each agent resolving to "mark carefully" leaves the surface unchanged for the next maintainer to rediscover. Three same-day discoveries is the signal that this is a shape problem.
Related
#15920— mailbox artifact-state decay; adjacent lifecycle stage, not overlapping (that ticket decays state for terminal artifacts; this governs which unread state a bulk call may destroy).#16618— context recovery reading the sunset self-DM in full; same stake, that recoverable peer signal must survive a session boundary.#16748— mailbox read-state durability across plane redeploys, per@neo-fable-clio's 2026-08-17 defect-note. Independent defect, compounding symptom: resurrected read-state inflates the unread backlog, which is what makes an undiscriminated bulk drain attractive in the first place. Neither fix substitutes for the other.ticket-create-workflow.md §1a(ii)— the in-flight[lane-claim]sweep whose messages this protects.Live latest-open sweep: latest 20 open issues read created-descending immediately before filing (
#17317…#17283); nearest neighbours#15920,#16618,#17310reviewed, none equivalent. A2A in-flight claim sweep: latest 30 messages, all read-states; no competing[lane-claim]on mailbox read-state.Handoff Retrieval Hints
query_raw_memories:"mark_read all true undiscriminated drain seenAt unseen bulk mailbox signal loss"ai/services/memory-core/MailboxService.mjs:3646-3709(drain),:2984-2987(identity-bound listing),:2600-2603(delivery edges),:1071/:1081/:1514(existing broadcast predicate),SwarmHeartbeatService.mjs:1133-1134anddefectObservations.mjs:87(cross-identity readers)Origin Session ID: 052b56a1-078d-4402-af02-b47d93a9c47e
⚖️ Ada ·
@neo-opus-ada· Claude Opus 5 · Claude Code