fix(server): turn starts in long threads no longer stall the server

Closed
BO
3d ago
+64-4

Review-only pull request on the fork, for CodeRabbit and cubic. It is not meant to be merged here; the same diff is intended for pingdotgg/t3code.

Problem

Every agent turn start reads the thread's runtime recovery state four times (ProviderTurnStartService). Several of those reads look up the thread's unfinished turn items by status IN ('pending', 'running', 'waiting') without a type filter. No partial index matched, so SQLite read every turn item in the thread. In long threads each read took 75-93 ms, so a single turn start blocked the server's event loop for roughly 300-370 ms. Every client request waited behind it, including keystrokes and clicks in the server-streamed browser preview.

Fix

  • Migration 065 adds a partial index on unfinished turn items, keyed (thread_id, ordinal, turn_item_id) so it also serves the recovery read's ordering.
  • Drops a redundant item.run_id IS NOT NULL filter from one subquery. It feeds run.run_id IN (...), where a NULL never matches, and it steered the planner onto the full-history thread_run_idx.
  • A test captures the recovery statements and checks that every turn-item lookup in their query plans uses a partial index. It fails with the new index disabled.

Measurements

Per call to getRuntimeRecoveryProjection, median of 5, on a copy of a real 15.8 GB database with planner statistics cleared, as the server runs:

Thread size (turn items)BeforeAfter
17,51792.6 ms4.5 ms
13,88575.3 ms4.4 ms
13,69980.7 ms6.6 ms
small4.7 ms1.7 ms
  • Building the index on that database took 0.4-0.9 s with a warm cache and 7.3 s with a cold cache, once, at upgrade. The index is about 340 KB.
  • Write cost is unmeasured: an item gets an index entry when it is created unfinished and loses it when it finishes. Updates that change only its payload do not touch the index.

Checks

Ran vp test run on the touched and plan-asserting files: all migration tests (28 files), plus ProjectionRecovery, ProjectionStore, ProjectionControlReads and ProjectionSettlement (all passed). Also ran server typecheck and lint on the changed files, both clean.


Summary by cubic

Fixes server stalls when starting turns in long threads. Turn starts read runtime recovery state four times, and several reads looked up unfinished turn items without a type filter, so no partial index applied and SQLite read every item in the thread. On threads with 14,000-17,500 items each read cost 75–93 ms, blocking the event loop for roughly 300–370 ms per turn start and stalling every client request.

  • Adds migration 065 with a partial index on unfinished turn items, keyed (thread_id, ordinal, turn_item_id) so it also serves the recovery read's ordering; those reads now take 4–7 ms on the same threads.
  • Removes a redundant item.run_id IS NOT NULL filter from one subquery that steered the planner onto the full-history run index.
  • Adds a test asserting every turn-item lookup in recovery query plans uses a partial index; it fails with the new index disabled.