feat: sync thread unread state across clients - #6662
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Warning Your free Security trial is over. An organization admin can activate Security or dismiss this notice. Comment |
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: Scenario and decoded snapshot size10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This PR introduces cross-client unread-state synchronization across web, mobile, orchestration, event relay, and persisted server projections, including a migration that changes the initial state of existing threads. Its broad production behavior and default enablement warrant human review despite strong backward-compatibility and test coverage. No code changes detected at You can add or adjust custom eligibility rules. Learn more. |
120eab2 to
2e47e2e
Compare
dc0e9ba to
af30d1d
Compare
91116b1 to
46c3bd6
Compare
277c9aa to
ed6e465
Compare
b3065a9 to
3f85698
Compare
3f85698 to
b862ca4
Compare
f4435df to
4cb57f4
Compare
4cb57f4 to
6a1c5b7
Compare
6a1c5b7 to
192e968
Compare
192e968 to
c55dd68
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit c55dd68. Configure here.
|
Note 🤖 Claude Fable 5.1 responding on behalf of Theo Closing in favor of #9124, which keeps this branch's server model (viewedAt column, thread.view and thread.mark-unread, monotonic decider, capability gate) and drops the client-side pending, retry, and reconciliation state. Every review round here added another layer of optimistic client machinery, and the diff grew past 950 non-test lines. Since #8600 moved settlement server-side with clients rendering server state directly, read state can use the same shape. #9124 is about 420 non-test lines with the same behavior and no dev-database migration repair. |

Thread unread state lived in each web client, so viewing or marking a thread unread did not follow the user to another client.
This stores a server-owned viewed timestamp and sends view and mark-unread commands through the existing event stream. Existing threads are backfilled as viewed, older servers keep the local fallback, and mobile reports completed threads when opened.
Tests:
Created by GPT-5.6 Sol through the Codex harness in T3 Code.
Note
Medium Risk
Touches orchestration commands, projections, and multi-client optimistic concurrency for thread metadata; behavior is heavily tested but incorrect guards could leave threads stuck read/unread across devices.
Overview
Adds server-backed thread read state so clearing a Done badge or marking a thread unread can follow the user across web and mobile, with a local-only fallback when the environment does not advertise
threadViewState.The server introduces
thread.viewandthread.mark-unreadorchestration commands (with optionalexpectedViewedAt/expectedCompletedAtguards), persistsviewedAtvia existingthread.meta-updatedevents and projectionviewed_at, advertises thethreadViewStatecapability, and runs migration 044 to backfill existing rows as read. Client runtime wires the new commands on a dedicated view-state scheduler so passive acknowledgements do not block turn actions.Web replaces direct
markThreadVisited/markThreadUnreadstore calls withuseThreadViewStateandresolveThreadViewedAt, including pending/local-only markers, visibility-gated completion acknowledgement in chat, and sidebar/status indicator updates. Mobile sends guarded view commands when a thread route is focused, the app is active, and the server is connected. Agent awareness ignores meta updates that only change view timestamps.Reviewed by Cursor Bugbot for commit c55dd68. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add server-backed thread view/unread state with
thread.viewandthread.mark-unreadcommandsthread.viewandthread.mark-unreadorchestration commands validated indecider.decideOrchestrationCommand, emittingthread.meta-updatedevents withviewedAt; contracts, client runtime commands, and projector all updated to carry the new fieldviewed_attoprojection_threads(backfilled fromupdated_at/created_at) and repairs missingunsettled_atuseThreadViewStatehook andresolveThreadViewedAtutil that coordinate local pending markers with server state; sidebar, chat view, and status indicators now display resolved visited timestampsThreadRouteScreenderives guarded view commands viaresolveThreadViewCommandand dispatches them on focus when the server advertisesthreadViewStatecapabilityAgentAwarenessRelay.shouldPublishAgentAwarenessEventsuppresses publication for meta updates that only changeviewedAt/updatedAtOrchestrationThread,OrchestrationThreadShell, andThreadMetaUpdatedPayloadgain an optionalviewedAt; old clients/servers still decode but ignore it.projection_threads.viewed_atis initialized tocreated_atfor new threads.persistState/parsePersistedStatenow persist only valid local-onlythreadViewStatePendingByIdentries📊 Macroscope summarized c55dd68. 30 files reviewed, 3 issues evaluated, 3 issues filtered, 0 comments posted
🗂️ Filtered Issues
apps/mobile/src/features/threads/threadViewState.ts — 0 comments posted, 1 evaluated, 1 filtered
resolveAppliedThreadViewBoundaryonly recognizes an applied view when the server timestamp exactly equals a requested boundary. If a completed timestamp is ahead of server time, the server caps the view to its current time; that valid response remains belowcompletedAtbut does not match the requested future boundary. The tracking effect then leavesretryBoundaryunset, so its focus callback is not re-created and the mobile client never retries after the boundary becomes reachable, leaving the thread unread until an unrelated focus/dependency change. [ Already posted ]apps/web/src/components/Sidebar.tsx — 0 comments posted, 1 evaluated, 1 filtered
acknowledgeWokecallsmarkViewedwhile an environment's server configuration can still be unavailable. In that state,useThreadViewStatetakes its local-only fallback when the shell has noviewedAt; this sidebar callback is only invoked by the one user click and is never retried when thethreadViewStatecapability later loads. Thus dismissing a Woke notification during startup remains local and other clients continue to show the thread as unread/woken despite the server supporting synchronization. [ Already posted ]apps/web/src/uiStateStore.ts — 0 comments posted, 1 evaluated, 1 filtered
persistStatestrips every in-flight view/unread operation becausesanitizeLocalThreadViewStateonly retains entries whoselocalOnlyis alreadytrue. If the tab is closed or reloaded aftersetThreadViewStatePendingand the optimistic local timestamp update but before the asynchronous server command resolves, the unload flush saves no pending intent. On the next loadresolveThreadViewedAttreats the old server timestamp as authoritative, so a view or mark-unread action that was not processed by the server is silently lost rather than retried or retained locally. [ Out of scope (triage) ]