opencode-usage-insights 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1,17 +1,16 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/index.ts", "../src/stats/db.ts", "../src/stats/paths.ts", "../src/text.ts", "../src/core.ts", "../src/stats/queries.ts", "../src/endpoint.ts", "../src/recorder.ts"],
3
+ "sources": ["../src/index.ts", "../src/stats/db.ts", "../src/stats/paths.ts", "../src/text.ts", "../src/core.ts", "../src/stats/queries.ts", "../src/recorder.ts"],
4
4
  "sourcesContent": [
5
- "import { Plugin } from \"@opencode/plugin\"\nimport type { Database } from \"bun:sqlite\"\nimport { createRecorder, type HttpResponseInput, type RecorderEvent } from \"./recorder\"\nimport { openStatsDb } from \"./stats/db\"\nimport { resolveDbPath } from \"./stats/paths\"\n\n/**\n * Server-side recorder. Subscribes to the session event stream and persists\n * every assistant step, prompt and lifecycle event into the stats database;\n * the TUI entry reads the same database for its live line and stats panel.\n *\n * The plugin is observational: it never rewrites prompts, requests or\n * responses, and a failure inside the recorder is swallowed so a session can\n * never be interrupted by its own telemetry.\n */\n\nexport type Options = {\n /** Turn recording off: no event subscription, no database. */\n enabled?: boolean | undefined\n /** SQLite database path override (defaults to the per-OS state directory). */\n db?: string | undefined\n}\n\nfunction tryOpen(path: string): Database | undefined {\n try {\n return openStatsDb(path)\n } catch {\n return undefined\n }\n}\n\nexport default Plugin.define({\n id: \"usage-insights\",\n async setup(ctx) {\n const options = (ctx.options ?? {}) as Options\n if (options.enabled === false) return\n\n const db = tryOpen(resolveDbPath(options.db))\n if (db === undefined) return\n\n // The recorder's connection runs on the agent's own event path, so it waits\n // at most a second for another writer and then defers the write: a stalled\n // event handler is worse than a record that lands on the next event.\n try {\n db.exec(\"PRAGMA busy_timeout = 1000\")\n } catch {\n // A database that cannot take the pragma is still usable.\n }\n\n const recorder = createRecorder(db)\n const abort = new AbortController()\n const stream = (async () => {\n try {\n for await (const event of ctx.event.subscribe({ signal: abort.signal })) {\n try {\n recorder.handleEvent(event as RecorderEvent)\n } catch {\n // A malformed event must never break the stream.\n }\n }\n } catch {\n // Subscription closed (shutdown or transport error): nothing to do.\n }\n })()\n\n let hook: { dispose: () => Promise<void> } | undefined\n try {\n hook = await ctx.session.hook(\"http.response\", (event) => {\n try {\n recorder.handleHttpResponse(event as HttpResponseInput)\n } catch {\n // Discovery is best-effort.\n }\n })\n } catch {\n hook = undefined\n }\n\n return async () => {\n await hook?.dispose()\n abort.abort()\n await stream\n db.close()\n }\n },\n})\n",
6
- "import { Database } from \"bun:sqlite\"\nimport { chmodSync, mkdirSync } from \"node:fs\"\nimport { dirname } from \"node:path\"\nimport type { EndpointInfo } from \"../endpoint\"\nimport { ensureRegularFile } from \"./paths\"\nimport { normalizeToken, sanitizeLabel, TEXT_LIMIT } from \"../text\"\n\n/**\n * Persistent usage store. One SQLite database holds every recorded step,\n * prompt, event and session row — and nothing else: the recorder only writes\n * what a view reads.\n *\n * The database is opened in WAL mode with a busy timeout so the server\n * recorder process and the TUI reader process can share it safely. The two\n * sides open it differently on purpose:\n *\n * - `openStatsDb` writes: schema, migrations and recording (the server).\n * - `openStatsDbReadOnly` only reads (the TUI), so no view can change the file\n * and no reader pays for a schema write.\n * - `openStatsDbWrite` is a short-lived maintenance connection (prune, vacuum)\n * that gives up quickly instead of holding the recorder up.\n */\n\nexport const SCHEMA_VERSION = 8\n\n/** Longest title or path kept: a label on a line, not a document. */\nconst LABEL_LIMIT = 4 * TEXT_LIMIT\n\nconst SCHEMA = `\nCREATE TABLE IF NOT EXISTS meta (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS sessions (\n session_id TEXT PRIMARY KEY,\n parent_id TEXT,\n directory TEXT,\n title TEXT,\n started_at INTEGER,\n updated_at INTEGER\n);\n\nCREATE TABLE IF NOT EXISTS prompts (\n session_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n created_at INTEGER,\n PRIMARY KEY (session_id, message_id)\n);\nCREATE INDEX IF NOT EXISTS prompts_created ON prompts(created_at);\n\nCREATE TABLE IF NOT EXISTS steps (\n message_id TEXT PRIMARY KEY,\n session_id TEXT NOT NULL,\n parent_id TEXT,\n provider_id TEXT,\n model_id TEXT,\n variant TEXT,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n reasoning_tokens INTEGER NOT NULL DEFAULT 0,\n cache_read INTEGER NOT NULL DEFAULT 0,\n cache_write INTEGER NOT NULL DEFAULT 0,\n cost REAL NOT NULL DEFAULT 0,\n created_at INTEGER,\n streamed_at INTEGER,\n completed_at INTEGER,\n finish TEXT,\n router TEXT,\n recorded_at INTEGER NOT NULL\n);\nCREATE INDEX IF NOT EXISTS steps_session ON steps(session_id, created_at);\nCREATE INDEX IF NOT EXISTS steps_model ON steps(provider_id, model_id, variant);\nCREATE INDEX IF NOT EXISTS steps_completed ON steps(completed_at);\nCREATE INDEX IF NOT EXISTS steps_router ON steps(router);\n-- Every range filter and every ordering compares the step's own clock, which\n-- falls back to the recording time for a step that was already in flight when\n-- the plugin loaded. The index has to cover that expression, or every ranged\n-- view is a full table scan.\nCREATE INDEX IF NOT EXISTS steps_at ON steps(COALESCE(created_at, recorded_at));\nCREATE INDEX IF NOT EXISTS steps_session_at ON steps(session_id, COALESCE(created_at, recorded_at));\n\nCREATE TABLE IF NOT EXISTS events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id TEXT NOT NULL,\n kind TEXT NOT NULL,\n message_id TEXT,\n at INTEGER,\n data TEXT\n);\nCREATE INDEX IF NOT EXISTS events_session ON events(session_id, at);\nCREATE INDEX IF NOT EXISTS events_kind ON events(kind, at);\n`\n\n/** Tables carried by `exportNdjson` / `importNdjson`, in import-safe order. */\nexport const TABLES = [\"sessions\", \"prompts\", \"events\", \"steps\"] as const\n\nexport type TableName = (typeof TABLES)[number]\n\nexport type StepRecord = {\n messageID: string\n sessionID: string\n parentID?: string | undefined\n providerID?: string | undefined\n modelID?: string | undefined\n variant?: string | undefined\n input?: number | undefined\n output?: number | undefined\n reasoning?: number | undefined\n cacheRead?: number | undefined\n cacheWrite?: number | undefined\n cost?: number | undefined\n createdAt?: number | undefined\n streamedAt?: number | undefined\n completedAt?: number | undefined\n finish?: string | undefined\n /** Gateway routing endpoint that served the request (`orcarouter-dsv4.1flash`). */\n router?: string | undefined\n /** Clock of the recording itself, defaults to `Date.now()`. */\n recordedAt?: number | undefined\n}\n\nexport type PromptRecord = {\n sessionID: string\n messageID: string\n createdAt?: number | undefined\n}\n\nexport type EventRecord = {\n sessionID: string\n kind: string\n messageID?: string | undefined\n at?: number | undefined\n data?: unknown\n}\n\nexport type SessionRecord = {\n sessionID: string\n parentID?: string | undefined\n directory?: string | undefined\n title?: string | undefined\n startedAt?: number | undefined\n updatedAt?: number | undefined\n}\n\nfunction jsonText(value: unknown): string | null {\n if (value === undefined || value === null) return null\n if (typeof value === \"string\") return value\n try {\n return JSON.stringify(value)\n } catch {\n return null\n }\n}\n\n/**\n * Columns added after the first release, applied to an existing database, with\n * an optional statement that backfills them.\n */\nconst MIGRATIONS: ReadonlyArray<readonly [string, string, string?]> = [\n [\"router\", \"ALTER TABLE steps ADD COLUMN router TEXT\"],\n]\n\n/**\n * Columns and tables the recorder used to write and no view ever read. They are\n * dropped from an existing database, together with the rows of the events that\n * followed the same fate, so a long-lived file shrinks instead of carrying dead\n * weight forever.\n */\nconst REMOVED_COLUMNS = [\"provider_state\", \"upstream\", \"upstream_source\", \"upstream_model\", \"tps\"] as const\nconst REMOVED_EVENT_KINDS = [\"execution\", \"step-failed\", \"model-selected\", \"usage-recorded\"] as const\n\nfunction dropUnused(db: Database): void {\n const objects = db.query(\"SELECT name, type FROM sqlite_master WHERE type IN ('table', 'index')\").all() as {\n name: string\n type: string\n }[]\n const tables = new Set(objects.filter((row) => row.type === \"table\").map((row) => row.name))\n const indexes = new Set(objects.filter((row) => row.type === \"index\").map((row) => row.name))\n\n if (tables.has(\"http_responses\")) db.exec(\"DROP TABLE http_responses\")\n // The index reads a column that is about to go: SQLite refuses to drop an\n // indexed column, so the index goes first.\n if (indexes.has(\"steps_upstream\")) db.exec(\"DROP INDEX steps_upstream\")\n\n const columns = new Set((db.query(\"PRAGMA table_info(steps)\").all() as { name: string }[]).map((column) => column.name))\n for (const name of REMOVED_COLUMNS) {\n if (columns.has(name)) db.exec(`ALTER TABLE steps DROP COLUMN ${name}`)\n }\n\n if (tables.has(\"events\")) {\n const kinds = REMOVED_EVENT_KINDS.map((kind) => `'${kind}'`).join(\", \")\n db.exec(`DELETE FROM events WHERE kind IN (${kinds})`)\n }\n // A gateway name is not an endpoint: the old discovery stored the host of the\n // gateway itself when the gateway named no downstream endpoint. Clearing those\n // values makes the history agree with what the recorder writes now.\n db.exec(\"UPDATE steps SET router = NULL WHERE router IN ('opencode', 'openrouter', 'vercel-ai-gateway')\")\n}\n\nfunction schemaVersion(db: Database): number {\n const row = db.query(\"SELECT value FROM meta WHERE key = 'schema_version'\").get() as { value: string } | null\n const parsed = row === null ? Number.NaN : Number.parseInt(row.value, 10)\n return Number.isFinite(parsed) ? parsed : 0\n}\n\n/**\n * The endpoint identity is a grouping key: two spellings of the same endpoint\n * would split its history and read as a change. Values recorded before the\n * rule existed are folded into the lowercase form.\n */\nfunction normalizeRouters(db: Database): void {\n db.exec(\"UPDATE steps SET router = lower(router) WHERE router IS NOT NULL AND router <> lower(router)\")\n}\n\nfunction migrate(db: Database): void {\n const version = schemaVersion(db)\n const steps = db.query(\"PRAGMA table_info(steps)\").all() as { name: string }[]\n const existing = new Set(steps.map((column) => column.name))\n for (const [name, statement, backfill] of MIGRATIONS) {\n if (existing.has(name)) continue\n db.exec(statement)\n if (backfill !== undefined) db.exec(backfill)\n }\n // Anything the recorder used to write and no view ever read is dropped here.\n // It is cheap and idempotent, so it also heals a database a half-applied\n // migration left behind.\n dropUnused(db)\n if (version < 7) normalizeRouters(db)\n}\n\n/**\n * The file holds session titles, project directories and usage history, so it\n * is created for the user alone. Windows takes its access from the parent\n * directory's ACL, which is not something a plugin should rewrite.\n */\nconst POSIX = process.platform !== \"win32\"\n\nfunction hardenDirectory(path: string): void {\n if (!POSIX) return\n try {\n chmodSync(path, 0o700)\n } catch {\n // A shared or read-only directory: the database is still usable.\n }\n}\n\nfunction hardenFiles(path: string): void {\n if (!POSIX || path === \":memory:\") return\n for (const file of [path, `${path}-wal`, `${path}-shm`]) {\n try {\n chmodSync(file, 0o600)\n } catch {\n // The WAL and the shared-memory file are created on the first write and\n // removed on the last close, so one of them is often simply absent.\n }\n }\n}\n\n/** Directory for a database path, created private on POSIX. */\nfunction prepareDirectory(path: string): void {\n if (path === \":memory:\") return\n mkdirSync(dirname(path), { recursive: true, mode: 0o700 })\n hardenDirectory(dirname(path))\n}\n\n/** Open (creating when needed) the stats database and apply the schema. */\nexport function openStatsDb(path: string): Database {\n if (path !== \":memory:\") ensureRegularFile(path)\n prepareDirectory(path)\n const db = new Database(path, { create: true })\n db.exec(\"PRAGMA journal_mode = WAL\")\n db.exec(\"PRAGMA busy_timeout = 5000\")\n db.exec(\"PRAGMA synchronous = NORMAL\")\n db.exec(SCHEMA)\n migrate(db)\n db.query(\"INSERT INTO meta (key, value) VALUES ('schema_version', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value\").run(\n String(SCHEMA_VERSION),\n )\n // The schema write is what creates the WAL and its shared-memory file.\n hardenFiles(path)\n return db\n}\n\n/**\n * Reader connection for the TUI: no schema, no migration, no write. It throws\n * when the database is absent or was never written by the recorder, so the\n * caller can say so instead of showing an empty panel.\n */\nexport function openStatsDbReadOnly(path: string): Database {\n ensureRegularFile(path)\n const db = new Database(path, { readonly: true })\n try {\n db.query(\"SELECT 1 FROM steps LIMIT 1\").get()\n } catch (error) {\n db.close()\n throw error\n }\n return db\n}\n\n/**\n * Short-lived write connection for maintenance. The busy timeout is short on\n * purpose: a prune or a vacuum that cannot take the lock must fail in front of\n * the user, not hold the recorder's response path for seconds.\n */\nexport function openStatsDbWrite(path: string): Database {\n if (path !== \":memory:\") ensureRegularFile(path)\n prepareDirectory(path)\n const db = new Database(path, { create: true })\n db.exec(\"PRAGMA busy_timeout = 250\")\n db.exec(\"PRAGMA synchronous = NORMAL\")\n hardenFiles(path)\n return db\n}\n\n/**\n * Insert or refresh an assistant step. Idempotent by message ID so replayed\n * events, restarts and NDJSON imports never duplicate usage. Non-null fields\n * discovered later (the routing endpoint) survive a re-record with `NULL`\n * through COALESCE.\n */\nexport function recordStep(db: Database, record: StepRecord): void {\n db.query(\n `INSERT INTO steps (\n message_id, session_id, parent_id, provider_id, model_id, variant,\n input_tokens, output_tokens, reasoning_tokens, cache_read, cache_write, cost,\n created_at, streamed_at, completed_at, finish, router, recorded_at\n ) VALUES (\n $messageID, $sessionID, $parentID, $providerID, $modelID, $variant,\n $input, $output, $reasoning, $cacheRead, $cacheWrite, $cost,\n $createdAt, $streamedAt, $completedAt, $finish, $router, $recordedAt\n )\n ON CONFLICT(message_id) DO UPDATE SET\n session_id = excluded.session_id,\n parent_id = COALESCE(excluded.parent_id, steps.parent_id),\n provider_id = COALESCE(excluded.provider_id, steps.provider_id),\n model_id = COALESCE(excluded.model_id, steps.model_id),\n variant = COALESCE(excluded.variant, steps.variant),\n input_tokens = excluded.input_tokens,\n output_tokens = excluded.output_tokens,\n reasoning_tokens = excluded.reasoning_tokens,\n cache_read = excluded.cache_read,\n cache_write = excluded.cache_write,\n cost = excluded.cost,\n created_at = COALESCE(excluded.created_at, steps.created_at),\n streamed_at = COALESCE(excluded.streamed_at, steps.streamed_at),\n completed_at = COALESCE(excluded.completed_at, steps.completed_at),\n finish = COALESCE(excluded.finish, steps.finish),\n router = COALESCE(excluded.router, steps.router),\n recorded_at = excluded.recorded_at`,\n ).run({\n $messageID: record.messageID,\n $sessionID: record.sessionID,\n $parentID: record.parentID ?? null,\n $providerID: record.providerID ?? null,\n $modelID: record.modelID ?? null,\n $variant: record.variant ?? null,\n $input: record.input ?? 0,\n $output: record.output ?? 0,\n $reasoning: record.reasoning ?? 0,\n $cacheRead: record.cacheRead ?? 0,\n $cacheWrite: record.cacheWrite ?? 0,\n $cost: record.cost ?? 0,\n $createdAt: record.createdAt ?? null,\n $streamedAt: record.streamedAt ?? null,\n $completedAt: record.completedAt ?? null,\n $finish: record.finish === undefined ? null : sanitizeLabel(record.finish, 64),\n $router: record.router === undefined ? null : (normalizeToken(record.router) ?? null),\n $recordedAt: record.recordedAt ?? Date.now(),\n })\n}\n\nexport function recordPrompt(db: Database, record: PromptRecord): void {\n db.query(\n `INSERT INTO prompts (session_id, message_id, created_at)\n VALUES ($sessionID, $messageID, $createdAt)\n ON CONFLICT(session_id, message_id) DO UPDATE SET created_at = COALESCE(excluded.created_at, prompts.created_at)`,\n ).run({\n $sessionID: record.sessionID,\n $messageID: record.messageID,\n $createdAt: record.createdAt ?? null,\n })\n}\n\nexport function recordEvent(db: Database, record: EventRecord): void {\n db.query(\n `INSERT INTO events (session_id, kind, message_id, at, data)\n VALUES ($sessionID, $kind, $messageID, $at, $data)`,\n ).run({\n $sessionID: record.sessionID,\n $kind: sanitizeLabel(record.kind, 64),\n $messageID: record.messageID ?? null,\n $at: record.at ?? Date.now(),\n $data: jsonText(record.data),\n })\n}\n\nexport function upsertSession(db: Database, record: SessionRecord): void {\n db.query(\n `INSERT INTO sessions (session_id, parent_id, directory, title, started_at, updated_at)\n VALUES ($sessionID, $parentID, $directory, $title, $startedAt, $updatedAt)\n ON CONFLICT(session_id) DO UPDATE SET\n parent_id = COALESCE(excluded.parent_id, sessions.parent_id),\n directory = COALESCE(excluded.directory, sessions.directory),\n title = COALESCE(excluded.title, sessions.title),\n started_at = COALESCE(sessions.started_at, excluded.started_at),\n updated_at = COALESCE(excluded.updated_at, sessions.updated_at)`,\n ).run({\n $sessionID: record.sessionID,\n $parentID: record.parentID ?? null,\n // Titles are written by a model and directories come from the host: both\n // are displayed, so neither may carry control characters into a terminal.\n $directory: record.directory === undefined ? null : sanitizeLabel(record.directory, LABEL_LIMIT),\n $title: record.title === undefined ? null : sanitizeLabel(record.title, LABEL_LIMIT),\n $startedAt: record.startedAt ?? null,\n $updatedAt: record.updatedAt ?? null,\n })\n}\n\n/**\n * Attach the endpoint discovered after the step was recorded. A step that\n * already carries the same endpoint is left alone: the recorder runs on the\n * request path, and a no-op write there is a cost with no reader.\n */\nexport function setStepRouter(db: Database, messageID: string, info: EndpointInfo): void {\n const router = info.router === undefined ? undefined : normalizeToken(info.router)\n if (router === undefined) return\n db.query(\n `UPDATE steps\n SET router = $router\n WHERE message_id = $messageID AND router IS NOT $router`,\n ).run({ $router: router, $messageID: messageID })\n}\n\n/**\n * Apply the endpoint learned from a compaction to every step of that session\n * and model that still lacks one. Returns the number of rows touched.\n */\nexport function setStepRouterForSessionModel(\n db: Database,\n sessionID: string,\n modelID: string | undefined,\n info: EndpointInfo,\n): number {\n const router = info.router === undefined ? undefined : normalizeToken(info.router)\n if (router === undefined) return 0\n const result = db\n .query(\n `UPDATE steps\n SET router = $router\n WHERE session_id = $sessionID AND router IS NULL AND ($modelID IS NULL OR model_id = $modelID)`,\n )\n .run({ $router: router, $sessionID: sessionID, $modelID: modelID ?? null })\n return result.changes\n}\n\n/** Dump every table as NDJSON (`{\"table\": \"...\", \"row\": {...}}` per line). */\nexport function exportNdjson(db: Database): string {\n const lines: string[] = []\n for (const table of TABLES) {\n const rows = db.query(`SELECT * FROM ${table}`).all() as Record<string, unknown>[]\n for (const row of rows) lines.push(JSON.stringify({ table, row }))\n }\n return lines.length > 0 ? `${lines.join(\"\\n\")}\\n` : \"\"\n}\n\n/** Column shape an import has to respect: the type and the NOT NULL rules. */\ntype ImportColumn = {\n name: string\n /** Declared affinity: `INTEGER`, `REAL`, `TEXT`, `BLOB` or empty. */\n type: string\n notNull: boolean\n primaryKey: boolean\n /** A column with a default may be left out of the import entirely. */\n hasDefault: boolean\n}\n\nfunction numericAffinity(type: string): boolean {\n const upper = type.toUpperCase()\n return upper.includes(\"INT\") || upper.includes(\"REAL\") || upper.includes(\"NUM\") || upper.includes(\"DEC\") || upper.includes(\"DOUB\")\n}\n\n/**\n * One imported value, forced into the shape of its column. Text is sanitized\n * and capped like everything else that is displayed; a number that is not a\n * finite number becomes NULL, so a crafted dump cannot store `NaN` or a\n * string where a view does arithmetic.\n */\nfunction importValue(value: unknown, column: ImportColumn): string | number | null {\n if (value === null || value === undefined) return null\n if (numericAffinity(column.type)) {\n const parsed = typeof value === \"number\" ? value : Number(value)\n return Number.isFinite(parsed) ? parsed : null\n }\n if (typeof value === \"number\") return Number.isFinite(value) ? value : null\n if (typeof value === \"boolean\") return value ? 1 : 0\n if (typeof value === \"object\") return sanitizeLabel(JSON.stringify(value) ?? \"\", LABEL_LIMIT)\n return sanitizeLabel(String(value), LABEL_LIMIT)\n}\n\n/**\n * Load an `exportNdjson` dump back in. Rows are inserted with `OR IGNORE`, so\n * importing the same dump twice is harmless. Unknown tables and rows that are\n * not objects are rejected; a row that cannot fill a required column is\n * skipped rather than aborting the rest of the import. A column the row does\n * not carry is left to its default, the same way a hand-written insert is.\n */\nexport function importNdjson(db: Database, text: string): number {\n const columns = new Map<TableName, ImportColumn[]>()\n for (const table of TABLES) {\n const info = db.query(`PRAGMA table_info(${table})`).all() as {\n name: string\n type: string\n notnull: number\n pk: number\n dflt_value: string | null\n }[]\n columns.set(\n table,\n info.map((column) => ({\n name: column.name,\n type: column.type,\n notNull: column.notnull === 1,\n primaryKey: column.pk > 0,\n hasDefault: column.dflt_value !== null,\n })),\n )\n }\n\n let imported = 0\n for (const line of text.split(\"\\n\")) {\n const trimmed = line.trim()\n if (!trimmed) continue\n let parsed: { table?: string; row?: Record<string, unknown> }\n try {\n parsed = JSON.parse(trimmed) as { table?: string; row?: Record<string, unknown> }\n } catch {\n continue\n }\n const table = parsed.table as TableName | undefined\n const row = parsed.row\n if (!table || !row || !(TABLES as readonly string[]).includes(table)) continue\n const shape = columns.get(table) ?? []\n if (shape.length === 0) continue\n const names: string[] = []\n const values: (string | number)[] = []\n let importable = true\n for (const column of shape) {\n const value = importValue(row[column.name], column)\n if (value !== null) {\n names.push(column.name)\n values.push(value)\n continue\n }\n // NULL for a column that cannot hold one has no default to fall back on,\n // and a NULL in a text primary key is a key the database cannot use; an\n // INTEGER PRIMARY KEY is a rowid, so leaving it out assigns one.\n const required = (column.notNull || (column.primaryKey && !numericAffinity(column.type))) && !column.hasDefault\n if (required) {\n importable = false\n break\n }\n }\n if (!importable) continue\n const placeholders = names.map(() => \"?\").join(\", \")\n db.query(`INSERT OR IGNORE INTO ${table} (${names.join(\", \")}) VALUES (${placeholders})`).run(...values)\n imported += 1\n }\n return imported\n}\n",
5
+ "import { Plugin } from \"@opencode/plugin\"\nimport type { Database } from \"bun:sqlite\"\nimport { createRecorder, type RecorderEvent } from \"./recorder\"\nimport { openStatsDb } from \"./stats/db\"\nimport { resolveDbPath } from \"./stats/paths\"\n\n/**\n * Server-side recorder. Subscribes to the session event stream and persists\n * every assistant step, prompt and lifecycle event into the stats database;\n * the TUI entry reads the same database for its live line and stats panel.\n *\n * Everything the recorder stores comes from the event stream — the provider\n * and model a step ran as, the tokens and the timing. The plugin is\n * observational: it never rewrites prompts, requests or responses, registers\n * no hook on the request path, and a failure inside the recorder is swallowed\n * so a session can never be interrupted by its own telemetry.\n */\n\nexport type Options = {\n /** Turn recording off: no event subscription, no database. */\n enabled?: boolean | undefined\n /** SQLite database path override (defaults to the per-OS state directory). */\n db?: string | undefined\n}\n\nfunction tryOpen(path: string): Database | undefined {\n try {\n return openStatsDb(path)\n } catch {\n return undefined\n }\n}\n\nexport default Plugin.define({\n id: \"usage-insights\",\n async setup(ctx) {\n const options = (ctx.options ?? {}) as Options\n if (options.enabled === false) return\n\n const db = tryOpen(resolveDbPath(options.db))\n if (db === undefined) return\n\n // The recorder's connection runs on the agent's own event path, so it waits\n // at most a second for another writer and then defers the write: a stalled\n // event handler is worse than a record that lands on the next event.\n try {\n db.exec(\"PRAGMA busy_timeout = 1000\")\n } catch {\n // A database that cannot take the pragma is still usable.\n }\n\n const recorder = createRecorder(db)\n const abort = new AbortController()\n const stream = (async () => {\n try {\n for await (const event of ctx.event.subscribe({ signal: abort.signal })) {\n try {\n recorder.handleEvent(event as RecorderEvent)\n } catch {\n // A malformed event must never break the stream.\n }\n }\n } catch {\n // Subscription closed (shutdown or transport error): nothing to do.\n }\n })()\n\n return async () => {\n abort.abort()\n await stream\n db.close()\n }\n },\n})\n",
6
+ "import { Database } from \"bun:sqlite\"\nimport { chmodSync, mkdirSync } from \"node:fs\"\nimport { dirname } from \"node:path\"\nimport { ensureRegularFile } from \"./paths\"\nimport { isToken, sanitizeLabel, TEXT_LIMIT } from \"../text\"\n\n/**\n * Persistent usage store. One SQLite database holds every recorded step,\n * prompt, event and session row — and nothing else: the recorder only writes\n * what a view reads.\n *\n * The database is opened in WAL mode with a busy timeout so the server\n * recorder process and the TUI reader process can share it safely. The two\n * sides open it differently on purpose:\n *\n * - `openStatsDb` writes: schema, migrations and recording (the server).\n * - `openStatsDbReadOnly` only reads (the TUI), so no view can change the file\n * and no reader pays for a schema write.\n * - `openStatsDbWrite` is a short-lived maintenance connection (prune, vacuum)\n * that gives up quickly instead of holding the recorder up.\n */\n\nexport const SCHEMA_VERSION = 9\n\n/** Longest title or path kept: a label on a line, not a document. */\nconst LABEL_LIMIT = 4 * TEXT_LIMIT\n\nconst SCHEMA = `\nCREATE TABLE IF NOT EXISTS meta (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS sessions (\n session_id TEXT PRIMARY KEY,\n parent_id TEXT,\n directory TEXT,\n title TEXT,\n started_at INTEGER,\n updated_at INTEGER\n);\n\nCREATE TABLE IF NOT EXISTS prompts (\n session_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n created_at INTEGER,\n PRIMARY KEY (session_id, message_id)\n);\nCREATE INDEX IF NOT EXISTS prompts_created ON prompts(created_at);\n\nCREATE TABLE IF NOT EXISTS steps (\n message_id TEXT PRIMARY KEY,\n session_id TEXT NOT NULL,\n parent_id TEXT,\n provider_id TEXT,\n model_id TEXT,\n variant TEXT,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n reasoning_tokens INTEGER NOT NULL DEFAULT 0,\n cache_read INTEGER NOT NULL DEFAULT 0,\n cache_write INTEGER NOT NULL DEFAULT 0,\n cost REAL NOT NULL DEFAULT 0,\n created_at INTEGER,\n streamed_at INTEGER,\n completed_at INTEGER,\n finish TEXT,\n recorded_at INTEGER NOT NULL\n);\nCREATE INDEX IF NOT EXISTS steps_session ON steps(session_id, created_at);\nCREATE INDEX IF NOT EXISTS steps_model ON steps(provider_id, model_id, variant);\nCREATE INDEX IF NOT EXISTS steps_completed ON steps(completed_at);\n-- Every range filter and every ordering compares the step's own clock, which\n-- falls back to the recording time for a step that was already in flight when\n-- the plugin loaded. The index has to cover that expression, or every ranged\n-- view is a full table scan.\nCREATE INDEX IF NOT EXISTS steps_at ON steps(COALESCE(created_at, recorded_at));\nCREATE INDEX IF NOT EXISTS steps_session_at ON steps(session_id, COALESCE(created_at, recorded_at));\n\nCREATE TABLE IF NOT EXISTS events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id TEXT NOT NULL,\n kind TEXT NOT NULL,\n message_id TEXT,\n at INTEGER,\n data TEXT\n);\nCREATE INDEX IF NOT EXISTS events_session ON events(session_id, at);\nCREATE INDEX IF NOT EXISTS events_kind ON events(kind, at);\n`\n\n/** Tables carried by `exportNdjson` / `importNdjson`, in import-safe order. */\nexport const TABLES = [\"sessions\", \"prompts\", \"events\", \"steps\"] as const\n\nexport type TableName = (typeof TABLES)[number]\n\nexport type StepRecord = {\n messageID: string\n sessionID: string\n parentID?: string | undefined\n /** The provider the model ran through, as OpenCode configured it (`opencode-go`). */\n providerID?: string | undefined\n modelID?: string | undefined\n variant?: string | undefined\n input?: number | undefined\n output?: number | undefined\n reasoning?: number | undefined\n cacheRead?: number | undefined\n cacheWrite?: number | undefined\n cost?: number | undefined\n createdAt?: number | undefined\n streamedAt?: number | undefined\n completedAt?: number | undefined\n finish?: string | undefined\n /** Clock of the recording itself, defaults to `Date.now()`. */\n recordedAt?: number | undefined\n}\n\nexport type PromptRecord = {\n sessionID: string\n messageID: string\n createdAt?: number | undefined\n}\n\nexport type EventRecord = {\n sessionID: string\n kind: string\n messageID?: string | undefined\n at?: number | undefined\n data?: unknown\n}\n\nexport type SessionRecord = {\n sessionID: string\n parentID?: string | undefined\n directory?: string | undefined\n title?: string | undefined\n startedAt?: number | undefined\n updatedAt?: number | undefined\n}\n\nfunction jsonText(value: unknown): string | null {\n if (value === undefined || value === null) return null\n if (typeof value === \"string\") return value\n try {\n return JSON.stringify(value)\n } catch {\n return null\n }\n}\n\n/**\n * Columns and tables the recorder used to write and no view ever read. They are\n * dropped from an existing database, together with the rows of the events that\n * followed the same fate, so a long-lived file shrinks instead of carrying dead\n * weight forever.\n *\n * `router` is here because the gateway that named the routing endpoint in a\n * response header (`x-opencode-endpoint-id`) stopped naming one: the identity a\n * step is grouped by is the provider the model ran through, and the endpoint a\n * gateway happened to route to is no longer observable at all.\n */\nconst REMOVED_COLUMNS = [\"provider_state\", \"upstream\", \"upstream_source\", \"upstream_model\", \"tps\", \"router\"] as const\nconst REMOVED_EVENT_KINDS = [\"execution\", \"step-failed\", \"model-selected\", \"usage-recorded\"] as const\n\nfunction dropUnused(db: Database): void {\n const objects = db.query(\"SELECT name, type FROM sqlite_master WHERE type IN ('table', 'index')\").all() as {\n name: string\n type: string\n }[]\n const tables = new Set(objects.filter((row) => row.type === \"table\").map((row) => row.name))\n const indexes = new Set(objects.filter((row) => row.type === \"index\").map((row) => row.name))\n\n if (tables.has(\"http_responses\")) db.exec(\"DROP TABLE http_responses\")\n // These indexes read a column that is about to go: SQLite refuses to drop an\n // indexed column, so the index goes first.\n if (indexes.has(\"steps_upstream\")) db.exec(\"DROP INDEX steps_upstream\")\n if (indexes.has(\"steps_router\")) db.exec(\"DROP INDEX steps_router\")\n\n const columns = new Set((db.query(\"PRAGMA table_info(steps)\").all() as { name: string }[]).map((column) => column.name))\n for (const name of REMOVED_COLUMNS) {\n if (columns.has(name)) db.exec(`ALTER TABLE steps DROP COLUMN ${name}`)\n }\n\n if (tables.has(\"events\")) {\n const kinds = REMOVED_EVENT_KINDS.map((kind) => `'${kind}'`).join(\", \")\n db.exec(`DELETE FROM events WHERE kind IN (${kinds})`)\n }\n}\n\n/**\n * Bring an existing file up to date. There is no version ladder: a column the\n * schema carries is created with the table, and a column the recorder stopped\n * writing is dropped by `dropUnused`, which is idempotent and also heals a\n * database a half-applied run left behind.\n */\nfunction migrate(db: Database): void {\n dropUnused(db)\n}\n\n/**\n * The file holds session titles, project directories and usage history, so it\n * is created for the user alone. Windows takes its access from the parent\n * directory's ACL, which is not something a plugin should rewrite.\n */\nconst POSIX = process.platform !== \"win32\"\n\nfunction hardenDirectory(path: string): void {\n if (!POSIX) return\n try {\n chmodSync(path, 0o700)\n } catch {\n // A shared or read-only directory: the database is still usable.\n }\n}\n\nfunction hardenFiles(path: string): void {\n if (!POSIX || path === \":memory:\") return\n for (const file of [path, `${path}-wal`, `${path}-shm`]) {\n try {\n chmodSync(file, 0o600)\n } catch {\n // The WAL and the shared-memory file are created on the first write and\n // removed on the last close, so one of them is often simply absent.\n }\n }\n}\n\n/** Directory for a database path, created private on POSIX. */\nfunction prepareDirectory(path: string): void {\n if (path === \":memory:\") return\n mkdirSync(dirname(path), { recursive: true, mode: 0o700 })\n hardenDirectory(dirname(path))\n}\n\n/** Open (creating when needed) the stats database and apply the schema. */\nexport function openStatsDb(path: string): Database {\n if (path !== \":memory:\") ensureRegularFile(path)\n prepareDirectory(path)\n const db = new Database(path, { create: true })\n db.exec(\"PRAGMA journal_mode = WAL\")\n db.exec(\"PRAGMA busy_timeout = 5000\")\n db.exec(\"PRAGMA synchronous = NORMAL\")\n db.exec(SCHEMA)\n migrate(db)\n db.query(\"INSERT INTO meta (key, value) VALUES ('schema_version', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value\").run(\n String(SCHEMA_VERSION),\n )\n // The schema write is what creates the WAL and its shared-memory file.\n hardenFiles(path)\n return db\n}\n\n/**\n * Reader connection for the TUI: no schema, no migration, no write. It throws\n * when the database is absent or was never written by the recorder, so the\n * caller can say so instead of showing an empty panel.\n */\nexport function openStatsDbReadOnly(path: string): Database {\n ensureRegularFile(path)\n const db = new Database(path, { readonly: true })\n try {\n db.query(\"SELECT 1 FROM steps LIMIT 1\").get()\n } catch (error) {\n db.close()\n throw error\n }\n return db\n}\n\n/**\n * Short-lived write connection for maintenance. The busy timeout is short on\n * purpose: a prune or a vacuum that cannot take the lock must fail in front of\n * the user, not hold the recorder's response path for seconds.\n */\nexport function openStatsDbWrite(path: string): Database {\n if (path !== \":memory:\") ensureRegularFile(path)\n prepareDirectory(path)\n const db = new Database(path, { create: true })\n db.exec(\"PRAGMA busy_timeout = 250\")\n db.exec(\"PRAGMA synchronous = NORMAL\")\n hardenFiles(path)\n return db\n}\n\n/**\n * Insert or refresh an assistant step. Idempotent by message ID so replayed\n * events, restarts and NDJSON imports never duplicate usage. A non-null field\n * (the provider, the model, the finish reason) survives a re-record with `NULL`\n * through COALESCE.\n *\n * The provider is the identity every view groups by, so it is validated as a\n * token before it is stored: a value that is not a provider id is recorded as\n * absent instead of becoming a grouping key.\n */\nexport function recordStep(db: Database, record: StepRecord): void {\n db.query(\n `INSERT INTO steps (\n message_id, session_id, parent_id, provider_id, model_id, variant,\n input_tokens, output_tokens, reasoning_tokens, cache_read, cache_write, cost,\n created_at, streamed_at, completed_at, finish, recorded_at\n ) VALUES (\n $messageID, $sessionID, $parentID, $providerID, $modelID, $variant,\n $input, $output, $reasoning, $cacheRead, $cacheWrite, $cost,\n $createdAt, $streamedAt, $completedAt, $finish, $recordedAt\n )\n ON CONFLICT(message_id) DO UPDATE SET\n session_id = excluded.session_id,\n parent_id = COALESCE(excluded.parent_id, steps.parent_id),\n provider_id = COALESCE(excluded.provider_id, steps.provider_id),\n model_id = COALESCE(excluded.model_id, steps.model_id),\n variant = COALESCE(excluded.variant, steps.variant),\n input_tokens = excluded.input_tokens,\n output_tokens = excluded.output_tokens,\n reasoning_tokens = excluded.reasoning_tokens,\n cache_read = excluded.cache_read,\n cache_write = excluded.cache_write,\n cost = excluded.cost,\n created_at = COALESCE(excluded.created_at, steps.created_at),\n streamed_at = COALESCE(excluded.streamed_at, steps.streamed_at),\n completed_at = COALESCE(excluded.completed_at, steps.completed_at),\n finish = COALESCE(excluded.finish, steps.finish),\n recorded_at = excluded.recorded_at`,\n ).run({\n $messageID: record.messageID,\n $sessionID: record.sessionID,\n $parentID: record.parentID ?? null,\n $providerID: record.providerID !== undefined && isToken(record.providerID) ? record.providerID : null,\n $modelID: record.modelID ?? null,\n $variant: record.variant ?? null,\n $input: record.input ?? 0,\n $output: record.output ?? 0,\n $reasoning: record.reasoning ?? 0,\n $cacheRead: record.cacheRead ?? 0,\n $cacheWrite: record.cacheWrite ?? 0,\n $cost: record.cost ?? 0,\n $createdAt: record.createdAt ?? null,\n $streamedAt: record.streamedAt ?? null,\n $completedAt: record.completedAt ?? null,\n $finish: record.finish === undefined ? null : sanitizeLabel(record.finish, 64),\n $recordedAt: record.recordedAt ?? Date.now(),\n })\n}\n\nexport function recordPrompt(db: Database, record: PromptRecord): void {\n db.query(\n `INSERT INTO prompts (session_id, message_id, created_at)\n VALUES ($sessionID, $messageID, $createdAt)\n ON CONFLICT(session_id, message_id) DO UPDATE SET created_at = COALESCE(excluded.created_at, prompts.created_at)`,\n ).run({\n $sessionID: record.sessionID,\n $messageID: record.messageID,\n $createdAt: record.createdAt ?? null,\n })\n}\n\nexport function recordEvent(db: Database, record: EventRecord): void {\n db.query(\n `INSERT INTO events (session_id, kind, message_id, at, data)\n VALUES ($sessionID, $kind, $messageID, $at, $data)`,\n ).run({\n $sessionID: record.sessionID,\n $kind: sanitizeLabel(record.kind, 64),\n $messageID: record.messageID ?? null,\n $at: record.at ?? Date.now(),\n $data: jsonText(record.data),\n })\n}\n\nexport function upsertSession(db: Database, record: SessionRecord): void {\n db.query(\n `INSERT INTO sessions (session_id, parent_id, directory, title, started_at, updated_at)\n VALUES ($sessionID, $parentID, $directory, $title, $startedAt, $updatedAt)\n ON CONFLICT(session_id) DO UPDATE SET\n parent_id = COALESCE(excluded.parent_id, sessions.parent_id),\n directory = COALESCE(excluded.directory, sessions.directory),\n title = COALESCE(excluded.title, sessions.title),\n started_at = COALESCE(sessions.started_at, excluded.started_at),\n updated_at = COALESCE(excluded.updated_at, sessions.updated_at)`,\n ).run({\n $sessionID: record.sessionID,\n $parentID: record.parentID ?? null,\n // Titles are written by a model and directories come from the host: both\n // are displayed, so neither may carry control characters into a terminal.\n $directory: record.directory === undefined ? null : sanitizeLabel(record.directory, LABEL_LIMIT),\n $title: record.title === undefined ? null : sanitizeLabel(record.title, LABEL_LIMIT),\n $startedAt: record.startedAt ?? null,\n $updatedAt: record.updatedAt ?? null,\n })\n}\n\n/** Dump every table as NDJSON (`{\"table\": \"...\", \"row\": {...}}` per line). */\nexport function exportNdjson(db: Database): string {\n const lines: string[] = []\n for (const table of TABLES) {\n const rows = db.query(`SELECT * FROM ${table}`).all() as Record<string, unknown>[]\n for (const row of rows) lines.push(JSON.stringify({ table, row }))\n }\n return lines.length > 0 ? `${lines.join(\"\\n\")}\\n` : \"\"\n}\n\n/** Column shape an import has to respect: the type and the NOT NULL rules. */\ntype ImportColumn = {\n name: string\n /** Declared affinity: `INTEGER`, `REAL`, `TEXT`, `BLOB` or empty. */\n type: string\n notNull: boolean\n primaryKey: boolean\n /** A column with a default may be left out of the import entirely. */\n hasDefault: boolean\n}\n\nfunction numericAffinity(type: string): boolean {\n const upper = type.toUpperCase()\n return upper.includes(\"INT\") || upper.includes(\"REAL\") || upper.includes(\"NUM\") || upper.includes(\"DEC\") || upper.includes(\"DOUB\")\n}\n\n/**\n * One imported value, forced into the shape of its column. Text is sanitized\n * and capped like everything else that is displayed; a number that is not a\n * finite number becomes NULL, so a crafted dump cannot store `NaN` or a\n * string where a view does arithmetic.\n */\nfunction importValue(value: unknown, column: ImportColumn): string | number | null {\n if (value === null || value === undefined) return null\n if (numericAffinity(column.type)) {\n const parsed = typeof value === \"number\" ? value : Number(value)\n return Number.isFinite(parsed) ? parsed : null\n }\n if (typeof value === \"number\") return Number.isFinite(value) ? value : null\n if (typeof value === \"boolean\") return value ? 1 : 0\n if (typeof value === \"object\") return sanitizeLabel(JSON.stringify(value) ?? \"\", LABEL_LIMIT)\n return sanitizeLabel(String(value), LABEL_LIMIT)\n}\n\n/**\n * Load an `exportNdjson` dump back in. Rows are inserted with `OR IGNORE`, so\n * importing the same dump twice is harmless. Unknown tables and rows that are\n * not objects are rejected; a row that cannot fill a required column is\n * skipped rather than aborting the rest of the import. A column the row does\n * not carry is left to its default, the same way a hand-written insert is.\n */\nexport function importNdjson(db: Database, text: string): number {\n const columns = new Map<TableName, ImportColumn[]>()\n for (const table of TABLES) {\n const info = db.query(`PRAGMA table_info(${table})`).all() as {\n name: string\n type: string\n notnull: number\n pk: number\n dflt_value: string | null\n }[]\n columns.set(\n table,\n info.map((column) => ({\n name: column.name,\n type: column.type,\n notNull: column.notnull === 1,\n primaryKey: column.pk > 0,\n hasDefault: column.dflt_value !== null,\n })),\n )\n }\n\n let imported = 0\n for (const line of text.split(\"\\n\")) {\n const trimmed = line.trim()\n if (!trimmed) continue\n let parsed: { table?: string; row?: Record<string, unknown> }\n try {\n parsed = JSON.parse(trimmed) as { table?: string; row?: Record<string, unknown> }\n } catch {\n continue\n }\n const table = parsed.table as TableName | undefined\n const row = parsed.row\n if (!table || !row || !(TABLES as readonly string[]).includes(table)) continue\n const shape = columns.get(table) ?? []\n if (shape.length === 0) continue\n const names: string[] = []\n const values: (string | number)[] = []\n let importable = true\n for (const column of shape) {\n const value = importValue(row[column.name], column)\n if (value !== null) {\n names.push(column.name)\n values.push(value)\n continue\n }\n // NULL for a column that cannot hold one has no default to fall back on,\n // and a NULL in a text primary key is a key the database cannot use; an\n // INTEGER PRIMARY KEY is a rowid, so leaving it out assigns one.\n const required = (column.notNull || (column.primaryKey && !numericAffinity(column.type))) && !column.hasDefault\n if (required) {\n importable = false\n break\n }\n }\n if (!importable) continue\n const placeholders = names.map(() => \"?\").join(\", \")\n db.query(`INSERT OR IGNORE INTO ${table} (${names.join(\", \")}) VALUES (${placeholders})`).run(...values)\n imported += 1\n }\n return imported\n}\n",
7
7
  "import { homedir } from \"node:os\"\nimport { join } from \"node:path\"\nimport { statSync } from \"node:fs\"\n\n/**\n * SQLite database used when no `db` option is configured: the per-user state\n * directory of the current OS, never a shared temp directory.\n *\n * The subdirectory is the plugin's package name, never a generic one: the\n * `opencode` tree is shared with OpenCode itself and with every other plugin,\n * so a `stats/` of our own would be a collision waiting to happen.\n *\n * - Windows: `%LOCALAPPDATA%\\opencode\\opencode-usage-insights\\usage.db`\n * - macOS: `~/Library/Application Support/opencode/opencode-usage-insights/usage.db`\n * - Linux/other: `$XDG_STATE_HOME/opencode/opencode-usage-insights/usage.db`,\n * falling back to `~/.local/state/opencode/opencode-usage-insights/usage.db`\n *\n * Pure by design so every OS can be asserted from any machine.\n */\nexport function defaultDbPath(env: Record<string, string | undefined> = process.env, platform: string = process.platform): string {\n const name = join(\"opencode\", \"opencode-usage-insights\", \"usage.db\")\n if (platform === \"win32\") {\n return join(env.LOCALAPPDATA ?? join(homedir(), \"AppData\", \"Local\"), name)\n }\n if (platform === \"darwin\") {\n return join(homedir(), \"Library\", \"Application Support\", name)\n }\n return join(env.XDG_STATE_HOME ?? join(homedir(), \".local\", \"state\"), name)\n}\n\n/**\n * Expand a leading `~` (or `~/`), so a configured path reads the way every\n * shell does. Windows accepts the backslash form too.\n */\nexport function expandHome(path: string, home: string = homedir()): string {\n if (path === \"~\") return home\n if (path.startsWith(\"~/\") || path.startsWith(\"~\\\\\")) return join(home, path.slice(2))\n return path\n}\n\n/**\n * Resolve the effective database path: explicit plugin option first, then the\n * `OPENCODE_USAGE_INSIGHTS_DB` escape hatch (handy for dev and tests), then\n * the per-OS state directory. A leading `~` is expanded in all three.\n */\nexport function resolveDbPath(\n configured?: string | undefined,\n env: Record<string, string | undefined> = process.env,\n platform: string = process.platform,\n): string {\n const home = env.HOME ?? env.USERPROFILE ?? homedir()\n if (configured) return expandHome(configured, home)\n if (env.OPENCODE_USAGE_INSIGHTS_DB) return expandHome(env.OPENCODE_USAGE_INSIGHTS_DB, home)\n return defaultDbPath(env, platform)\n}\n\n/**\n * A configured path must not be a directory, a device or a FIFO: SQLite would\n * fail obscurely, or write where nobody expects it. Symlinks to a regular file\n * are followed (`statSync`, not `lstat`) because moving the state directory to\n * another disk is a legitimate setup. A missing file is fine — the plugin\n * creates it.\n */\nexport function ensureRegularFile(path: string): void {\n let stats\n try {\n stats = statSync(path)\n } catch {\n return\n }\n if (!stats.isFile()) throw new Error(`not a regular file: ${path}`)\n}\n",
8
- "/**\n * Text hygiene for everything that reaches a terminal.\n *\n * Some of what the plugin displays is not written by the user: a gateway names\n * its own endpoint in a response header, a model writes a session title, an\n * NDJSON export can come from anywhere. A control character in any of those is\n * interpreted by the terminal that renders it (title changes, cursor moves,\n * colours), so every such string is stripped of control characters before it\n * is stored or drawn.\n *\n * The rule is deliberately blunt: drop C0 controls, DEL and the C1 block, keep\n * everything else (the text is otherwise arbitrary Unicode), and cap the\n * length so a hostile value cannot bloat the database or the frame. Layout is\n * never touched: whitespace is preserved, because callers build indentation\n * into the strings they truncate.\n */\n\n/** Longest value kept by default: a title, an id or a path, not a document. */\nexport const TEXT_LIMIT = 512\n\n/**\n * Strip control characters and cap the length. Newlines and tabs go too: a\n * line is one line, and the renderers lay out their own spacing.\n */\nexport function sanitizeText(value: string, limit = TEXT_LIMIT): string {\n if (limit <= 0) return \"\"\n let cleaned = \"\"\n for (const character of value) {\n const code = character.codePointAt(0) ?? 0\n if (code <= 0x1f || code === 0x7f || (code >= 0x80 && code <= 0x9f)) continue\n cleaned += character\n if (cleaned.length >= limit) break\n }\n return cleaned\n}\n\n/** `sanitizeText` without the layout, for a stored label: trimmed as well. */\nexport function sanitizeLabel(value: string, limit = TEXT_LIMIT): string {\n return sanitizeText(value, limit).trim()\n}\n\n/**\n * A single identifier token: the shape a gateway endpoint, a model id or a\n * provider id has. Used to validate values that become identities in the\n * database, so a header cannot inject anything into a grouping key.\n */\nconst TOKEN = /^[A-Za-z0-9][A-Za-z0-9._:/-]*$/\n\nexport function isToken(value: string, limit = 64): boolean {\n return value.length > 0 && value.length <= limit && TOKEN.test(value)\n}\n\n/** The lowercase form of a token identity, so the same endpoint groups once. */\nexport function normalizeToken(value: string, limit = 64): string | undefined {\n const trimmed = value.trim().toLowerCase()\n return isToken(trimmed, limit) ? trimmed : undefined\n}\n",
9
- "/**\n * Pure turn accounting shared by the TUI line, the server recorder and the\n * stats engine.\n *\n * Everything here operates on structural shapes that mirror the session\n * messages published by OpenCode, so the logic stays testable without a host\n * runtime and without the effect-based schema types.\n */\n\nexport type ModelRefLike = {\n providerID: string\n id: string\n variant?: string | undefined\n}\n\nexport type AssistantMessage = {\n type: \"assistant\"\n id: string\n time?: { created?: number | undefined; streamed?: number | undefined; completed?: number | undefined } | undefined\n model?: ModelRefLike | undefined\n tokens?:\n | {\n input?: number | undefined\n output?: number | undefined\n reasoning?: number | undefined\n cache?: { read?: number | undefined; write?: number | undefined } | undefined\n }\n | undefined\n cost?: number | undefined\n finish?: string | null | undefined\n rawFinish?: string | null | undefined\n}\n\nexport type PromptMessage = {\n type: \"user\" | \"synthetic\" | \"system\"\n id?: string | undefined\n time?: { created?: number | undefined } | undefined\n}\n\nexport type TranscriptMessage =\n | AssistantMessage\n | PromptMessage\n | { type: string; id?: string | undefined; time?: { created?: number | undefined } | undefined }\n\n/** Accumulated token usage for a group of assistant steps. */\nexport type Totals = {\n input: number\n cached: number\n output: number\n reasoning: number\n cacheWrite: number\n cost: number\n steps: number\n}\n\nexport type CacheDrop = {\n messageID: string\n /** Tokens that were expected to hit the prompt cache but did not. */\n tokens: number\n /** `cache.read` of the step that was used as the reuse reference. */\n prevCached: number\n /** Time between the reference step's completion and this step's request. */\n gapMs: number | undefined\n}\n\nexport type TurnStats = {\n /** Wall clock time of the last step's completion. */\n endAt: number | undefined\n /** Creation time of the first settled step of the turn. */\n startAt: number | undefined\n /** Creation time of the prompt that opened this turn. */\n promptAt: number | undefined\n /** Time between the previous prompt and this turn's prompt. */\n promptGapMs: number | undefined\n providerID: string | undefined\n modelID: string | undefined\n variant: string | undefined\n /** Gateway routing endpoint that served the turn, when a source identifies it. */\n router: string | undefined\n /**\n * The displayed identity differs from the previous turn's. The TUI fills it\n * in (the transcript carries no routing information); the line warns with the\n * same `changed` marker it uses for a provider switch.\n */\n identityChanged: boolean\n affinityKey: string | undefined\n /** Provider ID differs from the previous turn in the same transcript. */\n providerChanged: boolean\n /** Provider is the same but the model or variant differs. */\n modelChanged: boolean\n totals: Totals\n /** Passed through from the caller (session-wide accumulated usage). */\n session: Totals | undefined\n /**\n * Prompt-side usage of the last finished step: the context the turn ended\n * with. Summing `input`/`cached` over the steps of a turn counts the same\n * re-sent prompt once per step, which is not what a context view means.\n */\n latest: Totals\n drops: CacheDrop[]\n cacheDropTokens: number\n cacheExpired: boolean\n /** Assistant steps in the turn. */\n steps: number\n /**\n * Message ids of the settled steps of the turn, oldest first. The TUI looks\n * up the endpoint the recorder stored for these steps, so the line reports\n * who served *this* turn: a lookup by provider and model alone would tag a\n * turn with the endpoint of an older one.\n */\n stepIDs: string[]\n}\n\nexport type TurnSegment = {\n assistants: AssistantMessage[]\n prompt: TranscriptMessage | undefined\n}\n\n/** Message types that open a turn: a run of assistants belongs to the last one. */\nexport const PROMPT_BOUNDARIES: ReadonlySet<string> = new Set([\"user\", \"synthetic\", \"system\"])\n\n/**\n * Split a transcript into turns. A turn is a run of assistant steps opened by\n * a prompt boundary (`user`, `synthetic`, `system`). Idle notices, info\n * messages, compactions and shell entries keep the current turn open so a\n * single execution is never split in two.\n */\nexport function transcriptSegments(messages: readonly TranscriptMessage[]): TurnSegment[] {\n const segments: TurnSegment[] = []\n let current: AssistantMessage[] = []\n let prompt: TranscriptMessage | undefined\n\n for (const message of messages) {\n if (message.type === \"assistant\") {\n current.push(message as AssistantMessage)\n continue\n }\n if (!PROMPT_BOUNDARIES.has(message.type)) continue\n if (current.length > 0) {\n segments.push({ assistants: current, prompt })\n current = []\n }\n prompt = message\n }\n if (current.length > 0) segments.push({ assistants: current, prompt })\n return segments\n}\n\n/**\n * The last `count` segments, found by walking the transcript backwards. The\n * live line only needs the turn that just ended and the one before it, and a\n * long session holds tens of thousands of messages: the tail walk stops as\n * soon as it has them, instead of building a segment for every turn.\n */\nexport function transcriptTail(messages: readonly TranscriptMessage[], count: number): TurnSegment[] {\n const segments: TurnSegment[] = []\n let current: AssistantMessage[] = []\n\n for (let index = messages.length - 1; index >= 0; index -= 1) {\n const message = messages[index]!\n if (message.type === \"assistant\") {\n current.unshift(message as AssistantMessage)\n continue\n }\n if (!PROMPT_BOUNDARIES.has(message.type)) continue\n if (current.length === 0) continue\n // The boundary closes the run collected after it.\n segments.unshift({ assistants: current, prompt: message })\n if (segments.length >= count) return segments\n current = []\n }\n // A leading run with no prompt before it is a segment of its own.\n if (current.length > 0 && segments.length < count) segments.unshift({ assistants: current, prompt: undefined })\n return segments\n}\n\nexport function tokenCounts(message: AssistantMessage): { input: number; output: number; reasoning: number; read: number; write: number } {\n const tokens = message.tokens\n return {\n input: tokens?.input ?? 0,\n output: tokens?.output ?? 0,\n reasoning: tokens?.reasoning ?? 0,\n read: tokens?.cache?.read ?? 0,\n write: tokens?.cache?.write ?? 0,\n }\n}\n\nexport function totalize(steps: readonly AssistantMessage[]): Totals {\n const totals: Totals = { input: 0, cached: 0, output: 0, reasoning: 0, cacheWrite: 0, cost: 0, steps: steps.length }\n for (const step of steps) {\n const counts = tokenCounts(step)\n totals.input += counts.input\n totals.cached += counts.read\n totals.output += counts.output\n totals.reasoning += counts.reasoning\n totals.cacheWrite += counts.write\n totals.cost += step.cost ?? 0\n }\n return totals\n}\n\nexport function affinityKey(model: ModelRefLike | undefined): string | undefined {\n if (!model) return undefined\n return `${model.providerID}/${model.id}${model.variant ? `#${model.variant}` : \"\"}`\n}\n\nexport function cacheRead(message: AssistantMessage | undefined): number {\n return message?.tokens?.cache?.read ?? 0\n}\n\nexport function stepCreated(message: AssistantMessage | undefined): number | undefined {\n return message?.time?.created\n}\n\nexport function stepEnd(message: AssistantMessage | undefined): number | undefined {\n const time = message?.time\n if (!time) return undefined\n return time.completed ?? time.streamed ?? time.created\n}\n\n/**\n * Cache reuse loss against a reference step, mirroring OpenCode's own\n * `turn-usage` heuristic: only comparable when the model is identical, and\n * OpenAI's endpoint reports small reads that are not worth flagging.\n */\nexport function cacheDrop(reference: AssistantMessage | undefined, current: AssistantMessage): number | undefined {\n if (!reference) return undefined\n const before = reference.model\n const after = current.model\n if (!before || !after) return undefined\n if (before.providerID !== after.providerID || before.id !== after.id || (before.variant ?? \"\") !== (after.variant ?? \"\")) {\n return undefined\n }\n const drop = cacheRead(reference) - cacheRead(current)\n if (isNoiseDrop(after.providerID, drop)) return undefined\n return drop > 0 ? drop : undefined\n}\n\n/** Milliseconds between a reference step's completion and the next request. */\nexport function stepGap(reference: AssistantMessage | undefined, current: AssistantMessage): number | undefined {\n const ended = stepEnd(reference)\n const started = stepCreated(current)\n if (ended === undefined || started === undefined) return undefined\n return Math.max(0, started - ended)\n}\n\n/**\n * Streaming duration of a step: request start to the last streamed chunk. The\n * host's `streamed` stamp marks the end of streaming, so this is the window a\n * generation rate belongs to — not a time to first token, which the session\n * messages do not record.\n */\nexport function stepStreamMs(message: AssistantMessage): number | undefined {\n const created = message.time?.created\n const streamed = message.time?.streamed\n if (created === undefined || streamed === undefined || streamed <= created) return undefined\n return streamed - created\n}\n\n/**\n * Tokens produced per second over a window. For a step the window is the\n * streaming one: request start to the last streamed chunk (`created` →\n * `streamed`) — `completed` marks the end of the step, which for a tool-call\n * step happens after the tool calls were handled. Returns undefined when the\n * window or the produced tokens are unknown. Single definition, shared by the\n * recorder (which stores it per step) and the latency view.\n */\nexport function tokensPerSecond(produced: number, from: number | undefined, to: number | undefined): number | undefined {\n if (from === undefined || to === undefined || to <= from) return undefined\n if (produced <= 0) return undefined\n return (produced / (to - from)) * 1000\n}\n\n\n/** Output-plus-reasoning tokens per second of a step. */\nexport function stepTps(message: AssistantMessage): number | undefined {\n const counts = tokenCounts(message)\n return tokensPerSecond(counts.output + counts.reasoning, message.time?.created, message.time?.streamed)\n}\n\nconst DROP_EXEMPT_PROVIDERS = new Set([\"openai\"])\n\n/**\n * True when a step has final numbers. A step that is still streaming reports\n * partial tokens (usually zero) and no cache read yet, so reading it would\n * render an empty line and invent a cache-expiry warning against it.\n */\nexport function isSettledStep(message: AssistantMessage): boolean {\n if (message.time?.completed !== undefined) return true\n return message.finish !== undefined && message.finish !== null\n}\n\n/**\n * Compute the stats of the most recent turn in a transcript, including the\n * comparison against the previous turn (affinity and cache reuse).\n *\n * Returns undefined while the newest turn is still being generated: the line\n * appears when a turn ends, never with the zeroes of an in-flight response.\n */\nexport function computeTurn(messages: readonly TranscriptMessage[], session?: Totals): TurnStats | undefined {\n const segments = transcriptTail(messages, 2)\n const last = segments[segments.length - 1]\n if (!last) return undefined\n const previous = segments[segments.length - 2]\n const lastStep = last.assistants[last.assistants.length - 1]\n if (!lastStep) return undefined\n if (!isSettledStep(lastStep)) return undefined\n const previousStep = previous?.assistants[previous.assistants.length - 1]\n // Defensive: a step that never settled mid-turn carries no usable numbers,\n // so it is neither a cache reference nor a reuse candidate.\n const settled = last.assistants.filter(isSettledStep)\n\n const drops: CacheDrop[] = []\n let reference: AssistantMessage | undefined = previousStep !== undefined && isSettledStep(previousStep) ? previousStep : undefined\n for (const step of settled) {\n const drop = cacheDrop(reference, step)\n if (drop !== undefined) {\n drops.push({\n messageID: step.id,\n tokens: drop,\n prevCached: cacheRead(reference),\n gapMs: stepGap(reference, step),\n })\n }\n reference = step\n }\n\n const provider = lastStep.model?.providerID\n const previousProvider = previousStep?.model?.providerID\n const key = affinityKey(lastStep.model)\n const previousKey = affinityKey(previousStep?.model)\n\n return {\n endAt: stepEnd(lastStep),\n startAt: stepCreated(settled[0] ?? lastStep),\n promptAt: last.prompt?.time?.created,\n promptGapMs: promptGap(last.prompt, previous?.prompt),\n providerID: provider,\n modelID: lastStep.model?.id,\n variant: lastStep.model?.variant,\n router: undefined,\n identityChanged: false,\n affinityKey: key,\n providerChanged: previousProvider !== undefined && provider !== undefined && previousProvider !== provider,\n modelChanged: previousKey !== undefined && key !== undefined && previousKey !== key && previousProvider === provider,\n totals: totalize(settled),\n latest: totalize([lastStep]),\n session,\n drops,\n cacheDropTokens: drops.reduce((sum, drop) => sum + drop.tokens, 0),\n cacheExpired: drops.length > 0,\n steps: settled.length,\n stepIDs: settled.map((step) => step.id),\n }\n}\n\nfunction promptGap(current: TranscriptMessage | undefined, previous: TranscriptMessage | undefined): number | undefined {\n const now = current?.time?.created\n const before = previous?.time?.created\n if (now === undefined || before === undefined) return undefined\n return Math.max(0, now - before)\n}\n\n/** True when a cache drop is small enough to be endpoint noise. */\nexport function isNoiseDrop(providerID: string | undefined, drop: number): boolean {\n if (providerID === undefined) return false\n return DROP_EXEMPT_PROVIDERS.has(providerID) && drop >= 1024 && drop <= 2048\n}\n\n// ---------------------------------------------------------------------------\n// Formatting\n// ---------------------------------------------------------------------------\n\nexport type LineTone = \"base\" | \"muted\" | \"warning\"\n\nexport type LineSegment = { text: string; tone: LineTone }\n\nexport type LineLabels = {\n delta: string\n turn: string\n session: string\n step: string\n steps: string\n input: string\n cached: string\n output: string\n reasoning: string\n cost: string\n cacheExpired: string\n providerChanged: string\n modelChanged: string\n}\n\n/**\n * How the turn segment counts prompt usage. `latest` shows the prompt size the\n * turn ended with — the context view, and what OpenCode's own turn-token debug\n * line reports. `sum` adds every step of the turn: the billing view, where a\n * tool-call loop re-sends (and re-reads) the same context once per step.\n */\nexport type TurnTotalsView = \"latest\" | \"sum\"\n\n/**\n * Which identity the line leads with. `endpoint` shows the gateway endpoint\n * that served the turn (`orcarouter-dsv4.1flash`) when a recorder learned one,\n * falling back to `provider/model`; `model` always shows `provider/model`.\n * The same provider and model can be routed to different endpoints per request,\n * so the endpoint is the identity a change is measured against.\n */\nexport type LineIdentity = \"endpoint\" | \"model\"\n\nexport type LineFields = {\n clock: boolean\n delta: boolean\n model: boolean\n variant: boolean\n turn: boolean\n session: boolean\n reasoning: boolean\n cost: boolean\n cacheExpiry: boolean\n}\n\nexport const DEFAULT_LABELS: LineLabels = {\n delta: \"Δ\",\n // The turn segment is unlabelled by default to keep the line short; set it\n // back to \"turn\" (or anything else) through `labels.turn`.\n turn: \"\",\n session: \"sess\",\n step: \"step\",\n steps: \"steps\",\n input: \"in\",\n cached: \"cached\",\n output: \"out\",\n reasoning: \"reason\",\n cost: \"cost\",\n cacheExpired: \"cache expired\",\n providerChanged: \"changed\",\n modelChanged: \"model changed\",\n}\n\nexport const DEFAULT_FIELDS: LineFields = {\n clock: true,\n delta: true,\n model: true,\n variant: true,\n turn: true,\n session: true,\n reasoning: false,\n cost: false,\n cacheExpiry: true,\n}\n\nexport function formatClock(ms: number): string {\n const date = new Date(ms)\n const pad = (value: number) => String(value).padStart(2, \"0\")\n return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`\n}\n\nexport function formatDuration(ms: number): string {\n const totalSeconds = Math.max(0, Math.floor(ms / 1000))\n const seconds = totalSeconds % 60\n const minutes = Math.floor(totalSeconds / 60) % 60\n const hours = Math.floor(totalSeconds / 3600) % 24\n const days = Math.floor(totalSeconds / 86400)\n if (days > 0) return `${days}d${hours}h`\n if (hours > 0) return `${hours}h${String(minutes).padStart(2, \"0\")}m`\n if (minutes > 0) return `${minutes}m${String(seconds).padStart(2, \"0\")}s`\n return `${seconds}s`\n}\n\nexport function formatTokens(value: number): string {\n const n = Math.max(0, Math.round(value))\n if (n < 1_000) return String(n)\n if (n < 999_500) {\n const k = n / 1_000\n return `${trimZeros(k.toFixed(k < 10 ? 2 : 1))}k`\n }\n if (n < 999_500_000) {\n const m = n / 1_000_000\n return `${trimZeros(m.toFixed(m < 10 ? 2 : 1))}M`\n }\n return `${Math.round(n / 1_000_000)}M`\n}\n\nfunction trimZeros(value: string): string {\n return value.includes(\".\") ? value.replace(/0+$/, \"\").replace(/\\.$/, \"\") : value\n}\n\nexport function formatCost(value: number): string {\n if (value <= 0) return \"$0\"\n if (value < 1) return `$${value.toFixed(4)}`\n return `$${value.toFixed(2)}`\n}\n\n/**\n * The buckets the line displays, derived from a totals group. `input` is the\n * *uncached* part of the prompt (fresh input plus cache writes, i.e. what the\n * debug line's `new` counts on the prompt side), `prompt` is the whole prompt\n * of the request and `output` is everything the model generated (visible output\n * + reasoning). OpenCode's own debug line reconciles with these:\n * `new = in + out` and `total = prompt + out`, with `prompt = in + cached`.\n * The stored buckets stay raw; only the display folds.\n */\nexport type DisplayTotals = Totals & {\n /** Whole prompt of the request: fresh input + cached reads + cache writes. */\n prompt: number\n}\n\nexport function displayTotals(totals: Totals): DisplayTotals {\n const prompt = totals.input + totals.cached + totals.cacheWrite\n return {\n input: prompt - totals.cached,\n cached: totals.cached,\n cacheWrite: totals.cacheWrite,\n output: totals.output + totals.reasoning,\n reasoning: totals.reasoning,\n cost: totals.cost,\n steps: totals.steps,\n prompt,\n }\n}\n\nfunction totalsText(prefix: string, totals: Totals, labels: LineLabels, fields: LineFields, stepCount = 0): string {\n const display = displayTotals(totals)\n // The parenthesised value is the whole prompt, shown only when it differs\n // from the uncached part the label leads with.\n const input =\n display.prompt === display.input\n ? `${labels.input} ${formatTokens(display.input)}`\n : `${labels.input} ${formatTokens(display.input)} (${formatTokens(display.prompt)})`\n const parts = [\n input,\n `${labels.cached} ${formatTokens(display.cached)}`,\n `${labels.output} ${formatTokens(display.output)}`,\n ]\n if (fields.reasoning) parts.push(`${labels.reasoning} ${formatTokens(display.reasoning)}`)\n if (fields.cost) parts.push(`${labels.cost} ${formatCost(display.cost)}`)\n // An empty label (the default for the turn) leaves the numbers unlabelled; a\n // multi-step turn still says how many requests it took.\n const steps = stepCount > 1 ? `${stepCount} ${stepCount === 1 ? labels.step : labels.steps}` : \"\"\n const head = [prefix, steps].filter((piece) => piece !== \"\").join(\" \")\n const numbers = parts.join(\", \")\n if (head === \"\") return numbers\n return stepCount > 1 ? `${head} · ${numbers}` : `${head} ${numbers}`\n}\n\n/**\n * Render the telemetry line as toned segments. The caller maps tones onto\n * theme colors; `formatLine` joins the segments as plain text.\n */\nexport function formatLineSegments(\n stats: TurnStats,\n options?: { labels?: Partial<LineLabels>; fields?: Partial<LineFields>; turnTotals?: TurnTotalsView; identity?: LineIdentity },\n): LineSegment[] {\n const labels = { ...DEFAULT_LABELS, ...options?.labels }\n const fields = { ...DEFAULT_FIELDS, ...options?.fields }\n const turnTotals = options?.turnTotals ?? \"latest\"\n const identity = options?.identity ?? \"endpoint\"\n const segments: LineSegment[] = []\n\n if (fields.clock && stats.endAt !== undefined) {\n segments.push({ text: formatClock(stats.endAt), tone: \"muted\" })\n }\n if (fields.delta && stats.promptGapMs !== undefined) {\n segments.push({ text: `${labels.delta} ${formatDuration(stats.promptGapMs)}`, tone: \"muted\" })\n }\n if (identity === \"endpoint\" && stats.router !== undefined) {\n // The gateway endpoint is the identity: the same provider and model can be\n // routed to a different endpoint per request, so that is what `changed`\n // reports.\n segments.push({ text: stats.router, tone: stats.identityChanged ? \"warning\" : \"base\" })\n if (stats.identityChanged) segments.push({ text: labels.providerChanged, tone: \"warning\" })\n } else if (stats.providerID !== undefined) {\n let text = stats.providerID\n if (fields.model && stats.modelID !== undefined) text += `/${stats.modelID}`\n if (fields.variant && stats.variant) text += ` ${stats.variant}`\n segments.push({ text, tone: stats.providerChanged ? \"warning\" : \"base\" })\n if (stats.providerChanged) segments.push({ text: labels.providerChanged, tone: \"warning\" })\n else if (stats.modelChanged) segments.push({ text: labels.modelChanged, tone: \"muted\" })\n }\n if (fields.turn && stats.totals.steps > 0) {\n const totals = turnTotals === \"sum\" ? stats.totals : stats.latest\n segments.push({ text: totalsText(labels.turn, totals, labels, fields, turnTotals === \"sum\" ? stats.totals.steps : stats.steps), tone: \"base\" })\n }\n if (fields.session && stats.session) {\n segments.push({ text: totalsText(labels.session, stats.session, labels, fields), tone: \"muted\" })\n }\n if (fields.cacheExpiry && stats.cacheExpired) {\n const count = stats.drops.length > 1 ? `×${stats.drops.length}` : \"\"\n segments.push({ text: `⚠ ${labels.cacheExpired} (−${formatTokens(stats.cacheDropTokens)}${count})`, tone: \"warning\" })\n }\n return segments\n}\n\nexport function formatLine(\n stats: TurnStats,\n options?: { labels?: Partial<LineLabels>; fields?: Partial<LineFields>; turnTotals?: TurnTotalsView; identity?: LineIdentity },\n): string {\n return formatLineSegments(stats, options)\n .map((segment) => segment.text)\n .join(\" · \")\n}\n",
10
- "import type { Database } from \"bun:sqlite\"\nimport { cacheDrop, stepGap, tokensPerSecond, type AssistantMessage } from \"../core\"\n\n/**\n * Read side of the stats engine: every function takes an open database and\n * returns plain rows for the TUI panel, the report script and the NDJSON\n * exports. Aggregations stay in SQL; comparisons and percentiles that would\n * need window functions or custom SQLite builds are computed in JS.\n */\n\n/** Event kind written by the recorder when a compaction completes. */\nexport const COMPACTION_KIND = \"compaction\"\n\n/**\n * Idle gap above which a cache reuse loss is attributed to provider-side\n * expiry rather than to a mid-session event. Heuristic: the empirical TTL\n * evidence view narrows this down with real observations.\n */\nexport const DEFAULT_TTL_HINT_MS = 10 * 60 * 1000\n\nexport type Range = {\n from?: number | undefined\n to?: number | undefined\n}\n\n/**\n * Panel filters. They narrow every aggregation to one model, one endpoint or\n * one session, and are applied with the same `WHERE` builder the range uses so\n * a filtered view and a drill-down agree.\n */\nexport type Filters = {\n /** Model id, variant excluded. */\n modelID?: string | undefined\n /**\n * One model *and* its variant, as `modelKey` builds it (`m1#max`). A variant\n * is a different model to the provider, so a drill-down into one variant must\n * not pick up its family.\n */\n modelKey?: string | undefined\n /**\n * The routing endpoint that served the request (`router`). It is the one\n * identity that answers \"who ran this inference\": the provider you configured\n * says where the request came from, which is evidence, not an answer.\n */\n router?: string | undefined\n /**\n * One session, exactly. Used by the `session` range and by the overview's\n * \"this session\" block, which must not pick up the steps of every other\n * session that happened to run after it started.\n */\n sessionID?: string | undefined\n}\n\nexport type Usage = {\n steps: number\n input: number\n cached: number\n cacheWrite: number\n output: number\n reasoning: number\n cost: number\n}\n\ntype UsageRow = {\n steps: number | null\n input: number | null\n cached: number | null\n cacheWrite: number | null\n output: number | null\n reasoning: number | null\n cost: number | null\n}\n\nexport const STEP_TIME = \"COALESCE(created_at, recorded_at)\"\n\n/**\n * The identity of a model *and* its variant, in SQL: `modelKey` says a variant\n * is a different model to the provider, so a variant never shares a row with\n * its family. The JS twin is `modelKey` below; both have to agree.\n */\nexport const MODEL_KEY_SQL = \"COALESCE(model_id, '(unknown)') || '#' || COALESCE(variant, '')\"\n\n/** `modelKey(modelID, variant)`: the family, with the variant as a suffix. */\nexport function modelKey(modelID: string, variant: string | undefined): string {\n return `${modelID}#${variant ?? \"\"}`\n}\n\n/** Human label of a model key or a model id and variant. */\nexport function modelLabel(modelID: string, variant: string | undefined): string {\n return variant === undefined ? modelID : `${modelID} ${variant}`\n}\nconst USAGE_SELECT = `COUNT(*) AS steps,\n COALESCE(SUM(input_tokens), 0) AS input,\n COALESCE(SUM(cache_read), 0) AS cached,\n COALESCE(SUM(cache_write), 0) AS cacheWrite,\n COALESCE(SUM(output_tokens), 0) AS output,\n COALESCE(SUM(reasoning_tokens), 0) AS reasoning,\n COALESCE(SUM(cost), 0) AS cost`\n\nfunction usageFrom(row: UsageRow): Usage {\n return {\n steps: row.steps ?? 0,\n input: row.input ?? 0,\n cached: row.cached ?? 0,\n cacheWrite: row.cacheWrite ?? 0,\n output: row.output ?? 0,\n reasoning: row.reasoning ?? 0,\n cost: row.cost ?? 0,\n }\n}\n\nfunction rangeWhere(range: Range | undefined, column = STEP_TIME): { where: string; params: number[] } {\n const clauses: string[] = []\n const params: number[] = []\n if (range?.from !== undefined) {\n clauses.push(`${column} >= ?`)\n params.push(range.from)\n }\n if (range?.to !== undefined) {\n clauses.push(`${column} < ?`)\n params.push(range.to)\n }\n return { where: clauses.length > 0 ? `WHERE ${clauses.join(\" AND \")}` : \"\", params }\n}\n\n/**\n * `WHERE` for a range plus the panel filters. Shared with the analysis layer so\n * a filtered view and a drill-down agree on what is in scope.\n */\nexport function buildWhere(\n range: Range | undefined,\n filters?: Filters | undefined,\n column = STEP_TIME,\n): { where: string; params: (string | number)[] } {\n const rangeClause = rangeWhere(range, column)\n const clauses = rangeClause.where === \"\" ? [] : [rangeClause.where.slice(\"WHERE \".length)]\n const params: (string | number)[] = [...rangeClause.params]\n if (filters?.modelID !== undefined && filters.modelID !== \"\") {\n clauses.push(\"model_id = ?\")\n params.push(filters.modelID)\n }\n if (filters?.modelKey !== undefined && filters.modelKey !== \"\") {\n clauses.push(`${MODEL_KEY_SQL} = ?`)\n params.push(filters.modelKey)\n }\n if (filters?.router !== undefined && filters.router !== \"\") {\n clauses.push(\"router = ?\")\n params.push(filters.router)\n }\n if (filters?.sessionID !== undefined && filters.sessionID !== \"\") {\n clauses.push(\"session_id = ?\")\n params.push(filters.sessionID)\n }\n return { where: clauses.length > 0 ? `WHERE ${clauses.join(\" AND \")}` : \"\", params }\n}\n\n/**\n * Share of the whole prompt that came from the cache. The denominator is what\n * the provider saw: cache reads, fresh input and cache writes.\n */\nexport function cacheHitRatio(usage: Pick<Usage, \"input\" | \"cached\" | \"cacheWrite\">): number | undefined {\n const total = usage.cached + usage.input + usage.cacheWrite\n return total > 0 ? usage.cached / total : undefined\n}\n\nexport type Overview = Usage & {\n sessions: number\n prompts: number\n firstAt: number | undefined\n lastAt: number | undefined\n cacheHitRatio: number | undefined\n /**\n * Tokens generated per second of streaming, over the whole selection: the\n * produced tokens divided by the summed streaming windows. A weighted average,\n * not the mean of the per-step rates the latency view reports as p50.\n */\n tps: number | undefined\n}\n\nexport function overview(db: Database, range?: Range, filters?: Filters): Overview {\n const steps = buildWhere(range, filters)\n const row = db\n .query(\n `SELECT ${USAGE_SELECT},\n COUNT(DISTINCT session_id) AS sessions,\n MIN(${STEP_TIME}) AS firstAt,\n MAX(COALESCE(completed_at, ${STEP_TIME})) AS lastAt,\n COALESCE(SUM(CASE WHEN streamed_at > created_at THEN output_tokens + reasoning_tokens ELSE 0 END), 0) AS produced,\n COALESCE(SUM(CASE WHEN streamed_at > created_at THEN streamed_at - created_at ELSE 0 END), 0) AS window\n FROM steps ${steps.where}`,\n )\n .get(...steps.params) as\n | (UsageRow & { sessions: number | null; firstAt: number | null; lastAt: number | null; produced: number | null; window: number | null })\n | null\n\n const prompts = rangeWhere(range, \"created_at\")\n const promptRow = db.query(`SELECT COUNT(*) AS prompts FROM prompts ${prompts.where}`).get(...prompts.params) as { prompts: number } | null\n\n const usage = usageFrom(row ?? { steps: 0, input: 0, cached: 0, cacheWrite: 0, output: 0, reasoning: 0, cost: 0 })\n return {\n ...usage,\n sessions: row?.sessions ?? 0,\n prompts: promptRow?.prompts ?? 0,\n firstAt: row?.firstAt ?? undefined,\n lastAt: row?.lastAt ?? undefined,\n cacheHitRatio: cacheHitRatio(usage),\n tps: row === null ? undefined : tokensPerSecond(row.produced ?? 0, 0, row.window ?? 0),\n }\n}\n\nexport type ProviderUsage = Usage & {\n providerID: string\n /** Gateway routing endpoints that served the requests. */\n routers: string[]\n firstAt: number | undefined\n lastAt: number | undefined\n}\n\nexport type ModelUsage = {\n modelID: string\n variant: string | undefined\n providers: ProviderUsage[]\n totals: Usage\n}\n\ntype ModelRow = UsageRow & {\n provider_id: string | null\n model_id: string | null\n variant: string | null\n router_list: string | null\n first_at: number | null\n last_at: number | null\n}\n\n/**\n * Usage per model, each with the providers that served it. This is the view\n * that answers \"which inference providers is this model actually coming from\n * over the selected period\".\n */\nexport function models(db: Database, range?: Range, filters?: Filters): ModelUsage[] {\n const filter = buildWhere(range, filters)\n const rows = db\n .query(\n `SELECT provider_id, model_id, variant,\n GROUP_CONCAT(DISTINCT router) AS router_list,\n ${USAGE_SELECT},\n MIN(${STEP_TIME}) AS first_at,\n MAX(COALESCE(completed_at, recorded_at)) AS last_at\n FROM steps ${filter.where}\n GROUP BY provider_id, model_id, variant\n ORDER BY steps DESC, cached DESC`,\n )\n .all(...filter.params) as ModelRow[]\n\n const models = new Map<string, ModelUsage>()\n for (const row of rows) {\n const modelID = row.model_id ?? \"(unknown)\"\n const variant = row.variant ?? undefined\n const key = `${modelID}#${variant ?? \"\"}`\n let model = models.get(key)\n if (!model) {\n model = { modelID, variant, providers: [], totals: emptyUsage() }\n models.set(key, model)\n }\n const usage = usageFrom(row)\n model.providers.push({\n ...usage,\n providerID: row.provider_id ?? \"(unknown)\",\n routers: row.router_list ? row.router_list.split(\",\").filter(Boolean) : [],\n firstAt: row.first_at ?? undefined,\n lastAt: row.last_at ?? undefined,\n })\n addUsage(model.totals, usage)\n }\n return [...models.values()].sort((a, b) => billedTokens(b.totals) - billedTokens(a.totals))\n}\n\nfunction emptyUsage(): Usage {\n return { steps: 0, input: 0, cached: 0, cacheWrite: 0, output: 0, reasoning: 0, cost: 0 }\n}\n\nfunction addUsage(target: Usage, source: Usage): void {\n target.steps += source.steps\n target.input += source.input\n target.cached += source.cached\n target.cacheWrite += source.cacheWrite\n target.output += source.output\n target.reasoning += source.reasoning\n target.cost += source.cost\n}\n\nfunction billedTokens(usage: Pick<Usage, \"input\" | \"cached\" | \"output\">): number {\n return usage.input + usage.cached + usage.output\n}\n\nexport type TimelineBucket = { at: number } & Usage\n\n/** Usage aggregated into fixed time buckets (for example one hour or day). */\nexport function timeline(db: Database, range: Range | undefined, bucketMs: number): TimelineBucket[] {\n const filter = rangeWhere(range)\n const rows = db\n .query(\n `SELECT CAST(${STEP_TIME} / ? AS INTEGER) * ? AS bucket, ${USAGE_SELECT}\n FROM steps ${filter.where}\n GROUP BY bucket\n ORDER BY bucket`,\n )\n .all(bucketMs, bucketMs, ...filter.params) as (UsageRow & { bucket: number })[]\n return rows.map((row) => ({ at: row.bucket, ...usageFrom(row) }))\n}\n\nexport type DropReason = \"compaction\" | \"expired\" | \"unknown\"\n\nexport type CacheDropRow = {\n sessionID: string\n messageID: string\n providerID: string | undefined\n modelID: string | undefined\n variant: string | undefined\n at: number\n tokens: number\n gapMs: number | undefined\n prevCached: number\n reason: DropReason\n}\n\nexport type TtlEvidenceRow = {\n providerID: string\n modelID: string\n samples: number\n /** Largest idle gap that still hit the cache. */\n maxHitGapMs: number | undefined\n /** Smallest idle gap after which reuse was lost. */\n minDropGapMs: number | undefined\n}\n\nexport type LatencyStats = {\n count: number\n p50: number | undefined\n p90: number | undefined\n p99: number | undefined\n}\n\nexport type LatencyReport = {\n /** Milliseconds from request to completed response. */\n wall: LatencyStats\n /** Milliseconds from request to the last streamed chunk (the streaming window). */\n stream: LatencyStats\n /** Output-plus-reasoning tokens per second over that window. */\n tps: LatencyStats\n}\n\n/** Nearest-rank percentile over an unsorted sample. */\nexport function percentile(values: readonly number[], p: number): number | undefined {\n if (values.length === 0) return undefined\n const sorted = [...values].sort((a, b) => a - b)\n const rank = Math.ceil((p / 100) * sorted.length)\n const index = Math.min(sorted.length - 1, Math.max(0, rank - 1))\n return sorted[index]\n}\n\nexport function latencyStats(values: number[]): LatencyStats {\n return {\n count: values.length,\n p50: percentile(values, 50),\n p90: percentile(values, 90),\n p99: percentile(values, 99),\n }\n}\n\nconst statsFor = latencyStats\n\nexport type SessionUsage = Usage & {\n sessionID: string\n parentID: string | undefined\n title: string | undefined\n directory: string | undefined\n providers: string[]\n models: string[]\n prompts: number\n firstAt: number | undefined\n lastAt: number | undefined\n cacheHitRatio: number | undefined\n}\n\nexport function sessions(\n db: Database,\n options?: { range?: Range; filters?: Filters; limit?: number; sessionIDs?: readonly string[] },\n): SessionUsage[] {\n if (options?.sessionIDs !== undefined && options.sessionIDs.length === 0) return []\n const range = options?.range\n const filter = buildWhere(range, options?.filters, \"COALESCE(st.created_at, st.recorded_at)\")\n const limit = options?.limit ?? 20\n const clauses = filter.where === \"\" ? [] : [filter.where.slice(\"WHERE \".length)]\n const params: (number | string)[] = [...filter.params]\n if (options?.sessionIDs !== undefined) {\n // A specific set of sessions (the subagents of one session): the group by\n // then reads one index range per session instead of every step recorded.\n const ids = options.sessionIDs.slice(0, 500)\n clauses.push(`st.session_id IN (${ids.map(() => \"?\").join(\", \")})`)\n params.push(...ids)\n }\n const rows = db\n .query(\n `SELECT st.session_id, s.title, s.directory, s.parent_id,\n GROUP_CONCAT(DISTINCT st.provider_id) AS providers,\n GROUP_CONCAT(DISTINCT st.model_id) AS models,\n (SELECT COUNT(*) FROM prompts p WHERE p.session_id = st.session_id) AS prompts,\n COUNT(*) AS steps,\n COALESCE(SUM(st.input_tokens), 0) AS input,\n COALESCE(SUM(st.cache_read), 0) AS cached,\n COALESCE(SUM(st.cache_write), 0) AS cacheWrite,\n COALESCE(SUM(st.output_tokens), 0) AS output,\n COALESCE(SUM(st.reasoning_tokens), 0) AS reasoning,\n COALESCE(SUM(st.cost), 0) AS cost,\n MIN(COALESCE(st.created_at, st.recorded_at)) AS first_at,\n MAX(COALESCE(st.completed_at, st.created_at, st.recorded_at)) AS last_at\n FROM steps st\n LEFT JOIN sessions s ON s.session_id = st.session_id\n ${clauses.length > 0 ? `WHERE ${clauses.join(\" AND \")}` : \"\"}\n GROUP BY st.session_id\n ORDER BY last_at DESC\n LIMIT ?`,\n )\n .all(\n ...params,\n limit,\n ) as (UsageRow & {\n session_id: string\n title: string | null\n directory: string | null\n parent_id: string | null\n providers: string | null\n models: string | null\n prompts: number | null\n first_at: number | null\n last_at: number | null\n })[]\n\n return rows.map((row) => {\n const usage = usageFrom(row)\n return {\n ...usage,\n sessionID: row.session_id,\n parentID: row.parent_id ?? undefined,\n title: row.title ?? undefined,\n directory: row.directory ?? undefined,\n providers: row.providers ? row.providers.split(\",\").filter(Boolean) : [],\n models: row.models ? row.models.split(\",\").filter(Boolean) : [],\n prompts: row.prompts ?? 0,\n firstAt: row.first_at ?? undefined,\n lastAt: row.last_at ?? undefined,\n cacheHitRatio: cacheHitRatio(usage),\n }\n })\n}\n\n/** Sessions whose parent is `parentID`: the subagents of one session. */\nexport function childSessionIDs(db: Database, parentID: string): string[] {\n return (db.query(\"SELECT session_id FROM sessions WHERE parent_id = ? ORDER BY session_id\").all(parentID) as { session_id: string }[]).map(\n (row) => row.session_id,\n )\n}\n\n/** Session usage rows for a specific set of sessions (subagent rollups). */\nexport function sessionUsageOf(db: Database, sessionIDs: readonly string[]): SessionUsage[] {\n return sessions(db, { sessionIDs, limit: sessionIDs.length })\n}\n\nexport type TurnDrop = {\n messageID: string\n tokens: number\n gapMs: number | undefined\n}\n\n/**\n * One finished turn of a session, built from the recorded steps and prompts. It\n * carries the same buckets the telemetry line uses, so the panel can render the\n * per-prompt history with the line's own formatter.\n */\nexport type TurnUsage = {\n sessionID: string\n /** Creation time of the prompt that opened the turn, when it was recorded. */\n promptAt: number | undefined\n endAt: number | undefined\n /** Time between the previous prompt of the session and this turn's prompt. */\n promptGapMs: number | undefined\n providerID: string | undefined\n modelID: string | undefined\n variant: string | undefined\n router: string | undefined\n /** The displayed identity differs from the previous turn of the session. */\n routerChanged: boolean\n providerChanged: boolean\n modelChanged: boolean\n steps: number\n /** Raw buckets of the last step: the prompt the turn ended with. */\n latest: Usage\n /** Raw buckets summed over every step of the turn. */\n totals: Usage\n drops: TurnDrop[]\n /** Finish reason of the turn's last step. */\n finish: string | undefined\n /**\n * Tokens generated per second of streaming over the turn's steps: produced\n * tokens divided by their summed streaming windows. For a one-step turn it is\n * that step's rate.\n */\n tps: number | undefined\n}\n\ntype TurnStepRow = {\n message_id: string\n session_id: string\n provider_id: string | null\n model_id: string | null\n variant: string | null\n input_tokens: number | null\n cache_read: number | null\n cache_write: number | null\n output_tokens: number | null\n reasoning_tokens: number | null\n cost: number | null\n created_at: number | null\n streamed_at: number | null\n recorded_at: number\n completed_at: number | null\n finish: string | null\n router: string | null\n}\n\n/** A turn is finished when its last step ended without asking for more tools. */\nfunction turnFinished(finish: string | null): boolean {\n return finish !== null && finish !== \"\" && finish !== \"tool-calls\"\n}\n\nfunction stepUsage(row: TurnStepRow): Usage {\n return {\n steps: 1,\n input: row.input_tokens ?? 0,\n cached: row.cache_read ?? 0,\n cacheWrite: row.cache_write ?? 0,\n output: row.output_tokens ?? 0,\n reasoning: row.reasoning_tokens ?? 0,\n cost: row.cost ?? 0,\n }\n}\n\n/** Structural view of a recorded step, so the shared cache-drop rule applies. */\nfunction stepMessage(row: TurnStepRow): AssistantMessage {\n return {\n type: \"assistant\",\n id: row.message_id,\n time: { created: row.created_at ?? row.recorded_at, completed: row.completed_at ?? undefined },\n model:\n row.provider_id !== null && row.model_id !== null\n ? { providerID: row.provider_id, id: row.model_id, variant: row.variant ?? undefined }\n : undefined,\n tokens: {\n input: row.input_tokens ?? 0,\n output: row.output_tokens ?? 0,\n reasoning: row.reasoning_tokens ?? 0,\n cache: { read: row.cache_read ?? 0, write: row.cache_write ?? 0 },\n },\n finish: row.finish ?? undefined,\n }\n}\n\n/**\n * Finished turns, newest first. Steps belong to the newest prompt recorded at or\n * before them; a turn whose last step asked for tools is dropped, because it is\n * still running — the same rule the telemetry line follows.\n *\n * For one session the read is bounded by a window: only the steps at or after\n * the `limit`-th newest prompt are loaded, so a session with tens of thousands\n * of steps costs the same as a short one. The extra prompt is kept as the\n * reference the oldest returned turn measures its gap against.\n */\nexport function turns(\n db: Database,\n options?: { sessionID?: string | undefined; range?: Range | undefined; filters?: Filters | undefined; limit?: number | undefined },\n): TurnUsage[] {\n const limit = options?.limit ?? 50\n const filter = buildWhere(options?.range, options?.filters)\n const clauses = filter.where === \"\" ? [] : [filter.where.slice(\"WHERE \".length)]\n const params: (number | string)[] = [...filter.params]\n if (options?.sessionID !== undefined) {\n clauses.push(\"session_id = ?\")\n params.push(options.sessionID)\n }\n const floor = options?.sessionID === undefined ? undefined : promptFloor(db, options.sessionID, limit + 1)\n if (floor !== undefined) {\n clauses.push(`${STEP_TIME} >= ?`)\n params.push(floor)\n }\n const steps = db\n .query(\n `SELECT message_id, session_id, provider_id, model_id, variant,\n input_tokens, cache_read, cache_write, output_tokens, reasoning_tokens, cost,\n created_at, streamed_at, recorded_at, completed_at, finish, router\n FROM steps ${clauses.length > 0 ? `WHERE ${clauses.join(\" AND \")}` : \"\"}\n ORDER BY session_id, ${STEP_TIME}`,\n )\n .all(...params) as TurnStepRow[]\n\n const promptClause = options?.sessionID === undefined ? \"\" : `WHERE session_id = ?${floor === undefined ? \"\" : \" AND created_at >= ?\"}`\n const promptParams = options?.sessionID === undefined ? [] : floor === undefined ? [options.sessionID] : [options.sessionID, floor]\n const promptRows = db\n .query(`SELECT session_id, created_at FROM prompts ${promptClause} ORDER BY session_id, created_at`)\n .all(...promptParams) as { session_id: string; created_at: number | null }[]\n\n const promptsBySession = new Map<string, number[]>()\n for (const row of promptRows) {\n if (row.created_at === null) continue\n const list = promptsBySession.get(row.session_id) ?? []\n list.push(row.created_at)\n promptsBySession.set(row.session_id, list)\n }\n\n const result: TurnUsage[] = []\n let sessionID: string | undefined\n let bucket: TurnStepRow[] = []\n let promptIndex = -2\n let promptAt: number | undefined\n let previousPromptAt: number | undefined\n\n function close(): void {\n const last = bucket[bucket.length - 1]\n if (last === undefined || !turnFinished(last.finish)) {\n bucket = []\n return\n }\n const totals = emptyUsage()\n for (const row of bucket) addUsage(totals, stepUsage(row))\n const drops: TurnDrop[] = []\n let reference: AssistantMessage | undefined\n for (const row of bucket) {\n const message = stepMessage(row)\n const drop = cacheDrop(reference, message)\n if (drop !== undefined) {\n drops.push({ messageID: row.message_id, tokens: drop, gapMs: stepGap(reference, message) })\n }\n reference = message\n }\n const lastWith = <Value>(pick: (row: TurnStepRow) => Value | null): Value | undefined => {\n for (let index = bucket.length - 1; index >= 0; index -= 1) {\n const value = pick(bucket[index]!)\n if (value !== null) return value\n }\n return undefined\n }\n const providerID = last.provider_id ?? undefined\n const modelID = last.model_id ?? undefined\n const variant = last.variant ?? undefined\n const router = lastWith((row) => row.router)\n let produced = 0\n let window = 0\n for (const row of bucket) {\n if (row.created_at === null || row.streamed_at === null || row.streamed_at <= row.created_at) continue\n produced += (row.output_tokens ?? 0) + (row.reasoning_tokens ?? 0)\n window += row.streamed_at - row.created_at\n }\n const previous = result.length > 0 && result[result.length - 1]!.sessionID === last.session_id ? result[result.length - 1] : undefined\n result.push({\n sessionID: last.session_id,\n promptAt,\n endAt: last.completed_at ?? last.created_at ?? last.recorded_at,\n promptGapMs: promptAt !== undefined && previousPromptAt !== undefined ? Math.max(0, promptAt - previousPromptAt) : undefined,\n providerID,\n modelID,\n variant,\n router,\n routerChanged: previous?.router !== undefined && router !== undefined && previous.router !== router,\n providerChanged: previous?.providerID !== undefined && providerID !== undefined && previous.providerID !== providerID,\n modelChanged:\n previous !== undefined &&\n previous.providerID === providerID &&\n `${previous.modelID ?? \"\"}#${previous.variant ?? \"\"}` !== `${modelID ?? \"\"}#${variant ?? \"\"}`,\n steps: bucket.length,\n latest: stepUsage(last),\n totals,\n drops,\n finish: last.finish ?? undefined,\n tps: tokensPerSecond(produced, 0, window),\n })\n bucket = []\n }\n\n for (const row of steps) {\n if (row.session_id !== sessionID) {\n close()\n sessionID = row.session_id\n promptIndex = -2\n promptAt = undefined\n previousPromptAt = undefined\n }\n const time = row.created_at ?? row.recorded_at\n const prompts = promptsBySession.get(row.session_id) ?? []\n let index = -1\n for (let candidate = prompts.length - 1; candidate >= 0; candidate -= 1) {\n if ((prompts[candidate] ?? Number.POSITIVE_INFINITY) <= time) {\n index = candidate\n break\n }\n }\n if (index !== promptIndex) {\n close()\n promptIndex = index\n promptAt = index >= 0 ? prompts[index] : undefined\n previousPromptAt = index >= 1 ? prompts[index - 1] : undefined\n }\n bucket.push(row)\n }\n close()\n\n return result.reverse().slice(0, limit)\n}\n\n/**\n * Creation time of the `count`-th newest prompt of a session, or undefined\n * when it has fewer. Used to bound the steps a `turns` read has to load.\n */\nfunction promptFloor(db: Database, sessionID: string, count: number): number | undefined {\n const rows = db\n .query(\"SELECT created_at FROM prompts WHERE session_id = ? AND created_at IS NOT NULL ORDER BY created_at DESC LIMIT ?\")\n .all(sessionID, count) as { created_at: number }[]\n return rows.length === count ? rows[count - 1]!.created_at : undefined\n}\n\n/**\n * Routing endpoint of the newest step recorded before `at`. The TUI compares\n * it with the endpoint of the turn it is about to render, because the same\n * provider and model can be routed to a different endpoint per request.\n */\nexport function routerBefore(db: Database, sessionID: string, at: number | undefined): string | undefined {\n if (at === undefined) return undefined\n const row = db\n .query(\n `SELECT router FROM steps\n WHERE session_id = $sessionID AND router IS NOT NULL AND COALESCE(created_at, recorded_at) < $at\n ORDER BY COALESCE(created_at, recorded_at) DESC LIMIT 1`,\n )\n .get({ $sessionID: sessionID, $at: at }) as { router: string | null } | null\n return row?.router ?? undefined\n}\n\n/**\n * Routing endpoint of one turn: the newest endpoint the recorder stored for\n * the given steps of a session. The TUI reads this for the telemetry line\n * because the line renders from the TUI process while the recorder (the server\n * process) is still writing the step that just finished. Scoped to the turn's\n * own steps on purpose: the same provider and model can be routed to a\n * different endpoint per request, so the newest endpoint of the session is not\n * necessarily the one that served the turn being rendered.\n */\nexport function routerForSteps(db: Database, sessionID: string, messageIDs: readonly string[]): string | undefined {\n if (messageIDs.length === 0) return undefined\n const placeholders = messageIDs.map(() => \"?\").join(\", \")\n const row = db\n .query(\n `SELECT router FROM steps\n WHERE session_id = ? AND message_id IN (${placeholders}) AND router IS NOT NULL\n ORDER BY recorded_at DESC LIMIT 1`,\n )\n .get(sessionID, ...messageIDs) as { router: string | null } | null\n return row?.router ?? undefined\n}\n\n// ---------------------------------------------------------------------------\n// Filter pickers\n// ---------------------------------------------------------------------------\n\nexport type FilterOption = { value: string; steps: number }\n\n/** Distinct models and endpoints, most used first. */\nexport function filterOptions(db: Database): { models: FilterOption[]; endpoints: FilterOption[] } {\n const pick = (column: string): FilterOption[] =>\n (\n db\n .query(`SELECT ${column} AS value, COUNT(*) AS steps FROM steps WHERE ${column} IS NOT NULL GROUP BY ${column} ORDER BY steps DESC`)\n .all() as { value: string; steps: number }[]\n ).map((row) => ({ value: row.value, steps: row.steps }))\n return { models: pick(\"model_id\"), endpoints: pick(\"router\") }\n}\n",
11
- "/**\n * Endpoint discovery.\n *\n * OpenCode routes a model through a provider, and the endpoint the request was\n * actually sent to is the identity that answers \"who ran this inference\": a\n * gateway names its own routing endpoint in the response headers, and a\n * per-operator provider is named by the host of its API. The recorder stores\n * that endpoint on the step and nothing else about the route — the operator a\n * header happens to mention is not the endpoint, so it is not kept.\n *\n * Everything here is pure so the discovery can be tested without a network\n * stack.\n */\n\nimport { normalizeToken } from \"./text\"\n\n/** Header a gateway sets to name the routing endpoint that served the request. */\nexport const ENDPOINT_HEADER = \"x-opencode-endpoint-id\" as const\n\n/** Longest endpoint id kept: a routing label, not a sentence. */\nexport const ENDPOINT_MAX = 64\n\n/**\n * Request hosts that name the operator behind the request.\n *\n * A gateway host is deliberately absent: `opencode.ai`, `openrouter.ai` and\n * `vercel-ai-gateway` are routers, not the platform that ran the model, and\n * treating them as the endpoint made every request served without an endpoint\n * header read as \"opencode\" — the provider's name wearing the endpoint's\n * clothes. When the gateway does not name its downstream endpoint, the endpoint\n * is unknown, and the views say so.\n */\nconst KNOWN_ENDPOINT_HOSTS: ReadonlyArray<readonly [string, string]> = [\n [\"api.fireworks.ai\", \"fireworks\"],\n [\"api.deepseek.com\", \"deepseek\"],\n [\"api.together.xyz\", \"together\"],\n [\"api.groq.com\", \"groq\"],\n [\"api.cerebras.ai\", \"cerebras\"],\n [\"api.sambanova.ai\", \"sambanova\"],\n [\"api.hyperbolic.xyz\", \"hyperbolic\"],\n [\"api.deepinfra.com\", \"deepinfra\"],\n [\"api.baseten.co\", \"baseten\"],\n [\"api.novita.ai\", \"novita\"],\n [\"api.parasail.io\", \"parasail\"],\n [\"api.moonshot.ai\", \"moonshot\"],\n [\"api.moonshot.cn\", \"moonshot\"],\n [\"api.z.ai\", \"zhipu\"],\n [\"open.bigmodel.cn\", \"zhipu\"],\n [\"api.minimax.chat\", \"minimax\"],\n [\"dashscope.aliyuncs.com\", \"alibaba\"],\n [\"api.x.ai\", \"xai\"],\n [\"api.anthropic.com\", \"anthropic\"],\n [\"api.openai.com\", \"openai\"],\n [\"openai.azure.com\", \"azure\"],\n [\"bedrock-runtime.*.amazonaws.com\", \"bedrock\"],\n [\"generativelanguage.googleapis.com\", \"google\"],\n [\"aiplatform.googleapis.com\", \"vertex\"],\n [\"api.nvidia.com\", \"nvidia\"],\n [\"api.mistral.ai\", \"mistral\"],\n [\"api.cloudflare.com\", \"cloudflare\"],\n]\n\nexport type EndpointInfo = {\n /** Routing endpoint or host that served the request. */\n router?: string | undefined\n /** Where the finding came from: `header:<name>` or `endpoint:<host>`. */\n source?: string | undefined\n}\n\nfunction readHeader(headers: Headers | Record<string, string | undefined>, name: string): string | undefined {\n const get = (headers as Headers).get\n if (typeof get === \"function\") {\n const value = get.call(headers, name)\n return value === null || value === \"\" ? undefined : value\n }\n for (const key of Object.keys(headers)) {\n if (key.toLowerCase() !== name) continue\n const value = (headers as Record<string, string | undefined>)[key]\n return value ? value : undefined\n }\n return undefined\n}\n\n/**\n * Endpoint the gateway named in the response headers, when it named one.\n *\n * The value is a routing label, so it is validated as a token and lowercased:\n * a gateway that changes the case between responses would otherwise read as a\n * different endpoint and split its own history in two. Anything that is not a\n * token (control characters, spaces, an essay) is not an endpoint id.\n */\nexport function inferFromHeaders(headers: Headers | Record<string, string | undefined> | undefined): EndpointInfo | undefined {\n if (!headers) return undefined\n const value = readHeader(headers, ENDPOINT_HEADER)\n if (value === undefined) return undefined\n const router = normalizeToken(value, ENDPOINT_MAX)\n if (router === undefined) return undefined\n return { router, source: `header:${ENDPOINT_HEADER}` }\n}\n\n/**\n * True when `host` is `suffix`, a subdomain of it (`cdn.opencode.ai`), or a\n * regional host that carries it as a label tail (`us-central1-aiplatform.googleapis.com`).\n * The boundary has to fall on a dot or a dash: a host that merely contains a\n * known domain (`api.anthropic.com.evil.tld`) names nothing.\n *\n * A `*` in the pattern stands for one label, for the hosts whose region sits\n * in the middle (`bedrock-runtime.*.amazonaws.com`).\n */\nexport function hostMatches(host: string, pattern: string): boolean {\n const hostLabels = host.split(\".\")\n const patternLabels = pattern.split(\".\")\n if (patternLabels.includes(\"*\")) {\n if (hostLabels.length !== patternLabels.length) return false\n return patternLabels.every((label, index) => label === \"*\" || label === hostLabels[index])\n }\n if (host === pattern) return true\n if (host.endsWith(`.${pattern}`)) return true\n return host.endsWith(`-${pattern}`)\n}\n\n/** Endpoint named by the host of the request URL. */\nexport function inferFromUrl(url: string | undefined): EndpointInfo | undefined {\n if (url === undefined) return undefined\n let host: string\n try {\n host = new URL(url).hostname.toLowerCase()\n } catch {\n return undefined\n }\n if (host === \"\") return undefined\n for (const [suffix, router] of KNOWN_ENDPOINT_HOSTS) {\n if (hostMatches(host, suffix)) return { router, source: `endpoint:${host}` }\n }\n return undefined\n}\n\n/** First candidate that named an endpoint, headers before the URL. */\nexport function mergeEndpoint(...candidates: readonly (EndpointInfo | undefined)[]): EndpointInfo | undefined {\n const merged: EndpointInfo = {}\n for (const candidate of candidates) {\n if (candidate === undefined) continue\n if (merged.router === undefined && candidate.router !== undefined) merged.router = candidate.router\n if (merged.source === undefined && candidate.source !== undefined) merged.source = candidate.source\n }\n return merged.router === undefined ? undefined : merged\n}\n",
12
- "import type { Database } from \"bun:sqlite\"\nimport { recordEvent, recordPrompt, recordStep, setStepRouterForSessionModel, upsertSession } from \"./stats/db\"\nimport { COMPACTION_KIND } from \"./stats/queries\"\nimport { inferFromHeaders, inferFromUrl, mergeEndpoint, type EndpointInfo } from \"./endpoint\"\n\n/**\n * Event-driven recorder. Every handler is idempotent and defensive: the\n * plugin must never break a session because an event arrived in an\n * unexpected shape, so unknown events are ignored and malformed payloads are\n * dropped rather than thrown.\n *\n * Pure with respect to its inputs (database plus events) so the whole\n * recording policy can be unit-tested without the plugin runtime.\n */\n\nexport type SessionModelRef = {\n id: string\n providerID: string\n variant?: string | undefined\n}\n\nexport type TokenUsageLike = {\n input?: number | undefined\n output?: number | undefined\n reasoning?: number | undefined\n cache?: { read?: number | undefined; write?: number | undefined } | undefined\n}\n\nexport type RecorderEventData = {\n sessionID?: string | undefined\n assistantMessageID?: string | undefined\n inboxID?: string | undefined\n item?: { type?: string | undefined } | undefined\n started?: number | undefined\n model?: SessionModelRef | undefined\n previous?: SessionModelRef | undefined\n tokens?: TokenUsageLike | undefined\n cost?: number | undefined\n finish?: string | undefined\n rawFinish?: string | undefined\n reason?: string | undefined\n location?: { directory?: string | undefined } | undefined\n parentID?: string | undefined\n title?: string | undefined\n}\n\nexport type RecorderEvent = {\n type: string\n created?: number | undefined\n data?: RecorderEventData | undefined\n}\n\nexport type HttpResponseInput = {\n sessionID?: string | undefined\n kind?: string | undefined\n model?: SessionModelRef | undefined\n request?: { method?: string | undefined; url?: string | undefined } | undefined\n response?: { status?: number | undefined; headers?: Headers | Record<string, string | undefined> | undefined } | undefined\n}\n\nexport type PendingStep = {\n messageID: string\n sessionID: string\n started: number | undefined\n streamed: number | undefined\n model: SessionModelRef | undefined\n}\n\nexport type RecorderOptions = {\n /** Clock override, for tests. */\n now?: (() => number) | undefined\n}\n\nexport type Recorder = {\n /** Apply one session event. Unknown or malformed events are ignored. */\n handleEvent(event: RecorderEvent): void\n /** Learn the routing endpoint from one model response. */\n handleHttpResponse(event: HttpResponseInput): void\n pendingFor(sessionID: string): PendingStep | undefined\n}\n\nexport function createRecorder(db: Database, options?: RecorderOptions): Recorder {\n /**\n * Ceiling on the per-session maps. A session that ends takes its entries\n * with it; a host running for weeks has thousands of sessions, and a map that\n * only ever grows is a leak with a nice name.\n */\n const MAX_TRACKED_SESSIONS = 512\n /** Ceiling on in-flight steps; a step that never ends is a leak too. */\n const MAX_PENDING = 256\n\n /** Steps in flight between `step.started` and `step.ended`. */\n const pending = new Map<string, PendingStep>()\n /** Last model selected per session, used when a step starts unobserved. */\n const lastModel = new Map<string, SessionModelRef>()\n /** Endpoint evidence learned per session, applied to its steps. */\n const known = new Map<string, { modelID: string | undefined; info: EndpointInfo }>()\n const now = options?.now ?? (() => Date.now())\n\n /**\n * Ceiling on the writes kept while the database is busy. A maintenance\n * operation (a prune, a vacuum) holds the write lock; waiting for it would\n * stall the agent, and dropping the record would lose usage. The recorder\n * defers instead, retrying on the next event, and only a lock held for longer\n * than this many records takes a record with it.\n */\n const MAX_DEFERRED = 256\n const deferred: Array<() => void> = []\n\n /** True when the database was locked rather than the statement being wrong. */\n function isBusy(error: unknown): boolean {\n const code = (error as { code?: unknown } | null | undefined)?.code\n return code === \"SQLITE_BUSY\" || code === \"SQLITE_LOCKED\"\n }\n\n function enqueue(run: () => void): void {\n if (deferred.length >= MAX_DEFERRED) deferred.shift()\n deferred.push(run)\n }\n\n /** Retry what a busy database postponed, oldest first, stopping at the first failure. */\n function flushDeferred(): void {\n while (deferred.length > 0) {\n const next = deferred[0]!\n try {\n next()\n } catch {\n return\n }\n deferred.shift()\n }\n }\n\n /**\n * Run one write. On a busy database the work is kept for the next event\n * instead of waiting for the lock: the recorder is on the agent's path, and\n * the record is worth more late than the agent is worth stalled.\n */\n function write(run: () => void): void {\n if (deferred.length > 0) {\n flushDeferred()\n // Still locked: this write has to wait behind the queued ones.\n if (deferred.length > 0) {\n enqueue(run)\n return\n }\n }\n try {\n run()\n } catch (error) {\n if (isBusy(error)) enqueue(run)\n // Anything else is swallowed, as before: telemetry never breaks a session.\n }\n }\n\n /** Insertion-ordered map with a size ceiling: the oldest entry goes first. */\n function remember<Value>(map: Map<string, Value>, key: string, value: Value, limit: number): void {\n map.delete(key)\n map.set(key, value)\n while (map.size > limit) {\n const oldest = map.keys().next()\n if (oldest.done === true) break\n map.delete(oldest.value)\n }\n }\n\n /** Drop everything remembered about a session that is gone. */\n function forget(sessionID: string): void {\n lastModel.delete(sessionID)\n known.delete(sessionID)\n for (const [messageID, step] of pending) {\n if (step.sessionID === sessionID) pending.delete(messageID)\n }\n }\n\n /**\n * How much an observation is worth: the gateway naming its own routing\n * endpoint beats inferring it from the host, which beats nothing. Without\n * this a response that carries no endpoint header would downgrade the one a\n * previous response named.\n */\n function strength(info: EndpointInfo | undefined): number {\n if (info?.router === undefined) return 0\n return info.source?.startsWith(\"header:\") === true ? 2 : 1\n }\n\n function noteRouter(sessionID: string, modelID: string | undefined, info: EndpointInfo): void {\n const previous = known.get(sessionID)\n // A stronger observation wins; at equal strength the newest one does, so a\n // gateway that re-routes is still seen.\n const merged = strength(previous?.info) > strength(info) ? previous!.info : mergeEndpoint(info, previous?.info)\n const info2: EndpointInfo = merged ?? info\n // The same answer for the same model means the database already agrees: the\n // rows recorded since the last observation carry the endpoint at insert\n // time, and the older ones were filled when it was learned. Writing again\n // would cost the response path a statement to change nothing.\n const settled = previous !== undefined && previous.modelID === modelID && previous.info.router === info2.router\n remember(known, sessionID, { modelID, info: info2 }, MAX_TRACKED_SESSIONS)\n if (!settled) write(() => setStepRouterForSessionModel(db, sessionID, modelID, info2))\n }\n\n function recordPendingStep(messageID: string, data: RecorderEventData, finish: string | undefined, at: number): void {\n const sessionID = data.sessionID\n if (typeof sessionID !== \"string\") return\n const info = pending.get(messageID)\n pending.delete(messageID)\n const model = info?.model ?? lastModel.get(sessionID)\n const learned = known.get(sessionID)\n const matched = learned !== undefined && (learned.modelID === undefined || learned.modelID === model?.id) ? learned.info : undefined\n write(() =>\n recordStep(db, {\n messageID,\n sessionID,\n providerID: model?.providerID,\n modelID: model?.id,\n variant: model?.variant,\n input: data.tokens?.input,\n output: data.tokens?.output,\n reasoning: data.tokens?.reasoning,\n cacheRead: data.tokens?.cache?.read,\n cacheWrite: data.tokens?.cache?.write,\n cost: data.cost,\n createdAt: info?.started,\n streamedAt: info?.streamed,\n completedAt: at,\n finish,\n router: matched?.router,\n recordedAt: now(),\n }),\n )\n }\n\n function handleEvent(event: RecorderEvent): void {\n // A previous write may have been postponed: this is the cheapest moment to\n // land it, and the check costs nothing while the queue is empty.\n if (deferred.length > 0) flushDeferred()\n const data = event.data ?? {}\n const at = typeof event.created === \"number\" ? event.created : now()\n const sessionID = typeof data.sessionID === \"string\" ? data.sessionID : undefined\n\n switch (event.type) {\n case \"session.created\": {\n if (sessionID === undefined) return\n write(() =>\n upsertSession(db, {\n sessionID,\n parentID: data.parentID,\n directory: data.location?.directory,\n title: data.title,\n startedAt: at,\n updatedAt: at,\n }),\n )\n if (data.model !== undefined) remember(lastModel, sessionID, data.model, MAX_TRACKED_SESSIONS)\n return\n }\n case \"session.deleted\": {\n // The session is gone: its in-flight steps will never end and nothing\n // else about it will be needed again.\n if (sessionID !== undefined) forget(sessionID)\n return\n }\n case \"session.renamed\": {\n if (sessionID === undefined) return\n write(() => upsertSession(db, { sessionID, title: data.title, updatedAt: at }))\n return\n }\n case \"session.moved\": {\n if (sessionID === undefined) return\n write(() => upsertSession(db, { sessionID, directory: data.location?.directory, updatedAt: at }))\n return\n }\n case \"session.inbox.enqueued\": {\n if (sessionID === undefined) return\n if (data.item?.type !== \"user\" && data.item?.type !== \"synthetic\") return\n // Narrowed into a local: a closure does not keep a property's narrowing.\n const inboxID = data.inboxID\n if (typeof inboxID !== \"string\") return\n write(() => recordPrompt(db, { sessionID, messageID: inboxID, createdAt: at }))\n return\n }\n case \"session.model.selected\": {\n // The session's current model, remembered so a step that started before\n // the plugin loaded is still attributed. Nothing is written for it: no\n // view reads a model-selection event.\n if (sessionID !== undefined && data.model !== undefined) remember(lastModel, sessionID, data.model, MAX_TRACKED_SESSIONS)\n return\n }\n case \"session.step.started\": {\n if (sessionID === undefined || typeof data.assistantMessageID !== \"string\") return\n remember(\n pending,\n data.assistantMessageID,\n {\n messageID: data.assistantMessageID,\n sessionID,\n started: typeof data.started === \"number\" ? data.started : undefined,\n streamed: undefined,\n model: data.model,\n },\n MAX_PENDING,\n )\n return\n }\n case \"session.step.streamed\": {\n if (typeof data.assistantMessageID !== \"string\") return\n const info = pending.get(data.assistantMessageID)\n if (info !== undefined) info.streamed = at\n return\n }\n case \"session.step.ended\": {\n if (typeof data.assistantMessageID !== \"string\") return\n recordPendingStep(data.assistantMessageID, data, data.finish ?? \"unknown\", at)\n return\n }\n case \"session.step.failed\": {\n if (typeof data.assistantMessageID !== \"string\") return\n // The step row keeps the outcome through `finish`; the error event is\n // not written because no view reads it.\n recordPendingStep(data.assistantMessageID, data, data.finish ?? \"error\", at)\n return\n }\n case \"session.compaction.ended\": {\n if (sessionID === undefined) return\n write(() =>\n recordEvent(db, {\n sessionID,\n kind: COMPACTION_KIND,\n at,\n data: { reason: data.reason, model: data.model, tokens: data.tokens, cost: data.cost },\n }),\n )\n return\n }\n default:\n return\n }\n }\n\n function pendingFor(sessionID: string): PendingStep | undefined {\n let found: PendingStep | undefined\n for (const item of pending.values()) {\n if (item.sessionID !== sessionID) continue\n if (found === undefined || (item.started ?? 0) >= (found.started ?? 0)) found = item\n }\n return found\n }\n\n function handleHttpResponse(event: HttpResponseInput): void {\n const sessionID = event.sessionID\n if (typeof sessionID !== \"string\") return\n\n // The endpoint that served the request: the gateway's own header first,\n // then the host the request went to. Nothing else about the exchange is\n // kept — the headers a gateway happens to send are not who ran the model.\n const inferred = mergeEndpoint(inferFromHeaders(event.response?.headers), inferFromUrl(event.request?.url))\n if (inferred === undefined) return\n\n noteRouter(sessionID, event.model?.id, inferred)\n // The step in flight is not a row yet: `recordPendingStep` writes it when it\n // ends, carrying the endpoint learned from this response. Writing here would\n // be a statement that changes nothing, on the request path.\n }\n\n return { handleEvent, handleHttpResponse, pendingFor }\n}\n"
8
+ "/**\n * Text hygiene for everything that reaches a terminal.\n *\n * Some of what the plugin displays is not written by the user: a model writes\n * a session title, an NDJSON export can come from anywhere. A control\n * character in any of those is interpreted by the terminal that renders it\n * (title changes, cursor moves, colours), so every such string is stripped of\n * control characters before it is stored or drawn.\n *\n * The rule is deliberately blunt: drop C0 controls, DEL and the C1 block, keep\n * everything else (the text is otherwise arbitrary Unicode), and cap the\n * length so a hostile value cannot bloat the database or the frame. Layout is\n * never touched: whitespace is preserved, because callers build indentation\n * into the strings they truncate.\n */\n\n/** Longest value kept by default: a title, an id or a path, not a document. */\nexport const TEXT_LIMIT = 512\n\n/**\n * Strip control characters and cap the length. Newlines and tabs go too: a\n * line is one line, and the renderers lay out their own spacing.\n */\nexport function sanitizeText(value: string, limit = TEXT_LIMIT): string {\n if (limit <= 0) return \"\"\n let cleaned = \"\"\n for (const character of value) {\n const code = character.codePointAt(0) ?? 0\n if (code <= 0x1f || code === 0x7f || (code >= 0x80 && code <= 0x9f)) continue\n cleaned += character\n if (cleaned.length >= limit) break\n }\n return cleaned\n}\n\n/** `sanitizeText` without the layout, for a stored label: trimmed as well. */\nexport function sanitizeLabel(value: string, limit = TEXT_LIMIT): string {\n return sanitizeText(value, limit).trim()\n}\n\n/**\n * A single identifier token: the shape a provider id or a model id has. Used\n * to validate the values that become grouping keys in the database, so a\n * configured provider cannot inject anything into an identity a view groups\n * by. Case is left alone: the provider is shown as it is configured, and\n * folding it would report a provider the user never chose.\n */\nconst TOKEN = /^[A-Za-z0-9][A-Za-z0-9._:/-]*$/\n\nexport function isToken(value: string, limit = 64): boolean {\n return value.length > 0 && value.length <= limit && TOKEN.test(value)\n}\n",
9
+ "/**\n * Pure turn accounting shared by the TUI line, the server recorder and the\n * stats engine.\n *\n * Everything here operates on structural shapes that mirror the session\n * messages published by OpenCode, so the logic stays testable without a host\n * runtime and without the effect-based schema types.\n */\n\nexport type ModelRefLike = {\n providerID: string\n id: string\n variant?: string | undefined\n}\n\nexport type AssistantMessage = {\n type: \"assistant\"\n id: string\n time?: { created?: number | undefined; streamed?: number | undefined; completed?: number | undefined } | undefined\n model?: ModelRefLike | undefined\n tokens?:\n | {\n input?: number | undefined\n output?: number | undefined\n reasoning?: number | undefined\n cache?: { read?: number | undefined; write?: number | undefined } | undefined\n }\n | undefined\n cost?: number | undefined\n finish?: string | null | undefined\n rawFinish?: string | null | undefined\n}\n\nexport type PromptMessage = {\n type: \"user\" | \"synthetic\" | \"system\"\n id?: string | undefined\n time?: { created?: number | undefined } | undefined\n}\n\nexport type TranscriptMessage =\n | AssistantMessage\n | PromptMessage\n | { type: string; id?: string | undefined; time?: { created?: number | undefined } | undefined }\n\n/** Accumulated token usage for a group of assistant steps. */\nexport type Totals = {\n input: number\n cached: number\n output: number\n reasoning: number\n cacheWrite: number\n cost: number\n steps: number\n}\n\nexport type CacheDrop = {\n messageID: string\n /** Tokens that were expected to hit the prompt cache but did not. */\n tokens: number\n /** `cache.read` of the step that was used as the reuse reference. */\n prevCached: number\n /** Time between the reference step's completion and this step's request. */\n gapMs: number | undefined\n}\n\nexport type TurnStats = {\n /** Wall clock time of the last step's completion. */\n endAt: number | undefined\n /** Creation time of the first settled step of the turn. */\n startAt: number | undefined\n /** Creation time of the prompt that opened this turn. */\n promptAt: number | undefined\n /** Time between the previous prompt and this turn's prompt. */\n promptGapMs: number | undefined\n providerID: string | undefined\n modelID: string | undefined\n variant: string | undefined\n /** Provider ID differs from the previous turn in the same transcript. */\n providerChanged: boolean\n /** Provider is the same but the model or variant differs. */\n modelChanged: boolean\n totals: Totals\n /** Passed through from the caller (session-wide accumulated usage). */\n session: Totals | undefined\n /**\n * Prompt-side usage of the last finished step: the context the turn ended\n * with. Summing `input`/`cached` over the steps of a turn counts the same\n * re-sent prompt once per step, which is not what a context view means.\n */\n latest: Totals\n drops: CacheDrop[]\n cacheDropTokens: number\n cacheExpired: boolean\n /** Assistant steps in the turn. */\n steps: number\n}\n\nexport type TurnSegment = {\n assistants: AssistantMessage[]\n prompt: TranscriptMessage | undefined\n}\n\n/** Message types that open a turn: a run of assistants belongs to the last one. */\nexport const PROMPT_BOUNDARIES: ReadonlySet<string> = new Set([\"user\", \"synthetic\", \"system\"])\n\n/**\n * Split a transcript into turns. A turn is a run of assistant steps opened by\n * a prompt boundary (`user`, `synthetic`, `system`). Idle notices, info\n * messages, compactions and shell entries keep the current turn open so a\n * single execution is never split in two.\n */\nexport function transcriptSegments(messages: readonly TranscriptMessage[]): TurnSegment[] {\n const segments: TurnSegment[] = []\n let current: AssistantMessage[] = []\n let prompt: TranscriptMessage | undefined\n\n for (const message of messages) {\n if (message.type === \"assistant\") {\n current.push(message as AssistantMessage)\n continue\n }\n if (!PROMPT_BOUNDARIES.has(message.type)) continue\n if (current.length > 0) {\n segments.push({ assistants: current, prompt })\n current = []\n }\n prompt = message\n }\n if (current.length > 0) segments.push({ assistants: current, prompt })\n return segments\n}\n\n/**\n * The last `count` segments, found by walking the transcript backwards. The\n * live line only needs the turn that just ended and the one before it, and a\n * long session holds tens of thousands of messages: the tail walk stops as\n * soon as it has them, instead of building a segment for every turn.\n */\nexport function transcriptTail(messages: readonly TranscriptMessage[], count: number): TurnSegment[] {\n const segments: TurnSegment[] = []\n let current: AssistantMessage[] = []\n\n for (let index = messages.length - 1; index >= 0; index -= 1) {\n const message = messages[index]!\n if (message.type === \"assistant\") {\n current.unshift(message as AssistantMessage)\n continue\n }\n if (!PROMPT_BOUNDARIES.has(message.type)) continue\n if (current.length === 0) continue\n // The boundary closes the run collected after it.\n segments.unshift({ assistants: current, prompt: message })\n if (segments.length >= count) return segments\n current = []\n }\n // A leading run with no prompt before it is a segment of its own.\n if (current.length > 0 && segments.length < count) segments.unshift({ assistants: current, prompt: undefined })\n return segments\n}\n\nexport function tokenCounts(message: AssistantMessage): { input: number; output: number; reasoning: number; read: number; write: number } {\n const tokens = message.tokens\n return {\n input: tokens?.input ?? 0,\n output: tokens?.output ?? 0,\n reasoning: tokens?.reasoning ?? 0,\n read: tokens?.cache?.read ?? 0,\n write: tokens?.cache?.write ?? 0,\n }\n}\n\nexport function totalize(steps: readonly AssistantMessage[]): Totals {\n const totals: Totals = { input: 0, cached: 0, output: 0, reasoning: 0, cacheWrite: 0, cost: 0, steps: steps.length }\n for (const step of steps) {\n const counts = tokenCounts(step)\n totals.input += counts.input\n totals.cached += counts.read\n totals.output += counts.output\n totals.reasoning += counts.reasoning\n totals.cacheWrite += counts.write\n totals.cost += step.cost ?? 0\n }\n return totals\n}\n\n/**\n * The identity of a model reference: the provider it runs through, the model\n * and its variant. Two steps with the same key ran the same thing the same way,\n * which is what `modelChanged` compares.\n */\nexport function identityKey(model: ModelRefLike | undefined): string | undefined {\n if (!model) return undefined\n return `${model.providerID}/${model.id}${model.variant ? `#${model.variant}` : \"\"}`\n}\n\nexport function cacheRead(message: AssistantMessage | undefined): number {\n return message?.tokens?.cache?.read ?? 0\n}\n\nexport function stepCreated(message: AssistantMessage | undefined): number | undefined {\n return message?.time?.created\n}\n\nexport function stepEnd(message: AssistantMessage | undefined): number | undefined {\n const time = message?.time\n if (!time) return undefined\n return time.completed ?? time.streamed ?? time.created\n}\n\n/**\n * Which identity the two steps must share for a reuse loss to be measurable.\n * `provider` is the strict one: two providers are two caches, so a switch\n * between them is not a loss but a different cache. `model` drops that test,\n * for the reports that measure what an identity change costs: forfeiting the\n * previous provider's cache is exactly the cost they report.\n */\nexport type CacheDropScope = \"provider\" | \"model\"\n\n/**\n * Cache reuse loss against a reference step, mirroring OpenCode's own\n * `turn-usage` heuristic: only comparable when the model is identical, and\n * OpenAI's API reports small reads that are not worth flagging.\n */\nexport function cacheDrop(reference: AssistantMessage | undefined, current: AssistantMessage, scope: CacheDropScope = \"provider\"): number | undefined {\n if (!reference) return undefined\n const before = reference.model\n const after = current.model\n if (!before || !after) return undefined\n if (scope === \"provider\" && before.providerID !== after.providerID) return undefined\n if (before.id !== after.id || (before.variant ?? \"\") !== (after.variant ?? \"\")) {\n return undefined\n }\n const drop = cacheRead(reference) - cacheRead(current)\n if (isNoiseDrop(after.providerID, drop)) return undefined\n return drop > 0 ? drop : undefined\n}\n\n/** Milliseconds between a reference step's completion and the next request. */\nexport function stepGap(reference: AssistantMessage | undefined, current: AssistantMessage): number | undefined {\n const ended = stepEnd(reference)\n const started = stepCreated(current)\n if (ended === undefined || started === undefined) return undefined\n return Math.max(0, started - ended)\n}\n\n/**\n * Streaming duration of a step: request start to the last streamed chunk. The\n * host's `streamed` stamp marks the end of streaming, so this is the window a\n * generation rate belongs to — not a time to first token, which the session\n * messages do not record.\n */\nexport function stepStreamMs(message: AssistantMessage): number | undefined {\n const created = message.time?.created\n const streamed = message.time?.streamed\n if (created === undefined || streamed === undefined || streamed <= created) return undefined\n return streamed - created\n}\n\n/**\n * Tokens produced per second over a window. For a step the window is the\n * streaming one: request start to the last streamed chunk (`created` →\n * `streamed`) — `completed` marks the end of the step, which for a tool-call\n * step happens after the tool calls were handled. Returns undefined when the\n * window or the produced tokens are unknown. Single definition, shared by the\n * recorder (which stores it per step) and the latency view.\n */\nexport function tokensPerSecond(produced: number, from: number | undefined, to: number | undefined): number | undefined {\n if (from === undefined || to === undefined || to <= from) return undefined\n if (produced <= 0) return undefined\n return (produced / (to - from)) * 1000\n}\n\n\n/** Output-plus-reasoning tokens per second of a step. */\nexport function stepTps(message: AssistantMessage): number | undefined {\n const counts = tokenCounts(message)\n return tokensPerSecond(counts.output + counts.reasoning, message.time?.created, message.time?.streamed)\n}\n\nconst DROP_EXEMPT_PROVIDERS = new Set([\"openai\"])\n\n/**\n * True when a step has final numbers. A step that is still streaming reports\n * partial tokens (usually zero) and no cache read yet, so reading it would\n * render an empty line and invent a cache-expiry warning against it.\n */\nexport function isSettledStep(message: AssistantMessage): boolean {\n if (message.time?.completed !== undefined) return true\n return message.finish !== undefined && message.finish !== null\n}\n\n/**\n * Compute the stats of the most recent turn in a transcript, including the\n * comparison against the previous turn (a provider or model switch, and cache\n * reuse).\n *\n * Returns undefined while the newest turn is still being generated: the line\n * appears when a turn ends, never with the zeroes of an in-flight response.\n */\nexport function computeTurn(messages: readonly TranscriptMessage[], session?: Totals): TurnStats | undefined {\n const segments = transcriptTail(messages, 2)\n const last = segments[segments.length - 1]\n if (!last) return undefined\n const previous = segments[segments.length - 2]\n const lastStep = last.assistants[last.assistants.length - 1]\n if (!lastStep) return undefined\n if (!isSettledStep(lastStep)) return undefined\n const previousStep = previous?.assistants[previous.assistants.length - 1]\n // Defensive: a step that never settled mid-turn carries no usable numbers,\n // so it is neither a cache reference nor a reuse candidate.\n const settled = last.assistants.filter(isSettledStep)\n\n const drops: CacheDrop[] = []\n let reference: AssistantMessage | undefined = previousStep !== undefined && isSettledStep(previousStep) ? previousStep : undefined\n for (const step of settled) {\n const drop = cacheDrop(reference, step)\n if (drop !== undefined) {\n drops.push({\n messageID: step.id,\n tokens: drop,\n prevCached: cacheRead(reference),\n gapMs: stepGap(reference, step),\n })\n }\n reference = step\n }\n\n const provider = lastStep.model?.providerID\n const previousProvider = previousStep?.model?.providerID\n const key = identityKey(lastStep.model)\n const previousKey = identityKey(previousStep?.model)\n\n return {\n endAt: stepEnd(lastStep),\n startAt: stepCreated(settled[0] ?? lastStep),\n promptAt: last.prompt?.time?.created,\n promptGapMs: promptGap(last.prompt, previous?.prompt),\n providerID: provider,\n modelID: lastStep.model?.id,\n variant: lastStep.model?.variant,\n providerChanged: previousProvider !== undefined && provider !== undefined && previousProvider !== provider,\n modelChanged: previousKey !== undefined && key !== undefined && previousKey !== key && previousProvider === provider,\n totals: totalize(settled),\n latest: totalize([lastStep]),\n session,\n drops,\n cacheDropTokens: drops.reduce((sum, drop) => sum + drop.tokens, 0),\n cacheExpired: drops.length > 0,\n steps: settled.length,\n }\n}\n\nfunction promptGap(current: TranscriptMessage | undefined, previous: TranscriptMessage | undefined): number | undefined {\n const now = current?.time?.created\n const before = previous?.time?.created\n if (now === undefined || before === undefined) return undefined\n return Math.max(0, now - before)\n}\n\n/** True when a cache drop is small enough to be provider noise. */\nexport function isNoiseDrop(providerID: string | undefined, drop: number): boolean {\n if (providerID === undefined) return false\n return DROP_EXEMPT_PROVIDERS.has(providerID) && drop >= 1024 && drop <= 2048\n}\n\n// ---------------------------------------------------------------------------\n// Formatting\n// ---------------------------------------------------------------------------\n\nexport type LineTone = \"base\" | \"muted\" | \"warning\"\n\nexport type LineSegment = { text: string; tone: LineTone }\n\nexport type LineLabels = {\n delta: string\n turn: string\n session: string\n step: string\n steps: string\n input: string\n cached: string\n output: string\n reasoning: string\n cost: string\n cacheExpired: string\n providerChanged: string\n modelChanged: string\n}\n\n/**\n * How the turn segment counts prompt usage. `latest` shows the prompt size the\n * turn ended with — the context view, and what OpenCode's own turn-token debug\n * line reports. `sum` adds every step of the turn: the billing view, where a\n * tool-call loop re-sends (and re-reads) the same context once per step.\n */\nexport type TurnTotalsView = \"latest\" | \"sum\"\n\nexport type LineFields = {\n clock: boolean\n delta: boolean\n model: boolean\n variant: boolean\n turn: boolean\n session: boolean\n reasoning: boolean\n cost: boolean\n cacheExpiry: boolean\n}\n\nexport const DEFAULT_LABELS: LineLabels = {\n delta: \"Δ\",\n // The turn segment is unlabelled by default to keep the line short; set it\n // back to \"turn\" (or anything else) through `labels.turn`.\n turn: \"\",\n session: \"sess\",\n step: \"step\",\n steps: \"steps\",\n input: \"in\",\n cached: \"cached\",\n output: \"out\",\n reasoning: \"reason\",\n cost: \"cost\",\n cacheExpired: \"cache expired\",\n providerChanged: \"changed\",\n modelChanged: \"model changed\",\n}\n\nexport const DEFAULT_FIELDS: LineFields = {\n clock: true,\n delta: true,\n model: true,\n variant: true,\n turn: true,\n session: true,\n reasoning: false,\n // The turn's and the session's cost are on by default: what a session costs\n // is the question the tokens only half answer. A zero is left out, so a free\n // model reads as free instead of repeating `$0` on every line.\n cost: true,\n cacheExpiry: true,\n}\n\nexport function formatClock(ms: number): string {\n const date = new Date(ms)\n const pad = (value: number) => String(value).padStart(2, \"0\")\n return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`\n}\n\nexport function formatDuration(ms: number): string {\n const totalSeconds = Math.max(0, Math.floor(ms / 1000))\n const seconds = totalSeconds % 60\n const minutes = Math.floor(totalSeconds / 60) % 60\n const hours = Math.floor(totalSeconds / 3600) % 24\n const days = Math.floor(totalSeconds / 86400)\n if (days > 0) return `${days}d${hours}h`\n if (hours > 0) return `${hours}h${String(minutes).padStart(2, \"0\")}m`\n if (minutes > 0) return `${minutes}m${String(seconds).padStart(2, \"0\")}s`\n return `${seconds}s`\n}\n\nexport function formatTokens(value: number): string {\n const n = Math.max(0, Math.round(value))\n if (n < 1_000) return String(n)\n if (n < 999_500) {\n const k = n / 1_000\n return `${trimZeros(k.toFixed(k < 10 ? 2 : 1))}k`\n }\n if (n < 999_500_000) {\n const m = n / 1_000_000\n return `${trimZeros(m.toFixed(m < 10 ? 2 : 1))}M`\n }\n return `${Math.round(n / 1_000_000)}M`\n}\n\nfunction trimZeros(value: string): string {\n return value.includes(\".\") ? value.replace(/0+$/, \"\").replace(/\\.$/, \"\") : value\n}\n\nexport function formatCost(value: number): string {\n if (value <= 0) return \"$0\"\n if (value < 1) return `$${value.toFixed(4)}`\n return `$${value.toFixed(2)}`\n}\n\n/**\n * The buckets the line displays, derived from a totals group. `input` is the\n * *uncached* part of the prompt (fresh input plus cache writes, i.e. what the\n * debug line's `new` counts on the prompt side), `prompt` is the whole prompt\n * of the request and `output` is everything the model generated (visible output\n * + reasoning). OpenCode's own debug line reconciles with these:\n * `new = in + out` and `total = prompt + out`, with `prompt = in + cached`.\n * The stored buckets stay raw; only the display folds.\n */\nexport type DisplayTotals = Totals & {\n /** Whole prompt of the request: fresh input + cached reads + cache writes. */\n prompt: number\n}\n\nexport function displayTotals(totals: Totals): DisplayTotals {\n const prompt = totals.input + totals.cached + totals.cacheWrite\n return {\n input: prompt - totals.cached,\n cached: totals.cached,\n cacheWrite: totals.cacheWrite,\n output: totals.output + totals.reasoning,\n reasoning: totals.reasoning,\n cost: totals.cost,\n steps: totals.steps,\n prompt,\n }\n}\n\n/**\n * The numbers of one totals group. `cost` is passed explicitly because the two\n * callers mean different money: a turn's cost is the sum over its steps\n * whatever view its tokens follow, and money never comes from a \"latest\" step.\n * A zero cost is left out of the line.\n */\nfunction totalsText(prefix: string, totals: Totals, labels: LineLabels, fields: LineFields, stepCount = 0, cost = totals.cost): string {\n const display = displayTotals(totals)\n // The parenthesised value is the whole prompt, shown only when it differs\n // from the uncached part the label leads with.\n const input =\n display.prompt === display.input\n ? `${labels.input} ${formatTokens(display.input)}`\n : `${labels.input} ${formatTokens(display.input)} (${formatTokens(display.prompt)})`\n const parts = [\n input,\n `${labels.cached} ${formatTokens(display.cached)}`,\n `${labels.output} ${formatTokens(display.output)}`,\n ]\n if (fields.reasoning) parts.push(`${labels.reasoning} ${formatTokens(display.reasoning)}`)\n if (fields.cost && cost > 0) parts.push(`${labels.cost} ${formatCost(cost)}`)\n // An empty label (the default for the turn) leaves the numbers unlabelled; a\n // multi-step turn still says how many requests it took.\n const steps = stepCount > 1 ? `${stepCount} ${stepCount === 1 ? labels.step : labels.steps}` : \"\"\n const head = [prefix, steps].filter((piece) => piece !== \"\").join(\" \")\n const numbers = parts.join(\", \")\n if (head === \"\") return numbers\n return stepCount > 1 ? `${head} · ${numbers}` : `${head} ${numbers}`\n}\n\n/**\n * Render the telemetry line as toned segments. The caller maps tones onto\n * theme colors; `formatLine` joins the segments as plain text.\n */\nexport function formatLineSegments(\n stats: TurnStats,\n options?: { labels?: Partial<LineLabels>; fields?: Partial<LineFields>; turnTotals?: TurnTotalsView },\n): LineSegment[] {\n const labels = { ...DEFAULT_LABELS, ...options?.labels }\n const fields = { ...DEFAULT_FIELDS, ...options?.fields }\n const turnTotals = options?.turnTotals ?? \"latest\"\n const segments: LineSegment[] = []\n\n if (fields.clock && stats.endAt !== undefined) {\n segments.push({ text: formatClock(stats.endAt), tone: \"muted\" })\n }\n if (fields.delta && stats.promptGapMs !== undefined) {\n segments.push({ text: `${labels.delta} ${formatDuration(stats.promptGapMs)}`, tone: \"muted\" })\n }\n // The turn's identity is the provider the model ran through and the model\n // itself: the same model can come from different providers, so both are\n // shown, and a provider switch is the change the line warns about.\n if (stats.providerID !== undefined) {\n let text = stats.providerID\n if (fields.model && stats.modelID !== undefined) text += `/${stats.modelID}`\n if (fields.variant && stats.variant) text += ` ${stats.variant}`\n segments.push({ text, tone: stats.providerChanged ? \"warning\" : \"base\" })\n if (stats.providerChanged) segments.push({ text: labels.providerChanged, tone: \"warning\" })\n else if (stats.modelChanged) segments.push({ text: labels.modelChanged, tone: \"muted\" })\n }\n if (fields.turn && stats.totals.steps > 0) {\n const totals = turnTotals === \"sum\" ? stats.totals : stats.latest\n // The tokens follow the view — `latest` is the context the turn ended with\n // — but money is paid per step: the turn's cost is the sum over its steps,\n // never the last step's share of it.\n segments.push({\n text: totalsText(labels.turn, totals, labels, fields, turnTotals === \"sum\" ? stats.totals.steps : stats.steps, stats.totals.cost),\n tone: \"base\",\n })\n }\n if (fields.session && stats.session) {\n segments.push({ text: totalsText(labels.session, stats.session, labels, fields, 0, stats.session.cost), tone: \"muted\" })\n }\n if (fields.cacheExpiry && stats.cacheExpired) {\n const count = stats.drops.length > 1 ? `×${stats.drops.length}` : \"\"\n segments.push({ text: `⚠ ${labels.cacheExpired} (−${formatTokens(stats.cacheDropTokens)}${count})`, tone: \"warning\" })\n }\n return segments\n}\n\nexport function formatLine(\n stats: TurnStats,\n options?: { labels?: Partial<LineLabels>; fields?: Partial<LineFields>; turnTotals?: TurnTotalsView },\n): string {\n return formatLineSegments(stats, options)\n .map((segment) => segment.text)\n .join(\" · \")\n}\n",
10
+ "import type { Database } from \"bun:sqlite\"\nimport { cacheDrop, stepGap, tokensPerSecond, type AssistantMessage } from \"../core\"\n\n/**\n * Read side of the stats engine: every function takes an open database and\n * returns plain rows for the TUI panel, the report script and the NDJSON\n * exports. Aggregations stay in SQL; comparisons and percentiles that would\n * need window functions or custom SQLite builds are computed in JS.\n */\n\n/** Event kind written by the recorder when a compaction completes. */\nexport const COMPACTION_KIND = \"compaction\"\n\n/**\n * Idle gap above which a cache reuse loss is attributed to provider-side\n * expiry rather than to a mid-session event. Heuristic: the empirical TTL\n * evidence view narrows this down with real observations.\n */\nexport const DEFAULT_TTL_HINT_MS = 10 * 60 * 1000\n\nexport type Range = {\n from?: number | undefined\n to?: number | undefined\n}\n\n/**\n * Panel filters. They narrow every aggregation to one model, one provider or\n * one session, and are applied with the same `WHERE` builder the range uses so\n * a filtered view and a drill-down agree.\n */\nexport type Filters = {\n /** Model id, variant excluded. */\n modelID?: string | undefined\n /**\n * One model *and* its variant, as `modelKey` builds it (`m1#max`). A variant\n * is a different model to the provider, so a drill-down into one variant must\n * not pick up its family.\n */\n modelKey?: string | undefined\n /**\n * The provider the model ran through, as OpenCode configured it\n * (`opencode-go`, `deepseek`, `deepinfra`). It is the identity every view\n * groups by: a gateway may re-route the request, but the upstream that\n * actually ran it is not observable.\n */\n provider?: string | undefined\n /**\n * One session, exactly. Used by the `session` range and by the overview's\n * \"this session\" block, which must not pick up the steps of every other\n * session that happened to run after it started.\n */\n sessionID?: string | undefined\n}\n\nexport type Usage = {\n steps: number\n input: number\n cached: number\n cacheWrite: number\n output: number\n reasoning: number\n cost: number\n}\n\ntype UsageRow = {\n steps: number | null\n input: number | null\n cached: number | null\n cacheWrite: number | null\n output: number | null\n reasoning: number | null\n cost: number | null\n}\n\nexport const STEP_TIME = \"COALESCE(created_at, recorded_at)\"\n\n/**\n * The identity of a model *and* its variant, in SQL: `modelKey` says a variant\n * is a different model to the provider, so a variant never shares a row with\n * its family. The JS twin is `modelKey` below; both have to agree.\n */\nexport const MODEL_KEY_SQL = \"COALESCE(model_id, '(unknown)') || '#' || COALESCE(variant, '')\"\n\n/** `modelKey(modelID, variant)`: the family, with the variant as a suffix. */\nexport function modelKey(modelID: string, variant: string | undefined): string {\n return `${modelID}#${variant ?? \"\"}`\n}\n\n/** Human label of a model key or a model id and variant. */\nexport function modelLabel(modelID: string, variant: string | undefined): string {\n return variant === undefined ? modelID : `${modelID} ${variant}`\n}\nconst USAGE_SELECT = `COUNT(*) AS steps,\n COALESCE(SUM(input_tokens), 0) AS input,\n COALESCE(SUM(cache_read), 0) AS cached,\n COALESCE(SUM(cache_write), 0) AS cacheWrite,\n COALESCE(SUM(output_tokens), 0) AS output,\n COALESCE(SUM(reasoning_tokens), 0) AS reasoning,\n COALESCE(SUM(cost), 0) AS cost`\n\nfunction usageFrom(row: UsageRow): Usage {\n return {\n steps: row.steps ?? 0,\n input: row.input ?? 0,\n cached: row.cached ?? 0,\n cacheWrite: row.cacheWrite ?? 0,\n output: row.output ?? 0,\n reasoning: row.reasoning ?? 0,\n cost: row.cost ?? 0,\n }\n}\n\nfunction rangeWhere(range: Range | undefined, column = STEP_TIME): { where: string; params: number[] } {\n const clauses: string[] = []\n const params: number[] = []\n if (range?.from !== undefined) {\n clauses.push(`${column} >= ?`)\n params.push(range.from)\n }\n if (range?.to !== undefined) {\n clauses.push(`${column} < ?`)\n params.push(range.to)\n }\n return { where: clauses.length > 0 ? `WHERE ${clauses.join(\" AND \")}` : \"\", params }\n}\n\n/**\n * `WHERE` for a range plus the panel filters. Shared with the analysis layer so\n * a filtered view and a drill-down agree on what is in scope.\n */\nexport function buildWhere(\n range: Range | undefined,\n filters?: Filters | undefined,\n column = STEP_TIME,\n): { where: string; params: (string | number)[] } {\n const rangeClause = rangeWhere(range, column)\n const clauses = rangeClause.where === \"\" ? [] : [rangeClause.where.slice(\"WHERE \".length)]\n const params: (string | number)[] = [...rangeClause.params]\n if (filters?.modelID !== undefined && filters.modelID !== \"\") {\n clauses.push(\"model_id = ?\")\n params.push(filters.modelID)\n }\n if (filters?.modelKey !== undefined && filters.modelKey !== \"\") {\n clauses.push(`${MODEL_KEY_SQL} = ?`)\n params.push(filters.modelKey)\n }\n if (filters?.provider !== undefined && filters.provider !== \"\") {\n clauses.push(\"provider_id = ?\")\n params.push(filters.provider)\n }\n if (filters?.sessionID !== undefined && filters.sessionID !== \"\") {\n clauses.push(\"session_id = ?\")\n params.push(filters.sessionID)\n }\n return { where: clauses.length > 0 ? `WHERE ${clauses.join(\" AND \")}` : \"\", params }\n}\n\n/**\n * Share of the whole prompt that came from the cache. The denominator is what\n * the provider saw: cache reads, fresh input and cache writes.\n */\nexport function cacheHitRatio(usage: Pick<Usage, \"input\" | \"cached\" | \"cacheWrite\">): number | undefined {\n const total = usage.cached + usage.input + usage.cacheWrite\n return total > 0 ? usage.cached / total : undefined\n}\n\nexport type Overview = Usage & {\n sessions: number\n prompts: number\n firstAt: number | undefined\n lastAt: number | undefined\n cacheHitRatio: number | undefined\n /**\n * Tokens generated per second of streaming, over the whole selection: the\n * produced tokens divided by the summed streaming windows. A weighted average,\n * not the mean of the per-step rates the latency view reports as p50.\n */\n tps: number | undefined\n}\n\nexport function overview(db: Database, range?: Range, filters?: Filters): Overview {\n const steps = buildWhere(range, filters)\n const row = db\n .query(\n `SELECT ${USAGE_SELECT},\n COUNT(DISTINCT session_id) AS sessions,\n MIN(${STEP_TIME}) AS firstAt,\n MAX(COALESCE(completed_at, ${STEP_TIME})) AS lastAt,\n COALESCE(SUM(CASE WHEN streamed_at > created_at THEN output_tokens + reasoning_tokens ELSE 0 END), 0) AS produced,\n COALESCE(SUM(CASE WHEN streamed_at > created_at THEN streamed_at - created_at ELSE 0 END), 0) AS window\n FROM steps ${steps.where}`,\n )\n .get(...steps.params) as\n | (UsageRow & { sessions: number | null; firstAt: number | null; lastAt: number | null; produced: number | null; window: number | null })\n | null\n\n const prompts = rangeWhere(range, \"created_at\")\n const promptRow = db.query(`SELECT COUNT(*) AS prompts FROM prompts ${prompts.where}`).get(...prompts.params) as { prompts: number } | null\n\n const usage = usageFrom(row ?? { steps: 0, input: 0, cached: 0, cacheWrite: 0, output: 0, reasoning: 0, cost: 0 })\n return {\n ...usage,\n sessions: row?.sessions ?? 0,\n prompts: promptRow?.prompts ?? 0,\n firstAt: row?.firstAt ?? undefined,\n lastAt: row?.lastAt ?? undefined,\n cacheHitRatio: cacheHitRatio(usage),\n tps: row === null ? undefined : tokensPerSecond(row.produced ?? 0, 0, row.window ?? 0),\n }\n}\n\nexport type ProviderUsage = Usage & {\n providerID: string\n firstAt: number | undefined\n lastAt: number | undefined\n}\n\nexport type ModelUsage = {\n modelID: string\n variant: string | undefined\n providers: ProviderUsage[]\n totals: Usage\n}\n\ntype ModelRow = UsageRow & {\n provider_id: string | null\n model_id: string | null\n variant: string | null\n first_at: number | null\n last_at: number | null\n}\n\n/**\n * Usage per model, each with the providers that served it. This is the view\n * that answers \"through which provider is this model actually coming, over the\n * selected period\".\n */\nexport function models(db: Database, range?: Range, filters?: Filters): ModelUsage[] {\n const filter = buildWhere(range, filters)\n const rows = db\n .query(\n `SELECT provider_id, model_id, variant,\n ${USAGE_SELECT},\n MIN(${STEP_TIME}) AS first_at,\n MAX(COALESCE(completed_at, recorded_at)) AS last_at\n FROM steps ${filter.where}\n GROUP BY provider_id, model_id, variant\n ORDER BY steps DESC, cached DESC`,\n )\n .all(...filter.params) as ModelRow[]\n\n const models = new Map<string, ModelUsage>()\n for (const row of rows) {\n const modelID = row.model_id ?? \"(unknown)\"\n const variant = row.variant ?? undefined\n const key = `${modelID}#${variant ?? \"\"}`\n let model = models.get(key)\n if (!model) {\n model = { modelID, variant, providers: [], totals: emptyUsage() }\n models.set(key, model)\n }\n const usage = usageFrom(row)\n model.providers.push({\n ...usage,\n providerID: row.provider_id ?? \"(unknown)\",\n firstAt: row.first_at ?? undefined,\n lastAt: row.last_at ?? undefined,\n })\n addUsage(model.totals, usage)\n }\n return [...models.values()].sort((a, b) => billedTokens(b.totals) - billedTokens(a.totals))\n}\n\nfunction emptyUsage(): Usage {\n return { steps: 0, input: 0, cached: 0, cacheWrite: 0, output: 0, reasoning: 0, cost: 0 }\n}\n\nfunction addUsage(target: Usage, source: Usage): void {\n target.steps += source.steps\n target.input += source.input\n target.cached += source.cached\n target.cacheWrite += source.cacheWrite\n target.output += source.output\n target.reasoning += source.reasoning\n target.cost += source.cost\n}\n\nfunction billedTokens(usage: Pick<Usage, \"input\" | \"cached\" | \"output\">): number {\n return usage.input + usage.cached + usage.output\n}\n\nexport type TimelineBucket = { at: number } & Usage\n\n/** Usage aggregated into fixed time buckets (for example one hour or day). */\nexport function timeline(db: Database, range: Range | undefined, bucketMs: number): TimelineBucket[] {\n const filter = rangeWhere(range)\n const rows = db\n .query(\n `SELECT CAST(${STEP_TIME} / ? AS INTEGER) * ? AS bucket, ${USAGE_SELECT}\n FROM steps ${filter.where}\n GROUP BY bucket\n ORDER BY bucket`,\n )\n .all(bucketMs, bucketMs, ...filter.params) as (UsageRow & { bucket: number })[]\n return rows.map((row) => ({ at: row.bucket, ...usageFrom(row) }))\n}\n\nexport type DropReason = \"compaction\" | \"expired\" | \"unknown\"\n\nexport type CacheDropRow = {\n sessionID: string\n messageID: string\n providerID: string | undefined\n modelID: string | undefined\n variant: string | undefined\n at: number\n tokens: number\n gapMs: number | undefined\n prevCached: number\n reason: DropReason\n}\n\nexport type TtlEvidenceRow = {\n providerID: string\n modelID: string\n samples: number\n /** Largest idle gap that still hit the cache. */\n maxHitGapMs: number | undefined\n /** Smallest idle gap after which reuse was lost. */\n minDropGapMs: number | undefined\n}\n\nexport type LatencyStats = {\n count: number\n p50: number | undefined\n p90: number | undefined\n p99: number | undefined\n}\n\nexport type LatencyReport = {\n /** Milliseconds from request to completed response. */\n wall: LatencyStats\n /** Milliseconds from request to the last streamed chunk (the streaming window). */\n stream: LatencyStats\n /** Output-plus-reasoning tokens per second over that window. */\n tps: LatencyStats\n}\n\n/** Nearest-rank percentile over an unsorted sample. */\nexport function percentile(values: readonly number[], p: number): number | undefined {\n if (values.length === 0) return undefined\n const sorted = [...values].sort((a, b) => a - b)\n const rank = Math.ceil((p / 100) * sorted.length)\n const index = Math.min(sorted.length - 1, Math.max(0, rank - 1))\n return sorted[index]\n}\n\nexport function latencyStats(values: number[]): LatencyStats {\n return {\n count: values.length,\n p50: percentile(values, 50),\n p90: percentile(values, 90),\n p99: percentile(values, 99),\n }\n}\n\nconst statsFor = latencyStats\n\nexport type SessionUsage = Usage & {\n sessionID: string\n parentID: string | undefined\n title: string | undefined\n directory: string | undefined\n providers: string[]\n models: string[]\n prompts: number\n firstAt: number | undefined\n lastAt: number | undefined\n cacheHitRatio: number | undefined\n}\n\nexport function sessions(\n db: Database,\n options?: { range?: Range; filters?: Filters; limit?: number; sessionIDs?: readonly string[] },\n): SessionUsage[] {\n if (options?.sessionIDs !== undefined && options.sessionIDs.length === 0) return []\n const range = options?.range\n const filter = buildWhere(range, options?.filters, \"COALESCE(st.created_at, st.recorded_at)\")\n const limit = options?.limit ?? 20\n const clauses = filter.where === \"\" ? [] : [filter.where.slice(\"WHERE \".length)]\n const params: (number | string)[] = [...filter.params]\n if (options?.sessionIDs !== undefined) {\n // A specific set of sessions (the subagents of one session): the group by\n // then reads one index range per session instead of every step recorded.\n const ids = options.sessionIDs.slice(0, 500)\n clauses.push(`st.session_id IN (${ids.map(() => \"?\").join(\", \")})`)\n params.push(...ids)\n }\n const rows = db\n .query(\n `SELECT st.session_id, s.title, s.directory, s.parent_id,\n GROUP_CONCAT(DISTINCT st.provider_id) AS providers,\n GROUP_CONCAT(DISTINCT st.model_id) AS models,\n (SELECT COUNT(*) FROM prompts p WHERE p.session_id = st.session_id) AS prompts,\n COUNT(*) AS steps,\n COALESCE(SUM(st.input_tokens), 0) AS input,\n COALESCE(SUM(st.cache_read), 0) AS cached,\n COALESCE(SUM(st.cache_write), 0) AS cacheWrite,\n COALESCE(SUM(st.output_tokens), 0) AS output,\n COALESCE(SUM(st.reasoning_tokens), 0) AS reasoning,\n COALESCE(SUM(st.cost), 0) AS cost,\n MIN(COALESCE(st.created_at, st.recorded_at)) AS first_at,\n MAX(COALESCE(st.completed_at, st.created_at, st.recorded_at)) AS last_at\n FROM steps st\n LEFT JOIN sessions s ON s.session_id = st.session_id\n ${clauses.length > 0 ? `WHERE ${clauses.join(\" AND \")}` : \"\"}\n GROUP BY st.session_id\n ORDER BY last_at DESC\n LIMIT ?`,\n )\n .all(\n ...params,\n limit,\n ) as (UsageRow & {\n session_id: string\n title: string | null\n directory: string | null\n parent_id: string | null\n providers: string | null\n models: string | null\n prompts: number | null\n first_at: number | null\n last_at: number | null\n })[]\n\n return rows.map((row) => {\n const usage = usageFrom(row)\n return {\n ...usage,\n sessionID: row.session_id,\n parentID: row.parent_id ?? undefined,\n title: row.title ?? undefined,\n directory: row.directory ?? undefined,\n providers: row.providers ? row.providers.split(\",\").filter(Boolean) : [],\n models: row.models ? row.models.split(\",\").filter(Boolean) : [],\n prompts: row.prompts ?? 0,\n firstAt: row.first_at ?? undefined,\n lastAt: row.last_at ?? undefined,\n cacheHitRatio: cacheHitRatio(usage),\n }\n })\n}\n\n/** Sessions whose parent is `parentID`: the subagents of one session. */\nexport function childSessionIDs(db: Database, parentID: string): string[] {\n return (db.query(\"SELECT session_id FROM sessions WHERE parent_id = ? ORDER BY session_id\").all(parentID) as { session_id: string }[]).map(\n (row) => row.session_id,\n )\n}\n\n/** Session usage rows for a specific set of sessions (subagent rollups). */\nexport function sessionUsageOf(db: Database, sessionIDs: readonly string[]): SessionUsage[] {\n return sessions(db, { sessionIDs, limit: sessionIDs.length })\n}\n\nexport type TurnDrop = {\n messageID: string\n tokens: number\n gapMs: number | undefined\n}\n\n/**\n * One finished turn of a session, built from the recorded steps and prompts. It\n * carries the same buckets the telemetry line uses, so the panel can render the\n * per-prompt history with the line's own formatter.\n */\nexport type TurnUsage = {\n sessionID: string\n /** Creation time of the prompt that opened the turn, when it was recorded. */\n promptAt: number | undefined\n endAt: number | undefined\n /** Time between the previous prompt of the session and this turn's prompt. */\n promptGapMs: number | undefined\n providerID: string | undefined\n modelID: string | undefined\n variant: string | undefined\n providerChanged: boolean\n modelChanged: boolean\n steps: number\n /** Raw buckets of the last step: the prompt the turn ended with. */\n latest: Usage\n /** Raw buckets summed over every step of the turn. */\n totals: Usage\n drops: TurnDrop[]\n /** Finish reason of the turn's last step. */\n finish: string | undefined\n /**\n * Tokens generated per second of streaming over the turn's steps: produced\n * tokens divided by their summed streaming windows. For a one-step turn it is\n * that step's rate.\n */\n tps: number | undefined\n}\n\ntype TurnStepRow = {\n message_id: string\n session_id: string\n provider_id: string | null\n model_id: string | null\n variant: string | null\n input_tokens: number | null\n cache_read: number | null\n cache_write: number | null\n output_tokens: number | null\n reasoning_tokens: number | null\n cost: number | null\n created_at: number | null\n streamed_at: number | null\n recorded_at: number\n completed_at: number | null\n finish: string | null\n}\n\n/** A turn is finished when its last step ended without asking for more tools. */\nfunction turnFinished(finish: string | null): boolean {\n return finish !== null && finish !== \"\" && finish !== \"tool-calls\"\n}\n\nfunction stepUsage(row: TurnStepRow): Usage {\n return {\n steps: 1,\n input: row.input_tokens ?? 0,\n cached: row.cache_read ?? 0,\n cacheWrite: row.cache_write ?? 0,\n output: row.output_tokens ?? 0,\n reasoning: row.reasoning_tokens ?? 0,\n cost: row.cost ?? 0,\n }\n}\n\n/** Structural view of a recorded step, so the shared cache-drop rule applies. */\nfunction stepMessage(row: TurnStepRow): AssistantMessage {\n return {\n type: \"assistant\",\n id: row.message_id,\n time: { created: row.created_at ?? row.recorded_at, completed: row.completed_at ?? undefined },\n model:\n row.provider_id !== null && row.model_id !== null\n ? { providerID: row.provider_id, id: row.model_id, variant: row.variant ?? undefined }\n : undefined,\n tokens: {\n input: row.input_tokens ?? 0,\n output: row.output_tokens ?? 0,\n reasoning: row.reasoning_tokens ?? 0,\n cache: { read: row.cache_read ?? 0, write: row.cache_write ?? 0 },\n },\n finish: row.finish ?? undefined,\n }\n}\n\n/**\n * Finished turns, newest first. Steps belong to the newest prompt recorded at or\n * before them; a turn whose last step asked for tools is dropped, because it is\n * still running — the same rule the telemetry line follows.\n *\n * For one session the read is bounded by a window: only the steps at or after\n * the `limit`-th newest prompt are loaded, so a session with tens of thousands\n * of steps costs the same as a short one. The extra prompt is kept as the\n * reference the oldest returned turn measures its gap against.\n */\nexport function turns(\n db: Database,\n options?: { sessionID?: string | undefined; range?: Range | undefined; filters?: Filters | undefined; limit?: number | undefined },\n): TurnUsage[] {\n const limit = options?.limit ?? 50\n const filter = buildWhere(options?.range, options?.filters)\n const clauses = filter.where === \"\" ? [] : [filter.where.slice(\"WHERE \".length)]\n const params: (number | string)[] = [...filter.params]\n if (options?.sessionID !== undefined) {\n clauses.push(\"session_id = ?\")\n params.push(options.sessionID)\n }\n const floor = options?.sessionID === undefined ? undefined : promptFloor(db, options.sessionID, limit + 1)\n if (floor !== undefined) {\n clauses.push(`${STEP_TIME} >= ?`)\n params.push(floor)\n }\n const steps = db\n .query(\n `SELECT message_id, session_id, provider_id, model_id, variant,\n input_tokens, cache_read, cache_write, output_tokens, reasoning_tokens, cost,\n created_at, streamed_at, recorded_at, completed_at, finish\n FROM steps ${clauses.length > 0 ? `WHERE ${clauses.join(\" AND \")}` : \"\"}\n ORDER BY session_id, ${STEP_TIME}`,\n )\n .all(...params) as TurnStepRow[]\n\n const promptClause = options?.sessionID === undefined ? \"\" : `WHERE session_id = ?${floor === undefined ? \"\" : \" AND created_at >= ?\"}`\n const promptParams = options?.sessionID === undefined ? [] : floor === undefined ? [options.sessionID] : [options.sessionID, floor]\n const promptRows = db\n .query(`SELECT session_id, created_at FROM prompts ${promptClause} ORDER BY session_id, created_at`)\n .all(...promptParams) as { session_id: string; created_at: number | null }[]\n\n const promptsBySession = new Map<string, number[]>()\n for (const row of promptRows) {\n if (row.created_at === null) continue\n const list = promptsBySession.get(row.session_id) ?? []\n list.push(row.created_at)\n promptsBySession.set(row.session_id, list)\n }\n\n const result: TurnUsage[] = []\n let sessionID: string | undefined\n let bucket: TurnStepRow[] = []\n let promptIndex = -2\n let promptAt: number | undefined\n let previousPromptAt: number | undefined\n\n function close(): void {\n const last = bucket[bucket.length - 1]\n if (last === undefined || !turnFinished(last.finish)) {\n bucket = []\n return\n }\n const totals = emptyUsage()\n for (const row of bucket) addUsage(totals, stepUsage(row))\n const drops: TurnDrop[] = []\n let reference: AssistantMessage | undefined\n for (const row of bucket) {\n const message = stepMessage(row)\n const drop = cacheDrop(reference, message)\n if (drop !== undefined) {\n drops.push({ messageID: row.message_id, tokens: drop, gapMs: stepGap(reference, message) })\n }\n reference = message\n }\n const providerID = last.provider_id ?? undefined\n const modelID = last.model_id ?? undefined\n const variant = last.variant ?? undefined\n let produced = 0\n let window = 0\n for (const row of bucket) {\n if (row.created_at === null || row.streamed_at === null || row.streamed_at <= row.created_at) continue\n produced += (row.output_tokens ?? 0) + (row.reasoning_tokens ?? 0)\n window += row.streamed_at - row.created_at\n }\n const previous = result.length > 0 && result[result.length - 1]!.sessionID === last.session_id ? result[result.length - 1] : undefined\n result.push({\n sessionID: last.session_id,\n promptAt,\n endAt: last.completed_at ?? last.created_at ?? last.recorded_at,\n promptGapMs: promptAt !== undefined && previousPromptAt !== undefined ? Math.max(0, promptAt - previousPromptAt) : undefined,\n providerID,\n modelID,\n variant,\n providerChanged: previous?.providerID !== undefined && providerID !== undefined && previous.providerID !== providerID,\n modelChanged:\n previous !== undefined &&\n previous.providerID === providerID &&\n `${previous.modelID ?? \"\"}#${previous.variant ?? \"\"}` !== `${modelID ?? \"\"}#${variant ?? \"\"}`,\n steps: bucket.length,\n latest: stepUsage(last),\n totals,\n drops,\n finish: last.finish ?? undefined,\n tps: tokensPerSecond(produced, 0, window),\n })\n bucket = []\n }\n\n for (const row of steps) {\n if (row.session_id !== sessionID) {\n close()\n sessionID = row.session_id\n promptIndex = -2\n promptAt = undefined\n previousPromptAt = undefined\n }\n const time = row.created_at ?? row.recorded_at\n const prompts = promptsBySession.get(row.session_id) ?? []\n let index = -1\n for (let candidate = prompts.length - 1; candidate >= 0; candidate -= 1) {\n if ((prompts[candidate] ?? Number.POSITIVE_INFINITY) <= time) {\n index = candidate\n break\n }\n }\n if (index !== promptIndex) {\n close()\n promptIndex = index\n promptAt = index >= 0 ? prompts[index] : undefined\n previousPromptAt = index >= 1 ? prompts[index - 1] : undefined\n }\n bucket.push(row)\n }\n close()\n\n return result.reverse().slice(0, limit)\n}\n\n/**\n * Creation time of the `count`-th newest prompt of a session, or undefined\n * when it has fewer. Used to bound the steps a `turns` read has to load.\n */\nfunction promptFloor(db: Database, sessionID: string, count: number): number | undefined {\n const rows = db\n .query(\"SELECT created_at FROM prompts WHERE session_id = ? AND created_at IS NOT NULL ORDER BY created_at DESC LIMIT ?\")\n .all(sessionID, count) as { created_at: number }[]\n return rows.length === count ? rows[count - 1]!.created_at : undefined\n}\n\n// ---------------------------------------------------------------------------\n// Filter pickers\n// ---------------------------------------------------------------------------\n\nexport type FilterOption = { value: string; steps: number }\n\n/** Distinct models and providers, most used first. */\nexport function filterOptions(db: Database): { models: FilterOption[]; providers: FilterOption[] } {\n const pick = (column: string): FilterOption[] =>\n (\n db\n .query(`SELECT ${column} AS value, COUNT(*) AS steps FROM steps WHERE ${column} IS NOT NULL GROUP BY ${column} ORDER BY steps DESC`)\n .all() as { value: string; steps: number }[]\n ).map((row) => ({ value: row.value, steps: row.steps }))\n return { models: pick(\"model_id\"), providers: pick(\"provider_id\") }\n}\n",
11
+ "import type { Database } from \"bun:sqlite\"\nimport { recordEvent, recordPrompt, recordStep, upsertSession } from \"./stats/db\"\nimport { COMPACTION_KIND } from \"./stats/queries\"\n\n/**\n * Event-driven recorder. Every handler is idempotent and defensive: the\n * plugin must never break a session because an event arrived in an\n * unexpected shape, so unknown events are ignored and malformed payloads are\n * dropped rather than thrown.\n *\n * Everything written comes from the event stream: the step's provider, model\n * and variant, its tokens, its timing and its outcome. Pure with respect to\n * its inputs (database plus events) so the whole recording policy can be\n * unit-tested without the plugin runtime.\n */\n\nexport type SessionModelRef = {\n id: string\n providerID: string\n variant?: string | undefined\n}\n\nexport type TokenUsageLike = {\n input?: number | undefined\n output?: number | undefined\n reasoning?: number | undefined\n cache?: { read?: number | undefined; write?: number | undefined } | undefined\n}\n\nexport type RecorderEventData = {\n sessionID?: string | undefined\n assistantMessageID?: string | undefined\n inboxID?: string | undefined\n item?: { type?: string | undefined } | undefined\n started?: number | undefined\n model?: SessionModelRef | undefined\n previous?: SessionModelRef | undefined\n tokens?: TokenUsageLike | undefined\n cost?: number | undefined\n finish?: string | undefined\n rawFinish?: string | undefined\n reason?: string | undefined\n location?: { directory?: string | undefined } | undefined\n parentID?: string | undefined\n title?: string | undefined\n}\n\nexport type RecorderEvent = {\n type: string\n created?: number | undefined\n data?: RecorderEventData | undefined\n}\n\nexport type PendingStep = {\n messageID: string\n sessionID: string\n started: number | undefined\n streamed: number | undefined\n model: SessionModelRef | undefined\n}\n\nexport type RecorderOptions = {\n /** Clock override, for tests. */\n now?: (() => number) | undefined\n}\n\nexport type Recorder = {\n /** Apply one session event. Unknown or malformed events are ignored. */\n handleEvent(event: RecorderEvent): void\n pendingFor(sessionID: string): PendingStep | undefined\n}\n\nexport function createRecorder(db: Database, options?: RecorderOptions): Recorder {\n /**\n * Ceiling on the per-session maps. A session that ends takes its entries\n * with it; a host running for weeks has thousands of sessions, and a map that\n * only ever grows is a leak with a nice name.\n */\n const MAX_TRACKED_SESSIONS = 512\n /** Ceiling on in-flight steps; a step that never ends is a leak too. */\n const MAX_PENDING = 256\n\n /** Steps in flight between `step.started` and `step.ended`. */\n const pending = new Map<string, PendingStep>()\n /** Last model selected per session, used when a step starts unobserved. */\n const lastModel = new Map<string, SessionModelRef>()\n const now = options?.now ?? (() => Date.now())\n\n /**\n * Ceiling on the writes kept while the database is busy. A maintenance\n * operation (a prune, a vacuum) holds the write lock; waiting for it would\n * stall the agent, and dropping the record would lose usage. The recorder\n * defers instead, retrying on the next event, and only a lock held for longer\n * than this many records takes a record with it.\n */\n const MAX_DEFERRED = 256\n const deferred: Array<() => void> = []\n\n /** True when the database was locked rather than the statement being wrong. */\n function isBusy(error: unknown): boolean {\n const code = (error as { code?: unknown } | null | undefined)?.code\n return code === \"SQLITE_BUSY\" || code === \"SQLITE_LOCKED\"\n }\n\n function enqueue(run: () => void): void {\n if (deferred.length >= MAX_DEFERRED) deferred.shift()\n deferred.push(run)\n }\n\n /** Retry what a busy database postponed, oldest first, stopping at the first failure. */\n function flushDeferred(): void {\n while (deferred.length > 0) {\n const next = deferred[0]!\n try {\n next()\n } catch {\n return\n }\n deferred.shift()\n }\n }\n\n /**\n * Run one write. On a busy database the work is kept for the next event\n * instead of waiting for the lock: the recorder is on the agent's path, and\n * the record is worth more late than the agent is worth stalled.\n */\n function write(run: () => void): void {\n if (deferred.length > 0) {\n flushDeferred()\n // Still locked: this write has to wait behind the queued ones.\n if (deferred.length > 0) {\n enqueue(run)\n return\n }\n }\n try {\n run()\n } catch (error) {\n if (isBusy(error)) enqueue(run)\n // Anything else is swallowed, as before: telemetry never breaks a session.\n }\n }\n\n /** Insertion-ordered map with a size ceiling: the oldest entry goes first. */\n function remember<Value>(map: Map<string, Value>, key: string, value: Value, limit: number): void {\n map.delete(key)\n map.set(key, value)\n while (map.size > limit) {\n const oldest = map.keys().next()\n if (oldest.done === true) break\n map.delete(oldest.value)\n }\n }\n\n /** Drop everything remembered about a session that is gone. */\n function forget(sessionID: string): void {\n lastModel.delete(sessionID)\n for (const [messageID, step] of pending) {\n if (step.sessionID === sessionID) pending.delete(messageID)\n }\n }\n\n function recordPendingStep(messageID: string, data: RecorderEventData, finish: string | undefined, at: number): void {\n const sessionID = data.sessionID\n if (typeof sessionID !== \"string\") return\n const info = pending.get(messageID)\n pending.delete(messageID)\n const model = info?.model ?? lastModel.get(sessionID)\n write(() =>\n recordStep(db, {\n messageID,\n sessionID,\n providerID: model?.providerID,\n modelID: model?.id,\n variant: model?.variant,\n input: data.tokens?.input,\n output: data.tokens?.output,\n reasoning: data.tokens?.reasoning,\n cacheRead: data.tokens?.cache?.read,\n cacheWrite: data.tokens?.cache?.write,\n cost: data.cost,\n createdAt: info?.started,\n streamedAt: info?.streamed,\n completedAt: at,\n finish,\n recordedAt: now(),\n }),\n )\n }\n\n function handleEvent(event: RecorderEvent): void {\n // A previous write may have been postponed: this is the cheapest moment to\n // land it, and the check costs nothing while the queue is empty.\n if (deferred.length > 0) flushDeferred()\n const data = event.data ?? {}\n const at = typeof event.created === \"number\" ? event.created : now()\n const sessionID = typeof data.sessionID === \"string\" ? data.sessionID : undefined\n\n switch (event.type) {\n case \"session.created\": {\n if (sessionID === undefined) return\n write(() =>\n upsertSession(db, {\n sessionID,\n parentID: data.parentID,\n directory: data.location?.directory,\n title: data.title,\n startedAt: at,\n updatedAt: at,\n }),\n )\n if (data.model !== undefined) remember(lastModel, sessionID, data.model, MAX_TRACKED_SESSIONS)\n return\n }\n case \"session.deleted\": {\n // The session is gone: its in-flight steps will never end and nothing\n // else about it will be needed again.\n if (sessionID !== undefined) forget(sessionID)\n return\n }\n case \"session.renamed\": {\n if (sessionID === undefined) return\n write(() => upsertSession(db, { sessionID, title: data.title, updatedAt: at }))\n return\n }\n case \"session.moved\": {\n if (sessionID === undefined) return\n write(() => upsertSession(db, { sessionID, directory: data.location?.directory, updatedAt: at }))\n return\n }\n case \"session.inbox.enqueued\": {\n if (sessionID === undefined) return\n if (data.item?.type !== \"user\" && data.item?.type !== \"synthetic\") return\n // Narrowed into a local: a closure does not keep a property's narrowing.\n const inboxID = data.inboxID\n if (typeof inboxID !== \"string\") return\n write(() => recordPrompt(db, { sessionID, messageID: inboxID, createdAt: at }))\n return\n }\n case \"session.model.selected\": {\n // The session's current model, remembered so a step that started before\n // the plugin loaded is still attributed. Nothing is written for it: no\n // view reads a model-selection event.\n if (sessionID !== undefined && data.model !== undefined) remember(lastModel, sessionID, data.model, MAX_TRACKED_SESSIONS)\n return\n }\n case \"session.step.started\": {\n if (sessionID === undefined || typeof data.assistantMessageID !== \"string\") return\n remember(\n pending,\n data.assistantMessageID,\n {\n messageID: data.assistantMessageID,\n sessionID,\n started: typeof data.started === \"number\" ? data.started : undefined,\n streamed: undefined,\n model: data.model,\n },\n MAX_PENDING,\n )\n return\n }\n case \"session.step.streamed\": {\n if (typeof data.assistantMessageID !== \"string\") return\n const info = pending.get(data.assistantMessageID)\n if (info !== undefined) info.streamed = at\n return\n }\n case \"session.step.ended\": {\n if (typeof data.assistantMessageID !== \"string\") return\n recordPendingStep(data.assistantMessageID, data, data.finish ?? \"unknown\", at)\n return\n }\n case \"session.step.failed\": {\n if (typeof data.assistantMessageID !== \"string\") return\n // The step row keeps the outcome through `finish`; the error event is\n // not written because no view reads it.\n recordPendingStep(data.assistantMessageID, data, data.finish ?? \"error\", at)\n return\n }\n case \"session.compaction.ended\": {\n if (sessionID === undefined) return\n write(() =>\n recordEvent(db, {\n sessionID,\n kind: COMPACTION_KIND,\n at,\n data: { reason: data.reason, model: data.model, tokens: data.tokens, cost: data.cost },\n }),\n )\n return\n }\n default:\n return\n }\n }\n\n function pendingFor(sessionID: string): PendingStep | undefined {\n let found: PendingStep | undefined\n for (const item of pending.values()) {\n if (item.sessionID !== sessionID) continue\n if (found === undefined || (item.started ?? 0) >= (found.started ?? 0)) found = item\n }\n return found\n }\n\n return { handleEvent, pendingFor }\n}\n"
13
12
  ],
14
- "mappings": "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",
15
- "debugId": "B7051C41CF32481164756E2164756E21",
13
+ "mappings": "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",
14
+ "debugId": "CB2A8AAFDC0E182864756E2164756E21",
16
15
  "names": []
17
16
  }