[comment audit] Complete repository-wide comment cleanup (#33249)

* [comment audit] Align legacy failure-read counts with cached display

* [comment audit] Condense shared provider lifecycle and browser recovery comments [skip ci]

* [comment audit] Condense shared generation, path and preview comments [skip ci]

* [comment audit] Condense host, file and Git contract rationale [skip ci]

* [comment audit] Tighten Git lifecycle and provider contract comments [skip ci]

* [comment audit] Clarify shared host authority and launch isolation [skip ci]

* [comment audit] simplify local access mobile and native chat contracts [skip ci]

* [comment audit] simplify native chat presentation and state contracts [skip ci]

* [comment audit] simplify native runtime and provider contracts [skip ci]

* [comment audit] simplify migration and orchestration contracts [skip ci]

* [comment audit] simplify plugin trust and process capture rationale [skip ci]

* [comment audit] simplify terminal delivery and remote compatibility rationale [skip ci]

* [comment audit] clarify remote recovery and launch wire rationale [skip ci]

* [comment audit] shorten shared RPC compatibility and runtime evidence comments [skip ci]

* [comment audit] simplify secure storage shell startup and sidebar rationale [skip ci]

* [comment audit] clarify sidebar, skill and SSH invariants [skip ci]

* [comment audit] condense structured chat delivery and timing rationale [skip ci]

* [comment audit] condense terminal replay and telemetry invariants [skip ci]

* [comment audit] clarify launch permission and terminal delivery rationale [skip ci]

* [comment audit] clarify workspace ownership and layout contracts [skip ci]

* [comment audit] wait for fitted and painted SSH terminal links

* [comment audit] clarify layout persistence and session ownership [skip ci]

* [comment audit] clarify host routing and workspace lifecycle contracts [skip ci]

* [comment audit] clarify startup persistence and app lifetime comments [skip ci]

* [comment audit] clarify recovery and activity boundaries [skip ci]

* [comment audit] condense activity and artifact invariants [skip ci]

* [comment audit] condense automation authority contracts [skip ci]

* [comment audit] simplify automation scheduling and history contracts [skip ci]

* [comment audit] clarify browser chrome and annotation lifecycles [skip ci]

* [comment audit] clarify browser routing and retained guest ownership [skip ci]

* [comment audit] clarify browser recovery and document ownership comments [skip ci]

* [comment audit] restore hosted runners required by native IME fixtures

* [comment audit] shorten preview, palette and dashboard invariants [skip ci]

* [comment audit] clarify terminal preview, dashboard and retention comments [skip ci]

* [comment audit] clarify diff-note recovery and editor lifecycle comments [skip ci]

* [comment audit] shorten rich-text and combined-diff ownership comments [skip ci]

* [comment audit] shorten autosave, draft recovery and model ownership comments [skip ci]

* [comment audit] shorten save-queue, notebook and Markdown constraints [skip ci]

* [comment audit] shorten Monaco, PDF and rich Markdown safeguards [skip ci]

* [comment audit] clarify rich Markdown source and selection safeguards [skip ci]

* [comment audit] clarify editor loading and emulator stream lifetimes [skip ci]

* [comment audit] trim emulator and feature tour narration [skip ci]

* [comment audit] clarify floating workspace and GitHub cache ownership [skip ci]

* [comment audit] trim provider controls and Linear filter explanations [skip ci]

* [comment audit] clarify pairing and chat resume lifetimes [skip ci]

* [comment audit] clarify chat delivery and transcript lifetimes [skip ci]

* [comment audit] clarify session options and host-owned status [skip ci]

* [comment audit] clarify draft durability and live-status precedence [skip ci]

* [comment audit] Trim chat send and model ownership explanations [skip ci]

* [comment audit] Shorten transcript ownership and send recovery comments [skip ci]

* [comment audit] Simplify native chat composer and transcript hook comments [skip ci]

* [comment audit] Shorten native chat scroll and structured session explanations [skip ci]

* [comment audit] Trim workspace picker and onboarding commentary [skip ci]

* [comment audit] Simplify pet rendering, review cache and quick open comments [skip ci]

* [comment audit] Trim sidebar and session history explanations [skip ci]

* [comment audit] Condense session resume and review checks comments [skip ci]

* [comment audit] Shorten checks state and file explorer refresh comments [skip ci]

* [comment audit] Trim plugin bridge and source-control action rationale [skip ci]

* [comment audit] Trim source-control listing and review rationale [skip ci]

* [comment audit] Shorten sidebar lifecycle and explorer safeguards [skip ci]

* [comment audit] Condense Settings account and appearance rationale [skip ci]

* [comment audit] Trim pairing and repository Settings explanations [skip ci]

* [comment audit] Condense Settings search SSH and terminal safeguards [skip ci]

* [comment audit] Shorten Settings voice and provider ownership comments [skip ci]

* [comment audit] Condense Settings draft persistence and host-state rationale [skip ci]

* [comment audit] Trim Settings hooks and sidebar setup rationale [skip ci]

* [comment audit] Condense sidebar deletion and board-filter safeguards [skip ci]

* [comment audit] Condense worktree cards, host ownership and Markdown contracts [skip ci]

* [comment audit] Condense sidebar drag, folder and filtering contracts [skip ci]

* [comment audit] Condense sidebar parity, sleep and host ownership contracts [skip ci]

* [comment audit] Prune focus-read baseline after comment cleanup

* [comment audit] Clarify sidebar visibility, board drops and agent evidence [skip ci]

* [comment audit] Tighten sidebar listing, drag and reveal contracts [skip ci]

* [comment audit] Clarify viewport geometry and skill update safeguards [skip ci]

* [comment audit] Tighten skill inventory, usage and host status comments [skip ci]

* [comment audit] Clarify usage ownership and resource kill safeguards [skip ci]

* [comment audit] Tighten tab selection, rename and search contracts [skip ci]

* [comment audit] Tighten tab-strip geometry and drag ownership comments [skip ci]

* [comment audit] Tighten Tasks recovery, pagination and mutation comments [skip ci]

* [comment audit] Tighten provider loading and terminal recovery comments [skip ci]

* [comment audit] Tighten completion, cadence and PTY ownership comments [skip ci]

* [comment audit] Clarify terminal restore and parked-pane boundaries [skip ci]

* [comment audit] Tighten PTY startup and replay explanations [skip ci]

* [comment audit] Clarify PTY identity and delivery ownership [skip ci]

* [comment audit] Shorten remote reconnect and input replay rationale [skip ci]

* [comment audit] Condense terminal parking clipboard and file link comments [skip ci]

* [comment audit] Clarify IME ownership and trim trace history [skip ci]

* [comment audit] Shorten pane recovery and parked watcher explanations [skip ci]

* [comment audit] Condense snapshot replay and visibility recovery rationale [skip ci]

* [comment audit] Shorten terminal parking and keyboard ownership rationale [skip ci]

* [comment audit] Trim renderer lifecycle and menu rationale [skip ci]

* [comment audit] Condense workspace cleanup ownership and settlement notes [skip ci]

* [comment audit] Clarify renderer hook evidence and source ownership [skip ci]

* [comment audit] Tighten IPC ownership and reconnect explanations [skip ci]

* [comment audit] Trim renderer focus, paste and attention comments [skip ci]

* [comment audit] Shorten scroll, locale and agent lifecycle comments [skip ci]

* [comment audit] Trim agent launch, resume and ownership commentary [skip ci]

* [comment audit] Shorten account, browser and editor comments [skip ci]

* [comment audit] Clarify host-mirror waits, previews and input ownership [skip ci]

* [comment audit] Condense launch recovery and editor tokenization comments [skip ci]

* [comment audit] Condense workspace selection and terminal pane rationale [skip ci]

* [comment audit] Condense terminal fitting and rendering recovery comments [skip ci]

* [comment audit] Condense terminal input and host routing explanations [skip ci]

* [comment audit] Condense ownership and sleeping-session recovery rationale [skip ci]

* [comment audit] Condense structured launch and terminal link explanations [skip ci]

* [comment audit] Condense terminal ownership and workspace file routing rationale [skip ci]

* [comment audit] Condense session persistence and workspace activation rationale [skip ci]

* [comment audit] Condense workspace routing and browser ownership rationale [skip ci]

* [comment audit] Condense mirror recovery and layout authority rationale [skip ci]

* [comment audit] Condense runtime transport and file safety rationale [skip ci]

* [comment audit] Condense runtime graph and structured session rationale [skip ci]

* [comment audit] Condense browser creation and remote tab recovery rationale [skip ci]

* [comment audit] Condense snapshot authority and publisher recovery rationale [skip ci]

* [comment audit] Condense frame, startup and provider cache rationale [skip ci]

* [comment audit] Condense host twin and agent lifecycle rationale [skip ci]

* [comment audit] Condense browser authority and editor cleanup rationale [skip ci]

* [comment audit] Condense editor buffer identity and review cache rationale [skip ci]

* [comment audit] Condense provider lifetime and repository ownership rationale [skip ci]

* [comment audit] Condense reconnect, hydration and terminal retirement rationale [skip ci]

* [comment audit] Condense recovery, reconciliation and persistence rationale [skip ci]

* [comment audit] Condense cleanup ownership and listing admission rationale [skip ci]

* [comment audit] Condense teardown ordering and terminal ownership rationale [skip ci]

* [comment audit] Condense recovery ledger and web host-boundary rationale [skip ci]

* [comment audit] Condense web reconnect and credential lifecycle rationale [skip ci]

* [comment audit] Condense cloud rollout and assignment safety rationale [skip ci]

* [comment audit] Condense relay session and database recovery rationale [skip ci]

* [comment audit] Condense relay placement and protocol compatibility rationale [skip ci]

* [comment audit] Condense build and packaging constraints [skip ci]

* [comment audit] Condense CI gates and validation isolation rationale [skip ci]

* [comment audit] Condense catalog identity and browser validation rationale [skip ci]

* [comment audit] Condense mobile browser and frame-budget rationale [skip ci]

* [comment audit] Shorten mobile route and browser test explanations [skip ci]

* [comment audit] Condense editor and session seam test rationale [skip ci]

* [comment audit] Trim session and terminal closure history [skip ci]

* [comment audit] Shorten terminal browser and census explanations [skip ci]

* [comment audit] Preserve root-only E2E diagnostic baseline

* [comment audit] Condense mobile seam and HTML preview test rationale [skip ci]

* [comment audit] Condense preview instrumentation and render harness rationale [skip ci]

* [comment audit] Condense bundle contracts and native-build constraints [skip ci]

* [comment audit] Shorten native packaging and CI routing rationale [skip ci]

* [comment audit] Condense release tooling and benchmark constraints [skip ci]

* [comment audit] Condense packaging and platform gate rationale [skip ci]

* [comment audit] Shorten mobile route and history rationale [skip ci]

* [comment audit] Condense mobile browser and component rationale [skip ci]

* [comment audit] Shorten drawer and workspace-form rationale [skip ci]

* [comment audit] Trim mobile review and editor-document explanations [skip ci]

* [comment audit] Condense editor lifecycle and Markdown explanations [skip ci]

* [comment audit] Trim mobile file, diagnostics and dictation commentary [skip ci]

* [comment audit] Simplify mobile host-list and workspace-view explanations [skip ci]

* [comment audit] Clarify mobile shell lifetime and bridge delivery constraints [skip ci]

* [comment audit] Condense bridge stream, native request and audio rationale [skip ci]

* [comment audit] Reduce bridge schema and page-client commentary [skip ci]

* [comment audit] Tighten mobile page bridge contracts [skip ci]

* [comment audit] Clarify mobile cache and session constraints [skip ci]

* [comment audit] Condense mobile route and lifecycle rationale [skip ci]

* [comment audit] Tighten mobile shell hook timing rationale [skip ci]

* [comment audit] Clarify route handoff and notification compatibility [skip ci]

* [comment audit] Clarify push registration and dictation cleanup constraints [skip ci]

* [comment audit] Condense keyboard geometry and native media ownership rationale [skip ci]

* [comment audit] Tighten mobile chat scroll and composer concurrency rationale [skip ci]

* [comment audit] Condense mobile reply compatibility and upload ownership rationale [skip ci]

* [comment audit] Condense optimistic echo and scroll ownership rationale [skip ci]

* [comment audit] Tighten stream replay and queued session ownership rationale [skip ci]

* [comment audit] Condense mobile composer and reply-schema constraints [skip ci]

* [comment audit] Clarify mobile transcript recovery and session ownership [skip ci]

* [comment audit] Clarify mobile queue and source-control compatibility [skip ci]

* [comment audit] Condense storage and task launch constraints [skip ci]

* [comment audit] Clarify task schemas and terminal document ownership [skip ci]

* [comment audit] Tighten terminal input and viewport rationale [skip ci]

* [comment audit] Shorten terminal and RPC recorder contracts [skip ci]

* [comment audit] Condense recorder engine and connection contracts [skip ci]

* [comment audit] Shorten host credential and relay recovery comments [skip ci]

* [comment audit] Tighten pairing recovery and bundle transport rationale [skip ci]

* [comment audit] Clarify RPC delivery and workspace catalog boundaries [skip ci]

* [comment audit] Condense catalog and relay entry contracts [skip ci]

* [comment audit] Shorten shutdown and SSH relay delivery contracts [skip ci]

* [comment audit] Condense WSL relay and E2E regression rationale [skip ci]

* [comment audit] Shorten cross-version harness and renderer fixture explanations [skip ci]

* [comment audit] Condense paired-host and E2E lifecycle rationale [skip ci]

* [comment audit] Shorten layout oracle and regression explanations [skip ci]

* [comment audit] Condense paired recovery and ownership regression contracts [skip ci]

* [comment audit] Shorten restart, sidebar and SSH lifecycle contracts [skip ci]

* [comment audit] Condense terminal rendering and IME regression rationale [skip ci]

* [comment audit] Shorten terminal recovery and benchmark contracts [skip ci]

* [comment audit] Condense tooling and Windows lifecycle proof rationale [skip ci]

* [comment audit] Condense workflow routing and release rationale [skip ci]

* [comment audit] Condense mobile security and installer contracts [skip ci]

* [comment audit] Shorten native rationale and stylesheet workarounds [skip ci]

* [comment audit] Shorten Windows visibility probe rationale [skip ci]

* [comment audit] Condense embedded release and signing rationale [skip ci]

* [comment audit] Retain current main test behavior in final reconciliation [skip ci]

* [comment audit] Complete final comment review across current source inventory

* [comment audit] Handle consolidated audit reports before final CI

* [comment audit] Preserve credential storage census detail [skip ci]

* [comment audit] Scale broad changed E2E selections with verified timing shards

* [comment audit] Align E2E assertions with browser close and settled session metadata

* [comment audit] Match existing E2Es to current resource and windowing contracts

* [comment audit] Observe the shared server outage notice in reconnect coverage

* [comment audit] Make existing renderer probes measure settled tab and PTY work

* [comment audit] Align palette E2E selector with task search

* [comment audit] Refresh remaining palette search selectors

* [comment audit] Keep selector correction within selected audit specs

* [comment audit] Await emoji fixture settings inside renderer evaluation

* [comment audit] Cancel fixture fit retries between terminal tests

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-10-25 04:05:46 -07:00
committed by GitHub
co-authored by OrcaWin
parent aa08c84f3b
commit 7d6603aa17
16196 changed files with 51015 additions and 179882 deletions
@@ -24,14 +24,11 @@ function getOpenCodeDatabaseOverride(environment: NodeJS.ProcessEnv): OpenCodeDa
}
}
// Why gated: the AI Vault's primary OpenCode source delegates here from inside
// its discovery fan-out, so a UNC data dir or a UNC OPENCODE_DB on a stalled
// distro would otherwise hang the whole scan on a raw syscall (STA-4049).
// Gate UNC access so stalled WSL paths cannot hang the AI Vault discovery fan-out.
export async function listOpenCodeDatabases(
/** Lets a caller report the refusal; an empty list otherwise reads as
* "OpenCode not used" rather than "we could not look". */
/** Distinguish refused access from an unused provider. */
onRefusal?: (path: string, error: WslTranscriptFsError) => void,
/** Every other stat/readdir failure, including ENOENT; also read as an empty list. */
/** Includes ENOENT; discovery otherwise returns an empty list. */
onFsError?: (path: string, error: unknown) => void,
signal?: AbortSignal,
environment: NodeJS.ProcessEnv = process.env
@@ -89,9 +86,7 @@ function reportFailure(
}
export function compareOpenCodeClaimPriority(left: string, right: string): number {
// Why: the canonical opencode.db is the live database; it must claim
// duplicated sessions ahead of stale sibling copies. Remaining ties use
// path order so ownership is deterministic across rescans.
// Prefer the live canonical database over stale copies; path order breaks remaining ties.
const leftRank = basename(left).toLowerCase() === 'opencode.db' ? 0 : 1
const rightRank = basename(right).toLowerCase() === 'opencode.db' ? 0 : 1
if (leftRank !== rightRank) {
@@ -2,10 +2,7 @@ import type { UsageProvider } from '../usage/usage-provider-contract'
import { scanOpenCodeUsageDatabasesViaWorker } from '../usage/usage-scan-worker-spawn'
import type { OpenCodeUsageDailyAggregate, OpenCodeUsageSession } from './types'
// Why: v4 reads OpenCode 2's `session_v2` table; v3 caches miss every v2 session.
// v5 merges a migrated session's two rows per column instead of picking one, so
// v4 caches hold zeroed costs and pre-migration metadata.
// v6 normalizes input; v7 preserves writes separately for telemetry.
// Invalidate older caches: merge migrated rows per column, normalize input and preserve cache writes.
export const OPENCODE_USAGE_SCHEMA_VERSION = 7
export const openCodeUsageProvider = {
@@ -30,16 +30,10 @@ type OpenCodeSessionUsageRow = {
tokens_cache_write: number
}
// Why: OpenCode 2 copies every v1 `session` row into `session_v2` and then only
// writes there, so a migrated opencode.db holds both tables and the same session
// id in each. Reading `session` alone loses every OpenCode 2 session (#15841);
// reading both unfiltered would double-count the migrated ones. First entry is
// the generation OpenCode still writes to.
// OpenCode 2 freezes session after copying to session_v2; prioritize live rows without double-counting.
const SESSION_TABLES_BY_PRIORITY = ['session_v2', 'session'] as const
// What the session *is*, with the SQL literal to substitute when no generation
// carries the column. The live generation answers these; an older twin only
// fills in what the live one left NULL.
// Prefer live metadata; legacy rows fill NULLs using these defaults for absent columns.
const SESSION_METADATA_COLUMNS: Record<string, string> = {
project_id: 'NULL',
directory: 'NULL',
@@ -57,12 +51,11 @@ const SESSION_TOKEN_COLUMNS = [
'tokens_cache_write'
] as const
// What the session *spent*. Merged per column, never row-at-a-time.
// Merge usage per column, not per row.
const SESSION_USAGE_COLUMNS = ['cost', ...SESSION_TOKEN_COLUMNS] as const
const SESSION_TOKEN_TOTAL = SESSION_TOKEN_COLUMNS.map((name) => `s.${name}`).join(' + ')
/** One generation's row for a session id, merged into the select that owns it. */
type SessionContributor = { table: string; alias: string }
function columnRef(
@@ -73,14 +66,7 @@ function columnRef(
return columnExists(db, contributor.table, name) ? `${contributor.alias}.${name}` : null
}
// Why the live generation rather than whichever row has the bigger numbers:
// after the v1 import, `session` is frozen while `session_v2` keeps being
// written, so a pre-migration directory, title or timestamp survives in the
// legacy twin indefinitely. The import also derives `session_v2.model` from the
// last user message when the v1 row had none (`transformSession` in upstream
// `v1-migration.bun.ts`), so the legacy row is the one that can be NULL here —
// 23 of 234 shared ids on a real migrated database. Older generations only fill
// NULLs.
// Frozen legacy metadata can outlive migration; use it only where live values are NULL.
function buildMetadataExpression(
db: Database.Database,
contributors: readonly SessionContributor[],
@@ -97,14 +83,8 @@ function buildMetadataExpression(
return `COALESCE(${[...refs, ...tail].join(', ')})`
}
// Why per column rather than picking a winning row: both rows aggregate the same
// assistant messages of the same session. The import re-derives every v2 total
// from decoded messages and drops the ones that fail to decode, so each v2
// column starts at or below its frozen legacy twin and then grows as the session
// keeps running. Neither side can invent usage, so each column's MAX is a
// strictly tighter lower bound on the truth than either row alone and can never
// exceed it. Choosing a row instead lets a token comparison zero a recorded
// cost, or a cost comparison zero recorded tokens.
// Migration may drop undecodable messages, then live totals grow: each column’s MAX preserves
// the strongest recorded lower bound without a winning row zeroing other cost/token columns.
function buildUsageExpression(
db: Database.Database,
contributors: readonly SessionContributor[],
@@ -116,8 +96,7 @@ function buildUsageExpression(
if (refs.length === 0 || contributors.length === 1) {
return refs[0] ?? '0'
}
// An outer-joined generation is NULL for ids it never held, and SQLite's
// scalar MAX() returns NULL if any argument is.
// Outer joins yield NULL for absent generations; SQLite scalar MAX propagates NULL.
const guarded = refs.map((ref) => `COALESCE(${ref}, 0)`)
return guarded.length === 1 ? (guarded[0] ?? '0') : `MAX(${guarded.join(', ')})`
}
@@ -134,8 +113,7 @@ function buildSessionTableSelect(
index: number
): string {
const table = tables[index] ?? ''
// Only lower-priority generations join in: a higher-priority one holding this
// id would have excluded the row outright, so it has nothing to contribute.
// Higher-priority holders exclude this row, so only lower generations can contribute.
const contributors: SessionContributor[] = [
{ table, alias: 't' },
...tables
@@ -154,9 +132,7 @@ function buildSessionTableSelect(
.slice(1)
.map((other) => `LEFT JOIN ${other.table} ${other.alias} ON ${other.alias}.id = t.id`)
.join(' ')
// Exactly one select claims each id: the highest-priority generation holding
// it. Exclusive because every lower select rejects an id a higher one has,
// exhaustive because the highest one holding it never rejects it.
// Each ID belongs to its highest-priority holder; lower selects exclude higher holders.
const exclusions = tables
.slice(0, index)
.map((other) => `NOT EXISTS (SELECT 1 FROM ${other} o WHERE o.id = t.id)`)
@@ -164,7 +140,6 @@ function buildSessionTableSelect(
return `SELECT t.id, ${columns.join(', ')} FROM ${table} t${joins ? ` ${joins}` : ''}${exclusions ? ` WHERE ${exclusions}` : ''}`
}
/** A single deduplicated session relation spanning every session table generation. */
function buildSessionSource(db: Database.Database, tables: readonly string[]): string {
const selects = tables.map((_table, index) => buildSessionTableSelect(db, tables, index))
return `(${selects.join(' UNION ALL ')})`
@@ -190,8 +165,7 @@ function getAssistantSessionMessageCount(db: Database.Database): number {
return row?.count ?? 0
}
// `some`, not `every`: the merged row takes each usage column from whichever
// generation carries it, so one table missing them costs nothing.
// One generation’s usage columns suffice because merging fills each column independently.
function hasSessionUsageColumns(db: Database.Database, tables: readonly string[]): boolean {
return tables.some((table) =>
['cost', 'tokens_input', 'tokens_output', 'tokens_reasoning', 'tokens_cache_read'].every(
@@ -264,8 +238,7 @@ export function selectUsageRows(db: Database.Database): OpenCodeUsageRow[] {
}
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.
// Session aggregates avoid parsing every message blob.
if (hasSessionUsageColumns(db, sessionTables) && getSessionUsageRowCount(db, sessionSource) > 0) {
return selectSessionUsageRows(db, sessionSource)
}
@@ -2,10 +2,8 @@ import { mkdirSync } from 'node:fs'
import { dirname } from 'node:path'
import SyncDatabase from '../sqlite/sync-database'
// The OpenCode schemas as the app itself creates them, written out in full
// rather than trimmed to the columns a reader names: every read probes for its
// columns, so a trimmed fixture would pass a probe the real database fails.
// Foreign keys are dropped so a generation can be built without its siblings.
// Use complete upstream schemas so column probes match real databases; omit foreign keys
// to construct generations independently.
const PROJECT_TABLE = `
CREATE TABLE project (
@@ -15,7 +13,7 @@ const PROJECT_TABLE = `
);
`
// OpenCode 1.17.x — and still present, frozen, after an OpenCode 2 migration.
// OpenCode 1.17.x; frozen after OpenCode 2 migration.
const LEGACY_SESSION_TABLE = `
CREATE TABLE session (
id TEXT PRIMARY KEY, project_id TEXT NOT NULL, parent_id TEXT, slug TEXT NOT NULL,
@@ -39,7 +37,6 @@ const LEGACY_SESSION_TABLE = `
);
`
// OpenCode 2.0.x — session rows move to `session_v2`, messages to `session_message`.
const V2_SESSION_TABLE = `
CREATE TABLE session_v2 (
id TEXT PRIMARY KEY, project_id TEXT NOT NULL, workspace_id TEXT, parent_id TEXT,
@@ -57,8 +54,7 @@ const V2_SESSION_TABLE = `
);
`
// A `session_v2` that never grew the token/cost columns. Reachable on databases
// whose v2 lineage predates them, and the shape that must not erase a legacy row.
// Older v2 schemas lack totals and must not erase legacy usage.
const V2_SESSION_TABLE_WITHOUT_TOKENS = `
CREATE TABLE session_v2 (
id TEXT PRIMARY KEY, project_id TEXT NOT NULL, workspace_id TEXT, parent_id TEXT,
@@ -82,7 +78,7 @@ export type OpenCodeUsageFixtureSession = {
id: string
directory: string
title?: string
/** `null` writes a NULL `model`, the shape a v1 row has before the import derives one. */
/** NULL matches v1 rows before migration derives a model. */
model?: string | null
cost?: number
tokensInput?: number
@@ -94,25 +90,17 @@ export type OpenCodeUsageFixtureSession = {
timeUpdated?: number
}
/**
* Which session tables the database carries.
*
* - `v1`: OpenCode 1 only.
* - `migrated`: what an OpenCode 2 upgrade leaves behind — both tables, with the
* pre-upgrade sessions copied into `session_v2` and `session` frozen.
* - `v2-only`: `session_v2` without the legacy table at all.
*/
/** Migrated databases retain frozen v1 rows alongside their live v2 copies. */
export type OpenCodeUsageFixtureGeneration = 'v1' | 'migrated' | 'v2-only'
export type OpenCodeUsageFixtureSpec = {
generation: OpenCodeUsageFixtureGeneration
/** Rows in the legacy `session` table; ignored for `v2-only`. */
/** Ignored for v2-only. */
legacySessions?: readonly OpenCodeUsageFixtureSession[]
/** Rows in `session_v2`; ignored for `v1`. */
/** Ignored for v1. */
v2Sessions?: readonly OpenCodeUsageFixtureSession[]
/** Build `session_v2` without the token/cost columns; ignored for `v1`. */
/** Omit v2 totals; ignored for v1. */
v2WithoutTokenColumns?: boolean
/** `project.worktree`, the repo root OpenCode recorded for the project. */
worktree?: string
}
@@ -158,11 +146,6 @@ function insertSession(
).run(...identity, ...usage)
}
/**
* Create an OpenCode usage database at `dbPath` for one schema generation.
* @param dbPath - Where to create the database; parent directories are created.
* @param spec - The generation to build and the session rows to write.
*/
export function writeOpenCodeUsageDatabase(dbPath: string, spec: OpenCodeUsageFixtureSpec): void {
mkdirSync(dirname(dbPath), { recursive: true })
const db = new SyncDatabase(dbPath)
@@ -13,11 +13,6 @@ import {
} from './opencode-usage-sqlite-fixture'
import { parseOpenCodeUsageDatabase } from './scanner'
// Why: OpenCode 2 copies v1 `session` rows into `session_v2` and then writes
// only there. A reader that knows just `session` reports zero usage for every
// OpenCode 2 session, and a reader that unions both double-counts the migrated
// ones (#15841). These tests pin both halves against the real v2 schema.
const WORKTREE = '/workspace/repo'
let tempDirs: string[] = []
@@ -108,7 +103,6 @@ describe('OpenCode 2 session_v2 usage', () => {
const [event] = readEvents(path)
expect(event?.cachedInputTokens).toBe(7_000)
expect(event?.inputTokens).toBe(7_010)
// Both cache buckets contribute to input; total counts them once.
expect(event?.totalTokens).toBe(7_011)
})
@@ -121,7 +115,6 @@ describe('OpenCode 2 session_v2 usage', () => {
}
const path = createFixture({
generation: 'migrated',
// The frozen pre-upgrade copy, still carrying the smaller snapshot totals.
legacySessions: [shared],
v2Sessions: [
{ ...shared, tokensInput: 180, tokensCacheRead: 60 },
@@ -152,8 +145,7 @@ describe('OpenCode 2 session_v2 usage', () => {
it('keeps the legacy totals when the migration recomputed session_v2 lower', () => {
const path = createFixture({
generation: 'migrated',
// Upstream recomputes v2 totals from decoded messages; undecodable ones
// are dropped, landing the v2 row below its frozen legacy copy.
// Migration drops undecodable messages, so v2 totals may be lower than frozen v1 totals.
legacySessions: [{ id: 'ses_shared', directory: WORKTREE, tokensInput: 900 }],
v2Sessions: [{ id: 'ses_shared', directory: WORKTREE, tokensInput: 120 }]
})
@@ -174,7 +166,6 @@ describe('OpenCode 2 session_v2 usage', () => {
const events = readEvents(path)
expect(events.map((event) => event.sessionId)).toEqual(['ses_shared'])
expect(events[0]?.inputTokens).toBe(64)
// Equal totals: the tie goes to the newer table, so its row supplies metadata.
expect(events[0]?.model).toContain('v2')
})
@@ -299,11 +290,6 @@ describe('OpenCode 2 session_v2 usage', () => {
})
})
// Why: a migrated session has two rows for one session, and neither is complete.
// `session_v2` is the row OpenCode still writes, so it says what the session is;
// each usage column is a lossy re-derivation, so each column takes the larger of
// the two. Ranking whole rows by one number let that number decide cost,
// directory and model too.
describe('OpenCode 2 migrated session column merge', () => {
const SHARED = 'ses_shared'
@@ -333,7 +319,6 @@ describe('OpenCode 2 migrated session column merge', () => {
const events = readEvents(path)
expect(events).toHaveLength(1)
// The legacy row still wins the token column; it must not drag metadata with it.
expect(events[0]?.inputTokens).toBe(900)
expect(events[0]?.cwd).toBe('/new/current-path')
})
@@ -341,8 +326,7 @@ describe('OpenCode 2 migrated session column merge', () => {
it('keeps the model session_v2 derived when the legacy row has none', () => {
const path = createFixture({
generation: 'migrated',
// The import fills session_v2.model from the last user message when the v1
// row had none: 23 of 234 shared ids on a real migrated database.
// Migration derives a missing v1 model from the last user message.
legacySessions: [{ id: SHARED, directory: WORKTREE, model: null, tokensInput: 900 }],
v2Sessions: [
{
@@ -4,7 +4,7 @@ import type {
OpenCodeUsagePersistedState
} from './types'
/** Fills cost fields older OpenCode caches never wrote. Electron-free: the scan worker runs it too. */
/** Normalize legacy cache cost fields without Electron so scan workers can use it. */
export function normalizeOpenCodeUsagePersistedDatabases(
databases: OpenCodeUsagePersistedDatabase[] | undefined
): OpenCodeUsagePersistedDatabase[] {
@@ -28,8 +28,7 @@ import {
WslTranscriptFsError
} from '../native-chat/wsl-transcript-fs-gate'
// A stalled task holds the gate's single scan slot until it settles, so every
// case releases its stall before the next one runs.
// Release stalled tasks between cases: each holds the gate’s single scan slot until settlement.
let releaseStall: (() => void) | undefined
function stalls<T>(): Promise<T> {
@@ -38,8 +37,7 @@ function stalls<T>(): Promise<T> {
})
}
// Complete: UNC readdir results pass through the child dispatcher's dirent
// serializer, which reads every kind flag.
// The child serializer reads every dirent kind flag.
function dirent(name: string) {
return {
name,
@@ -53,8 +51,6 @@ function dirent(name: string) {
}
}
// The point of the gate: an ungated syscall on a stalled 9P mount never returns,
// so the call must still be pending at the deadline and settle just after it.
async function settlesOnlyAtTheScanDeadline(pending: Promise<string[]>): Promise<string[]> {
let settled = false
const tracked = pending.then((value) => {
@@ -70,8 +66,7 @@ async function settlesOnlyAtTheScanDeadline(pending: Promise<string[]>): Promise
let originalDatabaseOverride: string | undefined
beforeEach(() => {
// blockedRoutes is persistent gate state: a prior stall must not quarantine
// this test's route.
// Persistent route quarantine must not leak between cases.
resetWslTranscriptFsGateForTests()
originalDatabaseOverride = process.env.OPENCODE_DB
delete process.env.OPENCODE_DB
@@ -95,14 +90,11 @@ afterEach(async () => {
}
})
// Why this file exists: the AI Vault's primary OpenCode source reaches these
// syscalls transitively, so the import-guard that pins direct `node:fs` use in
// the transcript modules never saw them (STA-4049).
// These transitive discovery syscalls escape direct filesystem import guards.
describe('OpenCode database discovery on a stalled WSL data directory', () => {
it('gates the data-directory listing instead of hanging the scan', async () => {
mocks.readdir.mockImplementation(stalls)
// Asserted because an empty list alone also matches a missing data dir; the
// AI Vault's OpenCode source turns this callback into the scan issue.
// Empty results alone cannot distinguish missing data from gated refusal.
const onRefusal = vi.fn()
expect(await settlesOnlyAtTheScanDeadline(listOpenCodeDatabases(onRefusal))).toEqual([])
@@ -116,8 +108,7 @@ describe('OpenCode database discovery on a stalled WSL data directory', () => {
expect(await settlesOnlyAtTheScanDeadline(listOpenCodeDatabases(onRefusal))).toEqual([])
expect(mocks.readdir).not.toHaveBeenCalled()
// Loose on the path: `isAbsolute` is host-flavoured, so a UNC override only
// stays verbatim on Windows. The refusal reaching the caller is the contract.
// UNC paths remain verbatim only on Windows; assert refusal across hosts.
expect(onRefusal).toHaveBeenCalledWith(
expect.stringContaining('opencode.db'),
expect.any(WslTranscriptFsError)
+2 -15
View File
@@ -442,8 +442,7 @@ describe('scanOpenCodeUsageDatabases', () => {
const db = new Database(path)
insertSessionTotalsRow(db, sessionId, inputTokens)
db.close()
// Why: the row fits the existing page and can land in the same coarse mtime
// tick, so pin a later mtime to change the scanner's mtime+size fingerprint.
// A same-page write may retain coarse mtime; force the scanner’s mtime/size fingerprint to change.
const changedAt = new Date(previousMtimeMs + 1000)
utimesSync(path, changedAt, changedAt)
}
@@ -462,8 +461,6 @@ describe('scanOpenCodeUsageDatabases', () => {
})
it('counts a session duplicated into a stale backup database exactly once', async () => {
// The backup holds a stale snapshot of session-1; the canonical db has
// grown since. The canonical totals must win and be counted once.
writeSessionTotalsDb('opencode.db', [
['session-1', 1000],
['session-2', 300]
@@ -483,8 +480,6 @@ describe('scanOpenCodeUsageDatabases', () => {
it('still counts backup-only sessions and stays stable across cached rescans', async () => {
const canonicalPath = writeSessionTotalsDb('opencode.db', [['session-1', 1000]])
// The backup duplicates session-1 (stale) and preserves session-9, which
// no longer exists in the canonical database.
writeSessionTotalsDb('opencode-backup.db', [
['session-1', 400],
['session-9', 50]
@@ -500,8 +495,6 @@ describe('scanOpenCodeUsageDatabases', () => {
second.dailyAggregates.reduce((total, aggregate) => total + aggregate.inputTokens, 0)
).toBe(1050)
// The canonical db keeps growing while the backup stays cached; session-1
// must stay owned by the canonical db.
appendLiveSessionRow(canonicalPath, 'session-2', 200)
const third = await scanOpenCodeUsageDatabases([], second.processedDatabases)
@@ -514,9 +507,6 @@ describe('scanOpenCodeUsageDatabases', () => {
})
it('reuses a fully-duplicate backup instead of reparsing it when the live db changes', async () => {
// The backup only holds stale copies of sessions the canonical db already
// owns, so it owns nothing. When the live db changes it must not be
// demoted back into the parse set — there is no claim to reclaim.
const canonicalPath = writeSessionTotalsDb('opencode.db', [['session-1', 1000]])
writeSessionTotalsDb('opencode-backup.db', [['session-1', 400]])
@@ -532,7 +522,7 @@ describe('scanOpenCodeUsageDatabases', () => {
const secondBackup = second.processedDatabases.find((database) =>
database.path.endsWith('opencode-backup.db')
)
// A reused cache entry is the same object; a reparse would produce a new one.
// Cache reuse preserves object identity.
expect(secondBackup).toBe(firstBackup)
expect(
second.dailyAggregates.reduce((total, aggregate) => total + aggregate.inputTokens, 0)
@@ -540,9 +530,6 @@ describe('scanOpenCodeUsageDatabases', () => {
})
it('lets the live database reclaim sessions after a sticky backup-only claim', async () => {
// First scan only has the backup (e.g. live db temporarily missing), so it
// owns session-1 at the stale snapshot. When opencode.db reappears with
// higher totals it must reclaim the session instead of staying frozen.
writeSessionTotalsDb('opencode-backup.db', [['session-1', 400]])
const first = await scanOpenCodeUsageDatabases([], [])
+6 -19
View File
@@ -76,8 +76,7 @@ export async function parseOpenCodeUsageDatabase(
if (!parsed) {
continue
}
// Why: a stale sibling copy of opencode.db carries the same sessions, so
// each session must be counted from exactly one database (#8006).
// Sibling backups duplicate sessions; count each from one owner.
let owned = claimedBySessionId.get(parsed.sessionId)
if (owned === undefined) {
owned = options.claimSession ? options.claimSession(parsed.sessionId) : true
@@ -118,13 +117,11 @@ export async function scanOpenCodeUsageDatabases(
const previousByPath = new Map(
previousProcessedDatabases.map((database) => [database.path, database])
)
// Why: one resolver for the whole scan so every database shares the per-cwd memo.
// Share per-cwd resolution memoization across databases.
const resolveWorktree = await createUsageWorktreeResolver(worktrees)
const currentPaths = new Set(dbPaths)
// Why: when a database that owned sessions is deleted, remaining siblings
// still contain those sessions but their caches record them as unowned.
// Only databases that previously deferred claims can reclaim.
// When an owner disappears, only caches with deferred claims can reclaim its sessions.
const lostOwnerPath = previousProcessedDatabases.some(
(database) =>
!currentPaths.has(database.path) &&
@@ -137,7 +134,6 @@ export async function scanOpenCodeUsageDatabases(
for (const dbPath of dbPaths) {
const databaseInfo = await getProcessedDatabaseInfo(dbPath)
const previous = previousByPath.get(dbPath)
// When an owner disappears, only deferred-claim databases need reparse.
const mustReclaimDeferred = lostOwnerPath && previous?.hasDeferredClaims !== false
const canReuse =
!mustReclaimDeferred &&
@@ -153,13 +149,8 @@ export async function scanOpenCodeUsageDatabases(
}
}
// Why: a sticky backup claim from a scan where opencode.db was missing would
// otherwise freeze a still-growing session at the backup snapshot when the
// live db reappears. Reparse a lower-priority sibling only when it still owns
// sessions a higher-priority path could reclaim; a sibling that owns nothing
// (the common case once the live db has claimed every shared session) has no
// claim to give back, so reparsing it every time the live db changes is pure
// work.
// A returning live database must reclaim sticky backup ownership; backups owning nothing
// need no reparse when live data changes.
const demotedReusePaths: string[] = []
for (const [dbPath, reused] of reusedByPath) {
if ((reused.ownedSessionIds?.length ?? 0) === 0) {
@@ -177,11 +168,7 @@ export async function scanOpenCodeUsageDatabases(
pathsToParse.push(dbPath)
}
// Why: `opencode-*.db` siblings are typically stale copies of `opencode.db`
// (backups), so mergeSessions would double every duplicated session (#8006).
// Each session is counted from exactly one database. The canonical live db
// claims first so a stale backup cannot freeze a still-growing session at
// its snapshot totals; cached databases keep the claims they persisted.
// The live database claims before stale backups; reused caches retain persisted ownership.
const sessionOwnerById = new Map<string, string>()
for (const dbPath of [...reusedByPath.keys()].sort(compareOpenCodeClaimPriority)) {
const previous = reusedByPath.get(dbPath)
-16
View File
@@ -1,16 +1,7 @@
import type SyncDatabase from '../sqlite/sync-database'
// Why: OpenCode's usage scanner and the AI Vault session scanner both need to
// probe the opencode.db schema shape across multiple DB generations. Centralizing
// the probes here avoids two private copies and keeps the contract testable.
type Database = SyncDatabase.Database
/**
* Check whether a table exists in the given SQLite database.
* @param db - A readonly or read-write SyncDatabase instance.
* @param tableName - The table name to look up in sqlite_master.
* @returns `true` if the table exists, `false` otherwise.
*/
export function tableExists(db: Database, tableName: string): boolean {
const row = db
.prepare("SELECT 1 AS found FROM sqlite_master WHERE type = 'table' AND name = ?")
@@ -18,13 +9,6 @@ export function tableExists(db: Database, tableName: string): boolean {
return row?.found === 1
}
/**
* Check whether a column exists on a table in the given SQLite database.
* @param db - A readonly or read-write SyncDatabase instance.
* @param tableName - The table to inspect via PRAGMA table_info.
* @param columnName - The column name to find.
* @returns `true` if the column exists on the table, `false` otherwise.
*/
export function columnExists(db: Database, tableName: string, columnName: string): boolean {
const rows = db.prepare(`PRAGMA table_info(${tableName})`).all() as { name?: string }[]
return rows.some((row) => row.name === columnName)
+2 -5
View File
@@ -91,12 +91,9 @@ export type OpenCodeUsageDailyAggregate = {
export type OpenCodeUsagePersistedDatabase = OpenCodeUsageProcessedDatabase & {
sessions: OpenCodeUsageSession[]
dailyAggregates: OpenCodeUsageDailyAggregate[]
/** Session ids this database counted. Sibling copies (opencode-backup.db)
* duplicate sessions; ownership keeps each session counted by exactly one
* cached database across incremental scans. */
/** Persist ownership so sibling backups cannot double-count sessions across scans. */
ownedSessionIds: string[]
/** True when this database saw sessions already claimed by another database.
* When that owner disappears, only deferred databases need reparse. */
/** Only databases with deferred claims need reparse when an owner disappears. */
hasDeferredClaims: boolean
}