Found by the #295 review, non-blocking there, and left out of #299 rather than smuggled in with a test change.
The defect
Mailbox.prunedUnread counts posts that landed after a reader's cursor and were pruned before it asked — so a reader is told what it missed instead of missing it silently. That was the point of adding it in #295.
But for a reader whose lastMailroomRead is nil — one that has never read — it reports 0. With no cursor there is no lower bound to count from, so a loop created during a busy stretch and reading for the first time is told it lost nothing, when posts it would have been shown were in fact pruned.
Reproduction (the review's probe, which fails on main):
aReaderThatNeverReadIsStillToldWhatItLost
Expectation failed: (answer.prunedUnread → 0) == 60
This is the same silent-loss shape the whole #288 series has been closing — the reader gets a well-formed answer that quietly understates what happened.
Why it is not a one-line fix
The obvious remedy is to stamp the cursor at node creation: give every new loop lastMailroomRead = the room's current latest id. Then prunedUnread has a lower bound and reports honestly.
But that also changes what a new loop sees. Today a loop with a nil cursor treats the whole room as unread, which is deliberate — a loop born after a busy week reads the backlog. Stamping the cursor at creation means a new loop starts with an empty inbox and never sees anything posted before it existed.
Both behaviours are defensible; they are different products. Pick one on purpose:
- Stamp at creation — new loops start clean,
prunedUnread is honest everywhere, and the backlog becomes something a loop asks for explicitly (mail list).
- Keep
nil meaning "everything is unread" — and give prunedUnread a different lower bound for that case, e.g. the oldest id the room still holds, so it can say "posts were pruned before you got here" without pretending to know how many.
The second keeps today's behaviour and still stops the silent understatement, so it is probably the smaller change — but it reports a bound rather than a count, and the field's name would need to stop promising an exact number.
Test
aReaderThatNeverReadIsStillToldWhatItLost is on branch review/295-audit and can be lifted straight in once the behaviour is decided.
Related: #288, #295, #299
🤖 Generated with Claude Code
https://claude.ai/code/session_01BP43ags4cn8fq2ZZdv85J9
Found by the #295 review, non-blocking there, and left out of #299 rather than smuggled in with a test change.
The defect
Mailbox.prunedUnreadcounts posts that landed after a reader's cursor and were pruned before it asked — so a reader is told what it missed instead of missing it silently. That was the point of adding it in #295.But for a reader whose
lastMailroomReadisnil— one that has never read — it reports 0. With no cursor there is no lower bound to count from, so a loop created during a busy stretch and reading for the first time is told it lost nothing, when posts it would have been shown were in fact pruned.Reproduction (the review's probe, which fails on
main):This is the same silent-loss shape the whole #288 series has been closing — the reader gets a well-formed answer that quietly understates what happened.
Why it is not a one-line fix
The obvious remedy is to stamp the cursor at node creation: give every new loop
lastMailroomRead =the room's current latest id. ThenprunedUnreadhas a lower bound and reports honestly.But that also changes what a new loop sees. Today a loop with a
nilcursor treats the whole room as unread, which is deliberate — a loop born after a busy week reads the backlog. Stamping the cursor at creation means a new loop starts with an empty inbox and never sees anything posted before it existed.Both behaviours are defensible; they are different products. Pick one on purpose:
prunedUnreadis honest everywhere, and the backlog becomes something a loop asks for explicitly (mail list).nilmeaning "everything is unread" — and giveprunedUnreada different lower bound for that case, e.g. the oldest id the room still holds, so it can say "posts were pruned before you got here" without pretending to know how many.The second keeps today's behaviour and still stops the silent understatement, so it is probably the smaller change — but it reports a bound rather than a count, and the field's name would need to stop promising an exact number.
Test
aReaderThatNeverReadIsStillToldWhatItLostis on branchreview/295-auditand can be lifted straight in once the behaviour is decided.Related: #288, #295, #299
🤖 Generated with Claude Code
https://claude.ai/code/session_01BP43ags4cn8fq2ZZdv85J9