Skip to content

feat: sync thread read state across clients - #9124

Open
t3dotgg wants to merge 6 commits into
mainfrom
t3code/server-owned-thread-read-state
Open

feat: sync thread read state across clients#9124
t3dotgg wants to merge 6 commits into
mainfrom
t3code/server-owned-thread-read-state

Conversation

@t3dotgg

@t3dotgg t3dotgg commented Sep 1, 2026

Copy link
Copy Markdown
Member

Opening a thread on one device did not clear its Done indicator anywhere else. Read state lived in each web client's local storage, and mobile had no read state at all.

The server now owns a viewedAt timestamp per thread, the same way it owns settlement since #8600. Clients send thread.view when a focused, visible thread's latest completion is read and thread.mark-unread from the existing menus, then render the server value directly. There is no optimistic local override, so the indicator flips when the shell update arrives, one round trip later.

Details:

  • The decider caps the boundary at server time and never moves it backward, so a stale view cannot undo a newer completion or a mark-unread.
  • Web only acknowledges a view when the document is visible and focused. Mobile only acknowledges when the thread route is focused, the app is active, and the environment is connected.
  • Viewing does not bump updatedAt, so reading a thread never reorders the list. View-only events are excluded from agent awareness.
  • Servers without the threadViewState capability keep the previous device-local fallback. Migration 044 backfills existing threads as read.

Replaces #6662, which carried the same server model plus about 550 lines of client-side pending, retry, and reconciliation state. This PR keeps the server half and drops the rest.

Tests: focused decider, migration, projection, relay, contracts, client-runtime, web, and mobile tests, plus typecheck for contracts, client-runtime, server, web, and mobile.

Made with Claude Fable 5.1 through Claude Code.


Note

Medium Risk
Changes orchestration commands, projection schema/migration backfill, and client read/unread timing; incorrect ack rules or backfill could show wrong Done state across devices.

Overview
Adds server-persisted thread read state so clearing a thread’s Done indicator on one device updates every connected client. Each thread gets a viewedAt boundary (DB migration 048, projection/query/reducer paths), and the server advertises the threadViewState capability.

thread.view records the completion the user actually saw (viewedThrough, capped at server time, never moved backward; does not bump updatedAt). thread.mark-unread sets viewedAt just before the latest completed turn. Both emit thread.meta-updated with viewedAt for backward-compatible event shape.

Web routes mark-read/unread through useThreadViewState: server commands when capable, otherwise the existing local visit store. Read acks wait for visible/focused document and a live connection when using the server path. Sidebars and indicators prefer thread.viewedAt over local stamps.

Mobile sends thread.view on focused thread routes when the app is active, connected, detail is loaded, and shouldAcknowledgeThreadView says the completion is still unread; viewedAt is included in thread selection shells.

Agent awareness ignores view-only thread.meta-updated events so read sync does not ping agents. User docs describe cross-device read/unread and the upgrade backfill (existing threads treated as read).

Reviewed by Cursor Bugbot for commit aa961ac. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add server-backed thread read state syncing across clients

  • Adds thread.view and thread.mark-unread commands to persist thread read state on the server.
  • Web, mobile, and sidebar components use useThreadViewState to sync read state when the server advertises the threadViewState capability.
  • Adds 048_ProjectionThreadsViewedAt migration with a nullable viewed_at column on projection_threads.
  • Risk: 048_ProjectionThreadsViewedAt backfills existing projection_threads rows by setting viewed_at to updated_at (or created_at if null), marking all old threads as read.

Macroscope summarized aa961ac.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Sep 1, 2026
Comment thread apps/web/src/hooks/useThreadViewState.ts Outdated
Comment thread packages/contracts/src/orchestration.ts
@github-actions

github-actions Bot commented Sep 1, 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 13.6 KiB 13.6 KiB −14 B (−0.1%) 15.1 KiB
Codex Thread snapshot wire 7.0 KiB 7.0 KiB +1 B (+0.0%) 7.3 KiB
Codex Live turn WebSocket wire 6.6 KiB 6.6 KiB −15 B (−0.2%) 7.8 KiB
Codex Live turn WebSocket decoded 57.1 KiB 57.1 KiB 0 B (0.0%) 66.4 KiB
Codex Live turn messages 10 10 0 (0.0%) 21
Claude Total thread wire 13.6 KiB 13.6 KiB +32 B (+0.2%) 15.1 KiB
Claude Thread snapshot wire 7.0 KiB 7.0 KiB 0 B (0.0%) 7.3 KiB
Claude Live turn WebSocket wire 6.5 KiB 6.6 KiB +32 B (+0.5%) 7.8 KiB
Claude Live turn WebSocket decoded 57.9 KiB 57.9 KiB 0 B (0.0%) 66.4 KiB
Claude Live turn messages 10 10 0 (0.0%) 21

Baseline: 2fa5ef4 · PR result: aa961ac · 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: 113.9 KiB
  • Claude decoded thread snapshot: 114.6 KiB

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

@github-actions github-actions Bot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Sep 1, 2026
Comment thread apps/web/src/components/ChatView.tsx
@macroscopeapp

macroscopeapp Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR adds server-owned thread read state across web and mobile, including new orchestration commands, persistence, migration backfill, and focus/connection-dependent client behavior. The cross-client workflow and changed default treatment of existing threads create a broad runtime surface that merits human review.

You can add or adjust custom eligibility rules. Learn more.

@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 d6aad18047e49be65cc9dd672893f8f24e3de3ca. Configure here.

Comment thread apps/web/src/components/ChatView.tsx Outdated
Comment thread apps/web/src/components/ChatView.tsx Outdated
@t3dotgg
t3dotgg force-pushed the t3code/server-owned-thread-read-state branch from 3626c5c to 9175d4f Compare September 2, 2026 01:52
Thread read and unread state lived in each web client's local storage,
so opening a thread on one device did not clear its Done indicator
anywhere else.

The server now owns a viewedAt timestamp per thread. Clients send
thread.view when a focused, visible thread's latest completion is read
and thread.mark-unread from the existing menus. The decider caps the
boundary at server time and never moves it backward. Clients render the
server value directly, with no optimistic override, the same shape as
server-side settlement. Servers without the threadViewState capability
keep the previous local fallback. Migration 044 backfills existing
threads as read.

Made with Claude Fable 5.1 through Claude Code.
An acknowledgement sent before the capability loaded fell through to
local storage and never reached other clients.
readEnvironmentSupportsViewState now returns undefined until the config
arrives, and the hook drops the write instead of falling back to local
storage on every entry point.
…nd load

Web kept its focus listeners only until the first acknowledgement, so a
thread marked unread elsewhere was not re-acked on refocus. It also sent
the acknowledgement while disconnected, where it failed silently with no
retry. Mobile acknowledged from the thread shell before messages loaded.

Web now keeps the listeners for the life of the effect and waits for a
connected environment, re-running on reconnect. Mobile acknowledges only
once the loaded detail includes the completion.
@t3dotgg
t3dotgg force-pushed the t3code/server-owned-thread-read-state branch from 9175d4f to aa961ac Compare September 5, 2026 05:59
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