Skip to content

fix(server): bound thread catch-up replay and stop full-DB snapshot hydration - #5147

Merged
t3dotgg merged 1 commit into
mainfrom
t3code/bound-thread-catchup
Jul 31, 2026
Merged

fix(server): bound thread catch-up replay and stop full-DB snapshot hydration#5147
t3dotgg merged 1 commit into
mainfrom
t3code/bound-thread-catchup

Conversation

@t3dotgg

@t3dotgg t3dotgg commented Jul 31, 2026

Copy link
Copy Markdown
Member

Reconnecting to a large database can OOM-kill the backend. Two unbounded reads are responsible:

  • subscribeThread's stale-cursor catch-up read the entire global event log (readEvents(afterSequence, Number.MAX_SAFE_INTEGER)) and filtered per-thread in JS afterwards. A cached cursor a few hundred thousand events behind forces the server to page and JSON-decode every intervening event's tool payloads — for every thread — only to discard almost all of them. Field traces in App Crash on heavy thread #996 measured cursors 271k–591k events behind producing 15k+ SQL pages per reconnect, multiplied by the sidebar's concurrent thread subscriptions, ending in FATAL ERROR: JavaScript heap out of memory crash-loops.
  • GET /api/orchestration/snapshot (and the offline t3 project CLI path) hydrated every message and activity payload in the database via getSnapshot(). App Crash on heavy thread #996 has a reproduction where a single authenticated request to this route deterministically OOMs the backend on a 1.5 GB database.

Fixes:

  • subscribeThread now bounds catch-up replay to the projection head with the same gap cap subscribeShell has used since perf(orchestration): speed up new-chat propagation and offline catch-up #4177 (THREAD_RESUME_MAX_GAP = 1000). Past the cap (or with a cursor ahead of the head) it falls through to the existing fresh-snapshot path instead of an unbounded replay. No client change needed: clients already consume snapshot frames on resume and dedupe overlapping events by sequence, so the only observable difference is that a long-stale reconnect gets a fresh snapshot instead of a multi-minute replay (or a dead server).
  • The snapshot HTTP route and the offline CLI now use the existing getCommandReadModel() — same OrchestrationReadModel wire shape, thread bodies empty. The route's only consumer in the monorepo is the project CLI, which reads projects alone; web/desktop/mobile load the shell + per-thread snapshots instead.

This is the short-term containment for the OOM class in #996 / #1686 / #4597 / #2761 on the current orchestration layer, shaped to be trivially portable to V2 (#2829). Longer term, moving tool payloads out of the hot paths entirely is the real fix.

Verification:

  • 3 new tests: large-gap fallback, ahead-of-head cursor reset, replay bounded to captured head
  • full server suite: 1828 passed
  • server typecheck, lint, format clean

Built by Claude Fable 5 in Claude Code.

🤖 Generated with Claude Code


Note

High Risk
Changes hot-path orchestration reads and WebSocket resume semantics on large event logs; incorrect bounds could drop events or change reconnect behavior, though clients already handle snapshot resume and sequence dedupe.

Overview
Addresses backend OOM on large databases by bounding unbounded orchestration reads in two places.

Snapshot hydration: GET /api/orchestration/snapshot and the offline t3 project CLI now call getCommandReadModel() instead of getSnapshot() — same wire shape but without hydrating every message/activity body. That route is only used for project-list reads; UIs already use shell and per-thread snapshots.

Thread WebSocket resume: subscribeThread catch-up no longer uses readEvents(..., Number.MAX_SAFE_INTEGER) on the global log (which decoded all threads’ payloads then filtered in JS). It mirrors subscribeShell: THREAD_RESUME_MAX_GAP = 1000, replay limited to the captured latestSequence, and when the gap is too large or the cursor is ahead of the head it falls through to a fresh thread detail snapshot instead of replaying.

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

Note

Bound subscribeThread catch-up replay and replace full-DB snapshot hydration with lightweight command read model

  • subscribeThread in ws.ts now caps catch-up replay to THREAD_RESUME_MAX_GAP (1,000 events) using the authoritative head at the time of the request, replacing the previous unbounded Number.MAX_SAFE_INTEGER replay.
  • If the gap exceeds the limit or the client cursor is ahead of the server head, a fresh thread snapshot is sent via getThreadDetailSnapshot() instead of replaying.
  • The orchestration snapshot route and offline snapshot generator in http.ts and project.ts now call getCommandReadModel() instead of getSnapshot(), avoiding full-DB hydration.
  • Three new tests in server.test.ts cover the large-gap snapshot fallback, ahead-of-head cursor handling, and bounded replay limit.
  • Behavioral Change: clients with a replay gap >1,000 events now receive a snapshot instead of a full replay, and snapshot endpoints return a lighter model.

Macroscope summarized 191a386.

subscribeThread's stale-cursor catch-up read the entire global event log
(readEvents with Number.MAX_SAFE_INTEGER) and filtered per-thread in JS.
Reconnecting clients with cursors hundreds of thousands of events behind
forced the server to page and JSON-decode every intervening event's
payload, OOM-killing the backend on large databases. The replay is now
bounded to the projection head with the same gap cap subscribeShell
already uses; past the cap the client is reset with a fresh thread
snapshot, which it already handles.

GET /api/orchestration/snapshot and the offline project CLI hydrated
every message and activity payload in the database via getSnapshot().
The route's only consumer (the project CLI) reads the project list, so
both now use the lightweight getCommandReadModel() with the same wire
shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 31, 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: 88a4f945-ab89-4218-99dc-3c188c42b84a

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:M 30-99 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Jul 31, 2026
@macroscopeapp

macroscopeapp Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved 191a386

Defensive bug fix that bounds thread catch-up replay and avoids full-database hydration to prevent OOM server crashes. Follows an existing pattern (SHELL_RESUME_MAX_GAP) with comprehensive test coverage.

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

@t3dotgg
t3dotgg merged commit ca72e38 into main Jul 31, 2026
17 checks passed
@t3dotgg
t3dotgg deleted the t3code/bound-thread-catchup branch July 31, 2026 23:14
github-actions Bot added a commit to omarcresp/t3code-flake that referenced this pull request Aug 1, 2026
## What's Changed
* feat(web): add settings sidebar search by @shivamhwp in pingdotgg/t3code#4682
* fix: threads with open PRs no longer auto-settle by @t3dotgg in pingdotgg/t3code#5151
* fix(server): bound thread catch-up replay and stop full-DB snapshot hydration by @t3dotgg in pingdotgg/t3code#5147


**Full Changelog**: pingdotgg/t3code@v0.0.32-nightly.20260731.968...v0.0.32-nightly.20260801.969

Upstream release: https://github.com/pingdotgg/t3code/releases/tag/v0.0.32-nightly.20260801.969
patroza pushed a commit to patroza/t3code that referenced this pull request Aug 1, 2026
…shot

pingdotgg#5147 drains buffered live events and the completion marker from the same
queue after a gap-cap snapshot, so their order is not guaranteed. Assert both
appear rather than a fixed [synchronized, event] sequence.
patroza pushed a commit to patroza/t3code that referenced this pull request Aug 1, 2026
…ESUME_MAX_GAP

Upstream pingdotgg#5147 bounds thread resume via orchestrationEngine.latestSequence.
Keep that path over the older pingdotgg#3510 subscriptionReplayLimit helper and update
the candidate test mock accordingly.
patroza pushed a commit to patroza/t3code that referenced this pull request Aug 1, 2026
…shot

pingdotgg#5147 drains buffered live events and the completion marker from the same
queue after a gap-cap snapshot, so their order is not guaranteed. Assert both
appear rather than a fixed [synchronized, event] sequence.
sollaholla pushed a commit to sollaholla/t3code that referenced this pull request Aug 1, 2026
…ydration (pingdotgg#5147)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit ca72e38)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M 30-99 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