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.
Area
apps/server (packages/provider-opencode/src/server/usage.ts, apps/server/src/usage/UsageService.ts)
Steps to reproduce
- Run a T3 server whose OpenCode 2 instance has a large
opencode.db. Here: OpenCode 2.2.5, 9.9 GB database, 481,648session_messagerows, 436,585 of themtype = 'assistant'inside a 30-day window (about 29k per day; averagedatalength 14 KB). - From a desktop client connected to that server, open Usage, choose "All environments" and 30 days.
- Close and reopen the page more than 5 minutes later, or press refresh.
Expected behavior
The remote environment's usage arrives in seconds, and repeat opens reuse earlier work. While an environment is still loading, the totals say they are partial.
Actual behavior
server.getUsageSummarytook 278 s. Almost all of it wasreadOpenCodeUsage(276 s). It pagessession_messageby rowid fromMIN(rowid)toMAX(rowid), 64 rows at a time, and runsJSON.parseon the fulldataof every in-window assistant row. The other readers finished in under 3 minutes, in parallel.- The OpenCode reader has no cache, so each new summary request repeats the full 4.6-minute scan.
- During the scan, the server's event loop sat at 100% utilization, with stalls of 2.4 to 4.3 s and RSS rising to 1.5 GB. Live agent turns on that server share this event loop.
- The page draws "All environments" totals before the slow environment answers. The screenshot showed 13 sessions and 21.5M tokens, all from the local Mac environment, with no visible sign that the remote one (about $2.7k/day of OpenCode usage) was still loading. It is easy to read as the real total.
#26613 (open) would make repeat scans cheap: it uses the time_created index plus a persisted row cache, and its loop also covers session_message. The first scan would still parse every in-window row. #27679 (json_object extraction in SQL) would shrink each row before it reaches JS. #27667 is a different failure (OOM from huge user rows).
Impact
Major degradation or frequent failure
Version or commit
Server 0.1.6-nightly.20261030.3940 (Linux); desktop 0.1.6-nightly.20261031.3956 (macOS). usage.ts is unchanged on main since 8f26ed7.
Environment
NixOS server, OpenCode 2.2.5 provider (all usage in session_message); macOS desktop client connected to it over WebSocket.
Logs or stack traces
# server.trace.ndjson (remote server), one Usage page open
ws.rpc.server.getUsageSummary 23:56:21 278s Success
UsageService.scanSummary 23:56:21 277s Success
UsageService.collectDirs 23:56:21 276s Success
openCodeUsageReader.scan 23:56:21 276s Success
readOpenCodeUsage 23:56:21 276s Success
UsageService.scanTranscriptDir 23:56:21 174s Success # claude/grok/codex, in parallel
# event-loop-stalls.ndjson during the scan
2026-10-31T23:57:04Z delayMaxMs=4331 utilization=1 rssMb=1313
2026-10-31T23:57:35Z delayMaxMs=2391 utilization=1 rssMb=1131
2026-10-31T23:59:08Z delayMaxMs=2355 utilization=1 rssMb=1507
# desktop client
EnvironmentRpc.request server.getUsageSummary local env 82 ms
EnvironmentRpc.request server.getUsageSummary remote env 278069 msWorkaround
Wait about 5 minutes on the Usage page before reading the totals. There is no workaround for the repeated scan cost.
Model and harness: Claude Opus 5.5 in OpenCode.
Activity
imadtg commentedon Nov 7, 2026
Same reader, a much smaller database, and still minutes per summary: on a spinning disk the cost follows bytes read, not row count.
Version: server 0.1.7-nightly.20261101.4016 (background service); current
main(298cb24) has the same uncached query inpackages/provider-opencode/src/server/usage.ts.Environment: Linux 7.2.8 (Omarchy), Node 24.22, OpenCode 2.1.4, 7.6 GB RAM, 5400 rpm SATA HDD under LUKS2 + Btrfs.
Database:
opencode.dbis 453 MB but holds only about 8,000messageand 8,300session_messagerows (about 28 KB ofdataper row). The scan selectsdatafor every in-window row, so with rows this large a summary can read most of the file from disk again.Server traces, 2026-11-04 21:41 to 2026-11-05 04:50. I excluded windows where other heavy disk jobs ran on this machine.
UsageService.scanSummaryopenCodeUsageReader.scanwithin itThe OpenCode reader dominates even in scans where everything else is cheap. At 04:27 it took 87.8 s, while the Antigravity reader took 0.5 s and the transcript walk 0.3 s.
Knock-on effect. In 3 of the 7 scans, the Antigravity reader's 9 per-database reads took 45–86 s in total. In the other 4 they took 0.4–7 s. The databases have not changed since October, so their fingerprints match and those reads should be cache hits: one
realpathand twostatcalls each. The extra time looks like queueing behind other I/O in the same scan, which is the shape described in #26417, but I have not confirmed that.What would help here, in order:
time_updated, so a repeat request only reads new or updated rows.datablob. Where the schema allows it, extract them in SQL, so a row's context is not read and parsed for its token counts.Related: #27667 (the same reader with unbounded page bytes), #26417 (filesystem work exhausting the libuv pool).
Written by
Claude Code (Claude Opus 5.5, claude-opus-5-5), following the t3 triage playbook, from measurements on the machine described above.
nkoynov commentedon Nov 11, 2026
Still happens on 0.1.8-nightly.20261107.4340. #33612 (in 4446 and later) makes each scan cheaper but leaves it on the event loop.
On 2026-11-11 one open of the Usage page in the desktop app started two summaries 8 s apart (two windows). Each ran its own
readOpenCodeUsageover a 13.3 GB OpenCode 2 database: 2,151 s and 2,153 s, side by side. While they ran:event-loop-stalls.ndjsonshowed utilization 1.0 in every sample (max delays 2.1–5.6 s), and RSS went from 0.6 to 1.1 GB.orchestrationV2.dispatch.withReceipttook over 1 s (p90 15 s, max 28.8 s), against 7 % (p90 0.7 s) outside the scans over the surrounding 2.5 days. Point queries on T3's own database waited 15–17 s for the connection.The same happened on 2026-11-09 (two scans, 1,272 s and 1,278 s; 43 % of dispatches over 1 s). A second server with a 2 GB database ran 22 scans of up to 85 s each within six minutes on 2026-11-10.
On 4515 the reader still opens
NodeSqliteClient.layer({ filename, readonly: true })withoutreaderWorker, so every page is a synchronousnode:sqlitecall on the event loop. A 30-day window covers most of an active database (91 % of the 750ksession_messagerows here). Opening it withreaderWorker: true, as T3's own database does, and pipingsql.withTransaction(read)throughNodeSqliteClient.readOnlywould move the transaction and every statement in it to the worker (the transaction picks its connection when it starts, so the wrapper has to be outside it). The database is WAL, which the reader worker requires. If the worker can't start, the pool turns off and reads fall back to the event loop with only a warning in the log, so this reader may be better off refusing in that case.Workaround here: a preload that refuses
session_message/messagestatements on read-only connections toopencode*.db. The reader then reports "Some OpenCode history could not be read." and the page shows the other sources.Model and harness: Claude Opus 5.5 in OpenCode, via T3 Code.
juliusmarminge commentedon Nov 13, 2026
This is the same problem as #34897, so I'm closing it in favour of that one. If it is different, say how and it will be reopened.
nkoynov commentedon Nov 14, 2026
Retested #34897 on nightly 4669 (db2cc20) with OpenCode 2.3.0. The stall part is fixed: during a 9.4 s cold scan of a 1.96 GB
opencode.db, other HTTP requests answered in under 15 ms.The repeat part only partly is. The row cache's version is the database file's identity, and that includes the main file's size and mtime. Both change at every WAL checkpoint, so the first refresh after a write that reaches a checkpoint drops the whole cache and rereads every body. In the VM, a refresh with nothing changed took 0.9 s, and one after a single turn took 3.2 s. On a busy server that's most refreshes.
Both tables here have
time_updatedand the per-row check already compares it withoctet_length. So would it be enough to key the version on dev, inode and birth time only, to catch a replaced file, and let the per-row markers pick up new and changed rows?