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.
Follow-up to #22659, as requested there: this issue covers only the part still open after #21122, the slow thread-shell and recovery queries and the six missing indexes. Startup time is out of scope here.
Related, each covering a different part: #21578 (one partial index for turn start), #21193 (windowed snapshot indexes), #21840 (long-thread open blocks the writer).
Area
apps/server
Steps to reproduce
- Use a large V2 state database: about 26.5 GB, 3,404 threads, event sequence about 4.95 million.
- On a copy of the database, run the server's thread-shell list query and its turn recovery queries directly in SQLite, outside the app.
- Time each query twice in a row and record
EXPLAIN QUERY PLAN.
Expected behavior
Thread-shell listing and turn recovery lookups stay well under a second as a thread's turn-item and run history grows.
Actual behavior
Several projection queries take 2–22 s each. Their plans use a per-thread index, then read the thread's whole turn-item or run history, because no existing index matches the filter or sort.
When measured: 2026-10-21, on a copy of the database, using build 0.1.2-nightly.20261021.3457, which already includes #21122. The copy is no longer available, so we could not check its planner statistics directly. A read-only snapshot of the same database, taken about 13 minutes after the baseline runs, has an empty sqlite_stat1 table (0 rows), and the with-indexes run cleared sqlite_stat1 explicitly. #21122 adds no index for these query shapes, and the maintainer reply on #22659 says these queries are still slow and the indexes are absent.
Times in seconds as (first run, second run). "Control" is a second pass without indexes, to separate cache warm-up from the index effect. Every query returned identical results with and without the indexes (same row count, same SHA-256 of the result rows).
| Query (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: latest run + unfinished items (recovery_1) |
1 | 2.31, 2.23 | – | 0.085, 0.0017 |
Recovery: live nodes/runs/items/subagents (recovery_3) |
1 | 2.18, 6.03 | – | 0.038, 0.0003 |
Recovery 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 |
Some lookups become fast once cached, even without indexes. The thread-shell list does not: it stays at 19–20 s warm without the indexes 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 (e64766c), which includes #21122; the build in use when measured
Environment
macOS 26.6.2, Apple Silicon, desktop app; V2 state database about 26.5 GB
Logs or stack traces
Proposed indexes (names are from our local test):
-- 1: unfinished turn items filtered by status, with or without type
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');
-- 2: user/assistant messages grouped by messageId
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');
-- 3: latest run per thread, with status covered
CREATE INDEX t3_recovery_test_runs_status_order_cover
ON orchestration_v2_projection_runs(thread_id, ordinal DESC, run_id DESC, status);
-- 4: last executed run per thread
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);
-- 5: interrupt (stop) request per run
CREATE INDEX t3_recovery_test_interrupt_lookup
ON orchestration_v2_projection_turn_items(thread_id, run_id)
WHERE type = 'run_interrupt_request';
-- 6: pending turn items in ordinal order
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');Which query each index serves (query shape and plan change only):
| Index | Query shape it serves | Queries | Plan before → after |
|---|---|---|---|
1 items_status_cover |
turn items WHERE thread_id = ? AND status IN (unfinished) [AND type = ?] [AND run_id > ?] |
recovery_1, recovery_3, recovery_6 |
turn_items_thread_run_idx (thread_id=?) or turn_items_recovery_idx (thread_id=?) → COVERING INDEX ... (thread_id=? AND status=? [AND type=?] [AND run_id>?]) |
2 message_order |
user/assistant turn items WHERE thread_id = ? grouped by payload_json.messageId, ordered by ordinal |
message_order |
turn_items_thread_run_idx (thread_id=?) + TEMP B-TREE FOR GROUP BY → message_order (thread_id=?) |
3 runs_status_order_cover |
runs WHERE thread_id = ? [AND status IN (...)] ORDER BY ordinal DESC, run_id DESC LIMIT 1 |
recovery_1, recovery_3, recovery_10, shell_all |
runs_thread_ordinal_idx or runs_recovery_idx (status=? AND thread_id=?) → COVERING INDEX ... (thread_id=?) |
4 runs_last_executed |
per thread, last executed run: WHERE thread_id = ? AND status <> 'queued' ... ORDER BY completed_at IS NULL DESC, completed_at DESC, ordinal DESC, run_id DESC LIMIT 1 |
shell_all (correlated subquery per thread) |
runs_thread_ordinal_idx + TEMP B-TREE FOR ORDER BY → runs_last_executed (thread_id=?) |
5 interrupt_lookup |
type = 'run_interrupt_request' AND thread_id = ? AND run_id = ? |
shell_all (correlated subquery per thread) |
turn_items_thread_run_idx (thread_id=? AND run_id=?) → COVERING INDEX ... (thread_id=? AND run_id=?) |
6 pending_items_order |
unfinished turn items WHERE thread_id = ? ORDER BY ordinal |
recovery_10 |
turn_items_thread_ordinal_idx (thread_id=?) → pending_items_order (thread_id=?) |
The common pattern: lookups filter turn items by status (sometimes without type), or sort a thread's runs by ordinal/completed_at. The existing indexes lead with thread_id but do not cover the filter or sort, so each lookup reads the thread's full history. The plans were recorded by running the queries directly in SQLite, outside the app, on a copy of a database written by a build that includes #21122.
Build time per index on the 26.5 GB database, about 7 minutes in total while holding the writer. A migration would need to plan for this on large installs:
| Index | Seconds |
|---|---|
| 1 items_status_cover | 101.5 |
| 2 message_order | 98.0 |
| 3 runs_status_order_cover | 2.5 |
| 4 runs_last_executed | 0.01 |
| 5 interrupt_lookup | 94.7 |
| 6 pending_items_order | 122.0 |
Some may overlap with #21578's partial index; we have not checked that combination.
Screenshots, recordings, or supporting files
No response
Workaround
The six indexes above, applied locally after a full backup. Nothing was deleted; event sequence and thread count were unchanged. Rollback is DROP INDEX IF EXISTS for each name. This is a local stopgap only: it may conflict with a future migration and does not exist on a fresh database. Ideally a migration owns equivalent indexes, or the queries change to fit existing ones.
Activity
mm0nst3r commentedon Oct 24, 2026
Windows confirmation on official nightly 3617: shared-reader waits disable controls; reproducible query/index evidence
Posting here to consolidate evidence for the already-reported recovery/history query problem, rather than opening a duplicate. The corresponding permission/UI failure is tracked in #24221.
New evidence: Windows 11, official
0.1.3-nightly.20261024.3617, a 53.98 GB state database, reader-lock waits up to 30.18 seconds, and an isolated query benchmark with exact ordered-result checks and an index removal/reapplication control. The candidate indexes have not been applied to the live installation.Observed behavior and impact
During ordinary work with long-lived threads and delegated agents, sending messages and other mutating controls intermittently become unusable. Settings → Connections shows “Permissions not checked”; the user also sees “This connection cannot change threads.”
Reloading the interface restored controls once, then failed to help during a later episode. That later episode recovered while only read-only diagnostics were being collected: no restart, index installation, or repair was performed during that recovery. This is intermittent, but it blocks foreground work when it occurs.
Expected: normal thread history and background work should not starve session validation or leave a valid local session represented as lacking permission.
Environment and provenance
10.0.26300).27e0c684c585f955252d65f06206dd11a143bff5.statev2.sqlite: 53,983,956,992 bytes (50.28 GiB) at measurement.Live trace evidence
Captured around 2026-10-24 13:14–13:28 UTC. The following examples retain operation names, durations and connection/lock attributes only. Tokens, request URLs, IDs, paths and conversation data are omitted.
{ "name": "sql.transaction", "durationMs": 30186.8685, "parent": "ws.orchestrationV2.projectShellItems", "attributes": { "db.name": "statev2.sqlite", "db.connection": "reader", "db.lock_wait_ms": 30182.046 } } { "name": "sql.transaction", "durationMs": 22185.4472, "parent": "mcp.loadCaller", "attributes": { "db.name": "statev2.sqlite", "db.connection": "reader", "db.lock_wait_ms": 22178.687 } } { "name": "sql.transaction", "durationMs": 18112.2613, "parent": "ThreadSettlementServiceV2.sweep", "attributes": { "db.name": "statev2.sqlite", "db.connection": "reader", "db.lock_wait_ms": 18110.35 } }The first transaction spent about 30,182 of 30,187 ms waiting to acquire the reader. Separate session-row SELECT spans reached 16,179.0155 ms, with a maximum recorded lock wait of 16,178.771 ms.
The recorded expensive query paths included:
ProjectionStore.getRuntimeRecoveryProjectionfrom turn start and finalization: unfinished-item subqueries for runs, nodes, subagents and provider threads.ProjectionStore.readCanonicalProjection → sql.transaction → IdleCompactionService.compactWhenDue: message-order and provider-thread/history reads.json_extract(payload_json, '$.messageId'), scanning historical turn items before grouping.Some SQL spans have no lock-wait annotation, so their duration cannot be treated as pure SQLite execution time. Summed waits across concurrent requests are also not wall-clock outage duration.
Relevant release-source paths
These links are pinned to the installed release, not a moving main branch:
Together these support the shared-reader contention diagnosis. They do not prove that every long wait has a single cause, or that the indexes alone resolve the complete UI failure.
Reproduction and isolated experiment
Observed workload reproduction
SessionStore.verify/auth SELECT spans anddb.connection=readerlock waits.EXPLAIN QUERY PLANoutput using the reproducer below.The live symptom is workload-dependent; this is not a claim of a deterministic small fresh-home reproduction.
Fixture construction and limits
The timing fixture was built from an immutable, closed 2026-10-23 backup, using the official 3617 table/index definitions captured from the current installation:
{}; unfinished payloads and message/subagent payloads were retained. This reduces private-data volume and fixture size while retaining the rows/payloads selected by the tested queries.Each query was executed three times per phase; the tables below use the median of those three timings. The recovery total is a sum of 11 per-query medians, not measured end-to-end server/UI latency. Ordered row counts and SHA-256 digests matched across every phase.
Timing controls
The variation is visible; these are same-host measurements under background machine load, not a controlled device-performance claim. The fresh public reproducer still shows about 5× lower recovery-query cost.
Initial per-query evidence
canonical_pending_controlusesstatus IN (SELECT value FROM json_each(?)). Its query plan did not switch to a candidate partial index. Its timing change is not evidence that this path was fixed; this remains relevant to #23926/#23929.Candidate indexes tested only in the fixture
This is evidence for useful access paths, not a request that these local names or this installation method become the upstream implementation.
The pending filter index and the pending ordinal index serve different access paths. The message-order index avoids the grouping scan. A status-leading general index was also checked at the schema/query-plan stage and did not remove the ordered pending scan, so it was not part of the measured candidate.
Representative before/after plans
The provider-thread outer scan remains in the plan. The change avoids its historical turn-item subquery scans; it does not eliminate every scan.
Reproducer
Save the following as
t3-reader-index-repro.mjs. Use only an isolated writable database copy, containing a thread with substantial history, plus its thread ID:It fetches the exact recovery SQL from the pinned release, runs baseline → indexed → rollback-baseline → reindexed, checks every ordered result, and removes its three test indexes before closing. It prints/saves only plans, row counts, hashes and timings, not payloads or thread IDs. The script was executed successfully against a separate copy of the private fixture; its measured results are included above. On a different database/hardware the numbers will differ.
Complete tested Node.js reproducer
Machine-readable public-reproducer measurements (all phases; no private payloads)
{ "commit": "27e0c684c585f955252d65f06206dd11a143bff5", "node": "v24.18.0", "identicalOrderedResults": true, "phases": [ { "label": "baseline", "recoverySumOfMediansMs": 530.8829, "queries": [ { "query": "recovery_0", "rows": 1, "timesMs": [ 0.6131, 0.4423, 0.5641 ], "sha256": "c5ababb0f5ba0118793efba54247b21fb5107af5e08196ab59bac4dc682f08e4" }, { "query": "recovery_1", "rows": 44, "timesMs": [ 92.8345, 86.2456, 86.1931 ], "sha256": "6e3b4b85fe396ac8c0c36b162b4dfe65d11f032e05bf49406665ccaf823a7517" }, { "query": "recovery_2", "rows": 39, "timesMs": [ 0.2377, 0.1107, 0.1038 ], "sha256": "90ef9bc383e68f831cc5f415815305e3ba09dbcfdb2982f352f45bf7af010876" }, { "query": "recovery_3", "rows": 45, "timesMs": [ 78.9255, 75.9954, 77.8129 ], "sha256": "2bb8e7f3f16d7f50b6d2988992e429b718bc933d8269f7c433e86d99bfcb57a9" }, { "query": "recovery_4", "rows": 6, "timesMs": [ 80.6573, 75.9124, 83.9005 ], "sha256": "4bc283c202288cdd9c8b877bea9a134cc692f6184ada1b4b569a6c2cfc40eadc" }, { "query": "recovery_5", "rows": 1, "timesMs": [ 0.2573, 0.0996, 0.0786 ], "sha256": "615b323a5607a4decf49b8c8bc56dc78127307b177c8aed93f49f8917bb2e699" }, { "query": "recovery_6", "rows": 13, "timesMs": [ 158.7557, 169.319, 172.5948 ], "sha256": "f8bd5b0a4a9b9e48ffb3028e67a675c47c056796c1d0df6e50b50324167b5150" }, { "query": "recovery_7", "rows": 0, "timesMs": [ 10.3385, 9.6735, 9.2509 ], "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" }, { "query": "recovery_8", "rows": 0, "timesMs": [ 0.1163, 0.0474, 0.0456 ], "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" }, { "query": "recovery_9", "rows": 0, "timesMs": [ 24.8671, 25.4204, 24.8029 ], "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" }, { "query": "recovery_10", "rows": 6, "timesMs": [ 81.4857, 77.702, 82.6206 ], "sha256": "62b626c7796749117432c04025294984c0cd72bb61209b4537b376594de7d2b5" }, { "query": "message_order", "rows": 18712, "timesMs": [ 239.6799, 229.5451, 220.0705 ], "sha256": "441d97035a93a0abae13e744f94b4d863485cbb9cd8d96c6438e80ff9d391f42" }, { "query": "canonical_pending_control", "rows": 11, "timesMs": [ 82.4767, 78.8608, 82.1859 ], "sha256": "48ef96f696db4462268b4698f9ce3786c8545ed9078e90111677206a5306b715" } ] }, { "label": "indexed", "recoverySumOfMediansMs": 105.3072, "queries": [ { "query": "recovery_0", "rows": 1, "timesMs": [ 0.514, 0.7055, 0.6442 ], "sha256": "c5ababb0f5ba0118793efba54247b21fb5107af5e08196ab59bac4dc682f08e4" }, { "query": "recovery_1", "rows": 44, "timesMs": [ 19.7152, 21.0318, 22.4767 ], "sha256": "6e3b4b85fe396ac8c0c36b162b4dfe65d11f032e05bf49406665ccaf823a7517" }, { "query": "recovery_2", "rows": 39, "timesMs": [ 0.4032, 0.2572, 0.185 ], "sha256": "90ef9bc383e68f831cc5f415815305e3ba09dbcfdb2982f352f45bf7af010876" }, { "query": "recovery_3", "rows": 45, "timesMs": [ 17.3416, 14.1544, 12.2148 ], "sha256": "2bb8e7f3f16d7f50b6d2988992e429b718bc933d8269f7c433e86d99bfcb57a9" }, { "query": "recovery_4", "rows": 6, "timesMs": [ 11.2805, 9.9605, 11.4954 ], "sha256": "4bc283c202288cdd9c8b877bea9a134cc692f6184ada1b4b569a6c2cfc40eadc" }, { "query": "recovery_5", "rows": 1, "timesMs": [ 0.3114, 0.1642, 0.0999 ], "sha256": "615b323a5607a4decf49b8c8bc56dc78127307b177c8aed93f49f8917bb2e699" }, { "query": "recovery_6", "rows": 13, "timesMs": [ 12.297, 12.0273, 12.5402 ], "sha256": "f8bd5b0a4a9b9e48ffb3028e67a675c47c056796c1d0df6e50b50324167b5150" }, { "query": "recovery_7", "rows": 0, "timesMs": [ 13.1131, 12.0477, 11.27 ], "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" }, { "query": "recovery_8", "rows": 0, "timesMs": [ 0.1273, 0.0961, 0.0632 ], "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" }, { "query": "recovery_9", "rows": 0, "timesMs": [ 33.1803, 32.5401, 34.9586 ], "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" }, { "query": "recovery_10", "rows": 6, "timesMs": [ 0.3861, 0.1538, 0.1274 ], "sha256": "62b626c7796749117432c04025294984c0cd72bb61209b4537b376594de7d2b5" }, { "query": "message_order", "rows": 18712, "timesMs": [ 124.8226, 112.0123, 112.7417 ], "sha256": "441d97035a93a0abae13e744f94b4d863485cbb9cd8d96c6438e80ff9d391f42" }, { "query": "canonical_pending_control", "rows": 11, "timesMs": [ 98.5949, 94.6558, 103.4883 ], "sha256": "48ef96f696db4462268b4698f9ce3786c8545ed9078e90111677206a5306b715" } ] }, { "label": "rollback-baseline", "recoverySumOfMediansMs": 636.4608, "queries": [ { "query": "recovery_0", "rows": 1, "timesMs": [ 0.7391, 0.7133, 0.5373 ], "sha256": "c5ababb0f5ba0118793efba54247b21fb5107af5e08196ab59bac4dc682f08e4" }, { "query": "recovery_1", "rows": 44, "timesMs": [ 108.9142, 115.5612, 109.7496 ], "sha256": "6e3b4b85fe396ac8c0c36b162b4dfe65d11f032e05bf49406665ccaf823a7517" }, { "query": "recovery_2", "rows": 39, "timesMs": [ 0.2099, 0.1099, 0.1003 ], "sha256": "90ef9bc383e68f831cc5f415815305e3ba09dbcfdb2982f352f45bf7af010876" }, { "query": "recovery_3", "rows": 45, "timesMs": [ 96.2767, 96.1683, 91.529 ], "sha256": "2bb8e7f3f16d7f50b6d2988992e429b718bc933d8269f7c433e86d99bfcb57a9" }, { "query": "recovery_4", "rows": 6, "timesMs": [ 88.3469, 95.3638, 92.9917 ], "sha256": "4bc283c202288cdd9c8b877bea9a134cc692f6184ada1b4b569a6c2cfc40eadc" }, { "query": "recovery_5", "rows": 1, "timesMs": [ 0.3675, 0.1187, 0.1102 ], "sha256": "615b323a5607a4decf49b8c8bc56dc78127307b177c8aed93f49f8917bb2e699" }, { "query": "recovery_6", "rows": 13, "timesMs": [ 189.3902, 206.8118, 204.9509 ], "sha256": "f8bd5b0a4a9b9e48ffb3028e67a675c47c056796c1d0df6e50b50324167b5150" }, { "query": "recovery_7", "rows": 0, "timesMs": [ 10.4749, 12.0891, 12.4979 ], "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" }, { "query": "recovery_8", "rows": 0, "timesMs": [ 0.1217, 0.0682, 0.0641 ], "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" }, { "query": "recovery_9", "rows": 0, "timesMs": [ 28.2487, 29.0954, 26.9574 ], "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" }, { "query": "recovery_10", "rows": 6, "timesMs": [ 85.8592, 91.2524, 95.2322 ], "sha256": "62b626c7796749117432c04025294984c0cd72bb61209b4537b376594de7d2b5" }, { "query": "message_order", "rows": 18712, "timesMs": [ 331.5935, 293.4237, 316.5496 ], "sha256": "441d97035a93a0abae13e744f94b4d863485cbb9cd8d96c6438e80ff9d391f42" }, { "query": "canonical_pending_control", "rows": 11, "timesMs": [ 116.7779, 103.1621, 139.1138 ], "sha256": "48ef96f696db4462268b4698f9ce3786c8545ed9078e90111677206a5306b715" } ] }, { "label": "reindexed", "recoverySumOfMediansMs": 116.1605, "queries": [ { "query": "recovery_0", "rows": 1, "timesMs": [ 0.553, 0.4451, 0.5182 ], "sha256": "c5ababb0f5ba0118793efba54247b21fb5107af5e08196ab59bac4dc682f08e4" }, { "query": "recovery_1", "rows": 44, "timesMs": [ 18.3613, 19.9846, 18.4133 ], "sha256": "6e3b4b85fe396ac8c0c36b162b4dfe65d11f032e05bf49406665ccaf823a7517" }, { "query": "recovery_2", "rows": 39, "timesMs": [ 0.3077, 0.1962, 0.1931 ], "sha256": "90ef9bc383e68f831cc5f415815305e3ba09dbcfdb2982f352f45bf7af010876" }, { "query": "recovery_3", "rows": 45, "timesMs": [ 12.7506, 9.9795, 11.7886 ], "sha256": "2bb8e7f3f16d7f50b6d2988992e429b718bc933d8269f7c433e86d99bfcb57a9" }, { "query": "recovery_4", "rows": 6, "timesMs": [ 11.7299, 11.8293, 14.2459 ], "sha256": "4bc283c202288cdd9c8b877bea9a134cc692f6184ada1b4b569a6c2cfc40eadc" }, { "query": "recovery_5", "rows": 1, "timesMs": [ 0.2511, 0.0844, 0.0737 ], "sha256": "615b323a5607a4decf49b8c8bc56dc78127307b177c8aed93f49f8917bb2e699" }, { "query": "recovery_6", "rows": 13, "timesMs": [ 16.8995, 16.7008, 15.6496 ], "sha256": "f8bd5b0a4a9b9e48ffb3028e67a675c47c056796c1d0df6e50b50324167b5150" }, { "query": "recovery_7", "rows": 0, "timesMs": [ 19.0882, 16.0618, 14.7078 ], "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" }, { "query": "recovery_8", "rows": 0, "timesMs": [ 0.1558, 0.0894, 0.1383 ], "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" }, { "query": "recovery_9", "rows": 0, "timesMs": [ 40.2276, 40.641, 39.4036 ], "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" }, { "query": "recovery_10", "rows": 6, "timesMs": [ 0.4071, 0.202, 0.1502 ], "sha256": "62b626c7796749117432c04025294984c0cd72bb61209b4537b376594de7d2b5" }, { "query": "message_order", "rows": 18712, "timesMs": [ 138.0005, 143.9568, 118.6148 ], "sha256": "441d97035a93a0abae13e744f94b4d863485cbb9cd8d96c6438e80ff9d391f42" }, { "query": "canonical_pending_control", "rows": 11, "timesMs": [ 142.4805, 137.6745, 137.8482 ], "sha256": "48ef96f696db4462268b4698f9ce3786c8545ed9078e90111677206a5306b715" } ] } ] }Workaround and what remains unverified
Investigated and written with GPT-6 Astra through Codex, at the affected user's request. No private database, transcripts, credentials or unredacted logs are being published.
ATECHPCS commentedon Oct 27, 2026
Another data point, still slow after updating to v0.1.3 today.
Setup: headless Linux server (32 cores, 24 GB RAM),
t3 serveas a systemd user service, clients connect through T3 Connect.statev2.sqliteis 7.7 GB. On Oct 21 I rant3 doctor --fix, which took it from 8.1 GB down to 5.5 GB (about 50 minutes of downtime). Six days later it was back to 7.7 GB.t3 doctor --dry-runon 0.1.3 now says 0 KiB reclaimable, nothing to repair./api/orchestration/threads/<id>/boundedloads that end with "Incoming request ended abruptly: context canceled" andenvironment api request failedwith errorTagInterrupt.Plenty of my threads are delegated-task / subagent threads, if that matters for growth.
There is no way to archive or prune old threads to keep the database small, so the only workaround I see is moving the database aside and losing history. A retention or prune option, or the indexes proposed here, would help a lot.
Techie5879 commentedon Oct 28, 2026
Another large-history case on Linux, now running
0.1.4-nightly.20261027.3783: about 18,600 stored chats, 6,000 archived, and a 60 GiB database.Reader-connection waits reach about 13 seconds. Item reads and command dispatches take about 12 seconds. More detail is in our comment on #24895.
One detail for the indexing work: this database already has the partial index on
(thread_id, run_id) WHERE type = 'run_interrupt_request'. The slowdown persists with that index present. We have not benchmarked the other proposed indexes or established which query is holding the reader, so this is not a before/after index result.GPT-6.1 Sol, high reasoning, commenting on behalf of Aritra.
Dropje97 commentedon Oct 28, 2026
Another data point from a long thread on 0.1.3-nightly.20261027.3754, with two causes that #23009 does not cover.
One thread has 2,223 runs and 19,056 turn items.
getThreadShelltook 1.1-1.5 s for it, against 2 ms for an 82-run thread. Shell subscribers call it for every event batch of an active thread (projectShellItems). While that thread worked, this read took 45% of the reader time spent in statements of 250 ms or more.activity_run_status,activity_run_started_atand pending-secret subqueries filter onr.status IN (...), but walkruns_thread_ordinal_idxto avoid the sort. With no active run, each one reads every run row of the thread.INDEXED BY orchestration_v2_projection_runs_thread_status_idxanswers them from the existing(thread_id, status)index.runs(thread_id, completed_at, ordinal, run_id)for the last-executed-run subquery (rewritten asCOALESCE(<unfinished, ORDER BY ordinal DESC>, <completed, ORDER BY completed_at DESC, ordinal DESC>)), the statement drops from 687 ms to 6.5 ms. perf(server): index thread-shell and turn-recovery projection queries #23009 indexes that subquery too.selectShellItemCounts), 424 ms. TheLEFT JOIN runs r ON r.run_id = counts.run_idreads about 2,200 run rows, payloads included, only forstatus. A covering index onruns(run_id, status)takes it to 8.5 ms. For all 1,879 active threads, the same statement went from 4.6-4.8 s to 3.5-3.6 s.Equivalence: we ran a harness that calls the real
ProjectionStorecode before and after the change, on a block clone of our 22 GB database. It returned identical results for 209getThreadShellcalls and for the active, archive, all and unsettled shell snapshots.Production: we run these changes in a local build. Compared with the 85 minutes before deploying, at similar load (8-10 active runs), the hour after shows:
getThreadShellreads: from 516 s (936 statements) to 3.7 s (8 statements).Measured and written by Claude (Opus 5.5) in T3 Code, reviewed by Codex (gpt-6.1-sol); posted with the owner's approval.
VDrozd-Cyclis commentedon Oct 29, 2026
Additional macOS evidence from T3 triage on 2026-10-29, focused on the recovery queries in this issue.
The user noticed slow opening-to-thread-list startup on 0.1.3-nightly or 0.1.4-nightly (exact suffix unknown). The measured launch below was Desktop and bundled local server 0.1.5, on macOS arm64, Darwin 27.2.0. The triage context reports Node v24.22.1. The database was approximately 16.0 GiB, with 4,722 threads and 4,112 provider-thread records at inspection.
Existing traces from one actual launch show:
Read-only
EXPLAIN QUERY PLANfor the matching provider-thread SELECT reportsSCAN provider_thread, correlated owner/subagent lookups, and a temporary sort B-tree. The provider-thread SELECT's recorded reader-lock waits were at most 0.004 ms. This isolates repeated broad reads as the dominant measured cost, rather than a long-held writer transaction like #23372. It is not a benchmark of this issue's proposed indexes.A later read-only snapshot found one ready Codex session bound to 1,346 threads. Runtime candidate selection includes every binding of a non-stopped/non-error session. Recovery reads each candidate sequentially, using the provider-thread query. The later binding snapshot explains a possible amplifier; it is not the exact pre-recovery state.
No source, settings, database contents, or services were changed, and no restart was attempted. No cold-start repair was validated. Keeping the macOS app running by hiding it or closing its window avoids another server startup between uses; a real quit, update, or reboot still incurs recovery. This workaround was checked in the release source, not exercised during triage.
Private database contents and raw traces are not attached. All timings are from one observed launch; they do not establish when the regression began.
Prepared by GPT-6.1-Sol through the Codex harness in T3 Code, following the triage playbook.
alexgunsberg commentedon Nov 3, 2026
Note
🤖 Claude Opus 5.5 (Claude Code, following the
t3 triageplaybook) writing on behalf of @alexgunsbergAnother data point. This one shows that the number of unarchived threads drives thread-shell cost a lot, and that archiving them by hand is a working stopgap.
Environment: server
0.1.7-nightly.20261101.4035, headless background service on Linux arm64 (20 cores). The client is the iOS app over T3 Connect. Tailscale Serve is also set up. The workload is agent-heavy: many Claude sessions plus delegated child threads.Database:
statev2.sqliteis 19.04 GiB, andt3 doctor --dry-runreports 0 KiB reclaimable.orchestration_eventsis 10.7 GB andprojection_turn_items3.65 GB. It grows about 0.3–0.4 GB a day. There were 4,483 threads, 4,442 of them unarchived in one project.event-loop-stalls.ndjsonhas nothing since 10-20, so this is query latency, not event-loop freezes as in #26224.Before (about 30 min of
server.trace.ndjson, load average ~30):ws.orchestrationV2.projectShellItems(n=379)OrchestratorV2.getThreadShell(n=680)sql.transaction(n=2,204)/api/orchestration/shell(n=9)/api/orchestration/threads/:threadId/bounded(n=11)The mobile client gave up on these requests. In the service log, the relay reported
Incoming request ended abruptly: context canceledfor 40/api/orchestration/shellrequests, 12/boundedrequests and a handful of auth and websocket-ticket requests. To the user this looked like laggy sync on the phone.Change: I archived 2,152 threads that hadn't been updated in more than 7 days, using
thread.archiveover the/wsRPC. That left about 2,290 unarchived. Nothing else changed.After (about 6 min, with load average higher at 41–59 because of agent builds):
ws.orchestrationV2.projectShellItems(n=324)OrchestratorV2.getThreadShell(n=393)sql.transaction(n=1,756)The relay cancelled no
/api/orchestration/shellrequests after the change. It still cancelled 9/boundedthread opens, one of which took 5.4 s.Caveats: the "after" window is short and the load was different, so this is not a controlled benchmark. The direction is clear, though: p95 dropped about 3.5× while the machine was busier.
Also seen: 208
branchPullRequestlookups in about 30 min, p50 4.0 s (matches #24098).Suggestion: until retention (#17845 / #22630) lands, automatically archiving idle threads, or leaving them out of shell subscriptions, looks like a cheap way to cut most of this cost on agent-heavy servers.