@krtclcdy/tersio 2.25.5 → 2.25.6

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.
Files changed (73) hide show
  1. package/README.md +17 -36
  2. package/dashboard/dist/index.html +15 -15
  3. package/dist/cli/backups.js +7 -14
  4. package/dist/cli/banner.js +4 -3
  5. package/dist/cli/common.js +28 -12
  6. package/dist/cli/currency.js +1 -1
  7. package/dist/cli/dashboard.js +66 -35
  8. package/dist/cli/doctor-fix.js +204 -19
  9. package/dist/cli/doctor.js +226 -39
  10. package/dist/cli/hosts.js +25 -5
  11. package/dist/cli/install.js +144 -48
  12. package/dist/cli/interactive.js +3 -3
  13. package/dist/cli/manifest.js +87 -7
  14. package/dist/cli/profile.js +13 -8
  15. package/dist/cli/rtk-wiring.js +1 -1
  16. package/dist/cli/settings.js +20 -5
  17. package/dist/cli/uninstall.js +3 -3
  18. package/dist/cli/update.js +6 -7
  19. package/dist/cli/usage.js +6 -9
  20. package/dist/extensions/caveman/index.js +4 -2
  21. package/dist/extensions/caveman/rule-megacave.md +5 -3
  22. package/dist/extensions/caveman/rule-ultra.md +4 -4
  23. package/dist/extensions/caveman/rule.md +55 -45
  24. package/dist/extensions/combo/index.js +62 -21
  25. package/dist/extensions/commands/index.js +1 -1
  26. package/dist/extensions/pstack/index.js +119 -0
  27. package/dist/extensions/pstack/rule.md +65 -0
  28. package/dist/extensions/rtk/filter.js +0 -1
  29. package/dist/extensions/rtk/session.js +4 -4
  30. package/dist/extensions/server.js +90 -6
  31. package/dist/extensions/shared/agent-stats.js +214 -278
  32. package/dist/extensions/shared/brand-marks.js +33 -34
  33. package/dist/extensions/shared/carbon.js +1 -1
  34. package/dist/extensions/shared/plugin-settings.js +11 -6
  35. package/dist/extensions/shared/pricing.js +8 -16
  36. package/dist/extensions/shared/rtk-gain.js +4 -9
  37. package/dist/extensions/shared/session-state.js +15 -10
  38. package/dist/extensions/shared/sqlite.js +168 -0
  39. package/dist/extensions/shared/status.js +8 -8
  40. package/dist/extensions/shared/types.js +1 -1
  41. package/dist/extensions/shared/usage-ledger.js +53 -41
  42. package/dist/extensions/shared/usage-store.js +111 -52
  43. package/dist/extensions/shared/utils.js +5 -5
  44. package/dist/extensions/timing/index.js +31 -0
  45. package/dist/extensions/updater/index.js +3 -6
  46. package/dist/tersio.js +8 -3
  47. package/extensions/caveman/index.ts +4 -2
  48. package/extensions/caveman/rule-megacave.md +5 -3
  49. package/extensions/caveman/rule-ultra.md +4 -4
  50. package/extensions/caveman/rule.md +55 -45
  51. package/extensions/combo/index.ts +62 -19
  52. package/extensions/commands/index.ts +1 -1
  53. package/extensions/pstack/index.ts +128 -0
  54. package/extensions/pstack/rule.md +65 -0
  55. package/extensions/rtk/filter.ts +0 -1
  56. package/extensions/rtk/session.ts +4 -4
  57. package/extensions/server.ts +72 -7
  58. package/extensions/shared/agent-stats.ts +226 -278
  59. package/extensions/shared/brand-marks.ts +34 -34
  60. package/extensions/shared/carbon.ts +1 -1
  61. package/extensions/shared/plugin-settings.ts +12 -6
  62. package/extensions/shared/pricing.ts +10 -26
  63. package/extensions/shared/rtk-gain.ts +4 -9
  64. package/extensions/shared/session-state.ts +18 -13
  65. package/extensions/shared/sqlite.ts +168 -0
  66. package/extensions/shared/status.ts +9 -8
  67. package/extensions/shared/types.ts +2 -1
  68. package/extensions/shared/usage-ledger.ts +60 -47
  69. package/extensions/shared/usage-store.ts +120 -52
  70. package/extensions/shared/utils.ts +5 -5
  71. package/extensions/timing/index.ts +45 -0
  72. package/extensions/updater/index.ts +3 -8
  73. package/package.json +17 -8
@@ -1,10 +1,11 @@
1
- // Shared OMP extension host types. Structural — the real `pi` object comes from the OMP runtime; these describe the surface this package touches.
1
+ // Shared OMP extension host types. Structural — the real `pi` object comes from the OMP runtime.
2
2
 
3
3
  export interface ComboState {
4
4
  level: string;
5
5
  caveman: string;
6
6
  rtk: string;
7
7
  ponytail: string;
8
+ pstack: string;
8
9
  }
9
10
 
10
11
  export type ComboLevel = 'off' | 'medium' | 'balanced' | 'max' | 'custom';
@@ -1,8 +1,9 @@
1
- // Shared usage ledger: append-only JSON lines, read by the CLI, `/tersio`, and the Dashboard. Best-effort; corrupt lines are skipped on read.
1
+ // Usage ledger: append-only JSON lines, read by the CLI, `/tersio`, and the Dashboard. Corrupt lines are skipped.
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import { homeDir, isPiProcess, piAgentDir, resolveRtkBinary, tersioDataPath } from './utils.ts';
5
4
  import { execFileSync } from 'node:child_process';
5
+ import { homeDir, isPiProcess, piAgentDir, resolveRtkBinary, tersioDataPath } from './utils.ts';
6
+ import { sqliteRead } from './sqlite.ts';
6
7
 
7
8
  export type UsageKind = 'command' | 'toggle' | 'install' | 'update' | 'rtk-audit';
8
9
 
@@ -50,7 +51,7 @@ export function appendUsage(kind: UsageKind, detail: string): void {
50
51
  } catch { /* ledger is best-effort; never break the caller */ }
51
52
  }
52
53
 
53
- // --- Session tokens, tokscale-style ------------------------------------------ Assistant messages in the host transcripts carry usage, model, and a timestamp.
54
+ // Session tokens, tokscale-style: assistant messages in the host transcripts carry usage, model, and a timestamp.
54
55
  export interface TokenBreakdown {
55
56
  input: number;
56
57
  output: number;
@@ -67,6 +68,8 @@ export interface RecentRequest {
67
68
  o: number;
68
69
  t: number;
69
70
  d?: number;
71
+ /** First-token latency, when the host recorded one. */
72
+ tf?: number;
70
73
  /** Which agent wrote the session: pi, omp, or codex. */
71
74
  h?: string;
72
75
  cr?: number;
@@ -127,7 +130,7 @@ export function durOf(v: unknown): number | undefined {
127
130
  return typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : undefined;
128
131
  }
129
132
 
130
- // Measured cost: usage.cost.total, or a bare number in old fixtures. Anything else means unrecorded, which is deliberately not 0.
133
+ // Measured cost: usage.cost.total, or a bare number in old fixtures. Anything else means unrecorded, not 0.
131
134
  export function costOf(usage: Record<string, unknown>): number | undefined {
132
135
  const c = usage.cost;
133
136
  if (typeof c === 'number') return Number.isFinite(c) ? c : undefined;
@@ -162,7 +165,7 @@ function addInto(into: TokenBreakdown, u: { input?: unknown; output?: unknown; c
162
165
  export function sessionsDirs(): string[] {
163
166
  const override = process.env.TERSIO_SESSIONS_DIR;
164
167
  if (override) return [override];
165
- // Both hosts, not just the running one: a session on pi is missing from the dashboard entirely if we stop at the first directory that exists.
168
+ // Both hosts, not just the running one: a pi session is missing if we stop at the first existing directory.
166
169
  const dirs = isPiProcess()
167
170
  ? [path.join(piAgentDir(), 'sessions'), path.join(homeDir(), '.omp', 'agent', 'sessions')]
168
171
  : [path.join(homeDir(), '.omp', 'agent', 'sessions'), path.join(piAgentDir(), 'sessions')];
@@ -204,55 +207,55 @@ export interface OpencodeDbRow {
204
207
  id: string;
205
208
  created: number;
206
209
  row: OpencodeMessage;
210
+ /** Tool names this message called, comma-joined. Empty when it called none. */
211
+ tools?: string;
207
212
  }
208
213
 
209
- // Two on-disk shapes: legacy `message` (role/modelID/providerID live inside `data`),
210
- // current `session_message` (role in the `type` column, model under data.model).
211
- // Scalars only: the `data` blob holds newlines and tabs, which the sqlite3 CLI would split.
214
+ // `message` vs `session_message`: read scalars only, since the blob's newlines break the row-per-line output.
212
215
  const OC_DB_ORDER = ' ORDER BY time_created;';
213
216
  const OC_DB_COLUMNS =
214
- 'json_extract(data,\'$.time.created\'), json_extract(data,\'$.time.completed\'),' +
217
+ 'json_extract(data,\'$.time.created\'), json_extract(data,\'$.time.streamed\'), json_extract(data,\'$.time.completed\'),' +
215
218
  ' json_extract(data,\'$.tokens.input\'), json_extract(data,\'$.tokens.output\'),' +
216
219
  ' json_extract(data,\'$.tokens.cache.read\'), json_extract(data,\'$.tokens.cache.write\'), json_extract(data,\'$.cost\')';
220
+ const OC_DB_TOOLS =
221
+ '(SELECT group_concat(json_extract(j.value,\'$.name\')) FROM json_each(data,\'$.content\') j' +
222
+ ' WHERE json_extract(j.value,\'$.type\')=\'tool\')';
217
223
  const ocDbLegacy = (since: number): string =>
218
- `SELECT id, time_created, json_extract(data,'$.role'), json_extract(data,'$.providerID'), json_extract(data,'$.modelID'), ${OC_DB_COLUMNS}` +
224
+ `SELECT id, time_created, json_extract(data,'$.role'), json_extract(data,'$.providerID'), json_extract(data,'$.modelID'), ${OC_DB_COLUMNS}, ${OC_DB_TOOLS}` +
219
225
  ` FROM message WHERE json_extract(data,'$.role')='assistant' AND time_created > ${since}${OC_DB_ORDER}`;
220
226
  const ocDbCurrent = (since: number): string =>
221
- `SELECT id, time_created, 'assistant', json_extract(data,'$.model.providerID'), json_extract(data,'$.model.id'), ${OC_DB_COLUMNS}` +
227
+ `SELECT id, time_created, 'assistant', json_extract(data,'$.model.providerID'), json_extract(data,'$.model.id'), ${OC_DB_COLUMNS}, ${OC_DB_TOOLS}` +
222
228
  ` FROM session_message WHERE type='assistant' AND time_created > ${since}${OC_DB_ORDER}`;
223
229
 
224
230
  // A re-scan window covers tokens that land after the row was created, so a late fill is not lost.
225
231
  export const OPENCODE_DB_OVERLAP_MS = 3_600_000;
226
232
 
227
- // execFileSync caps output at 1MB; a busy table clears it in one SELECT.
228
- export const SQLITE_READ_BUFFER = 50 * 1024 * 1024;
229
-
230
- // Empty when sqlite3 is absent or the db is unreadable; that is a sync skip, not a failure.
233
+ // Empty when no SQLite is available or the db is unreadable; that is a sync skip, not a failure.
231
234
  export function readOpencodeDbRows(db: string, sinceMs: number): OpencodeDbRow[] {
232
235
  if (!Number.isFinite(sinceMs)) return [];
233
236
  try {
234
237
  if (!fs.existsSync(db)) return [];
235
238
  const since = Math.max(0, Math.floor(sinceMs));
236
239
  // Separate calls: a db holding only one table must not cancel the other.
237
- const run = (sql: string): string =>
238
- execFileSync('sqlite3', ['-separator', '\t', '-readonly', db, sql], { encoding: 'utf8', timeout: 30000, stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: SQLITE_READ_BUFFER });
240
+ const run = (sql: string): string => sqliteRead(db, sql);
239
241
  let out = '';
240
242
  try { out += run(ocDbLegacy(since)); } catch { /* no legacy table or unreadable */ }
241
243
  try { out += run(ocDbCurrent(since)); } catch { /* no session_message table or unreadable */ }
242
244
  const rows: OpencodeDbRow[] = [];
243
245
  for (const line of out.split('\n')) {
244
246
  if (!line.trim()) continue;
245
- const [id, created, role, providerID, modelID, tCreated, tCompleted, input, output, cacheRead, cacheWrite, cost] = line.split('\t');
247
+ const [id, created, role, providerID, modelID, tCreated, tStreamed, tCompleted, input, output, cacheRead, cacheWrite, cost, tools] = line.split('\t');
246
248
  const n = (v: string | undefined): unknown => (v !== undefined && v !== '' && Number.isFinite(Number(v)) ? Number(v) : undefined);
247
249
  rows.push({
248
250
  id,
249
251
  created: Number(created),
252
+ tools: tools ?? '',
250
253
  row: {
251
254
  role,
252
255
  modelID,
253
256
  providerID,
254
257
  tokens: { input: n(input), output: n(output), cache: { read: n(cacheRead), write: n(cacheWrite) } },
255
- time: { created: n(tCreated), completed: n(tCompleted) },
258
+ time: { created: n(tCreated), streamed: n(tStreamed), completed: n(tCompleted) },
256
259
  cost: n(cost),
257
260
  },
258
261
  });
@@ -318,7 +321,7 @@ export interface SessionAccum {
318
321
  export function newSessionAccum(): SessionAccum {
319
322
  return { byModel: {}, byDay: {}, byDayModel: {}, byTool: {}, byModelMessages: {}, byHost: {}, byAgent: {}, byAgentMessages: {}, byModelMeasured: {}, byModelCostMessages: {}, totals: zeroBreakdown(), messages: 0, costMeasured: 0, recent: [], recentFloor: Number.POSITIVE_INFINITY };
320
323
  }
321
- export function ingestSessionRow(accum: SessionAccum, model: string, usage: Record<string, unknown>, ts: string | number | undefined, durMs?: unknown, run?: { st: RunStatus; code?: number; note?: string }, toolNames?: string[], host?: string, id?: string, agent?: string): boolean {
324
+ export function ingestSessionRow(accum: SessionAccum, model: string, usage: Record<string, unknown>, ts: string | number | undefined, durMs?: unknown, run?: { st: RunStatus; code?: number; note?: string }, toolNames?: string[], host?: string, id?: string, agent?: string, ttftMs?: unknown): boolean {
322
325
  const watermark = readResetWatermark();
323
326
  const ms = ts === undefined ? NaN : typeof ts === 'number' ? ts : Date.parse(ts);
324
327
  if (watermark > 0 && (!Number.isFinite(ms) || ms < watermark)) return false;
@@ -342,11 +345,9 @@ export function ingestSessionRow(accum: SessionAccum, model: string, usage: Reco
342
345
  const num = (v: unknown): number => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? Math.floor(v) : 0);
343
346
  if (Number.isFinite(ms)) {
344
347
  const outcome = run ?? { st: 'completed' as RunStatus };
345
- // The store holds every message ever recorded, but only the newest few are ever read. Skip
346
- // anything older than the floor, and re-read the floor when the list is full, so a 50k-row
347
- // store costs 2000 rows of memory instead of 50k.
348
+ // Only the newest rows are read: skip below the floor, and a 50k-row store costs 2000 rows of memory.
348
349
  if (accum.recent.length < RECENT_LIMIT || ms >= accum.recentFloor) {
349
- accum.recent.push({ m: key, i: num(usage.input), o: num(usage.output), t: ms, d: durOf(durMs), h: host, cr: num(usage.cacheRead), cw: num(usage.cacheWrite), usd: costOf(usage), st: outcome.st, code: outcome.code, note: outcome.note, id });
350
+ accum.recent.push({ m: key, i: num(usage.input), o: num(usage.output), t: ms, d: durOf(durMs), tf: durOf(ttftMs), h: host, cr: num(usage.cacheRead), cw: num(usage.cacheWrite), usd: costOf(usage), st: outcome.st, code: outcome.code, note: outcome.note, id });
350
351
  if (accum.recent.length > RECENT_LIMIT) {
351
352
  let drop = 0;
352
353
  for (let k = 1; k < accum.recent.length; k += 1) if (accum.recent[k].t < accum.recent[drop].t) drop = k;
@@ -378,7 +379,7 @@ export function ingestSessionRow(accum: SessionAccum, model: string, usage: Reco
378
379
  return true;
379
380
  }
380
381
  export type SessionLineKind = 'codex_provider' | 'codex_model' | 'token_row' | 'assistant_row' | 'skip';
381
- export function classifySessionLine(row: { timestamp?: string | number; type?: unknown; message?: { role?: unknown; model?: unknown; usage?: Record<string, unknown>; duration?: unknown; completedAt?: unknown; timestamp?: unknown; stopReason?: unknown; errorStatus?: unknown; errorMessage?: unknown; isError?: unknown; content?: Array<{ type?: unknown; name?: unknown; arguments?: unknown }> }; payload?: { type?: unknown; model?: unknown; model_provider?: unknown; info?: { last_token_usage?: Record<string, unknown> } } }): { kind: SessionLineKind; provider?: string; model?: string; usage?: Record<string, unknown>; durMs?: number; run?: { st: RunStatus; code?: number; note?: string }; tools?: string[]; ms?: number } {
382
+ export function classifySessionLine(row: { timestamp?: string | number; type?: unknown; message?: { role?: unknown; model?: unknown; usage?: Record<string, unknown>; duration?: unknown; ttft?: unknown; completedAt?: unknown; timestamp?: unknown; stopReason?: unknown; errorStatus?: unknown; errorMessage?: unknown; isError?: unknown; content?: Array<{ type?: unknown; name?: unknown; arguments?: unknown }> }; payload?: { type?: unknown; model?: unknown; model_provider?: unknown; info?: { last_token_usage?: Record<string, unknown> } } }): { kind: SessionLineKind; provider?: string; model?: string; usage?: Record<string, unknown>; durMs?: number; ttftMs?: number; run?: { st: RunStatus; code?: number; note?: string }; tools?: string[]; ms?: number } {
382
383
  const payload = row.payload;
383
384
  if (payload && typeof payload === 'object') {
384
385
  if (row.type === 'session_meta' && typeof payload.model_provider === 'string') return { kind: 'codex_provider', provider: payload.model_provider };
@@ -412,7 +413,7 @@ export function classifySessionLine(row: { timestamp?: string | number; type?: u
412
413
  const command = (part.arguments as { command?: unknown } | null)?.command;
413
414
  tools.push(`bash:${leadBinary(command)}`);
414
415
  }
415
- return { kind: 'assistant_row', model, usage: msg.usage, durMs: typeof msg.duration === 'number' ? msg.duration : computedDur, run: statusOf(msg), tools };
416
+ return { kind: 'assistant_row', model, usage: msg.usage, durMs: typeof msg.duration === 'number' ? msg.duration : computedDur, ttftMs: typeof msg.ttft === 'number' ? msg.ttft : undefined, run: statusOf(msg), tools };
416
417
  }
417
418
  // True when at least one token bucket holds a positive finite count.
418
419
  export function hasPositiveUsage(usage: Record<string, unknown>): boolean {
@@ -428,13 +429,30 @@ export interface OpencodeMessage {
428
429
  tokens?: { input?: unknown; output?: unknown; reasoning?: unknown; cache?: { read?: unknown; write?: unknown } } | null;
429
430
  modelID?: unknown;
430
431
  providerID?: unknown;
431
- time?: { created?: unknown; completed?: unknown } | null;
432
+ time?: { created?: unknown; streamed?: unknown; completed?: unknown } | null;
432
433
  cost?: unknown;
433
434
  finish?: unknown;
435
+ /** The message's parts. Tool calls sit here, named under a part of type `tool`. */
436
+ content?: unknown;
437
+ }
438
+
439
+ /** Tool names an OpenCode message called; v2 passes SQL's comma-joined string as the blob holds newlines. */
440
+ export function opencodeToolNames(message: OpencodeMessage, joined?: string): string[] {
441
+ if (joined !== undefined) return joined === '' ? [] : joined.split(',');
442
+ const parts = message?.content;
443
+ if (!Array.isArray(parts)) return [];
444
+ const names: string[] = [];
445
+ for (const part of parts) {
446
+ if (!part || typeof part !== 'object') continue;
447
+ if ((part as Record<string, unknown>).type !== 'tool') continue;
448
+ const name = (part as Record<string, unknown>).name;
449
+ if (typeof name === 'string' && name) names.push(name);
450
+ }
451
+ return names;
434
452
  }
435
453
 
436
454
  // One OpenCode message file; assistant rows carry the token buckets.
437
- export function classifyOpencodeMessage(obj: OpencodeMessage | null | undefined): { model: string; usage: Record<string, unknown>; ms: number | undefined; durMs: number | undefined } | null {
455
+ export function classifyOpencodeMessage(obj: OpencodeMessage | null | undefined): { model: string; usage: Record<string, unknown>; ms: number | undefined; durMs: number | undefined; ttftMs: number | undefined } | null {
438
456
  if (!obj || typeof obj !== 'object' || obj.role !== 'assistant') return null;
439
457
  const t = obj.tokens;
440
458
  if (!t || typeof t !== 'object') return null;
@@ -451,15 +469,17 @@ export function classifyOpencodeMessage(obj: OpencodeMessage | null | undefined)
451
469
  const num = (v: unknown): number | undefined => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : undefined);
452
470
  const created = num(obj.time?.created);
453
471
  const completed = num(obj.time?.completed);
472
+ // `streamed` is when the first token arrived, so it is the host's own first-token latency.
473
+ const streamed = num(obj.time?.streamed);
454
474
  return {
455
475
  model: `opencode/${provider}/${model}`,
456
476
  usage,
457
477
  ms: created,
458
478
  durMs: created !== undefined && completed !== undefined && completed > created ? completed - created : undefined,
479
+ ttftMs: created !== undefined && streamed !== undefined && streamed > created ? streamed - created : undefined,
459
480
  };
460
481
  }
461
482
 
462
- // One OpenCode message file into the shared accum. The msg id is the file name.
463
483
  export function processOpencodeFile(accum: SessionAccum, text: string, file?: string): void {
464
484
  let obj: unknown;
465
485
  try {
@@ -467,17 +487,18 @@ export function processOpencodeFile(accum: SessionAccum, text: string, file?: st
467
487
  } catch {
468
488
  return;
469
489
  }
470
- const parsed = classifyOpencodeMessage(obj as OpencodeMessage);
490
+ const message = obj as OpencodeMessage;
491
+ const parsed = classifyOpencodeMessage(message);
471
492
  if (!parsed) return;
472
493
  const id = file ? path.basename(file).replace(/\.[^.]+$/, '') || undefined : undefined;
473
- ingestSessionRow(accum, parsed.model, parsed.usage, parsed.ms, parsed.durMs, { st: 'completed' as RunStatus }, undefined, 'opencode', id, 'main');
494
+ ingestSessionRow(accum, parsed.model, parsed.usage, parsed.ms, parsed.durMs, { st: 'completed' as RunStatus }, opencodeToolNames(message), 'opencode', id, 'main', parsed.ttftMs);
474
495
  }
475
496
 
476
497
  // One v2 message row into the shared accum. The msg id is the row primary key.
477
498
  export function ingestOpencodeDbRow(accum: SessionAccum, row: OpencodeDbRow): void {
478
499
  const parsed = classifyOpencodeMessage(row.row);
479
500
  if (!parsed) return;
480
- ingestSessionRow(accum, parsed.model, parsed.usage, parsed.ms, parsed.durMs, { st: 'completed' as RunStatus }, undefined, 'opencode', row.id, 'main');
501
+ ingestSessionRow(accum, parsed.model, parsed.usage, parsed.ms, parsed.durMs, { st: 'completed' as RunStatus }, opencodeToolNames(row.row, row.tools), 'opencode', row.id, 'main', parsed.ttftMs);
481
502
  }
482
503
 
483
504
  // Fold `:free`/`-free` suffixes and case variants into one chart key.
@@ -490,8 +511,7 @@ export function messageRowId(row: unknown): string | undefined {
490
511
  return typeof id === 'string' && id ? id : undefined;
491
512
  }
492
513
 
493
- // Vendor names mirror the dashboard map; CLI needs names only. Family first:
494
- // gateway segments shadow it (minimaxai holds xai, gemma rides nvidia).
514
+ // Vendor names mirror the dashboard map; family first, so gateway segments shadow it (minimaxai holds xai).
495
515
  const VENDOR_RES: Array<[RegExp, string]> = [
496
516
  [/inclusionai|ling-/i, 'InclusionAI'],
497
517
  [/minimax/i, 'MiniMax'],
@@ -617,9 +637,7 @@ export function hostOfSessionFile(file: string): string {
617
637
  return 'pi';
618
638
  }
619
639
 
620
- // Which agent ran a session file, from its own `session` row: a transcript with a parent
621
- // session is a subagent's, and everything else is the main agent. The row is the host's own
622
- // record, so a mirrored transcript carries the same answer as the stats database.
640
+ // A transcript with a parent session ran as a subagent; the host's own row, so a mirror answers the same.
623
641
  export function agentOfSessionText(text: string): string {
624
642
  if (!text.includes('parentSession')) return 'main';
625
643
  for (const line of text.split('\n')) {
@@ -725,7 +743,7 @@ export function processSessionText(accum: SessionAccum, state: { codexProvider:
725
743
  continue;
726
744
  }
727
745
  if (parsed.kind !== 'assistant_row' || !parsed.model || !parsed.usage) continue;
728
- ingestSessionRow(accum, parsed.model, parsed.usage, row.timestamp, parsed.durMs, parsed.run, parsed.tools, host, rid, agent);
746
+ ingestSessionRow(accum, parsed.model, parsed.usage, row.timestamp, parsed.durMs, parsed.run, parsed.tools, host, rid, agent, parsed.ttftMs);
729
747
  } catch { /* skip corrupt lines */ }
730
748
  }
731
749
  }
@@ -762,7 +780,7 @@ export function leadBinary(command: unknown): string {
762
780
  return 'bash';
763
781
  }
764
782
 
765
- // --- Pricing lives in ./pricing.ts (live LiteLLM cache + fallback table) ---
783
+ // Pricing lives in ./pricing.ts (live LiteLLM cache + fallback table).
766
784
  import { DEFAULT_PRICE, PRICE_TIERS_FIRM, canonicalPriceId, priceFor, pricesOverridePath, refreshPricesIfStale, type PriceHit, type PriceTier } from './pricing.ts';
767
785
  import { co2GramsFor, energyWhFor } from './carbon.ts';
768
786
  import type { ModelPrice } from './pricing.ts';
@@ -771,11 +789,7 @@ export { DEFAULT_PRICE, PRICE_TIERS_FIRM, priceFor, pricesOverridePath, refreshP
771
789
  export { co2GramsFor, energyWhFor };
772
790
  export type { PriceHit, PriceTier, ModelPrice };
773
791
 
774
- /**
775
- * Price one model's tokens. `measured` is the cost the host reported for those tokens; it outranks
776
- * every rate card, because it is the only figure from the party that billed. Pass undefined rather
777
- * than 0 for a cost nobody reported: `costOf` keeps the two apart.
778
- */
792
+ /** Price one model's tokens; `measured` outranks every rate card, so pass undefined when unreported. */
779
793
  export function usdCost(t: TokenBreakdown, model?: string, measured?: number): { usd: number; priced: boolean; tier: PriceTier; buckets: { input: number; output: number; cacheRead: number; cacheWrite: number } } {
780
794
  const m = 1 / 1_000_000;
781
795
  if (measured !== undefined) {
@@ -797,8 +811,7 @@ export function usdCost(t: TokenBreakdown, model?: string, measured?: number): {
797
811
  };
798
812
  return {
799
813
  usd: buckets.input + buckets.output + buckets.cacheRead + buckets.cacheWrite,
800
- // A tier the caller cannot stand behind is not a price. `tail` and `default` are estimates,
801
- // and a report that sums them beside measured cost reads as a bill it is not.
814
+ // `tail`/`default` are estimates; summing them beside measured cost reads as a bill it is not.
802
815
  priced: PRICE_TIERS_FIRM.includes(hit.tier),
803
816
  tier: hit.tier,
804
817
  buckets,
@@ -829,7 +842,7 @@ export function readUsage(): UsageRow[] {
829
842
  return rows;
830
843
  }
831
844
 
832
- // Delete the ledger file and return the rows cleared. Tersio-owned data only: transcripts and the RTK database are never touched.
845
+ // Delete the ledger file and return the rows cleared. Tersio-owned data only: transcripts and the RTK database stay.
833
846
  export function clearUsageLedger(): number {
834
847
  const rows = readUsage().length;
835
848
  try {