perf(server): visiting a thread no longer copies the whole thread into the event log#31849

closed2
main
GitHub fork: perf/slim-thread-visited
opened7d agobyBbowd
View on GitHubTwo-way sync enabledPR last imported 4h ago

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.visited path, which still leaves updatedAt alone.
  • applyToProjection and the client projection merge the read state into the thread they already hold.
  • A small applyOrchestrationV2ThreadVisit helper 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-recorded with payload { lastVisitedAt }.
    • "counts opens on the server clock…" asserts that an opened visit stores only lastVisitedAt and opens, and that a projection rebuild from the event log keeps the same opens.
    • On main, both fail: the stored event is thread.visited with the full thread.
  • orchestrationV2Projection.test.ts: a new case shows the client projection merges the read state, keeps opens unless the visit was an open, and doesn't bump updatedAt.

  • 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.ts gives 239/239 passing. From packages/client-runtime: vp test run src/state/orchestrationV2Projection.test.ts gives 17/17. vp run check:changed passed: 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 threadBeforeAfter
    Event payload written and sent~866 KB44 bytes (a few hundred bytes for an open)
    Encoding the event payload (Node 26 benchmark of that thread's real payload)~12 msnegligible

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

23 successful · 14 skipped
All checks passed
94a4801

Added 1 commit

  • perf(server): visiting a thread no longer copies the whole thread into the event loge05bb29
Synced from GitHub

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.

Synced from GitHub

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

Thread visits now emit thread.visit-recorded events 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.ts
The new visit payload carries lastVisitedAt and optional opens. The orchestrator emits thread.visit-recorded with 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.ts
The 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.ts
The client applies visit fields without replacing other thread fields or changing updatedAt. Tests cover omitted and supplied opens values.

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

Suggested labels: bugfix

Suggested 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 nameStatusExplanation
Title checkPassedThe title clearly summarizes the main change: thread visits no longer copy the full thread into the event log.
Description checkPassedThe 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 checkPassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes checkPassedCheck 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

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.ts
  • apps/server/src/orchestration-v2/ProjectionStore.ts
  • apps/server/src/orchestration-v2/runtimeLayer.test.ts
  • apps/server/src/relay/AgentAwarenessRelay.ts
  • packages/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.