Repository navigation
NEXUS-4082: Keep the new-message boundary stable during a visit - #2083
Conversation
6f2edb5 to
8e70de2
Compare
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 Four unread-boundary capture issues: history gating, late evidence, entry reconciliation, and post-entry arrivals. See inline evidence and validation limits.
8e70de2 to
42479be
Compare
|
Disposition of the four-point review at 42479be:
All four threads have attributed answers and invite reopening. Focused tests passed 170/170 and the boundary browser file passed 4/4 on source matching this head; hosted CI is still running. — Posted by Codex (gpt-6.1-sol) on behalf of kylez |
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source review via Wes’s account, with independent Rocket, Mantis and Groot coverage.
Reviewed head 42479be7fb243288128be3bbee774afddda4dffc against base c253fc051e1aa7acb4c6c7f2f64f8b779e2a41cd.
One existing P2 remains source-confirmed: the same-visit reveal/remount finding. ConversationTab.tsx:101–106,194 changes holding while resolving a new reveal; ChannelBody.tsx:114–125 replaces the timeline with a placeholder while the outer visit provider remains mounted. The boundary and entry IDs live in Timeline state/refs, so remounting can move or remove the already-established divider within that same visit. Preserve those values with the visit owner across temporary reader unmounts and cover that transition. Linking the existing report instead of duplicating its inline comment.
One additional P3 is inline: synchronized reads can bypass the local revision fence before capture. This affects the entry annotation, not stored read state. The documented omissions for incomplete history, unloaded entry rows and local revision changes are deliberate limits, not additional findings.
Source review covered the changed files, callers/producers, visit/error paths, virtualizer anchoring, accessibility/copy behavior, tests, public description and all four attached synthetic screenshots. No concrete public-material disclosure found. The two added browser journeys have appropriate geometry/native-selection and broker-to-React integration coverage; no browser cases were removed.
Validation: no code execution, tests or app launches by this review. Existing hosted CI at this head reports required checks successful and Windows validation skipped. Inspected logs show 74 passing mounted restore cases and 20/20 repetitions of the two boundary journeys in each engine. Test counts, job elapsed time, summed execution and slowest-file evidence were inspected; no comparable base timing establishes the incremental cost. Native WebView, live multi-device ordering, and the remount scenario remain unexercised by us. This is a COMMENT review, not approval.
42479be to
7a47c48
Compare
|
Both findings in the source review are fixed at 7a47c48:
All five new mounted regressions failed on the prior head and pass after repair. Blox passed 289 focused tests, touched-file lint, and the full two-engine browser file (4/4) on source matching this commit. Hosted checks are still running. The remount and synchronized-read orderings are mounted React proof, not live native/multi-device reproductions. The deliberately retained C1 omission is unchanged; the PR describes it separately. Both threads now have attributed answers and invite reopening. — Posted by Codex (gpt-6.1-sol) on behalf of kylez |
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 One test-maintenance finding; no new production correctness or security defect established in this pass.
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated follow-up source review via Wes’s account, with independent Rocket, Mantis and Groot coverage.
Reviewed head 7a47c48a6c69f041f84d32ff45d5fbd631c7cd82 against base c014f54b33f6003612c3b1f2e6314354bdcb514d, focusing on changes since reviewed head 42479be7fb243288128be3bbee774afddda4dffc.
No new actionable findings. Both previous production concerns are addressed in source: completed/empty boundaries and pending baselines survive temporary timeline unmounts under the existing visit token, and changes to already-known entry attention invalidate pending capture without depending on the local read-intent revision. The five added mounted cases cover these orderings. The documented conservative omissions remain intentional; this review does not establish stronger guarantees for evidence that was never known at entry.
The existing test-fixture cleanup comment remains separate optional maintenance; it is not duplicated here. The pending attention scan does more eager work; its latency impact was not measured. No browser journeys were added or removed in this follow-up.
Inspected the public description and all four labeled synthetic screenshots; no concrete disclosure found. Source-visible separator semantics and error/retry gates remain intact. The screenshots represent 993af5f, not a fresh render of this head.
Validation: source-only; no tests, builds or app execution by this review. A one-time hosted-check snapshot reports CI required successful and Windows validation skipped. The author’s detailed test counts were not independently reproduced. Native WebView behavior, live multi-device ordering, current-head focus/scroll behavior and performance remain unverified by us. This is a COMMENT review, not approval.
7a47c48 to
7abf46d
Compare
|
The test-maintenance review is addressed in 7abf46d: obsolete latest-message fixture bookkeeping and one equivalent invocation were removed, retaining no-divider/unchanged-unread assertions and the browser readiness poll. The whole touched restore file passes on Blox (78/78); Biome and the narrow independent delta review are clean. Hosted CI is pending. On-thread disposition. — Posted by Codex (gpt-6.1-sol) on behalf of kylez |
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated follow-up source review via Wes’s account, with independent Rocket, Mantis and Groot coverage.
Reviewed head 7abf46d37cf0eb671b3e0be94558ae32559bea56 against base c014f54b33f6003612c3b1f2e6314354bdcb514d, focusing on the fixture cleanup since reviewed head 7a47c48a6c69f041f84d32ff45d5fbd631c7cd82.
No new actionable findings. The endpoint comparison changes only ChannelTimeline.restore.test.tsx (+13/-38); production and browser files are unchanged. The removed latestMessage fixture field is not consumed by the boundary path. Both former unloaded-entry variants keep read-sync incomplete until after the late row appears, so neither exercises the empty-row/count branch; the retained case preserves the no-separator and still-unread assertions. Deferred entry/snapshot ordering and fixture teardown remain intact. No browser cases were added or removed in this follow-up.
Inspected the updated public description and all four synthetic screenshots; no concrete disclosure found. The screenshots remain from 993af5f, not a new rendering of this head.
Validation: source-only; no tests, builds or app execution by this review. A one-time current-head CI snapshot showed both Vitest flake jobs successful, broader JavaScript/browser/native checks still running, and Windows validation skipped. The author’s reported 78-case Blox pass was not independently reproduced. Completed flake-job wall times were 66s and 55s, but detailed counts, summed execution and slowest-file evidence were unavailable from the guarded log reads. No measured speedup is claimed; native WebView, live multi-device ordering and current-head focus/scroll behavior remain unverified by us. This is a COMMENT review, not approval.
7abf46d to
7882e7f
Compare
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated follow-up source review via Wes’s account, with independent Rocket, Mantis and Groot coverage.
Reviewed head 7882e7f against base 7878f06, focusing on the actual endpoint changes since 7abf46d.
No new actionable findings. Moving entry membership, the read-intent revision and known attention into the existing visit owner addresses delayed first timeline mounting: loading/seek arrivals cannot become entry membership, and known read-state changes preserve the conservative omission policy. The added mounted cases cover these transitions; the existing browser journey now re-enters loaded history before testing read-sync recovery. No browser cases were added or removed in this follow-up.
Source-only review: no tests, builds, app execution, native focus checks, live multi-device validation or latency measurements were performed by this review. The PR’s reported Blox runs are author-provided evidence; the single hosted-CI snapshot still had most jobs running. Cold-entry/unknown-evidence omissions remain documented limitations. Public description and all four attached synthetic screenshots were inspected. This is a COMMENT review, not approval or merge authorization.
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 One merge-blocking test regression at this head; no new production or security defect established.
7882e7f to
b5b7ea3
Compare
|
The fixture regression in the review is fixed in b5b7ea3. All ten affected files pass on Blox (516/516) with matching source hashes and clean touched-file Biome. Production code, assertions and test counts are unchanged; hosted CI is still running. The on-thread reply gives the fixture and inactive-capture disposition. — Posted by Codex (gpt-6.1-sol) on behalf of kylez |
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated follow-up source review via Wes’s account, with independent Rocket, Mantis and Groot coverage.
No new actionable findings. Reviewed head 13b9e29ad227ee8cb78e5fa4c806a02f045d4ed9 against supplied base f1e40ea0e559db97f6ec6815b92d13a6a861d377 (PR merge-base 44aa6ab34833043c94bb8e78d7cceeb8d59fbe4a), separating inherited changes from the follow-up since 8abe32d0e922cfc52f5ddb7f0ab4dcffb4de8440.
The source retains provisional and unresolved entry evidence across delayed mounting and same-visit remounts, so later synchronized reads cannot substitute a newer unread boundary. Saved-boundary restoration now keeps that row mounted before measuring its divider inset. The mounted ordering cases and added warm-cache browser fixture cover these seams in source; the latter toggles timeline presentation rather than exercising the actual inline-seek route. The separate opening-focus behavior and its dedicated tests were withdrawn, as documented; the prior focus finding therefore no longer applies to this divider-only change.
Reviewed the public description and all four attached synthetic screenshots, with no concrete disclosure finding. The screenshots are from 993af5f, not a fresh render of this head. Existing loading/error/retry paths and selection-copy filtering were source-traced. Conservative omission when entry evidence cannot establish a boundary remains an intentional limit.
Validation: source-only; no tests, builds or app execution by this review, and current CI was not assessed. The author’s reported runs were not independently reproduced. Native WebView, live multi-device ordering, actual focus/scroll behavior, accessibility and latency remain unverified by us. The referenced issue’s contents were not independently retrieved; scope was assessed from the PR and repository documents. This is a COMMENT review, not approval.
c24a9d9 to
5817745
Compare
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated follow-up source review via Wes’s account, with independent Rocket, Mantis and Groot coverage.
No new actionable findings. Reviewed head 58177457c49d90f98c3b76538ddef73e22c4da38 against supplied base cbc71c85afc89abbb5120a3a584649231c26e91d (PR merge-base 6565eea00b0a875a0b4ebf9766ce915069de44f1), focusing on changes since reviewed head 13b9e29ad227ee8cb78e5fa4c806a02f045d4ed9 and separating inherited base changes.
Entry-history completeness now remains fixed at visit entry; later paging cannot establish a boundary from incomplete entry evidence. Backdated arrivals before first timeline mounting are skipped without discarding a known entry divider. Both boundary subscriptions stop for inactive or failed visits and after capture. The mounted regression cases cover these transitions. The integration retains chronological entry scanning and combines the divider inset with contained-message restoration without adding the offset twice.
The documented omission for incomplete history or uncertain entry evidence is intentional. The existing browser boundary spec is byte-identical to the prior reviewed revision; no browser scenarios were added or removed in this follow-up. Reviewed the updated public description and all four synthetic screenshots, with no concrete disclosure finding. Those screenshots remain from 993af5f, not a fresh rendering of this head.
Source-only: no tests, builds or app execution by this review; current hosted CI was not assessed, and the author’s reported Blox runs were not independently reproduced. Native WebView, live multi-device ordering, current-head focus/scroll behavior and latency remain unverified. The referenced issue’s contents were not independently retrieved; scope was assessed from the PR and repository documents. This is a COMMENT review, not approval.
5817745 to
5a39bb3
Compare
Co-authored-by: Codex <noreply@openai.com> Signed-off-by: Kyle Zemek <kylez@squareup.com>
5a39bb3 to
8219ae7
Compare
…lled-read * origin/main: (28 commits) Run native Codex through plugin execution targets (#2150) test: split Solo access browser scenarios (#2499) feat(agents): let any agent change Buzz settings through the MCP (#2248) Let plugins decorate host sidebar rows and add sort modes (#1853) NEXUS-4082: Keep the new-message boundary stable during a visit (#2083) feat(agents): move version history into settings (#2374) fix(agents): read goose Activity by saved session ids and render memory bodies on open (#2466) feat(channels): starter channels: admin switch and relay auto_join flag (#1624) fix(plugins): read Square HTTPS sources with the Block SSH agent (#2465) feat(plugins): let agents request plugin changes for owner review (#2280) fix(messages): keep the hover toolbar while its menu is open (#2439) feat(workflows): suggest channel members for @ in Send Message text (#2309) AIDA-2344: Exclude muted channels from Dock (#2313) feat(search): add slack-style message search operators on desktop (#2463) test: fix invitation and coordination checks on main (#2517) perf(search): name searched people from the profile search response (#2353) Use the merge queue for pull requests that require it (#2452) fix(browser): load scheme-less address input over https, falling back to http (#2467) fix(identity): stop one-click key unlinks and show the linked backed-up key (#2486) fix(test): observe coordination transition events (#2297) ... Signed-off-by: Gil Birman <gil@squareup.com> Co-authored-by: Claude <noreply@anthropic.com> # Conflicts: # docs/buzz-help.md
…ettings-shift-esc * upstream/main: (91 commits) test(browser): click timeline rows only after the timeline settles (block#2560) ci: split Vitest across six shards so 3/4 stops hitting the job cap (block#2571) feat(github): unfurl PR and issue links inline with their title (block#1693) fix(auth): style federated sign-in callback pages (block#2549) fix(agents): align creation model controls with identity (block#2480) Run native Codex through plugin execution targets (block#2150) test: split Solo access browser scenarios (block#2499) feat(agents): let any agent change Buzz settings through the MCP (block#2248) Let plugins decorate host sidebar rows and add sort modes (block#1853) NEXUS-4082: Keep the new-message boundary stable during a visit (block#2083) feat(agents): move version history into settings (block#2374) fix(agents): read goose Activity by saved session ids and render memory bodies on open (block#2466) feat(channels): starter channels: admin switch and relay auto_join flag (block#1624) fix(plugins): read Square HTTPS sources with the Block SSH agent (block#2465) feat(plugins): let agents request plugin changes for owner review (block#2280) fix(messages): keep the hover toolbar while its menu is open (block#2439) feat(workflows): suggest channel members for @ in Send Message text (block#2309) AIDA-2344: Exclude muted channels from Dock (block#2313) feat(search): add slack-style message search operators on desktop (block#2463) test: fix invitation and coordination checks on main (block#2517) ... Signed-off-by: Cheng Peng <cheng@block.xyz> Co-authored-by: Cursor <cursoragent@cursor.com>
* upstream/main: (22 commits) test(browser): click timeline rows only after the timeline settles (block#2560) ci: split Vitest across six shards so 3/4 stops hitting the job cap (block#2571) feat(github): unfurl PR and issue links inline with their title (block#1693) fix(auth): style federated sign-in callback pages (block#2549) fix(agents): align creation model controls with identity (block#2480) Run native Codex through plugin execution targets (block#2150) test: split Solo access browser scenarios (block#2499) feat(agents): let any agent change Buzz settings through the MCP (block#2248) Let plugins decorate host sidebar rows and add sort modes (block#1853) NEXUS-4082: Keep the new-message boundary stable during a visit (block#2083) feat(agents): move version history into settings (block#2374) fix(agents): read goose Activity by saved session ids and render memory bodies on open (block#2466) feat(channels): starter channels: admin switch and relay auto_join flag (block#1624) fix(plugins): read Square HTTPS sources with the Block SSH agent (block#2465) feat(plugins): let agents request plugin changes for owner review (block#2280) fix(messages): keep the hover toolbar while its menu is open (block#2439) feat(workflows): suggest channel members for @ in Send Message text (block#2309) AIDA-2344: Exclude muted channels from Dock (block#2313) feat(search): add slack-style message search operators on desktop (block#2463) test: fix invitation and coordination checks on main (block#2517) ... Signed-off-by: Cheng Peng <cheng@block.xyz> Co-authored-by: Cursor <cursoragent@cursor.com>
* upstream/main: (91 commits) test(browser): click timeline rows only after the timeline settles (block#2560) ci: split Vitest across six shards so 3/4 stops hitting the job cap (block#2571) feat(github): unfurl PR and issue links inline with their title (block#1693) fix(auth): style federated sign-in callback pages (block#2549) fix(agents): align creation model controls with identity (block#2480) Run native Codex through plugin execution targets (block#2150) test: split Solo access browser scenarios (block#2499) feat(agents): let any agent change Buzz settings through the MCP (block#2248) Let plugins decorate host sidebar rows and add sort modes (block#1853) NEXUS-4082: Keep the new-message boundary stable during a visit (block#2083) feat(agents): move version history into settings (block#2374) fix(agents): read goose Activity by saved session ids and render memory bodies on open (block#2466) feat(channels): starter channels: admin switch and relay auto_join flag (block#1624) fix(plugins): read Square HTTPS sources with the Block SSH agent (block#2465) feat(plugins): let agents request plugin changes for owner review (block#2280) fix(messages): keep the hover toolbar while its menu is open (block#2439) feat(workflows): suggest channel members for @ in Send Message text (block#2309) AIDA-2344: Exclude muted channels from Dock (block#2313) feat(search): add slack-style message search operators on desktop (block#2463) test: fix invitation and coordination checks on main (block#2517) ... Signed-off-by: Cheng Peng <cheng@block.xyz> Co-authored-by: Cursor <cursoragent@cursor.com>
…nks-in-browser * origin/main: (138 commits) feat(composer): optional ⌘/Ctrl+Enter to send (block#2331) test: use keyboard activation for reply caret setup (block#2557) test: settle member search before invitation expansion (block#2532) test(agent-command): isolate the copied executable writer (block#2537) test(sidebar): wait for the rolled-back row to leave Starred before collapsing Work (block#2574) test(browser): click timeline rows only after the timeline settles (block#2560) ci: split Vitest across six shards so 3/4 stops hitting the job cap (block#2571) feat(github): unfurl PR and issue links inline with their title (block#1693) fix(auth): style federated sign-in callback pages (block#2549) fix(agents): align creation model controls with identity (block#2480) Run native Codex through plugin execution targets (block#2150) test: split Solo access browser scenarios (block#2499) feat(agents): let any agent change Buzz settings through the MCP (block#2248) Let plugins decorate host sidebar rows and add sort modes (block#1853) NEXUS-4082: Keep the new-message boundary stable during a visit (block#2083) feat(agents): move version history into settings (block#2374) fix(agents): read goose Activity by saved session ids and render memory bodies on open (block#2466) feat(channels): starter channels: admin switch and relay auto_join flag (block#1624) fix(plugins): read Square HTTPS sources with the Block SSH agent (block#2465) feat(plugins): let agents request plugin changes for owner review (block#2280) ... # Conflicts: # src/bundled/github/README.md
* commit 'a39dba132^': feat(github): unfurl PR and issue links inline with their title (block#1693) fix(auth): style federated sign-in callback pages (block#2549) fix(agents): align creation model controls with identity (block#2480) Run native Codex through plugin execution targets (block#2150) test: split Solo access browser scenarios (block#2499) feat(agents): let any agent change Buzz settings through the MCP (block#2248) Let plugins decorate host sidebar rows and add sort modes (block#1853) NEXUS-4082: Keep the new-message boundary stable during a visit (block#2083) feat(agents): move version history into settings (block#2374) fix(agents): read goose Activity by saved session ids and render memory bodies on open (block#2466) feat(channels): starter channels: admin switch and relay auto_join flag (block#1624) fix(plugins): read Square HTTPS sources with the Block SSH agent (block#2465) feat(plugins): let agents request plugin changes for owner review (block#2280) fix(messages): keep the hover toolbar while its menu is open (block#2439) Signed-off-by: damienrj <damien@squareup.com>
* origin/main: (121 commits) feat(composer): optional ⌘/Ctrl+Enter to send (block#2331) test: use keyboard activation for reply caret setup (block#2557) test: settle member search before invitation expansion (block#2532) test(agent-command): isolate the copied executable writer (block#2537) test(sidebar): wait for the rolled-back row to leave Starred before collapsing Work (block#2574) test(browser): click timeline rows only after the timeline settles (block#2560) ci: split Vitest across six shards so 3/4 stops hitting the job cap (block#2571) feat(github): unfurl PR and issue links inline with their title (block#1693) fix(auth): style federated sign-in callback pages (block#2549) fix(agents): align creation model controls with identity (block#2480) Run native Codex through plugin execution targets (block#2150) test: split Solo access browser scenarios (block#2499) feat(agents): let any agent change Buzz settings through the MCP (block#2248) Let plugins decorate host sidebar rows and add sort modes (block#1853) NEXUS-4082: Keep the new-message boundary stable during a visit (block#2083) feat(agents): move version history into settings (block#2374) fix(agents): read goose Activity by saved session ids and render memory bodies on open (block#2466) feat(channels): starter channels: admin switch and relay auto_join flag (block#1624) fix(plugins): read Square HTTPS sources with the Block SSH agent (block#2465) feat(plugins): let agents request plugin changes for owner review (block#2280) ... # Conflicts: # docs/buzz-help.md
…a channel (#2512) ## TL;DR Adds `conversation.ui.Conversation`, a public host-owned scope that lets a plugin's own `ui.Message` rows and `ui.Composer` for one channel get native Edit, confirmed Delete with recovery and retry, and host media review. Mounting it never marks anything read, follows a thread or enters the channel visit. Cross-channel overview cards need this; today a bare `ui.Message` row has neither Edit nor Delete, and `ui.Thread` always visits. Fixes NEXUS-4193 ## How it works `ui.Conversation({ session, scope, channelId, channelName?, threadId?, children(interactions) })` renders the existing `ConversationChannel` in its host-dialog, non-visiting mode (`host` plus `active={false}`). Inbox Drafts already uses this mode internally. Inside it: - `MessageManagement` supplies the row menu's Edit/Delete under the existing permission checks, the delete confirmation and the failed/unconfirmed recovery notice. - `MessageEditScope` makes Edit target the `ui.Composer` in the same scope. A scope without a composer offers no Edit, as in other previews. - `ConversationHost` owns media review and panel overlays, focus return, and the existing stale-session guard (nothing opens once the relay snapshot is no longer this ready session). `children` receives the host's `onOpenLink`, `canOpenLink` and `onOpenMediaReview` to pass to the rows. `EmbeddedThread` and the new scope now share one `HostedChannel` wrapper, so `ui.Thread` behavior is unchanged. No changes to `MessageManagement`, `ConversationHost`, `MessageComposer` or `contracts.ts`. ## Evidence `src/features/conversation/EmbeddedConversation.test.tsx` mounts two fictional channel cards (real relay session, real `ui.Message` rows and `ui.Composer` from `ConversationService`; only `MediaReviewViewer` is reduced, since it has its own suites). It covers: - the same rows without the scope have no Edit/Delete; inside it, each card offers both - mount, background presentation, re-present and unmount never call `enterChannel`, `leaveChannel`, `follow`, `markMessageRead`, `markChannelRead` or the roster's `profiles.reserve`, and publish nothing - Edit fills only that card's composer and publishes a kind-40003 edit tagged with that card's channel. The other card's message is untouched. - Delete: cancel publishes nothing; confirm publishes kind 5; a relay rejection shows the recovery notice in that card only; Retry resends the same event id and the row goes away - media review opens with the card's channel and message, and stops opening after the session stops being ready Fail-then-pass: with `active={false}` removed from the scope, all 4 tests fail (`enterChannel` and `profiles.reserve` are called). The scope restored, all pass. The packed `@buzz/author` consumer fixture (`tests/fixtures/conversation-consumer`) now renders its rows and composer inside `ui.Conversation` and passes `interactions.onOpenLink` to its rows. So the generated declarations must export the new component and its `children(interactions)` type, and the existing `conversation.spec.mjs` journey mounts it in the real app. The fixture type-checks against freshly built declarations. As a negative control, a misspelled interactions field fails `tsc`. Run on a Linux Blox box on base `a34ba4f` (the local host-build guard refused under load): - `vitest run src/features/conversation src/features/messages/MessageManagement.test.tsx src/bundled/inbox/DraftsView.test.tsx`: 19 files, 221 tests passed - `tsc --noEmit` and `biome check --error-on-warnings` on the touched files: clean - `pnpm author:build`, packed and installed into a copy of the consumer fixture, then `pnpm build` (`tsc`): clean No browser journeys were added or removed. The existing consumer journey now mounts the scope; it runs only in hosted CI, not locally. Focus, overlay and native-edit geometry are unchanged existing `ConversationHost`/composer behavior that existing suites already cover. ## Limits and coordination - Not exercised in the native desktop app or a browser journey. Hosted CI runs the full suites. - Two `ui.Conversation`s for the same channel each show that channel's recovery notice, because recovery is per channel. The docs say to render one per channel or card. - No overlap in hunks or behavior with #2483 (Solo list arrangement/row action in `service.tsx`), #1889, #2306 or #2303 (`contracts.ts`), #2083 (`MessageManagement.tsx`) or #2167 (`ConversationHost.tsx`). This PR does not edit `contracts.ts`, `MessageManagement.tsx` or `ConversationHost.tsx`. - This PR doesn't adopt the scope in any plugin. That is separate work. ## Initiative context [Buzz Threads status](https://g2.sqprod.co/apps/kylez-buzz-threads-status) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Kyle Zemek <kylez@squareup.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…3071) ## Problem The "New messages" divider from #2083 never appeared in a channel with more than 20 top-level messages, which is most channels anyone is behind in. The channel window loads 20 rows on entry, so such a channel always enters with `hasMore`, and the entry boundary refused to settle while history was incomplete. The guard existed for a real reason: with unloaded rows above the window, the first unread *loaded* row need not be the first unread row. **Repro** (fixture relay, `readState: true`, `historyCounts: { alpha: 25, beta: 15 }`): 1. Open #Alpha (25 messages), stay at the bottom for a second so the catch-up read saves. 2. Open #Beta (15 messages) and stay at the bottom. 3. From another account, post three messages to #Alpha, then reopen it: three new rows, **no line**. 4. Post three messages to #Beta, reopen it: the line sits above the first new row. Same read state, same three arrivals; only the history length differs. ## Fix - **`oldestUnreadId` on the channel snapshot** (`src/features/relay/unread.ts`): names the oldest unread message the timeline shows (a top-level post or broadcast reply, in rendered order). It ignores own messages, quiet agent rows and non-broadcast replies, follows reads, and clears when nothing is unread. It comes from the same bounded evidence as `observedCount`. `equal()` compares it so subscribers see it move. - **Timeline check** (`src/features/messages/ChannelTimeline.tsx`): when entry history is incomplete, the boundary settles with no line unless `oldestUnreadId` is a row loaded on entry. Complete-history visits still place the line at the first unread entry row, but no longer wait on `hasMore` or `historyLimited`, so a window that gains `hasMore` after entry (live arrivals trimming it) settles from the entry rows instead of staying pending. The pending boundary filters rows to the entry set before classifying them. The line still stays put for the visit as older pages load, rows arrive and the reader scrolls. `MessageManagement.tsx` is untouched; `entryHistoryComplete` means what it meant. - **`sampled` wait**: a new snapshot flag saying a repair has read every channel in unread scope since evidence was last emptied (cache clear or capacity overflow) or a channel entered that scope. Later repairs (reconnect, backstop, refresh) only add evidence and leave it set, so they publish no extra snapshots. Incomplete-history visits wait for it before settling. Without it, a Chromium probe that held the repair read and re-entered a 25-message channel drew the line at message 5 while messages 0–4 were unread too. - **Read-row guard**: with incomplete history, the line also needs a read entry row above the first unread one. This covers a repair batch that filled its 500-row limit and stopped inside the window, leaving the first loaded row the oldest unread message known. Catch-up's `activity:` mark reads all ordinary rows up to a timestamp, so a read ordinary row vouches for the rows above it. - **Cache clear now restarts the repair**: `clear()` used to leave a connected session without a repair until the 15-minute backstop or a reconnect; it now starts one at once, as a newly accessible channel does. An overflow still waits for the next recovery tick. - **Help and docs**: `docs/buzz-help.md` describes the rule instead of the old limitation; `docs/unread.md` documents both fields with the bounded-evidence caveat. ## Known limits - The line can appear a moment after entry, once the repair finishes. Until then the channel holds its own reading (dwell and bottom catch-up), so entering does not erase the evidence its divider waits for; the timeline asks the store for the repair itself, so a host without the shell's startup request still gets one. - If that repair fails or evidence overflows (freshness `stale`, including a stale reconnect), the hold is released and reading resumes with no divider for that visit. - Rows dwelt on during the hold and left before it lifts are not marked read by that dwell; the next dwell reads them. Catch-up at the bottom still reads the ordinary rows once the hold lifts. - No line when the unread messages begin exactly at the first loaded row (the read-row guard has nothing to vouch for them). - Mentions, broadcasts, broadcast replies and messages marked unread above the window are read only by their own `msg:`/`thread:` marks, never by `activity:`. A repair sample that stopped short can still hide one of them, and the line then lands below it. Recorded in `docs/unread.md`. - The flipped pinned test variant depends on mount timing: a remote read that leaves a loaded row the oldest unread places the line there only if it lands before the timeline is presented. The same read after the boundary has settled leaves no line, because the boundary is captured once per visit. ## Reviewer notes - **FOUNDATION**: `UnreadSnapshot` is re-exported through the FOUNDATION type entry `src/plugins/author.ts`. This adds two optional fields in `unread.ts` and does not edit `author.ts`, the same shape as #2998's `row` field. Named here so the rule is applied knowingly. - **Conflicts with #2401**, which rewrites `compute()` in `unread.ts`. The `oldestUnreadId` hunks will conflict with whichever lands second; this one is about 50 lines to re-apply. #2049 moves the divider JSX and #2652 touches a nearby effect; neither overlaps the boundary logic. - **Side effect**: because a cache clear now restarts the repair at once, sidebar unread counts and Inbox mentions return immediately after a clear instead of after the backstop. Three startup tests that pinned "nothing is read after a clear" now expect the restart; an Inbox test that expected activity to stay hidden after a clear now checks it is not reported as nothing-to-read before the repair returns. - The flipped variant of the pinned "outside the entry window" test is a deliberate product call (see Known limits). A reviewer who wants the stricter #2083 reading can keep the third variant and drop the new case, at the cost of no line in that race. ## Testing | Check | Result | |---|---| | `tsc --noEmit`, Biome on changed files | clean | | Four related Vitest files (`ChannelTimeline.restore`, `unread`, `MessageManagement`, `ChannelTimeline`) | 479 passed | | Relay, messages, channel-navigation, bundled/channels suites | 7,706 passed | | Full Vitest | 1,034 files passed | | `tests/browser/new-message-boundary.spec.mjs` | 4/4 Chromium, 4/4 WebKit, 20/20 repeated | | Mutation checks | removing each guard, the restart or the reset breaks at least one test | The new browser case seeds 25 messages, reads to the bottom, receives three from a peer and asserts the line sits above the first; it fails on main and passes here. New restore cases place the line inside incomplete history (presented and late mount, stable while older rows page in) and omit it at a first loaded row with no read row before it. `unread.test.ts` and `unread-startup.test.ts` cover `oldestUnreadId` and `sampled` through refresh, clear, overflow and a channel entering scope mid-repair. **Not run**: the native app via `just desktop`, the notification and Inbox DM entry paths, and the restart after a clear against a real relay. The final amend changed only tests and the publish order of one reset, so the browser spec and wider suites were not re-run after it. ## Before and after Same Playwright flow against the fixture relay (`readState: true`, `historyCounts: { alpha: 25, beta: 15 }`) on main `a905c4865` (left) and this branch (right). Around 8–13 s #Alpha (25 messages) is reopened after three peer messages: the left has nothing above "Later message 1", the right shows the "New messages" line between message 24 and "Later message 1". From about 15 s #Beta (15 messages) shows the line on both sides. https://github.com/user-attachments/assets/07f643fa-65b6-47f0-9012-44e37901a27c | Before (main `a905c4865`) | After | |---|---| |  |  | 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: mtoohey <mtoohey@squareup.com> Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
TL;DR
Keep the “New messages” divider fixed during a channel visit, even as reading advances unread state or the timeline temporarily remounts. Capture one verified entry boundary rather than deriving it from the moving unread cursor. Fixes NEXUS-4082.
How
The existing message-visit owner records loaded row IDs, verified history completeness, the read-intent revision, and read evidence before the timeline mounts. Unresolved attention stays uncertain; later synchronization cannot redefine entry. The timeline captures one unread message ID and keeps it under that visit token. It checks evidence in chronological order only through that first unread row. Messages received during loading or an inline seek cannot become the entry boundary. If the entry boundary cannot be proved, omit the divider rather than guess.
Read eligibility, focus requirements, membership checks, settled-reading dwell, and read-state writes remain with their existing owners. No new focus handoff or force-read operation is included. Normal guest-channel opening focus and Search Buzz dismissal belong to the human-owned #2140; this PR neither implements nor depends on that change.
Verification
Snapshot
8219ae795includes the merged selector repair #2493. Blox passed root typecheck, touched-file Biome, design checks, and 830 tests across 16 complete affected files. All 58 browser cases passed in Chromium and WebKit across the completenew-message-boundary,history-loading,activity-groups,activity-lifecycle, andnavigation-row-menufiles, without skips or retries. All 39 selected source hashes match the uploaded snapshot and committed files; downloaded artifact digests match the receipt. The box was deleted. Current-head hosted CI is pending.The six later-row ordering cases failed before this repair, while six earlier-row controls passed. All 12 now pass: later unresolved evidence cannot suppress an earlier proven unread boundary. Unresolved or changed evidence at or before that boundary still prevents choosing it.
The retained divider scope adds three browser scenarios (six engine cases), removing none. Browser-only coverage exercises real virtualizer geometry, keyboard scrolling, selection/copy, and signed delivery through the broker/store. The saved-offset regression previously failed with 32px drift in both engines; the original
<4pxtolerance is unchanged. The mounted ordering matrix stays in React Testing Library. No browser cases were added or removed by this repair.The remount fixture toggles timeline presentation under
MessageManagement; it does not drive the realConversationTab→ChannelBodyinline-seek route. That wiring was source-traced only. Native WebView and live multi-device behavior remain unproven.Earlier opening-focus scope was withdrawn: its ten mounted cases and two width-based browser journeys (four engine cases) were removed, with
ChannelsPage.tsxandchannel-permissions.spec.mjsrestored to base. Those cases covered the withdrawn focus behavior, not this divider. No replacement focus proof is claimed.Historical synthetic screenshots at
993af5f(not current-head native verification).Before the boundary.

After the boundary.

After channel revisit, with grouped messages.

Narrow, dark view with larger text.

Limits
The divider stays absent for the whole visit when history was incomplete or unverified at entry, even if older rows load later. It may also be absent if entry membership or evidence at or before the first unread row cannot be proved. Read-intent changes or changed evidence needed to locate the boundary before capture conservatively omit it. Uncertainty in a later row no longer suppresses an earlier proven unread boundary. A visible read/unread transition cannot exclude older unread messages, and delayed rows cannot establish entry membership.
A read change in another channel while entry awaits evidence can also suppress the divider for this visit. Entry reconciliation can clear a prior message-force marker and advance the same global revision. The public API does not expose a target-specific entry set or mutation cause. This PR retains that conservative guard; focused tests preserve the omission and verify the affected messages remain unread.
The original live #announcements unread report remains unproved. Private screenshots did not establish its channel role or focus state and are not published. This divider-only change claims no confirmed fix for that incident. Native WebView was not exercised.
Coordination
Based on main, including the merged held-reader resize and fixture repairs. Keeps the existing unread/relay owners and does not expand their APIs. The opening-focus overlap was removed rather than creating a competing owner or an ordering dependency on #2140.
— Generated with Codex (gpt-6.1-sol) on behalf of kylez.