Skip to content

feat(server): sync thread read status across clients - #5652

Open
t3dotgg wants to merge 1 commit into
mainfrom
t3code/server-side-thread-status
Open

feat(server): sync thread read status across clients#5652
t3dotgg wants to merge 1 commit into
mainfrom
t3code/server-side-thread-status

Conversation

@t3dotgg

@t3dotgg t3dotgg commented Aug 7, 2026

Copy link
Copy Markdown
Member

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 lastViewedAt into 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 touch updatedAt or 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, DB migration backfill, and cross-client read semantics; migration timestamps could briefly skew unread indicators until users view threads again.

Overview
Thread read/unread is now server-owned instead of per-browser threadLastVisitedAtById in uiStateStore. Threads carry lastViewedAt through contracts, projection (projection_threads + migration 039), snapshot queries, and client reducers; view updates emit thread.view-status-updated without bumping updatedAt.

New orchestration commands thread.mark-viewed and thread.mark-unread (with optimistic concurrency on viewed boundaries) are wired through client-runtime atoms (markViewed / markUnread) and advertised via the threadViewStatus capability on the server descriptor.

Web and mobile call markThreadViewed when opening a thread (and on wake/settle flows where applicable); sidebars, wake banners, and context menus use lastViewedAt and hasUnseenWake, and hide mark-unread when the server lacks support. Existing threads are backfilled on migrate so upgrades do not flood unread state.

Reviewed by Cursor Bugbot for commit 0125c6f. 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 state

  • Replaces client-local threadLastVisitedAtById timestamps in uiStateStore with server-persisted lastViewedAt, tracked via new thread.mark-viewed and thread.mark-unread orchestration commands.
  • Adds threadViewStatus capability to the server descriptor; clients check this capability before issuing view-state mutations, so older servers are unaffected.
  • The decider handles thread.mark-viewed (with optimistic concurrency via expectedLastViewedAt/supersededViewedAt) and thread.mark-unread (sets lastViewedAt to 1ms before the latest turn completion), emitting thread.view-status-updated events.
  • Migration 039 adds last_viewed_at to projection_threads and backfills existing rows with the migration timestamp.
  • Sidebar unread indicators, wake banners, and context-menu 'Mark unread' items now derive state from thread.lastViewedAt and are hidden when the server lacks threadViewStatus support.
  • Risk: existing threads are backfilled with the migration time as last_viewed_at, so threads that were previously read may briefly reappear as unread after the migration runs.

Macroscope summarized 0125c6f.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 99cc9244-8c4b-4e3b-8b93-4634f836c57d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added size:XL 500-999 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Aug 7, 2026
Comment thread apps/web/src/components/SidebarV2.tsx Outdated
Comment thread apps/mobile/src/features/threads/ThreadRouteScreen.tsx Outdated
Comment thread apps/web/src/components/ChatView.tsx
Comment thread apps/web/src/components/SidebarV2.tsx Outdated
Comment thread apps/server/src/orchestration/decider.ts
Comment thread apps/web/src/components/SidebarV2.tsx Outdated
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 11.3 KiB 11.3 KiB +9 B (+0.1%) 15.1 KiB
Codex Thread snapshot wire 5.5 KiB 5.5 KiB +11 B (+0.2%) 7.3 KiB
Codex Live turn WebSocket wire 5.9 KiB 5.9 KiB −2 B (−0.0%) 7.8 KiB
Codex Live turn WebSocket decoded 49.7 KiB 49.7 KiB 0 B (0.0%) 66.4 KiB
Codex Live turn messages 16 16 0 (0.0%) 21
Claude Total thread wire 11.3 KiB 11.4 KiB +18 B (+0.2%) 15.1 KiB
Claude Thread snapshot wire 5.5 KiB 5.5 KiB +11 B (+0.2%) 7.3 KiB
Claude Live turn WebSocket wire 5.9 KiB 5.9 KiB +7 B (+0.1%) 7.8 KiB
Claude Live turn WebSocket decoded 50.6 KiB 50.6 KiB 0 B (0.0%) 66.4 KiB
Claude Live turn messages 16 16 0 (0.0%) 21

Baseline: 0640410 · PR result: 0125c6f · Source CI: success

Scenario and decoded snapshot size

10 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.

  • Codex decoded thread snapshot: 94.7 KiB
  • Claude decoded thread snapshot: 95.4 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@macroscopeapp

macroscopeapp Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR adds a new feature for syncing thread read/unread status across clients via the server. It includes a database migration, new orchestration commands and events, and changes how read state is tracked (from client-local to server-synchronized). The scope and nature of these changes warrant human review.

No code changes detected at 0125c6f. Prior analysis still applies.

You can customize Macroscope's approvability policy. Learn more.

@t3dotgg
t3dotgg force-pushed the t3code/server-side-thread-status branch from 9b9ebb8 to 92bcd1e Compare August 8, 2026 08:01
Comment thread apps/web/src/hooks/useThreadActions.ts
Comment thread apps/web/src/components/Sidebar.logic.ts
@t3dotgg
t3dotgg force-pushed the t3code/server-side-thread-status branch from 92bcd1e to 1ec6fd1 Compare August 8, 2026 08:07
Comment thread apps/server/src/orchestration/decider.ts
@t3dotgg
t3dotgg force-pushed the t3code/server-side-thread-status branch from 1ec6fd1 to fededda Compare August 8, 2026 08:10
Comment thread apps/web/src/components/threadActionMenu.logic.ts Outdated
Comment thread apps/web/src/components/ChatView.tsx
@t3dotgg
t3dotgg force-pushed the t3code/server-side-thread-status branch 2 times, most recently from d3a0393 to 6f0466d Compare August 8, 2026 08:25
Comment thread apps/web/src/components/ChatView.tsx
Comment thread apps/mobile/src/features/threads/ThreadRouteScreen.tsx
@t3dotgg
t3dotgg force-pushed the t3code/server-side-thread-status branch from 6f0466d to 4a75259 Compare August 8, 2026 08:35
Comment thread apps/web/src/components/ChatView.tsx Outdated
@t3dotgg
t3dotgg force-pushed the t3code/server-side-thread-status branch from 4a75259 to f3e1ea0 Compare August 8, 2026 08:39
Comment thread apps/web/src/hooks/useThreadActions.ts
Comment thread packages/client-runtime/src/state/threadCommands.ts Outdated
@t3dotgg
t3dotgg force-pushed the t3code/server-side-thread-status branch from f3e1ea0 to f08fd94 Compare August 8, 2026 08:49
@t3dotgg

t3dotgg commented Aug 8, 2026

Copy link
Copy Markdown
Member Author

Terrible when compared to #5652

@t3dotgg

t3dotgg commented Aug 8, 2026

Copy link
Copy Markdown
Member Author

Wrong pr my bad, too many tabs lol

@t3dotgg
t3dotgg force-pushed the t3code/server-side-thread-status branch from f08fd94 to 0125c6f Compare August 8, 2026 10:47
threadId: ThreadId,
});

const ThreadMarkViewedCommand = Schema.Struct({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

.run(registry, {
environmentId: target.environmentId,
input: { ...input, expectedLastViewedAt, supersededViewedAt },
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale view token rejects open-after-unread

Medium Severity

markViewed snapshots expectedLastViewedAt from the shell before the per-thread serial queue runs. If mark-unread is ahead in that queue or has landed on the server before the shell refreshes, the following open-thread mark-viewed still sends the pre-unread token, fails the decider invariant, and leaves the thread unread while the user is viewing it.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0125c6f. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant