Skip to content

fix(messages): end the New messages hold when no repair can sample - #3169

Merged
matt2e merged 1 commit into
mainfrom
new-messages-line-doesnt-show
Nov 10, 2026
Merged

matt2e merged 1 commit into
mainfrom
new-messages-line-doesnt-show

Conversation

@matt2e

@matt2e matt2e commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #3071, which made a channel longer than one page wait for the unread store's repair before placing the "New messages" line and held the channel's own reading meanwhile. This addresses the three review threads left open when that PR merged.

Problem

  1. A failed or overflowed repair held reading forever (Kalvin, Wes). The hold keyed on sampled alone. A repair that failed, overflowed its evidence budget, or was abandoned by a dropped connection or an access change left sampled unset with requested already true, so ensure() started nothing. Once later live evidence made the store observed again, the timeline held reading until some unrelated repair happened to succeed.
  2. Any read row counted as the witness above the line (kazemek21). A row read only by its own msg: receipt (dwell short of the bottom) says nothing about the rows above it, so a short repair sample could place the line below a row whose predecessors were still unread.
  3. buzz-help overstated the hold (Wes's review summary). It said the divider settles before the channel marks anything read, as if that applied to every visit.

Fix

  1. Channel snapshots now report sampling while a repair that can set sampled is reading, about to restart, or not yet requested. The timeline holds reading only while it is set; otherwise the visit settles with no line and reading resumes, including when the channel is entered after such a repair already ended. sampling stays set across the gap between a repair settling and its restart (repairAgain), so the hold does not flicker.
  2. attention() reports prefixRead when a channel-wide read reaches the row: the channel's read frontier, the activity: catch-up mark for an ordinary top-level message, or the retention floor. Only such a row lets the line sit below it.
  3. docs/buzz-help.md scopes the wait to the history check and says reading resumes if that check fails; it also says the line is absent when the rows above were only read one by one. docs/unread.md and the code comments record both behaviours and the remaining residual.

Testing

Run with Hermit via bin/pnpm, on b76ba40 (parent 5cf1713, current main).

Scope Result
ChannelTimeline.restore.test.tsx, unread.test.ts, unread-startup.test.ts, use-reading.test.ts, use-reading.dm.test.ts 5 files, 422 passed
vitest related on unread.ts and ChannelTimeline.tsx 240 files, 7,143 passed (run on the parent c4b8ecd before the fast-forward to 5cf1713; that commit touches only Codex/MCP files)
tsc --noEmit, Biome, git diff --check clean
Chromium tests/browser/new-message-boundary.spec.mjs 4 passed, including the 25-message channel
Not run WebKit journey, the full suite, the native app, manual testing

Each new regression test fails without its fix:

  • Reverting the timeline's sampling release fails 3 tests: the failed-repair case where the store becomes observed again before any dwell, and entry after an already-failed and an already-overflowed repair.
  • Reverting the prefixRead check fails the receipt-only witness case.
  • Dropping the repairAgain term fails the store test's restart-gap assertion.
  • Removing sampling and prefixRead from the store fails the 3 store tests, including the startup test that walks failure, retry and overflow against a live session.

Known limits

  • A channel read part-way by scrolling (rows read only by their own msg: marks) now shows no line on return. The line needs a row above it that a channel-wide read reached.
  • Unread mentions, broadcasts, broadcast replies and marked-unread messages above the loaded window remain the documented residual: catch-up's activity: mark never reads them, so a short sample can still hide one.
  • In the startup test harness, the first queried roster revokes access and abandons an in-flight repair without restarting it. That store behaviour predates this change; sampling now reports it correctly. Possible follow-up, not changed here.

🤖 Generated with Claude Code

Follow-up to #3071, which made a channel longer than one page wait for
the unread store's repair before placing "New messages", holding the
channel's own reading meanwhile.

The hold keyed on `sampled` alone. A repair that failed, overflowed its
evidence budget, or was abandoned by a dropped connection or an access
change left `sampled` unset with `requested` already true, so `ensure()`
started nothing. Once later live evidence made the store observed again,
the timeline held reading until some unrelated repair succeeded. Channel
snapshots now report `sampling` while a repair that can set `sampled` is
reading, about to restart, or not yet requested. The timeline holds only
while it is set. Otherwise the visit settles with no line and reading
resumes, including when the channel is entered after such a repair has
already ended.

The line inside incomplete history needed a read row above it, but any
read row counted, including one read only by its own `msg:` receipt
(dwell short of the bottom), which says nothing about the rows above it.
`attention()` now reports `prefixRead` when the channel frontier, the
`activity:` catch-up mark (ordinary rows) or the retention floor reaches
the row, and only such a row lets the line sit below it. A channel read
part-way by scrolling now shows no line on return. Unread mentions,
broadcasts, broadcast replies and marked-unread rows above the window
remain the documented residual.

docs/unread.md records both. docs/buzz-help.md scopes the sentence about
waiting to mark anything read to the repair check, and says the line is
absent when the rows above were only read one by one.

Each new regression test fails without its fix: the hold after a failed
repair that becomes observed again, entry after a failed or overflowed
repair, the receipt-only witness, and the store's `sampling` and
`prefixRead` contracts, including the restart gap.

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@matt2e
matt2e marked this pull request as ready for review November 10, 2026 02:33
@matt2e
matt2e requested a review from a team as a code owner November 10, 2026 02:33
@matt2e
matt2e merged commit bd344a5 into main Nov 10, 2026
70 checks passed
@matt2e
matt2e deleted the new-messages-line-doesnt-show branch November 10, 2026 03:48
gilgtm added a commit that referenced this pull request Nov 10, 2026
…nt-progress-rows

* origin/main:
  Revert "ci: use larger GitHub runners for Rust and browser journeys (#3090)" (#3180)
  Show Goose's unset thinking level as Default, not Off (#3124)
  ci(release): publish stable and beta desktop releases from v tags (#3150)
  fix(messages): end the New messages hold when no repair can sample (#3169)

Signed-off-by: Gil Birman <gil@squareup.com>
damienrj added a commit that referenced this pull request Nov 10, 2026
…rminal-font-picker

* 'main' of https://github.com/block/buzz-app: (168 commits)
  Unify Membership sharing and live roster updates (#3147)
  fix(activity): show one working cue per agent request (#3166)
  Introduce opt-in first-agent setup (#2804)
  fix(codex): drop the security-review sign-in prompt from harness defaults (#3188)
  feat(releases): publish desktop download manifest (#3154)
  fix(mcp): preserve current attachments across edits (#2937)
  fix(agents): call the Claude and Codex first-run download "the integration" (#3165)
  chore(goose): bump bundled goose-acp to v1.58.1 (#3161)
  Revert "ci: use larger GitHub runners for Rust and browser journeys (#3090)" (#3180)
  Show Goose's unset thinking level as Default, not Off (#3124)
  ci(release): publish stable and beta desktop releases from v tags (#3150)
  fix(messages): end the New messages hold when no repair can sample (#3169)
  fix(search): make Command F search the Bestie conversation (#3084)
  fix(plugins): keep failed update checks on the plugin's Settings row instead of a toast (#3088)
  Show single-agent Stop on the activity bubble (#3140)
  fix: support explicit hosted browser origins (#3149)
  fix: keep Codex tool namespace descriptions bounded (#3128)
  fix(messages): show "New messages" in channels longer than one page (#3071)
  Keep shared agent guidance generic (#3134)
  fix(typing): a thread reply also ends the agent's channel-level typing (#3148)
  ...

Signed-off-by: damienrj <damien@squareup.com>

# Conflicts:
#	src-tauri/Cargo.toml
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants