feat(server): sync thread read status across clients - #5652
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:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 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: Needs human review 1 blocking correctness issue found. This PR introduces a new feature for server-synchronized thread read/unread status, including new commands, events, database migrations, and schema changes. The scope involves significant new runtime behavior. An unresolved Medium severity finding identifies a potential issue where clients can submit far-future timestamps to mark all future completions as read. You can customize Macroscope's approvability policy. Learn more. |
9b9ebb8 to
92bcd1e
Compare
92bcd1e to
1ec6fd1
Compare
1ec6fd1 to
fededda
Compare
d3a0393 to
6f0466d
Compare
6f0466d to
4a75259
Compare
4a75259 to
f3e1ea0
Compare
f3e1ea0 to
f08fd94
Compare
|
Terrible when compared to #5652 |
|
Wrong pr my bad, too many tabs lol |
f08fd94 to
0125c6f
Compare
| threadId: ThreadId, | ||
| }); | ||
|
|
||
| const ThreadMarkViewedCommand = Schema.Struct({ |
There was a problem hiding this comment.
🟡 Medium src/orchestration.ts:674
ThreadMarkViewedCommand.viewedAt is documented as a server-owned boundary copied from thread completion/wake state, but the schema accepts any client-supplied IsoDateTime. Because the decider persists this value directly (only comparing it against the previous one), a client can submit a far-future timestamp and make all subsequent completions and wakes appear already read. The server should derive or validate this boundary against actual thread state rather than trusting the client value.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/contracts/src/orchestration.ts around line 674:
`ThreadMarkViewedCommand.viewedAt` is documented as a server-owned boundary copied from thread completion/wake state, but the schema accepts any client-supplied `IsoDateTime`. Because the decider persists this value directly (only comparing it against the previous one), a client can submit a far-future timestamp and make all subsequent completions and wakes appear already read. The server should derive or validate this boundary against actual thread state rather than trusting the client value.
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 0125c6f. Configure here.
0125c6f to
19b837e
Compare

Thread read and unread state lived in web local storage, so browsers and devices could disagree about which completed threads had been seen.
This moves
lastViewedAtinto the event-sourced server projection and adds server-owned mark-viewed and mark-unread commands. Web and mobile now update the shared state, older servers are capability-gated, and view updates do not touchupdatedAtor reorder threads. Existing threads remain read by default after migration.Verified with focused decider, projection, persistence, command, reducer, sidebar, and UI-state tests (177 passing), plus scoped typechecks for contracts, client-runtime, server, web, and mobile.
Built with GPT-5.6-sol via the Codex harness in T3 Code.
Note
Medium Risk
Touches orchestration commands, projection, and migration backfill; upgrade backfill can briefly change unread semantics for existing threads until users view them again.
Overview
Thread read/unread state moves from per-browser local storage to the server, so web and mobile share the same
lastViewedAtand sidebar badges stay consistent across devices.The server gains
threadViewStatuscapability,thread.mark-viewed/thread.mark-unreadcommands,thread.view-status-updatedevents, andlast_viewed_atonprojection_threads(migration 041 backfills existing rows at upgrade time). The decider enforces optimistic concurrency on mark-viewed and never moves view timestamps backward; view updates do not bumpupdatedAt.Clients call
threadEnvironment.markViewed/markUnreadwhen the capability is present (mobile on screen focus, web on open thread / wake ack / settle-archive paths).uiStateStoredropsthreadLastVisitedAtById; unread, “Woke”, and mark-unread menus usethread.lastViewedAtandhasUnseenWake. Client-runtime wiresmarkViewedwith queuedsupersededViewedAtfor serialized views.Reviewed by Cursor Bugbot for commit 19b837e. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Sync thread read/unread status across clients via server-owned view status
thread.mark-viewedandthread.mark-unreadcommands to the orchestration layer, with athread.view-status-updatedevent andlastViewedAtfield persisted on threads inprojection_threads.markThreadVisited,threadLastVisitedAtById) fromuiStateStore; unread/woke indicators now derive from server-providedlastViewedAt.threadViewStatuscapability flag from the server; all mark-viewed/mark-unread UI actions (sidebar context menus, wake acknowledgement, archive/settle) are gated on this capability and fall back gracefully when unsupported.last_viewed_attoprojection_threadsand backfill existing rows with the current time.threadLastVisitedAtByIddata in localStorage is silently dropped on next load; read state is reset to server state for all users after upgrade.Macroscope summarized 19b837e.