perf(server): visiting a thread no longer copies the whole thread into the event log#31849
Problem
thread.visited carries the whole thread as its payload, so every visit writes a full copy of the thread into the event log and sends it to every client subscribed to that thread. A visit only changes lastVisitedAt, plus opens when the user opened the thread. The existing comment in dispatchThreadVisit already says a read receipt shouldn't scale with the thread's history. The payload still does, though, mostly through pullRequests.
On my server, one long-lived thread is linked to 847 pull requests, so its thread payload is 866 KB, 99.9% of it pull-request links. Its event log holds 2,557 thread.visited events from Nov 5 alone, 2,048 MB in total: about 0.8 MB written per visit. Compaction later deletes them, but each one has already been written, sent to subscribed clients and re-decoded.
This is the same duplication #19248 removed from pull-request sync events.
Change
Visits now emit a new thread.visit-recorded event whose payload is only the read state, { lastVisitedAt, opens? }. It is applied the way #19248 applies thread.pull-request-link-updated:
- The SQL projection reads the thread, applies the visit, and writes it through the existing
thread.visitedpath, which still leavesupdatedAtalone. applyToProjectionand the client projection merge the read state into the thread they already hold.- A small
applyOrchestrationV2ThreadVisithelper in contracts keeps the server and clients identical. - The agent-awareness relay treats it like
thread.visited.
Existing thread.visited events still decode and replay. Older clients skip unknown event types (OrchestrationV2UnknownThreadStreamEvent) and still get lastVisitedAt from the shell. There's no schema migration.
Scope and approval
This is a focused persistence fix for an obvious duplication: one visit copied every linked pull request into the event log. It follows the slim-event approach maintainers took in #19248. Visit behavior doesn't change: the stale-visit watermark, opens ranking and updatedAt handling all stay the same. Only the event payload becomes visit-specific.
Verification
runtimeLayer.test.ts, in two existing cases:- "carries the visited watermark through the V2 shell projection" now also asserts that the visit's stored event is
thread.visit-recordedwith payload{ lastVisitedAt }. - "counts opens on the server clock…" asserts that an opened visit stores only
lastVisitedAtandopens, and that a projection rebuild from the event log keeps the sameopens. - On
main, both fail: the stored event isthread.visitedwith the full thread.
- "carries the visited watermark through the V2 shell projection" now also asserts that the visit's stored event is
orchestrationV2Projection.test.ts: a new case shows the client projection merges the read state, keepsopensunless the visit was an open, and doesn't bumpupdatedAt.From
apps/server:vp test run src/orchestration-v2/runtimeLayer.test.ts src/orchestration-v2/ProjectionStore.test.ts src/orchestration-v2/FoundationPersistence.test.ts src/relay/AgentAwarenessRelay.test.tsgives 239/239 passing. Frompackages/client-runtime:vp test run src/state/orchestrationV2Projection.test.tsgives 17/17.vp run check:changedpassed: format, lint, knip, typecheck (36 packages), and 9 test files with 233 tests.Runtime numbers from my server's database, opened read-only:
Per visit to that thread Before After Event payload written and sent ~866 KB 44 bytes (a few hundred bytes for an open) Encoding the event payload (Node 26 benchmark of that thread's real payload) ~12 ms negligible
Not checked:
- The projection row is still rewritten on each visit, so a visit still reads and writes the thread row once. This change removes only the event copy and the client fan-out.
- I didn't measure inside a server build.
Model: Claude Opus 5.5, harness: Claude Code
🤖 Generated with Claude Code
Checks
Added 1 commit
- perf(server): visiting a thread no longer copies the whole thread into the event loge05bb29
Approvability
Verdict: Approved at e05bb29
Macroscope's review found this PR approvable — Thread visits now record only read-state fields rather than copying the entire thread into the event log. Server and client projections support the compact event while retaining legacy event handling and existing activity behavior.
You can add or adjust custom eligibility rules. Learn more.
No actionable comments were generated in the recent review.
Walkthrough
-
Thread visits now emit
thread.visit-recordedevents with read-state fields instead of full thread snapshots. Server and client projections apply those fields to existing thread data, and tests cover payloads and projection replay.Changes: Thread visit events
Layer / File(s) Summary Visit event contract and emission
packages/contracts/src/orchestrationV2.ts,apps/server/src/orchestration-v2/Orchestrator.tsThe new visit payload carries lastVisitedAtand optionalopens. The orchestrator emitsthread.visit-recordedwith those fields instead of a full thread snapshot.Server visit projection and replay
apps/server/src/orchestration-v2/ProjectionStore.ts,apps/server/src/relay/AgentAwarenessRelay.ts,apps/server/src/orchestration-v2/runtimeLayer.test.tsThe server applies visit fields to the existing thread and excludes the event from generic timestamp updates and agent-awareness publishing. Tests check the compact payload and replayed open state. Client visit projection
packages/client-runtime/src/state/orchestrationV2Projection.ts,packages/client-runtime/src/state/orchestrationV2Projection.test.tsThe client applies visit fields without replacing other thread fields or changing updatedAt. Tests cover omitted and suppliedopensvalues.Estimated code review effort: 3 (Moderate) | ~20 minutes
Change: Bug fix
Possibly related issues
- pingdotgg/t3code#24003: The PR replaces full thread snapshots with compact visit payloads, but does not address the issue’s separate concerns about write frequency or visits that do not advance the watermark.
- pingdotgg/t3code#30301: The PR reduces the visit event payload, but does not remove SQLite writer-queue waiting or make dispatch fire-and-forget.
Possibly related PRs
- pingdotgg/t3code#24145: Both PRs change visit-event generation in
dispatchThreadVisit; this PR uses a compact event payload. - pingdotgg/t3code#24343: Both PRs use the visit flow to track
openswith visit state.
Suggested labels:
bugfixSuggested reviewers:
yash-singh1,juliusmarminge
Merge Risk: External image Minimal · up to 94a48
-
Thread visits retain the intended compact event payload, with no identified issue requiring a fix before merge.
Pre-merge checks: Passed 4
-
Check name Status Explanation Title check Passed The title clearly summarizes the main change: thread visits no longer copy the full thread into the event log. Description check Passed The description includes the required Problem, Change, Scope and approval, and Verification sections. It explains the issue, the change, the focused scope, and specific test results and limitations. Linked Issues check Passed Check skipped because no linked issues were found for this pull request. Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Finishing Touches
-
Generate unit tests (beta)
-
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
External image Recent review info
-
External image Run configuration
-
- Source mode: Auto
- Repository: pingdotgg/t3code
- Branch: main
- File: .coderabbit.config.ts
- Commit: 6c9adfb
- Layers: Repository TS config
- Review profile: CHILL
- Plan: Advanced
- Run ID:
e1393e7e-3d91-4906-8bcf-3871ccf8b2d7
External image Commits
-
Reviewing files that changed from the base of the PR and between e05bb29421e3ca5b7de28599ac9af10452a6473e and 94a480121ca85ab0aca4ced95ca5110cc8b98819.
External image Files selected for processing: 5
-
apps/server/src/orchestration-v2/Orchestrator.tsapps/server/src/orchestration-v2/ProjectionStore.tsapps/server/src/orchestration-v2/runtimeLayer.test.tsapps/server/src/relay/AgentAwarenessRelay.tspackages/contracts/src/orchestrationV2.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.
External image Recent review info
-
External image Run configuration
-
- Source mode: Auto
- Repository: pingdotgg/t3code
- Branch: main
- File: .coderabbit.config.ts
- Commit: 6c9adfb
- Layers: Repository TS config
- Review profile: CHILL
- Plan: Advanced
- Run ID:
e1393e7e-3d91-4906-8bcf-3871ccf8b2d7
External image Commits
-
Reviewing files that changed from the base of the PR and between e05bb29421e3ca5b7de28599ac9af10452a6473e and 94a480121ca85ab0aca4ced95ca5110cc8b98819.
External image Files selected for processing: 5
-
apps/server/src/orchestration-v2/Orchestrator.tsapps/server/src/orchestration-v2/ProjectionStore.tsapps/server/src/orchestration-v2/runtimeLayer.test.tsapps/server/src/relay/AgentAwarenessRelay.tspackages/contracts/src/orchestrationV2.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.
-
Added 1 commit
- Merge remote-tracking branch 'github/main' into perf/slim-thread-visited94a4801
Thanks for this. Main has the change since #36233.
Main now implements the compact visit event from #31849: only lastVisitedAt and optional opens are persisted and transported, preserving the remaining thread fields. I compared its full diff with the merge and current main across contracts, SQL and in-memory projections, the shared client reducer, and awareness handling; main also suppresses stale visits that change nothing.
Closing as superseded.