Repository navigation
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Related, but each covers only part of this: #21578 (one partial index on unfinished turn items), #21193 (windowed snapshot indexes), #21840 (long-thread open blocks the writer), #17955 / #21122 (planner statistics).
Area
apps/server
Steps to reproduce
- Use a V2 state database at large scale:
~/.t3/userdata/statev2.sqliteof about 26.5 GB, 3,404 threads, event sequence about 4.95 million. - Start the app, or keep it running with several agent sessions active.
- Time server startup, and time the projection queries listed below on a copy of the database.
Expected behavior
Startup, thread-list loading and turn start/recovery lookups stay fast as the database grows, and the server stays responsive to MCP tool calls and queued messages.
Actual behavior
The app froze repeatedly. Startup took about 137 s. Symptoms seen from agent sessions while the backend was busy:
- MCP tool calls to the T3 server failed with
ECONNRESET, connect timeouts or "Error POSTing". - Queued messages stayed held for hours, while newer messages sent to the same thread started runs immediately.
- After a server restart, running agent sessions had their MCP authorization rejected until they reconnected. This may be a separate issue.
Benchmarks on a copy of the database showed several projection queries taking 2–22 s each. Adding the six indexes below brought startup to about 17 s, on the same machine and database. Every benchmarked query returned identical results before and after (same row counts, same SHA-256 digest of the result rows).
Each query was run twice in a row; times are in seconds as (first run, second run). "Control" is a second baseline pass without indexes, to separate cache warm-up from index effects.
| Query shape (local benchmark label) | Rows | Baseline | Control (no indexes) | With indexes |
|---|---|---|---|---|
Thread-shell list for all threads (shell_all) |
3,300 | 22.38, 17.58 | 20.14, 19.23 | 5.13, 0.25 |
Recovery lookup, latest run + unfinished items (recovery_1) |
1 | 2.31, 2.23 | – | 0.085, 0.0017 |
Recovery lookup, live nodes/runs/items/subagents (recovery_3) |
1 | 2.18, 6.03 | – | 0.038, 0.0003 |
Recovery lookup by provider thread (recovery_6) |
2 | 5.09, 5.43 | 2.74, 0.045 | 0.11, 0.0029 |
Pending turn items in order (recovery_10) |
1 | 2.15, 1.89 | 0.055, 0.021 | 0.0007, 0.0001 |
User/assistant message order by messageId (message_order) |
1,984 | 2.03, 0.033 | – | 0.25, 0.0084 |
So some lookups get fast once cached even without indexes. The thread-shell list stays at about 19–20 s warm without them and drops to 0.25 s warm with them.
Impact
Major degradation or frequent failure
Version or commit
T3 Code (Nightly) 0.1.2-nightly.20261021.3457
Environment
macOS 26.6.2, Apple Silicon (Mac16,10), desktop app; state database about 26.5 GB
Logs or stack traces
The six indexes applied as a local workaround:
CREATE INDEX t3_recovery_test_items_status_cover
ON orchestration_v2_projection_turn_items(thread_id, status, type, run_id, node_id, provider_thread_id)
WHERE status IN ('pending', 'running', 'waiting');
CREATE INDEX t3_recovery_test_message_order
ON orchestration_v2_projection_turn_items(thread_id, json_extract(payload_json, '$.messageId'), ordinal)
WHERE type IN ('user_message', 'assistant_message');
CREATE INDEX t3_recovery_test_runs_status_order_cover
ON orchestration_v2_projection_runs(thread_id, ordinal DESC, run_id DESC, status);
CREATE INDEX t3_recovery_test_runs_last_executed
ON orchestration_v2_projection_runs(thread_id, (completed_at IS NULL) DESC, completed_at DESC, ordinal DESC, run_id DESC)
WHERE status <> 'queued' AND NOT (status = 'cancelled' AND json_extract(payload_json, '$.startedAt') IS NULL);
CREATE INDEX t3_recovery_test_interrupt_lookup
ON orchestration_v2_projection_turn_items(thread_id, run_id)
WHERE type = 'run_interrupt_request';
CREATE INDEX t3_recovery_test_pending_items_order
ON orchestration_v2_projection_turn_items(thread_id, ordinal, turn_item_id)
WHERE status IN ('pending', 'running', 'waiting');Build time per index on the 26.5 GB database (about 7 minutes in total, holding the writer). A migration would need to plan for this:
| Index | Seconds |
|---|---|
| items_status_cover | 101.5 |
| message_order | 98.0 |
| runs_status_order_cover | 2.5 |
| runs_last_executed | 0.01 |
| interrupt_lookup | 94.7 |
| pending_items_order | 122.0 |
EXPLAIN QUERY PLAN, before and after (only the lines that changed):
recovery_1
before: SEARCH latest USING INDEX orchestration_v2_projection_runs_thread_ordinal_idx (thread_id=?)
after: SEARCH latest USING COVERING INDEX t3_recovery_test_runs_status_order_cover (thread_id=?)
before: SEARCH item USING INDEX orchestration_v2_projection_turn_items_thread_run_idx (thread_id=? AND run_id>?)
after: SEARCH item USING COVERING INDEX t3_recovery_test_items_status_cover (thread_id=? AND status=? AND type=? AND run_id>?)
recovery_3
before: SEARCH orchestration_v2_projection_runs USING INDEX orchestration_v2_projection_runs_recovery_idx (status=? AND thread_id=?)
after: SEARCH orchestration_v2_projection_runs USING COVERING INDEX t3_recovery_test_runs_status_order_cover (thread_id=?)
before: SEARCH item USING INDEX orchestration_v2_projection_turn_items_recovery_idx (thread_id=?)
after: SEARCH item USING COVERING INDEX t3_recovery_test_items_status_cover (thread_id=? AND status=? AND type=?)
before: SEARCH item USING INDEX orchestration_v2_projection_turn_items_thread_run_idx (thread_id=?)
after: SEARCH item USING INDEX t3_recovery_test_items_status_cover (thread_id=? AND status=? AND type=?)
recovery_6 (both turn-item subqueries)
before: SEARCH item USING INDEX orchestration_v2_projection_turn_items_thread_run_idx (thread_id=?)
after: SEARCH item USING COVERING INDEX t3_recovery_test_items_status_cover (thread_id=? AND status=?)
recovery_10
before: SEARCH item USING INDEX orchestration_v2_projection_turn_items_thread_ordinal_idx (thread_id=?)
after: SEARCH item USING INDEX t3_recovery_test_pending_items_order (thread_id=?)
before: SEARCH orchestration_v2_projection_runs USING INDEX orchestration_v2_projection_runs_recovery_idx (status=? AND thread_id=?)
after: SEARCH orchestration_v2_projection_runs USING COVERING INDEX t3_recovery_test_runs_status_order_cover (thread_id=?)
shell_all (correlated subqueries per thread)
before: SEARCH r USING INDEX orchestration_v2_projection_runs_thread_ordinal_idx (thread_id=?)
after: SEARCH r USING COVERING INDEX t3_recovery_test_runs_status_order_cover (thread_id=?)
before: SEARCH candidate USING INDEX orchestration_v2_projection_runs_thread_ordinal_idx (thread_id=?)
USE TEMP B-TREE FOR ORDER BY
after: SEARCH candidate USING INDEX t3_recovery_test_runs_last_executed (thread_id=?)
before: SEARCH stop_request USING INDEX orchestration_v2_projection_turn_items_thread_run_idx (thread_id=? AND run_id=?)
after: SEARCH stop_request USING COVERING INDEX t3_recovery_test_interrupt_lookup (thread_id=? AND run_id=?)
message_order
before: SEARCH orchestration_v2_projection_turn_items USING INDEX orchestration_v2_projection_turn_items_thread_run_idx (thread_id=?)
USE TEMP B-TREE FOR GROUP BY
after: SEARCH orchestration_v2_projection_turn_items USING INDEX t3_recovery_test_message_order (thread_id=?)
The pattern: lookups filter turn items by status without type, or order runs per thread by ordinal/completed_at, and the existing indexes do not match, so they read each thread's full turn-item or run history. With planner statistics cleared on open (#21122), nothing compensates for the mismatch.
Screenshots, recordings, or supporting files
No response
Workaround
The six indexes above, applied locally after a full database backup. Nothing was deleted, and the event sequence and thread count were unchanged afterwards. Rollback is DROP INDEX IF EXISTS for each of the six names.
This is a local workaround only. It could conflict with a future migration, and it does not survive a fresh database. Ideally a T3 migration would own equivalent indexes, or the queries would change to fit existing ones, with the build time above in mind for large installs.
Activity
t3dotgg commentedon Oct 21, 2026
Note
🤖 Claude Opus 5.5 responding on behalf of Theo
Thanks for the report. One part of this is fixed on main in #21122. Please test on the nightly built from main on 2026-10-21, or on the next stable release. One part is still open: thread-shell and recovery queries still take seconds on a 26 GB database; the six proposed indexes are absent. If that part still matters to you, please open a new issue for that part only.
juliusmarminge commentedon Oct 22, 2026
The remaining thread-shell and recovery query delays are now tracked in the scoped follow-up #23003, opened by the reporter as requested. Keeping this issue closed under the existing maintainer decision. No new runtime reproduction was performed in this triage.