opencode-usage-insights 0.0.0-stage → 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.
@@ -0,0 +1,16 @@
1
+ {
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/recorder.ts"],
4
+ "sourcesContent": [
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
+ "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 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"
12
+ ],
13
+ "mappings": "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",
14
+ "debugId": "CB2A8AAFDC0E182864756E2164756E21",
15
+ "names": []
16
+ }
package/dist/line.d.ts ADDED
@@ -0,0 +1,71 @@
1
+ import { type LineFields, type LineLabels, type LineTone, type Totals, type TranscriptMessage, type TurnStats, type TurnTotalsView } from "./core";
2
+ /**
3
+ * Adapter between the host's session messages and the pure turn accounting in
4
+ * `core`, plus the plain-text parts the TUI line is made of.
5
+ *
6
+ * The host types are shadowed structurally: the plugin only reads fields it
7
+ * relies on, so a host refactor degrades the line instead of breaking setup.
8
+ */
9
+ export type RuntimeModelRef = {
10
+ providerID?: string | undefined;
11
+ id?: string | undefined;
12
+ variant?: string | undefined;
13
+ };
14
+ export type RuntimeTokenUsage = {
15
+ input?: number | undefined;
16
+ output?: number | undefined;
17
+ reasoning?: number | undefined;
18
+ cache?: {
19
+ read?: number | undefined;
20
+ write?: number | undefined;
21
+ } | undefined;
22
+ };
23
+ export type RuntimeMessage = {
24
+ id?: string | undefined;
25
+ type?: string | undefined;
26
+ time?: {
27
+ created?: number | undefined;
28
+ streamed?: number | undefined;
29
+ completed?: number | undefined;
30
+ } | undefined;
31
+ model?: RuntimeModelRef | undefined;
32
+ tokens?: RuntimeTokenUsage | undefined;
33
+ cost?: number | undefined;
34
+ finish?: string | null | undefined;
35
+ rawFinish?: string | null | undefined;
36
+ };
37
+ export type RuntimeSessionInfo = {
38
+ cost?: number | undefined;
39
+ tokens?: RuntimeTokenUsage | undefined;
40
+ time?: {
41
+ created?: number | undefined;
42
+ } | undefined;
43
+ };
44
+ export type LinePart = {
45
+ text: string;
46
+ tone: LineTone;
47
+ };
48
+ /** Convert host messages into the structural transcript `core` understands. */
49
+ export declare function toTranscript(messages: readonly RuntimeMessage[]): TranscriptMessage[];
50
+ /**
51
+ * Convert only the messages of the last `segments` turns.
52
+ *
53
+ * The line is computed from the tail of a transcript (`computeTurn` walks it
54
+ * backwards), so converting the whole session was pure waste: on a long session
55
+ * it cost milliseconds at every turn end, on the thread that also paints the
56
+ * agent's output. Walking backwards costs what the last turns hold.
57
+ */
58
+ export declare function toTranscriptTail(messages: readonly RuntimeMessage[], segments?: number): TranscriptMessage[];
59
+ /** Session-wide accumulated usage, as reported by the host on the session. */
60
+ export declare function sessionTotals(info: RuntimeSessionInfo | undefined): Totals | undefined;
61
+ /**
62
+ * The telemetry line as renderable parts: each segment plus the muted
63
+ * separators between them, so the TUI can color every part independently.
64
+ */
65
+ export declare function lineParts(stats: TurnStats | undefined, options?: {
66
+ labels?: Partial<LineLabels>;
67
+ fields?: Partial<LineFields>;
68
+ turnTotals?: TurnTotalsView;
69
+ }): LinePart[];
70
+ /** Plain text of the same line, useful for tests and logs. */
71
+ export declare function lineText(parts: readonly LinePart[]): string;
@@ -0,0 +1,46 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import { type CacheStats, type PairRow, type SwitchRow } from "./stats/aggregates";
3
+ import { type Filters } from "./stats/queries";
4
+ import type { DbInfo } from "./stats/maintenance";
5
+ import { type Granularity, type LineFormatOptions, type PanelLine, type PanelRow, type PanelView, type RangeKey } from "./panel-data";
6
+ /**
7
+ * The panel's data assembly: read the database, run the queries the active view
8
+ * needs and hand plain lines to the renderer. Kept out of the component so
9
+ * every view can be asserted from an in-memory database, exactly as the CLI
10
+ * report renders it.
11
+ */
12
+ export type TurnsScope = "session" | "all";
13
+ /** Everything the panel shows, in one request object. */
14
+ export type PanelRequest = {
15
+ db: Database;
16
+ view: PanelView;
17
+ range: RangeKey;
18
+ filters: Filters;
19
+ granularity: Granularity;
20
+ turnsScope: TurnsScope;
21
+ sort: string;
22
+ sortDesc: boolean;
23
+ sessionID: string;
24
+ sessionStart: number | undefined;
25
+ now: number;
26
+ format?: LineFormatOptions | undefined;
27
+ /** Drill-down target: when set, the panel shows this detail instead. */
28
+ detail?: PanelRow | undefined;
29
+ /** The database view's own state: where the file is and what it holds. */
30
+ database?: DbInfo | undefined;
31
+ /** Set when the last vacuum could not take the write lock. */
32
+ busy?: boolean | undefined;
33
+ width: number;
34
+ };
35
+ /** Sorting options per view; the first is the default. */
36
+ export declare const SORT_KEYS: Partial<Record<PanelView, readonly string[]>>;
37
+ export declare function sortKeys(view: PanelView): readonly string[];
38
+ export declare function defaultSort(view: PanelView): string;
39
+ export declare function nextSort(view: PanelView, current: string, step: number): string;
40
+ export declare function detailLabel(row: PanelRow): string;
41
+ /** Build the panel lines for the request. Never throws: a failure is a line. */
42
+ export declare function buildPanel(request: PanelRequest): PanelLine[];
43
+ /** Token reuse per model, for the models view and the cache view. */
44
+ export declare function cacheByModel(pairs: readonly PairRow[]): Map<string, CacheStats>;
45
+ /** The switch summary per `modelKey`, as the models view reads it. */
46
+ export declare function switchesByModel(rows: readonly SwitchRow[]): Map<string, SwitchRow>;
@@ -0,0 +1,216 @@
1
+ import { type LineFields, type LineLabels, type TurnStats, type TurnTotalsView } from "./core";
2
+ import type { CacheDropRow, LatencyReport, ModelUsage, Overview, Range, SessionUsage, TurnUsage, TtlEvidenceRow } from "./stats/queries";
3
+ import type { DbInfo } from "./stats/maintenance";
4
+ import type { CacheStats, CoverageReport, ProviderRow, ProviderSummary, GapReport, LatencyGroupRow, PeriodGranularity, PeriodRow, SwitchRow } from "./stats/aggregates";
5
+ /**
6
+ * View models and formatting for the stats panel. Everything here is pure:
7
+ * the panel component feeds query results in and renders the lines out, which
8
+ * keeps the layout testable without a terminal. Lines may carry a `row`, the
9
+ * identity the cursor selects and Enter drills into.
10
+ */
11
+ export declare const PANEL_NAME = "usage-insights";
12
+ export declare const PANEL_VIEWS: readonly ["overview", "models", "providers", "cache", "periods", "latency", "sessions", "turns", "db"];
13
+ export type PanelView = (typeof PANEL_VIEWS)[number];
14
+ export declare const RANGE_KEYS: readonly ["session", "today", "7d", "30d", "all"];
15
+ export type RangeKey = (typeof RANGE_KEYS)[number];
16
+ export declare const GRANULARITIES: readonly ["day", "week", "month"];
17
+ export type Granularity = (typeof GRANULARITIES)[number];
18
+ export type PanelTone = "base" | "muted" | "warning" | "accent";
19
+ /** What a selectable line stands for; Enter drills into it. */
20
+ export type PanelRow = {
21
+ kind: "model" | "session" | "period" | "provider" | "turn" | "prune";
22
+ value: string;
23
+ variant?: string | undefined;
24
+ };
25
+ export type PanelLine = {
26
+ text: string;
27
+ tone?: PanelTone;
28
+ row?: PanelRow | undefined;
29
+ };
30
+ /** The views and ranges, as the command and the panel header understand them. */
31
+ export declare function parseArgs(input: string | undefined): {
32
+ view?: PanelView;
33
+ range?: RangeKey;
34
+ };
35
+ export declare function rangeFor(key: RangeKey, now: number, sessionStart?: number | undefined): Range | undefined;
36
+ export declare function rangeLabel(key: RangeKey): string;
37
+ export declare function nextIn<Value>(values: readonly Value[], current: Value, step: number): Value;
38
+ /** Which turns the `turns` view lists: the open session, or every session. */
39
+ export type TurnsScope = "session" | "all";
40
+ /**
41
+ * Everything the panel remembers between openings. Stored through the host's
42
+ * durable storage, so the panel reopens where the user left it.
43
+ */
44
+ export type PanelSettings = {
45
+ view: PanelView;
46
+ range: RangeKey;
47
+ sort: string;
48
+ sortDesc: boolean;
49
+ granularity: Granularity;
50
+ turnsScope: TurnsScope;
51
+ /** Empty string means "no filter". */
52
+ modelID: string;
53
+ /** The provider the model ran through. */
54
+ providerID: string;
55
+ };
56
+ export declare const DEFAULT_SETTINGS: PanelSettings;
57
+ /** Merge persisted state with the defaults, dropping anything unrecognized. */
58
+ export declare function sanitizeSettings(raw: Partial<PanelSettings> | undefined): PanelSettings;
59
+ export declare function formatPercent(value: number | undefined, digits?: number): string;
60
+ export declare function formatCount(value: number): string;
61
+ export declare function formatMs(value: number | undefined): string;
62
+ export declare function formatTps(value: number | undefined): string;
63
+ /** A rate that has to fit a sidebar: whole numbers from 10 up. */
64
+ export declare function formatTpsCompact(value: number | undefined): string;
65
+ /** Idle gaps are read at a glance: seconds, minutes, hours, days. */
66
+ export declare function formatGap(value: number | undefined): string;
67
+ /** Cut a value to the width, dropping control characters first. */
68
+ export declare function truncate(value: string, width: number): string;
69
+ /** Keep both ends of a long id: the family reads first, the `#variant` last. */
70
+ export declare function truncateMiddle(value: string, width: number): string;
71
+ /**
72
+ * Pick the widest column layout that fits the panel. A table that overflows
73
+ * wraps and stops reading as a table, so the columns are chosen, not squeezed.
74
+ */
75
+ export declare function tableWidths(width: number, layouts: readonly (readonly number[])[]): number[];
76
+ /**
77
+ * A model cell keeps the variant visible: two rows of the same family must not
78
+ * read alike after truncation.
79
+ */
80
+ export declare function modelCell(modelID: string, variant: string | undefined, width: number): string;
81
+ export declare function padCell(value: string, width: number, align?: "left" | "right"): string;
82
+ export declare function formatTableRow(cells: readonly string[], widths: readonly number[], aligns?: readonly ("left" | "right")[]): string;
83
+ export declare function count(n: number, singular: string, plural?: string): string;
84
+ /** Two label/value pairs per row, collapsed to one pair on narrow panels. */
85
+ export declare function kvGrid(pairs: readonly (readonly [string, string])[], width: number): PanelLine[];
86
+ /** Fold a series into at most `maxBars` summed buckets, so a sparkline stays a line. */
87
+ export declare function reduceSeries(values: readonly number[], maxBars?: number): number[];
88
+ /** Text sparkline: trends at a glance without a chart renderable. */
89
+ export declare function sparkline(values: readonly number[], maxBars?: number): string;
90
+ /**
91
+ * The live line's buckets, so every aggregate the panel shows reconciles with
92
+ * it: `in` is the uncached prompt (fresh input *and* cache writes), `prompt` is
93
+ * what the provider saw, `out` is everything generated (visible *and*
94
+ * reasoning). `write` and `reasoning` are the parts of `in` and `out`.
95
+ */
96
+ export type UsageBuckets = {
97
+ steps: number;
98
+ input: number;
99
+ prompt: number;
100
+ cached: number;
101
+ cacheWrite: number;
102
+ output: number;
103
+ reasoning: number;
104
+ cost: number;
105
+ hit: number | undefined;
106
+ };
107
+ export declare function usageBuckets(usage: {
108
+ steps: number;
109
+ input: number;
110
+ cached: number;
111
+ cacheWrite: number;
112
+ output: number;
113
+ reasoning: number;
114
+ cost: number;
115
+ }): UsageBuckets;
116
+ /**
117
+ * The view tabs. The active one is bracketed so a single tone still reads
118
+ * clearly; on a narrow panel the names shorten instead of wrapping, and the
119
+ * active view always keeps its full name.
120
+ */
121
+ export declare function viewBar(view: PanelView, width?: number): PanelLine;
122
+ export declare function keysFor(of: PanelView): string;
123
+ export declare function filterSummary(filters: {
124
+ modelID?: string | undefined;
125
+ providerID?: string | undefined;
126
+ }): string | undefined;
127
+ export declare function granularityLabel(granularity: PeriodGranularity): string;
128
+ /** Help overlay: the keys and the exact meaning of every number. */
129
+ export declare function helpLines(width: number): PanelLine[];
130
+ export declare function overviewLines(data: {
131
+ usage: Overview;
132
+ session: Overview | undefined;
133
+ /** Child sessions of the open session, when it has any. */
134
+ subagents?: readonly SessionUsage[] | undefined;
135
+ range: RangeKey;
136
+ activity?: readonly number[] | undefined;
137
+ coverage?: CoverageReport | undefined;
138
+ }, width: number): PanelLine[];
139
+ export type ModelExtras = {
140
+ /** Reuse stats per `modelKey`, computed from the step facts. */
141
+ cache?: Map<string, CacheStats> | undefined;
142
+ /** Switches per `modelKey`, computed from the consecutive step pairs. */
143
+ switches?: Map<string, SwitchRow> | undefined;
144
+ };
145
+ /**
146
+ * Usage per model with the cache it actually got, the providers it ran through
147
+ * and how often its identity changed. One line per model; `providers` keeps the
148
+ * per-provider detail.
149
+ */
150
+ export declare function modelsLines(rows: readonly ModelUsage[], width: number, extras?: ModelExtras, limit?: number): PanelLine[];
151
+ /** One row per provider, over every model it served. */
152
+ export declare function providerSummaryLines(rows: readonly ProviderSummary[], width: number, limit?: number): PanelLine[];
153
+ /** One row per model × provider: the providers each model ran through. */
154
+ export declare function providerLines(rows: readonly ProviderRow[], width: number, limit?: number): PanelLine[];
155
+ /** One model's reuse row, shared by the models and cache views. */
156
+ export type CacheModelRow = {
157
+ usage: {
158
+ modelID: string;
159
+ variant: string | undefined;
160
+ };
161
+ cache: CacheStats;
162
+ };
163
+ export declare function cacheLines(data: {
164
+ drops: readonly CacheDropRow[];
165
+ evidence: readonly TtlEvidenceRow[];
166
+ gap?: GapReport | undefined;
167
+ models?: readonly CacheModelRow[] | undefined;
168
+ }, width: number): PanelLine[];
169
+ export declare function periodLines(rows: readonly PeriodRow[], width: number, granularity: PeriodGranularity, limit?: number): PanelLine[];
170
+ export declare function latencyLines(report: LatencyReport, width: number, groups?: readonly LatencyGroupRow[] | undefined, limit?: number): PanelLine[];
171
+ export declare function sessionsLines(rows: readonly SessionUsage[], width: number, options?: {
172
+ limit?: number | undefined;
173
+ rollup?: {
174
+ subagentSteps: number;
175
+ subagentCost: number;
176
+ } | undefined;
177
+ }): PanelLine[];
178
+ /** The database view: where it lives, what it holds, and the two cleanups. */
179
+ export declare function dbLines(data: {
180
+ info: DbInfo;
181
+ busy?: boolean | undefined;
182
+ now: number;
183
+ }, width: number): PanelLine[];
184
+ export type LineFormatOptions = {
185
+ labels?: Partial<LineLabels> | undefined;
186
+ fields?: Partial<LineFields> | undefined;
187
+ turnTotals?: TurnTotalsView | undefined;
188
+ };
189
+ /** The line's own view of a recorded turn, so both render identically. */
190
+ export declare function turnStats(turn: TurnUsage): TurnStats;
191
+ /**
192
+ * Per-prompt history: one line per finished turn, rendered by the same
193
+ * formatter as the live line (without the session segment, which would repeat
194
+ * the same totals on every row).
195
+ */
196
+ export declare function turnsLines(rows: readonly TurnUsage[], width: number, options?: LineFormatOptions & {
197
+ sessionTitles?: Map<string, string> | undefined;
198
+ }): PanelLine[];
199
+ /**
200
+ * The accumulated usage of one session, as compact lines for the sidebar. The
201
+ * buckets are the live line's, so the sidebar, the line and the panel agree.
202
+ */
203
+ export declare function sessionSummaryLines(usage: Overview, budget?: number): PanelLine[];
204
+ /**
205
+ * The sidebar history: a compact block per turn — when and through which
206
+ * provider, then the prompt volumes, then the cache hit, the rate and the cost.
207
+ *
208
+ * The model gets a line of its own at both ends of the block and on every turn
209
+ * that changed it. The ends matter because the reader cannot tell whether a
210
+ * block is the whole history or a window of a long session: the newest turn
211
+ * names the model in use now, the oldest names the one the window started with,
212
+ * and everything between them is read against the nearest line below it.
213
+ */
214
+ export declare function sidebarLines(rows: readonly TurnUsage[], options?: LineFormatOptions & {
215
+ budget?: number | undefined;
216
+ }): PanelLine[];