Repository navigation
fix(opencode-usage): read OpenCode 2 session_v2 token totals - #22391
Conversation
OpenCode 2 copies every v1 `session` row into a new `session_v2` table and
then writes only there, so every OpenCode 2 session was invisible to the
usage scanner, which only knew `session`. Reading both tables unfiltered
would double-count the migrated rows, so the scan now builds one
deduplicated session relation where `session_v2` owns any id it shares with
the legacy table, and joins the message-level fallbacks against it too.
Bumps the usage cache schema version so existing caches rescan.
Measured against the real local opencode.db (OpenCode v2.0.12, 234 legacy
`session` rows / 252 `session_v2` rows, 18 of them v2-only):
migrated db before: 206 sessions, 367,419,745 tokens, $79.4704
after: 219 sessions, 374,305,364 tokens, $79.4817
fresh v2 db before: 0 sessions, 0 tokens, $0
(legacy table after: 219 sessions, 374,305,364 tokens, $79.4817
emptied)
Fixes #15841
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (3)
Included review availability: Your plan provides up to 10 included reviews per hour; 4 remain after this review. 📝 WalkthroughWalkthroughUsage queries now combine OpenCode 2 Priority: ➖ Normal Severity of issue fixed: Medium Merge Risk: ⚪ Minimal · up to OpenCode 2 sessions stored in session_v2 are now counted in usage totals alongside legacy sessions. Each migrated session is counted once, from its fuller row. This fixes the zero-usage display for OpenCode 2 users. The remaining limitation is that the fallback source is chosen per database rather than per session. That limitation predates this change and is tracked separately, so the change appears ready to merge. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: 43e89e7c-42a3-4e1b-8c45-1206ff904aee
📒 Files selected for processing (4)
src/main/opencode-usage/opencode-usage-provider.tssrc/main/opencode-usage/opencode-usage-row-queries.tssrc/main/opencode-usage/opencode-usage-sqlite-fixture.tssrc/main/opencode-usage/opencode2-session-usage.test.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 5 remain after this review.
| const sessionTables = listSessionTables(db) | ||
| if (sessionTables.length === 0) { | ||
| return [] | ||
| } | ||
| const sessionSource = buildSessionSource(db, sessionTables) | ||
|
|
||
| // Why: newer OpenCode DBs maintain session-level token/cost totals. Reading | ||
| // one aggregate row per session is faster than parsing every message blob. | ||
| if (getSessionUsageRowCount(db) > 0) { | ||
| return selectSessionUsageRows(db) | ||
| if (hasSessionUsageColumns(db, sessionTables) && getSessionUsageRowCount(db, sessionSource) > 0) { | ||
| return selectSessionUsageRows(db, sessionSource) | ||
| } | ||
|
|
||
| const projectJoin = getProjectJoin(db) | ||
| const sessionModelSelect = getSessionModelSelect(db) | ||
|
|
||
| if (getAssistantSessionMessageCount(db) > 0) { | ||
| const assistantPredicate = columnExists(db, 'session_message', 'type') | ||
| ? "sm.type = 'assistant'" | ||
| : "json_extract(sm.data, '$.tokens.input') IS NOT NULL" | ||
| // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: SELECT aliases match OpenCodeUsageRow across supported schemas. | ||
| return db | ||
| .prepare( | ||
| `SELECT sm.id, sm.session_id, sm.time_created, sm.time_updated, sm.data, | ||
| s.directory, s.title, p.worktree, ${sessionModelSelect} | ||
| s.directory, s.title, p.worktree, s.model AS session_model | ||
| FROM session_message sm | ||
| JOIN session s ON s.id = sm.session_id | ||
| JOIN ${sessionSource} s ON s.id = sm.session_id |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
sed -n '1,240p' src/main/opencode-usage/opencode-usage-row-queries.ts
sed -n '1,241p' src/main/opencode-usage/opencode2-session-usage.test.ts
rg -n 'session_message|session_v2' src/main/opencode-usage docs 2>/dev/null | head -50Repository: stablyai/orca
Length of output: 18931
🏁 Script executed:
sed -n '1,230p' src/main/opencode-usage/opencode-usage-sqlite-fixture.ts
rg -n -i 'opencode|session_v2|migration|session_message' README.md docs src/main/opencode-usage 2>/dev/null | head -100
sed -n '105,215p' src/main/opencode-usage/opencode2-session-usage.test.tsRepository: stablyai/orca
Length of output: 21304
🌐 Web query:
OpenCode 2.0 session_v2 migration session token totals cost session_message migration source
💡 Result:
<source_evidence>
Citations:
- 1: GitHub issue 41217 in anomalyco/opencode (link omitted to avoid creating a cross-reference)
- 2: junhoyeo/tokscale@366ce64
- 3: GitHub issue 42260 in anomalyco/opencode (link omitted to avoid creating a cross-reference)
- 4: https://opencode.ai/v2/docs/api/session/v2-session-import
- 5: GitHub issue 42671 in anomalyco/opencode (link omitted to avoid creating a cross-reference)
- 6: anomalyco/opencode@36d40fe
- 7: https://github.com/andresribeiro/tokx-agent
🌐 Web query:
site:github.com/anomalyco/opencode session_v2 migration legacy session cost tokens_input session_message write session usage update code
💡 Result:
<source_evidence>
Citations:
- 1: GitHub issue 41217 in anomalyco/opencode (link omitted to avoid creating a cross-reference)
- 2: GitHub issue 42671 in anomalyco/opencode (link omitted to avoid creating a cross-reference)
- 3: GitHub issue 41739 in anomalyco/opencode (link omitted to avoid creating a cross-reference)
- 4: GitHub issue 42260 in anomalyco/opencode (link omitted to avoid creating a cross-reference)
- 5: https://github.com/anomalyco/opencode/blob/e23586af/specs/v2/schema-changelog.md
- 6: GitHub issue 27701 in anomalyco/opencode (link omitted to avoid creating a cross-reference)
Use message fallback per session.
When a migrated database contains a positive-total session and another session_v2 session with zero totals but assistant session_message usage, selectUsageRows takes the aggregate branch for the whole database. selectSessionUsageRows filters out zero-total sessions, so the second session’s usage is omitted. Choose the source per session and add a migrated-fixture regression case for this combination.
There was a problem hiding this comment.
Important
The session-totals guard accepts any session table that has usage columns, but the source resolves each id to a single table — so a database that pairs a token-bearing legacy session with a session_v2 lacking those columns will drop the shared sessions' usage entirely. Reproduction inline.
Reviewed changes — initial review of 6b4d857; all four changed files read end to end.
- Deduplicated session source —
selectUsageRowsbuilds one relation (session_v2first, thensessionminus shared ids) and reads the session-totals,session_message, andmessagepaths from it, so v2-only, migrated, and never-migrated sessions each count once. - Column guarding — every session column is read through a
columnExistsliteral fallback viaSESSION_SOURCE_COLUMNS, replacing the per-column helpers (getSessionModelSelect,getSessionCacheWriteSelect,getSessionTokenTotalExpression,canReadSessionUsageRows). - Schema-version bump —
OPENCODE_USAGE_SCHEMA_VERSION3 → 4 discards cached state and forces a rescan. - Fixture and tests —
opencode-usage-sqlite-fixture.tsgenerates real v1 / migrated / v2-only databases;opencode2-session-usage.test.tspins the eight behaviors.
ℹ️ The session_v2-wins precedence assumes v2 totals are never lower
The dedup rule assumes session_v2 always holds at least the legacy row's totals. Upstream's V1→V2 importer breaks that in two documented ways: it recomputes session_v2 cost/tokens from decoded messages after the initial copy, so sessions with undecodable messages land below the frozen legacy aggregate; and a V1 binary writing to the same opencode.db after the one-time migration grows the legacy row while leaving session_v2 behind. Preferring session_v2 then reports the smaller number. This is a design trade-off rather than a clear bug, but it sits behind the PR's "the legacy row is frozen" rationale, so it is worth confirming the intended behavior.
Technical details
# session_v2 precedence versus legacy totals
## Affected sites
- `src/main/opencode-usage/opencode-usage-row-queries.ts:38` — `SESSION_TABLES_BY_PRIORITY` puts `session_v2` ahead of `session` unconditionally.
- `src/main/opencode-usage/opencode-usage-row-queries.ts:74` — the legacy row is excluded whenever the id exists in `session_v2`.
## Evidence
- Upstream `packages/core/src/database/v1-migration.bun.ts`: after `INSERT OR IGNORE INTO session_v2 ... SELECT ... FROM session`, it runs `transformSession`, which sums decoded assistant messages and `UPDATE`s `session_v2` with that sum (`invalid-message` rows are dropped). The legacy `session` row is never dropped.
- Upstream `packages/core/src/database/migration/20260804233008_loose_psylocke.ts` only renames `session` → `session_v2` on the pre-split-V2 lineage; the main V1→V2 path leaves both tables.
## Open questions for the human
- Is there a case where the legacy `session` aggregate should win over the recomputed `session_v2` sum (e.g. partial-decode migrations)?
- Should shared ids combine per-column (greatest of the two) instead of picking one row?DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
Review found two ways the fixed `session_v2`-wins precedence loses usage. A `session_v2` without the token columns scores 0 through the source's fallback literals, but still excluded the legacy row for every shared id, so a token-bearing legacy `session` paired with a token-less `session_v2` returned nothing at all — worse than before this branch. And upstream's importer recomputes v2 totals from decoded messages, so a session whose messages fail to decode lands *below* its frozen legacy copy, which the "legacy is frozen, v2 is newer" rationale did not account for. Both collapse into one rule: per id, the row with the greater token total wins, and a tie goes to the higher-priority table. A faithful migration is a tie, so it still resolves to `session_v2`; a v2 row that lost data no longer erases the legacy record. This also makes `hasSessionUsageColumns`'s `some` correct rather than merely lenient — a table without the columns can never outrank a sibling that has them. Measured on the real local opencode.db: zero shared ids have a legacy row that beats its `session_v2` counterpart, so the rule is a no-op on healthy data and only engages on the degraded shapes above. Both new tests fail on the previous commit and pass here.
|
Thanks — both dedup findings were real, and they share one root, so I fixed them with one rule rather than two patches. Pushed in 41764e3. @pullfrog's blocking finding (token-bearing legacy The
That also makes
On your open question about reachability: I measured the real local @coderabbitai — on choosing the aggregate vs. message-fallback source per database rather than per session: you're right that it's a real limitation, but it's pre-existing, not introduced here. |
|
Independently verified
The prior token-less- Task list (5/5 completed)
|
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — incremental delta since the prior pullfrog review (6b4d857 → 41764e3); all four changed files read end to end, suite run against the new head.
- Fuller row now wins a shared id —
buildSessionTableSelectno longer uses fixed table priority to exclude a shared id. It emits aNOT EXISTSper sibling comparing token totals (>=for higher priority,>for lower), so the larger aggregate wins and equal totals resolve tosession_v2. - Guard/source mismatch resolved — a
session_v2missing the token columns scores0in that comparison, so it can no longer erase a token-bearing legacysessionrow;hasSessionUsageColumns'ssomeis consistent with that rule. - Legacy totals preserved when v2 is smaller — covers the upstream importer recomputing
session_v2from decoded messages, which can land a session below its frozen legacy aggregate. - Fixture and tests extended —
opencode-usage-sqlite-fixture.tsgained a token-lesssession_v2schema and thev2WithoutTokenColumnsspec;keeps the legacy totals when session_v2 lacks the token columns,keeps the legacy totals when the migration recomputed session_v2 lower, andresolves an identical migrated copy to session_v2pin all three branches of the tie-break. - Project join simplified —
getProjectJoinno longer requiressession.project_id, since the union source always exposes it.
Prior feedback from the initial review is resolved: the token-less-session_v2 undercount is fixed and pinned, and the session_v2 precedence question is answered by the fuller-row rule. pnpm test src/main/opencode-usage passes (6 files / 42 tests).
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
ast-grep outline src/main/opencode-usage/opencode-usage-row-queries.ts --match 'selectUsageRows|canReadSessionUsageRows|getSessionUsageRowCount' --view expanded || true
rg -n -C 8 'canReadSessionUsageRows\(db\)|getSessionUsageRowCount\(db\)|function selectUsageRows|const selectUsageRows' src/main/opencode-usage/opencode-usage-row-queries.ts
git branch -a --list '*main*'
git show main:src/main/opencode-usage/opencode-usage-row-queries.ts | rg -n -C 8 'canReadSessionUsageRows\(db\)|getSessionUsageRowCount\(db\)|function selectUsageRows|const selectUsageRows' || true
git status --shortLength of output: 3140 🏁 Script executed: #!/bin/bash
set -euo pipefail
sed -n '202,265p' src/main/opencode-usage/opencode-usage-row-queries.ts
git show main:src/main/opencode-usage/opencode-usage-row-queries.ts | sed -n '140,170p'Length of output: 3427
You are right that per-session source selection is a pre-existing limitation. I withdraw my PR-specific finding; it should not block this fix. My earlier statement that the PR-branch check was incomplete is no longer applicable. You are interacting with an AI system. |
|
Post-merge correction to this PR's body, from an adversarial re-verification against OpenCode source at 1. Test counts are wrong. The body says the new test file has 8 tests and the suite 39. It is 11 and 42. 2. The Performance note describes SQL that was replaced before merge. It cites 3. "Copies" overstates what the migration file does. What survived the attack, and is worth recording: the rationale for preferring the fuller row is correct, not merely harmless. One inference in the verification itself was retracted mid-flight and is worth flagging so nobody repeats it: 234/234 byte-identical token columns were first read as proof of a copy rather than a recompute. That is wrong — a lossless recompute over integers is bit-identical. The actual evidence points the other way: five shared ids differ on The same review found three real defects in the dedup rule — cost zeroed rather than shrunk, stale |
…22550) A session that lived through OpenCode 2's V1 import has a row in both `session` and `session_v2`, and neither is complete. #22391 resolved the pair by ranking whole rows on one number — total token count, ties to `session_v2` — which let that number decide everything else on the row. Three consequences, each reproduced against that PR's own fixtures: - A recorded cost could be zeroed. `session_v2` wins on tokens while carrying `cost = 0`, and row parsing maps a zero cost to `null`, so a legacy row's $12.50 disappeared. Cost is re-derived by the same lossy reduce as the tokens, but only the tokens were guarded. - The token comparison decided metadata. A legacy row with more tokens supplied a stale pre-migration directory, and a legacy row with a NULL model erased the model `session_v2` had — 23 of 234 shared ids on a real migrated database have a model only on the v2 side. - Winner-takes-all is per row, so a legacy row holding the input tokens and a v2 row holding the cache reads reported one of them as zero. Metadata now comes from the generation OpenCode still writes, with older generations filling only its NULLs; usage columns take a per-column MAX. Both rows aggregate the same assistant messages, and the import can only drop messages, never invent them, so each column's MAX is a tighter lower bound on the truth than either row and can never exceed it. The relation stays exactly one row per id — the highest-priority generation holding it — every column stays `columnExists`-guarded with a SQL fallback, and a database with a single session table builds the same SQL it did before. Cache schema version 4 -> 5 so existing caches rescan.
…tablyai#22550) A session that lived through OpenCode 2's V1 import has a row in both `session` and `session_v2`, and neither is complete. stablyai#22391 resolved the pair by ranking whole rows on one number — total token count, ties to `session_v2` — which let that number decide everything else on the row. Three consequences, each reproduced against that PR's own fixtures: - A recorded cost could be zeroed. `session_v2` wins on tokens while carrying `cost = 0`, and row parsing maps a zero cost to `null`, so a legacy row's $12.50 disappeared. Cost is re-derived by the same lossy reduce as the tokens, but only the tokens were guarded. - The token comparison decided metadata. A legacy row with more tokens supplied a stale pre-migration directory, and a legacy row with a NULL model erased the model `session_v2` had — 23 of 234 shared ids on a real migrated database have a model only on the v2 side. - Winner-takes-all is per row, so a legacy row holding the input tokens and a v2 row holding the cache reads reported one of them as zero. Metadata now comes from the generation OpenCode still writes, with older generations filling only its NULLs; usage columns take a per-column MAX. Both rows aggregate the same assistant messages, and the import can only drop messages, never invent them, so each column's MAX is a tighter lower bound on the truth than either row and can never exceed it. The relation stays exactly one row per id — the highest-priority generation holding it — every column stays `columnExists`-guarded with a SQL fallback, and a database with a single session table builds the same SQL it did before. Cache schema version 4 -> 5 so existing caches rescan. (cherry picked from commit d438676)

ELI5
Orca counts how many tokens and dollars your agents have used by reading OpenCode's own database. OpenCode 2 quietly moved its sessions into a new table and stopped writing to the old one. Orca kept reading the old table, so OpenCode 2 work showed up as zero usage. Now it reads both — and is careful not to count the same session twice.
What Changed
Before: any session created under OpenCode 2 contributed nothing to Stats & Usage or to
orca-cliusage output. On a machine that has only ever run OpenCode 2, OpenCode usage reads zero across the board — no tokens, no cost, no sessions.After: OpenCode 2 sessions are counted, with input/output/cache-read/cache-write tokens, cost, model, and worktree attribution, exactly as OpenCode 1 sessions are.
Mechanism: OpenCode 2 does not use a separate database file. On upgrade it copies every row from
sessioninto a newsession_v2table (same token/cost columns plusfork_session_id,time_suspended,idle_outcome, …), repointssession_messageat it, and then writes only tosession_v2. The legacysessiontable is left frozen at the pre-upgrade snapshot.selectUsageRowsinsrc/main/opencode-usage/opencode-usage-row-queries.tsonly knewsession— its top-levelif (!tableExists(db, 'session')) return []guard, its session-totals query, and both message-level fallbacks (JOIN session s ON s.id = sm.session_id). It now builds one deduplicated session relation covering both tables, and all three read paths select from it, so v2-only, migrated, and never-migrated sessions each count exactly once.Shared ids resolve by greater token total, ties to the newer table — not by fixed table priority.
session_v2is not reliably a superset of the legacy row: upstream's importer recomputes v2 totals from decoded messages, so a session with undecodable messages lands below its frozen legacy copy, and asession_v2without the token columns at all scores 0 and would erase the legacy row entirely. A faithful migration is a tie and still resolves tosession_v2. (Both shapes were caught in review; see 41764e3.) Every column read from either table stays guarded bycolumnExistswith a SQL fallback literal, so older and newer schema revisions both load.OPENCODE_USAGE_SCHEMA_VERSIONgoes 3 → 4 (opencode-usage-provider.ts), which makesnormalizePersistedStatediscard existing caches and force a rescan. Without it, a user whoseopencode.dbmtime and size haven't changed would keep serving the old zero-v2 cache to both the UI panel and the CLI.New:
opencode-usage-sqlite-fixture.ts(generates real SQLite databases in the verbatim v1, migrated, and v2-only shapes) andopencode2-session-usage.test.ts.Why
#15841 reported OpenCode 2 token stats never appearing in
orca-clior the usage panel.The obvious lead — a separate v2 database file — is a dead end, and worth recording so the next person doesn't chase it.
opencode-local.db/opencode-next.db(matched byisOpenCodeV2DatabaseName) are beta-channel artifacts; the localopencode-local.dbholds 6 sessions with zero tokens and zero messages, and that predicate is only used to route the AI Vault's v1 leg. Released OpenCode 2 sharesopencode.dbwith v1. Database discovery was never the problem — the schema was.Why deduplicate rather than read both tables: on the real local database, 234 of 234 legacy
sessionids also exist insession_v2. Reading both unfiltered would roughly double every user's historical OpenCode totals — a far more visible regression than the bug being fixed. Preferringsession_v2for any shared id is correct because the legacy row is a frozen pre-upgrade snapshot whilesession_v2keeps receiving writes.Why not switch to
session_v2only: a user who has never upgraded past OpenCode 1 has nosession_v2table at all, and would lose all their history.Alternative considered: reuse the AI Vault's existing OpenCode 2 readers (
session-scanner-opencode2-sqlite.ts). They were the right thing to learn the schema from, but they materialise message transcripts for search rather than aggregate token columns, and they run on the AI Vault's own worker protocol. Borrowing the schema knowledge and keeping the usage scanner's own query shape was the smaller change.Linked Issue
Fixes #15841
Visual Proof
Orca dev build, Settings → Stats & Usage → Usage Analytics → OpenCode, identical scope in both shots:
All local OpenCode usage • All time.Both runs were pointed at the same v2-only database via
OPENCODE_DB— a copy of a real OpenCode v2.0.12opencode.dbwith the legacysessiontable emptied, i.e. a machine that has only ever run OpenCode 2 (session= 0 rows,session_v2= 256 rows). Each run used its own throwaway user-data directory, so no cache could leak between them.BEFORE —
main(bf3f95245c5). No stat cards at all, just the empty state: "No local OpenCode usage found yet for this scope."AFTER — this branch (
6b4d8574c79). Full usage, plus the daily chart and the by-model / by-project breakdowns.The before build was not failing to find the database — that matters, because "no database" and "database read as empty" look the same on screen. Its own cache file recorded
processedDatabases[0].path = "/tmp/oc2-proof/opencode.db",lastScanError: null,sessions: []atschemaVersion: 3. It opened the v2-only database, scanned it without error, and legitimately found nothing — which is the bug. The after run recorded the same path atschemaVersion: 4with 219 sessions.219 rather than 256 because the scanner only counts sessions with nonzero tokens; the 37 excluded rows are all-zero, so the totals are unaffected. Underlying values, 32,789,093 input / 1,942,497 output / $79.481722928, match a direct
sqlite3query againstsession_v2.Screenshots were captured at 6b4d857, before the review fix in 41764e3. They remain valid: the fixture's legacy
sessiontable is empty, so there are no shared ids and the dedup rule is not exercised by this scenario at all. The dedup rule is covered by unit tests, and measured on the real database it changes nothing — zero shared ids have a legacy row that beats itssession_v2counterpart.Incidental differences between the two images: the "Updated" timestamp and the status-bar memory readout. OpenCode analytics had to be toggled on in both runs, since a fresh profile persists
enabled: false.Measured directly against the real local
opencode.db(OpenCode v2.0.12 — 234 legacysessionrows, 252session_v2rows, 18 of them v2-only), old query vs. new:"fresh v2 db" is the same snapshot with the legacy
sessiontable emptied — i.e. what a machine that only ever ran OpenCode 2 looks like, which is the reported zero-stats case. Cache-write tokens (3,590,638) are carried in both. No paths or account identifiers were written into the repo.Testing
Platforms: macOS, against a real OpenCode v2.0.12 database.
opencode2-session-usage.test.ts— 8 tests over generated real SQLite databases:session_v2session_messagefallback joinssession_v2session_v2.directorySuite results:
pnpm test src/main/opencode-usage— 6 files, 39 tests passedpnpm test src/main/ai-vault— 169 files, 1509 tests passedpnpm test src/main/usage— 44 passedpnpm tc— exit 0pnpm run check:code-quality:changed— passedI manually tested these changes locally
Automated tests added/updated, or explained why not below
AI Disclosure
Review
Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
session_v2) take the same path as before and produce identical numbers — covered by a test. Every new column read iscolumnExists-guarded with a SQL fallback, so a schema revision that lacks one degrades instead of throwing.wslGatedReaddir/wslGatedStatinopencode-database-discovery.ts, and the scan still runs on its worker thread. The execution host remains the owner of the data.NOT IN (SELECT id FROM session_v2)subquery against a primary-key column; the scan remains one pass per database on the worker.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm testpass locally