OpenCode usage scan OOMs the server when individual message rows are huge (64-row pages aren't byte-bounded)#27667
What happened
T3 Code desktop on Windows, using a WSL backend, stopped working. The composer showed " is reconnecting", flickering on and off, and sending failed with "Not connected: message not sent. Reconnecting to the environment. Try again once it is connected". No messages reached Claude or OpenCode in WSL.
Diagnosis
The WSL backend crash-loops with a V8 heap OOM inside the OpenCode usage reader. The desktop app restarts it each time, which causes the reconnect flicker. The trace showed 19 server starts in about 11 minutes.
packages/provider-opencode/src/server/usage.ts (readOpenCodeUsage) pages the message table with a rowid cursor: MESSAGE_PAGE_SIZE = 64, read with StatementSync.all(). It selects the full data column for every row in the window. The page size bounds row count, not bytes. #17961 added this paging; it holds up for many small rows but not for a few very large rows.
OpenCode (v2.2.0) can store very large user messages. In this database, 38 user messages are 11–150 MB each, 3.25 GB in total. Nearly all of it is summary.diffs[].patch: file patches OpenCode records for the turn, here from a repository containing large generated files. Several of these rows share a 64-row page, so one page is hundreds of MB of string data before JSON.parse. The process dies in node::sqlite::ExtractRowValues → Statement::All.
The reader only needs token and cost fields from assistant messages. User message bodies, and summary.diffs in particular, contribute nothing to usage but are fully materialized.
usage.ts on main is byte-identical to v0.1.5, so this is not yet fixed.
Possible fixes:
- Filter in SQL to
json_extract(data, '$.role') = 'assistant', and/or select only the needed fields withjson_extract(tokens, cost, modelID, providerID, time) instead ofdata. - Bound pages by bytes, for example skip or flag rows where
length(data)exceeds a cap, instead of only by row count.
Steps to reproduce
- In an OpenCode data dir (
~/.local/share/opencode/opencode.db), have several user messages in the usage window whosedatais over 100 MB, adjacent enough to share a 64-rowid page. Real-world cause:summary.diffs[].patchon turns that touched large files. Synthetic repro: insert usermessagerows whosedatais{"role":"user","summary":{"diffs":[{"patch":"<150 MB string>"}]}}. - Start T3 Code (desktop app with a WSL backend here; any server should do) with the OpenCode provider configured.
- Let a client connect and trigger the usage scan.
- The server aborts with
FATAL ERROR: Ineffective mark-compacts near heap limitand restarts. The client shows "reconnecting" in a loop.
Version
0.1.5 (desktop app T3CodeBeta/0.1.5 on Windows; WSL server reports serverVersion 0.1.5). The triage CLI was also 0.1.5.
Environment
Windows host with T3 Code desktop 0.1.5, backend in WSL2 (kernel 6.18.54.1-microsoft-standard-WSL2, linux x64, memory=12GB, mirrored networking). Node v26.12.1 in WSL. opencode v2.2.0. opencode.db is 47.5 GB.
Evidence
| 1 | # desktop logs/server-child.log (Windows side), repeated every ~30-60s |
| 2 | FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory |
| 3 | 2: node::OOMErrorHandler(char const*, v8::OOMDetails const&) |
| 4 | 12: v8::String::NewFromUtf8(v8::Isolate*, char const*, v8::NewStringType, int) |
| 5 | 13: node::sqlite::ExtractRowValues(node::Environment*, sqlite3_stmt*, int, bool, v8::LocalVector<v8::Value>*) |
| 6 | 14: node::sqlite::StatementExecutionHelper::All(node::Environment*, node::sqlite::Statement*) |
| 7 | 15: node::sqlite::Statement::All(v8::FunctionCallbackInfo<v8::Value> const&) |
| 8 | backend child process failure output end details: pid=18140 code=6 |
| 9 | |
| 10 | # server.trace.ndjson: last spans before each silent restart are a burst of |
| 11 | UsageService.readFileRecords ... (then next process: cloud.service_launcher_client.resolve_startup) |
| 12 | |
| 13 | # opencode.db (read-only inspection) |
| 14 | message rows: 12954; rows with length(data) > 10 MB: 38 (all role=user), 3.25 GB total, max 150 MB |
| 15 | largest row: $.summary.diffs[0].patch = 140.7 MB |
| 16 | after clearing summary.diffs on those 38 rows: largest 64-row page = 10.8 MB |
Related issues
#17961 (closed) introduced the 64-row paging for the same OOM path; this is the remaining case where individual rows are huge. #21370 and #8648 are other heap OOMs with different causes (long uptime, hydration). None cover OpenCode rows of this size.
Fix applied or workaround
Backed up the 38 oversized rows to a separate SQLite file, then ran in one transaction on opencode.db:
UPDATE message SET data = json_set(data, '$.summary.diffs', json('[]')) WHERE length(data) > 10000000 AND json_type(data, '$.summary.diffs') = 'array'.
This keeps all messages and token data and drops only OpenCode's stored diff summaries for those turns. The backend stopped crash-looping, and messages from the desktop app reach the WSL environment again.
Filed by
Filed through pnpx t3 triage with Claude Code (claude-opus-5-5).