@krtclcdy/tersio 2.25.5 → 2.25.7

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 (231) hide show
  1. package/README.md +42 -38
  2. package/dashboard/dist/index.html +15 -15
  3. package/dist/bench/code-out/pfull-0/debounce.js +13 -0
  4. package/dist/bench/code-out/pfull-0/deepClone.js +18 -0
  5. package/dist/bench/code-out/pfull-0/groupBy.js +15 -0
  6. package/dist/bench/code-out/pfull-1/debounce.js +13 -0
  7. package/dist/bench/code-out/pfull-1/deepClone.js +17 -0
  8. package/dist/bench/code-out/pfull-1/groupBy.js +14 -0
  9. package/dist/bench/code-out/pfull-2/debounce.js +13 -0
  10. package/dist/bench/code-out/pfull-2/deepClone.js +18 -0
  11. package/dist/bench/code-out/pfull-2/groupBy.js +15 -0
  12. package/dist/bench/code-out/pfull-3/debounce.js +12 -0
  13. package/dist/bench/code-out/pfull-3/deepClone.js +17 -0
  14. package/dist/bench/code-out/pfull-3/groupBy.js +7 -0
  15. package/dist/bench/code-out/pfull-4/debounce.js +10 -0
  16. package/dist/bench/code-out/pfull-4/deepClone.js +17 -0
  17. package/dist/bench/code-out/pfull-4/groupBy.js +14 -0
  18. package/dist/bench/code-out/plite-0/debounce.js +13 -0
  19. package/dist/bench/code-out/plite-0/deepClone.js +18 -0
  20. package/dist/bench/code-out/plite-0/groupBy.js +15 -0
  21. package/dist/bench/code-out/plite-1/debounce.js +13 -0
  22. package/dist/bench/code-out/plite-1/deepClone.js +17 -0
  23. package/dist/bench/code-out/plite-1/groupBy.js +11 -0
  24. package/dist/bench/code-out/plite-2/debounce.js +12 -0
  25. package/dist/bench/code-out/plite-2/deepClone.js +17 -0
  26. package/dist/bench/code-out/plite-2/groupBy.js +14 -0
  27. package/dist/bench/code-out/plite-3/debounce.js +12 -0
  28. package/dist/bench/code-out/plite-3/deepClone.js +17 -0
  29. package/dist/bench/code-out/plite-3/groupBy.js +14 -0
  30. package/dist/bench/code-out/plite-4/debounce.js +12 -0
  31. package/dist/bench/code-out/plite-4/deepClone.js +17 -0
  32. package/dist/bench/code-out/plite-4/groupBy.js +14 -0
  33. package/dist/bench/code-out/pultra-0/debounce.js +12 -0
  34. package/dist/bench/code-out/pultra-0/deepClone.js +20 -0
  35. package/dist/bench/code-out/pultra-0/groupBy.js +14 -0
  36. package/dist/bench/code-out/pultra-1/debounce.js +10 -0
  37. package/dist/bench/code-out/pultra-1/deepClone.js +17 -0
  38. package/dist/bench/code-out/pultra-1/groupBy.js +7 -0
  39. package/dist/bench/code-out/pultra-2/debounce.js +13 -0
  40. package/dist/bench/code-out/pultra-2/deepClone.js +18 -0
  41. package/dist/bench/code-out/pultra-2/groupBy.js +15 -0
  42. package/dist/bench/code-out/pultra-3/debounce.js +13 -0
  43. package/dist/bench/code-out/pultra-3/deepClone.js +18 -0
  44. package/dist/bench/code-out/pultra-3/groupBy.js +15 -0
  45. package/dist/bench/code-out/pultra-4/debounce.js +13 -0
  46. package/dist/bench/code-out/pultra-4/deepClone.js +18 -0
  47. package/dist/bench/code-out/pultra-4/groupBy.js +15 -0
  48. package/dist/bench/code-out/run-0/debounce.js +12 -0
  49. package/dist/bench/code-out/run-0/deepClone.js +17 -0
  50. package/dist/bench/code-out/run-0/groupBy.js +14 -0
  51. package/dist/bench/code-out/run-1/debounce.js +13 -0
  52. package/dist/bench/code-out/run-1/deepClone.js +15 -0
  53. package/dist/bench/code-out/run-1/groupBy.js +13 -0
  54. package/dist/bench/code-out/run-2/debounce.js +12 -0
  55. package/dist/bench/code-out/run-2/deepClone.js +18 -0
  56. package/dist/bench/code-out/run-2/groupBy.js +13 -0
  57. package/dist/bench/code-out/run-3/debounce.js +12 -0
  58. package/dist/bench/code-out/run-3/deepClone.js +18 -0
  59. package/dist/bench/code-out/run-3/groupBy.js +14 -0
  60. package/dist/bench/code-out/run-4/debounce.js +12 -0
  61. package/dist/bench/code-out/run-4/deepClone.js +16 -0
  62. package/dist/bench/code-out/run-4/groupBy.js +13 -0
  63. package/dist/cli/backups.js +7 -14
  64. package/dist/cli/banner.js +3 -3
  65. package/dist/cli/common.js +29 -13
  66. package/dist/cli/currency.js +1 -1
  67. package/dist/cli/dashboard.js +66 -35
  68. package/dist/cli/doctor-fix.js +204 -19
  69. package/dist/cli/doctor.js +263 -40
  70. package/dist/cli/hosts.js +25 -6
  71. package/dist/cli/install.js +150 -49
  72. package/dist/cli/interactive.js +3 -3
  73. package/dist/cli/manifest.js +109 -9
  74. package/dist/cli/profile.js +13 -8
  75. package/dist/cli/rtk-wiring.js +1 -1
  76. package/dist/cli/settings.js +20 -5
  77. package/dist/cli/uninstall.js +4 -4
  78. package/dist/cli/update.js +6 -7
  79. package/dist/cli/usage.js +6 -9
  80. package/dist/extensions/caveman/index.js +10 -4
  81. package/dist/extensions/caveman/rule-lite.md +77 -0
  82. package/dist/extensions/caveman/rule-megacave-lite.md +59 -0
  83. package/dist/extensions/caveman/rule-megacave-ultra.md +59 -0
  84. package/dist/extensions/caveman/rule-megacave.md +5 -3
  85. package/dist/extensions/caveman/rule-ultra.md +4 -4
  86. package/dist/extensions/caveman/rule.md +55 -45
  87. package/dist/extensions/combo/index.js +85 -25
  88. package/dist/extensions/commands/index.js +1 -1
  89. package/dist/extensions/pstack/index.js +121 -0
  90. package/dist/extensions/pstack/rule.md +141 -0
  91. package/dist/extensions/rtk/filter.js +0 -1
  92. package/dist/extensions/rtk/session.js +20 -12
  93. package/dist/extensions/server.js +90 -6
  94. package/dist/extensions/shared/agent-stats.js +214 -278
  95. package/dist/extensions/shared/brand-marks.js +33 -34
  96. package/dist/extensions/shared/carbon.js +1 -1
  97. package/dist/extensions/shared/plugin-settings.js +11 -6
  98. package/dist/extensions/shared/pricing.js +8 -16
  99. package/dist/extensions/shared/rtk-eligible.js +84 -0
  100. package/dist/extensions/shared/rtk-gain.js +5 -10
  101. package/dist/extensions/shared/session-state.js +15 -10
  102. package/dist/extensions/shared/sqlite.js +162 -0
  103. package/dist/extensions/shared/status.js +8 -9
  104. package/dist/extensions/shared/types.js +1 -1
  105. package/dist/extensions/shared/usage-ledger.js +155 -61
  106. package/dist/extensions/shared/usage-store.js +111 -52
  107. package/dist/extensions/shared/utils.js +5 -5
  108. package/dist/extensions/timing/index.js +31 -0
  109. package/dist/extensions/updater/index.js +3 -6
  110. package/dist/tersio.js +8 -3
  111. package/extensions/caveman/index.ts +10 -4
  112. package/extensions/caveman/rule-lite.md +77 -0
  113. package/extensions/caveman/rule-megacave-lite.md +59 -0
  114. package/extensions/caveman/rule-megacave-ultra.md +59 -0
  115. package/extensions/caveman/rule-megacave.md +5 -3
  116. package/extensions/caveman/rule-ultra.md +4 -4
  117. package/extensions/caveman/rule.md +55 -45
  118. package/extensions/combo/index.ts +86 -23
  119. package/extensions/commands/index.ts +1 -1
  120. package/extensions/pstack/index.ts +130 -0
  121. package/extensions/pstack/playbooks/authoring-a-skill.md +12 -0
  122. package/extensions/pstack/playbooks/autonomous-run.md +13 -0
  123. package/extensions/pstack/playbooks/autopilot-full.md +13 -0
  124. package/extensions/pstack/playbooks/autopilot-stack.md +16 -0
  125. package/extensions/pstack/playbooks/babysit.md +23 -0
  126. package/extensions/pstack/playbooks/bug-fix.md +15 -0
  127. package/extensions/pstack/playbooks/eval.md +25 -0
  128. package/extensions/pstack/playbooks/feature.md +21 -0
  129. package/extensions/pstack/playbooks/hillclimb.md +21 -0
  130. package/extensions/pstack/playbooks/investigation.md +14 -0
  131. package/extensions/pstack/playbooks/multi-phase-plan.md +155 -0
  132. package/extensions/pstack/playbooks/opening-a-pr.md +34 -0
  133. package/extensions/pstack/playbooks/orchestrate.md +111 -0
  134. package/extensions/pstack/playbooks/pause-safely.md +10 -0
  135. package/extensions/pstack/playbooks/perf-issue.md +25 -0
  136. package/extensions/pstack/playbooks/prototype.md +14 -0
  137. package/extensions/pstack/playbooks/refactoring.md +16 -0
  138. package/extensions/pstack/playbooks/runtime-forensics.md +11 -0
  139. package/extensions/pstack/playbooks/session-pickup.md +11 -0
  140. package/extensions/pstack/playbooks/shipping.md +17 -0
  141. package/extensions/pstack/playbooks/trace-forensics.md +14 -0
  142. package/extensions/pstack/playbooks/visual-parity.md +11 -0
  143. package/extensions/pstack/playbooks/worktree-cleanup.md +12 -0
  144. package/extensions/pstack/principles/attack-the-premise.md +22 -0
  145. package/extensions/pstack/principles/boundary-discipline.md +33 -0
  146. package/extensions/pstack/principles/build-the-lever.md +22 -0
  147. package/extensions/pstack/principles/encode-lessons-in-structure.md +10 -0
  148. package/extensions/pstack/principles/exhaust-the-design-space.md +8 -0
  149. package/extensions/pstack/principles/experience-first.md +18 -0
  150. package/extensions/pstack/principles/explain-the-number.md +16 -0
  151. package/extensions/pstack/principles/fix-root-causes.md +23 -0
  152. package/extensions/pstack/principles/foundational-thinking.md +20 -0
  153. package/extensions/pstack/principles/guard-the-context-window.md +11 -0
  154. package/extensions/pstack/principles/laziness-protocol.md +9 -0
  155. package/extensions/pstack/principles/make-operations-idempotent.md +23 -0
  156. package/extensions/pstack/principles/migrate-callers-then-delete-legacy-apis.md +21 -0
  157. package/extensions/pstack/principles/minimize-reader-load.md +17 -0
  158. package/extensions/pstack/principles/model-the-domain.md +23 -0
  159. package/extensions/pstack/principles/never-block-on-the-human.md +14 -0
  160. package/extensions/pstack/principles/outcome-oriented-execution.md +20 -0
  161. package/extensions/pstack/principles/prove-it-works.md +21 -0
  162. package/extensions/pstack/principles/redesign-from-first-principles.md +15 -0
  163. package/extensions/pstack/principles/separate-before-serializing-shared-state.md +15 -0
  164. package/extensions/pstack/principles/sequence-verifiable-units.md +16 -0
  165. package/extensions/pstack/principles/subtract-before-you-add.md +17 -0
  166. package/extensions/pstack/principles/test-behavior-not-implementation.md +20 -0
  167. package/extensions/pstack/principles/type-system-discipline.md +17 -0
  168. package/extensions/pstack/references/bugbot-triage.md +142 -0
  169. package/extensions/pstack/rule.md +141 -0
  170. package/extensions/pstack/skills/architect/SKILL.md +78 -0
  171. package/extensions/pstack/skills/architect/references/design-red-flags.md +57 -0
  172. package/extensions/pstack/skills/architect/references/rationale-template.md +35 -0
  173. package/extensions/pstack/skills/architect/references/runner-prompt.md +20 -0
  174. package/extensions/pstack/skills/arena/SKILL.md +47 -0
  175. package/extensions/pstack/skills/benchmark-checklist/SKILL.md +38 -0
  176. package/extensions/pstack/skills/blast-radius/SKILL.md +49 -0
  177. package/extensions/pstack/skills/bro/SKILL.md +6 -0
  178. package/extensions/pstack/skills/correct/SKILL.md +31 -0
  179. package/extensions/pstack/skills/figure-it-out/SKILL.md +51 -0
  180. package/extensions/pstack/skills/how/SKILL.md +35 -0
  181. package/extensions/pstack/skills/how/references/explainer-prompt.md +55 -0
  182. package/extensions/pstack/skills/how/references/explorer-prompt.md +35 -0
  183. package/extensions/pstack/skills/interrogate/SKILL.md +73 -0
  184. package/extensions/pstack/skills/interrogate/references/code-quality-review.md +47 -0
  185. package/extensions/pstack/skills/interrogate/references/lead-judgment.md +58 -0
  186. package/extensions/pstack/skills/interrogate/references/reviewer-prompt.md +62 -0
  187. package/extensions/pstack/skills/interrogate/references/rubric.md +77 -0
  188. package/extensions/pstack/skills/no-comments/SKILL.md +23 -0
  189. package/extensions/pstack/skills/reflect/SKILL.md +66 -0
  190. package/extensions/pstack/skills/reflect/references/divergent-reviewer.md +38 -0
  191. package/extensions/pstack/skills/reflect/references/judgment-reviewer.md +37 -0
  192. package/extensions/pstack/skills/reflect/references/synthesizer.md +56 -0
  193. package/extensions/pstack/skills/reflect/references/tooling-reviewer.md +48 -0
  194. package/extensions/pstack/skills/show-me-your-work/SKILL.md +81 -0
  195. package/extensions/pstack/skills/swarm/SKILL.md +45 -0
  196. package/extensions/pstack/skills/tdd/SKILL.md +41 -0
  197. package/extensions/pstack/skills/technical-writing/SKILL.md +105 -0
  198. package/extensions/pstack/skills/unslop/SKILL.md +61 -0
  199. package/extensions/pstack/skills/why/SKILL.md +97 -0
  200. package/extensions/pstack/skills/why/references/epistemics.md +71 -0
  201. package/extensions/pstack/skills/why/references/investigator-prompt.md +84 -0
  202. package/extensions/pstack/skills/why/references/source-playbook.md +17 -0
  203. package/extensions/pstack/skills/why/references/sources/code-archaeology.md +51 -0
  204. package/extensions/pstack/skills/why/references/sources/databricks.md +70 -0
  205. package/extensions/pstack/skills/why/references/sources/datadog.md +97 -0
  206. package/extensions/pstack/skills/why/references/sources/incident-postmortem.md +5 -0
  207. package/extensions/pstack/skills/why/references/sources/linear.md +48 -0
  208. package/extensions/pstack/skills/why/references/sources/notion.md +55 -0
  209. package/extensions/pstack/skills/why/references/sources/sentry.md +66 -0
  210. package/extensions/pstack/skills/why/references/sources/slack.md +17 -0
  211. package/extensions/pstack/skills/why/references/synthesizer-prompt.md +129 -0
  212. package/extensions/rtk/filter.ts +0 -1
  213. package/extensions/rtk/session.ts +19 -11
  214. package/extensions/server.ts +72 -7
  215. package/extensions/shared/agent-stats.ts +226 -278
  216. package/extensions/shared/brand-marks.ts +34 -34
  217. package/extensions/shared/carbon.ts +1 -1
  218. package/extensions/shared/plugin-settings.ts +12 -6
  219. package/extensions/shared/pricing.ts +10 -26
  220. package/extensions/shared/rtk-eligible.ts +83 -0
  221. package/extensions/shared/rtk-gain.ts +5 -10
  222. package/extensions/shared/session-state.ts +18 -13
  223. package/extensions/shared/sqlite.ts +162 -0
  224. package/extensions/shared/status.ts +9 -9
  225. package/extensions/shared/types.ts +2 -1
  226. package/extensions/shared/usage-ledger.ts +157 -66
  227. package/extensions/shared/usage-store.ts +120 -52
  228. package/extensions/shared/utils.ts +5 -5
  229. package/extensions/timing/index.ts +44 -0
  230. package/extensions/updater/index.ts +3 -8
  231. package/package.json +21 -9
@@ -1,8 +1,10 @@
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 { eligibleHeads } from './rtk-eligible.ts';
7
+ import { sqliteRead } from './sqlite.ts';
6
8
 
7
9
  export type UsageKind = 'command' | 'toggle' | 'install' | 'update' | 'rtk-audit';
8
10
 
@@ -50,7 +52,7 @@ export function appendUsage(kind: UsageKind, detail: string): void {
50
52
  } catch { /* ledger is best-effort; never break the caller */ }
51
53
  }
52
54
 
53
- // --- Session tokens, tokscale-style ------------------------------------------ Assistant messages in the host transcripts carry usage, model, and a timestamp.
55
+ // Session tokens, tokscale-style: assistant messages in the host transcripts carry usage, model, and a timestamp.
54
56
  export interface TokenBreakdown {
55
57
  input: number;
56
58
  output: number;
@@ -67,6 +69,8 @@ export interface RecentRequest {
67
69
  o: number;
68
70
  t: number;
69
71
  d?: number;
72
+ /** First-token latency, when the host recorded one. */
73
+ tf?: number;
70
74
  /** Which agent wrote the session: pi, omp, or codex. */
71
75
  h?: string;
72
76
  cr?: number;
@@ -127,7 +131,7 @@ export function durOf(v: unknown): number | undefined {
127
131
  return typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : undefined;
128
132
  }
129
133
 
130
- // Measured cost: usage.cost.total, or a bare number in old fixtures. Anything else means unrecorded, which is deliberately not 0.
134
+ // Measured cost: usage.cost.total, or a bare number in old fixtures. Anything else means unrecorded, not 0.
131
135
  export function costOf(usage: Record<string, unknown>): number | undefined {
132
136
  const c = usage.cost;
133
137
  if (typeof c === 'number') return Number.isFinite(c) ? c : undefined;
@@ -162,7 +166,7 @@ function addInto(into: TokenBreakdown, u: { input?: unknown; output?: unknown; c
162
166
  export function sessionsDirs(): string[] {
163
167
  const override = process.env.TERSIO_SESSIONS_DIR;
164
168
  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.
169
+ // Both hosts, not just the running one: a pi session is missing if we stop at the first existing directory.
166
170
  const dirs = isPiProcess()
167
171
  ? [path.join(piAgentDir(), 'sessions'), path.join(homeDir(), '.omp', 'agent', 'sessions')]
168
172
  : [path.join(homeDir(), '.omp', 'agent', 'sessions'), path.join(piAgentDir(), 'sessions')];
@@ -204,55 +208,55 @@ export interface OpencodeDbRow {
204
208
  id: string;
205
209
  created: number;
206
210
  row: OpencodeMessage;
211
+ /** Tool names this message called, comma-joined. Empty when it called none. */
212
+ tools?: string;
207
213
  }
208
214
 
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.
215
+ // `message` vs `session_message`: read scalars only, since the blob's newlines break the row-per-line output.
212
216
  const OC_DB_ORDER = ' ORDER BY time_created;';
213
217
  const OC_DB_COLUMNS =
214
- 'json_extract(data,\'$.time.created\'), json_extract(data,\'$.time.completed\'),' +
218
+ 'json_extract(data,\'$.time.created\'), json_extract(data,\'$.time.streamed\'), json_extract(data,\'$.time.completed\'),' +
215
219
  ' json_extract(data,\'$.tokens.input\'), json_extract(data,\'$.tokens.output\'),' +
216
220
  ' json_extract(data,\'$.tokens.cache.read\'), json_extract(data,\'$.tokens.cache.write\'), json_extract(data,\'$.cost\')';
221
+ const OC_DB_TOOLS =
222
+ '(SELECT group_concat(json_extract(j.value,\'$.name\')) FROM json_each(data,\'$.content\') j' +
223
+ ' WHERE json_extract(j.value,\'$.type\')=\'tool\')';
217
224
  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}` +
225
+ `SELECT id, time_created, json_extract(data,'$.role'), json_extract(data,'$.providerID'), json_extract(data,'$.modelID'), ${OC_DB_COLUMNS}, ${OC_DB_TOOLS}` +
219
226
  ` FROM message WHERE json_extract(data,'$.role')='assistant' AND time_created > ${since}${OC_DB_ORDER}`;
220
227
  const ocDbCurrent = (since: number): string =>
221
- `SELECT id, time_created, 'assistant', json_extract(data,'$.model.providerID'), json_extract(data,'$.model.id'), ${OC_DB_COLUMNS}` +
228
+ `SELECT id, time_created, 'assistant', json_extract(data,'$.model.providerID'), json_extract(data,'$.model.id'), ${OC_DB_COLUMNS}, ${OC_DB_TOOLS}` +
222
229
  ` FROM session_message WHERE type='assistant' AND time_created > ${since}${OC_DB_ORDER}`;
223
230
 
224
231
  // A re-scan window covers tokens that land after the row was created, so a late fill is not lost.
225
232
  export const OPENCODE_DB_OVERLAP_MS = 3_600_000;
226
233
 
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.
234
+ // Empty when no SQLite is available or the db is unreadable; that is a sync skip, not a failure.
231
235
  export function readOpencodeDbRows(db: string, sinceMs: number): OpencodeDbRow[] {
232
236
  if (!Number.isFinite(sinceMs)) return [];
233
237
  try {
234
238
  if (!fs.existsSync(db)) return [];
235
239
  const since = Math.max(0, Math.floor(sinceMs));
236
240
  // 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 });
241
+ const run = (sql: string): string => sqliteRead(db, sql);
239
242
  let out = '';
240
243
  try { out += run(ocDbLegacy(since)); } catch { /* no legacy table or unreadable */ }
241
244
  try { out += run(ocDbCurrent(since)); } catch { /* no session_message table or unreadable */ }
242
245
  const rows: OpencodeDbRow[] = [];
243
246
  for (const line of out.split('\n')) {
244
247
  if (!line.trim()) continue;
245
- const [id, created, role, providerID, modelID, tCreated, tCompleted, input, output, cacheRead, cacheWrite, cost] = line.split('\t');
248
+ const [id, created, role, providerID, modelID, tCreated, tStreamed, tCompleted, input, output, cacheRead, cacheWrite, cost, tools] = line.split('\t');
246
249
  const n = (v: string | undefined): unknown => (v !== undefined && v !== '' && Number.isFinite(Number(v)) ? Number(v) : undefined);
247
250
  rows.push({
248
251
  id,
249
252
  created: Number(created),
253
+ tools: tools ?? '',
250
254
  row: {
251
255
  role,
252
256
  modelID,
253
257
  providerID,
254
258
  tokens: { input: n(input), output: n(output), cache: { read: n(cacheRead), write: n(cacheWrite) } },
255
- time: { created: n(tCreated), completed: n(tCompleted) },
259
+ time: { created: n(tCreated), streamed: n(tStreamed), completed: n(tCompleted) },
256
260
  cost: n(cost),
257
261
  },
258
262
  });
@@ -270,6 +274,55 @@ export function dayKey(ts: string | number): string | null {
270
274
  const p = (n: number): string => String(n).padStart(2, '0');
271
275
  return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
272
276
  }
277
+
278
+ // OpenCode tool parts carry the command in the message blob; sqliteRead is line-based, so newlines are flattened in SQL.
279
+ const OC_BASH_CMD = "replace(replace(json_extract(j.value,'$.state.input.command'), char(10), ' '), char(13), ' ')";
280
+ const OC_BASH_TOOL = "json_extract(j.value,'$.type')='tool' AND (json_extract(j.value,'$.name')='bash' OR json_extract(j.value,'$.name')='shell')";
281
+ const ocBashLegacy = `SELECT message.id, json_extract(message.data,'$.sessionID'), ${OC_BASH_CMD} FROM message, json_each(message.data,'$.content') j WHERE json_extract(message.data,'$.role')='assistant' AND ${OC_BASH_TOOL}`;
282
+ const ocBashCurrent = `SELECT session_message.id, session_message.session_id, ${OC_BASH_CMD} FROM session_message, json_each(session_message.data,'$.content') j WHERE session_message.type='assistant' AND ${OC_BASH_TOOL}`;
283
+
284
+ export interface OpencodeBashRow {
285
+ id: string;
286
+ sessionId: string;
287
+ command: string;
288
+ }
289
+
290
+ /** Every executed bash command in an OpenCode db, v2 first and the legacy table second. */
291
+ export function readOpencodeBashRows(db: string): OpencodeBashRow[] {
292
+ const rows: OpencodeBashRow[] = [];
293
+ try {
294
+ if (!fs.existsSync(db)) return rows;
295
+ let out = '';
296
+ try { out += sqliteRead(db, ocBashCurrent); } catch { /* no v2 table */ }
297
+ try { out += sqliteRead(db, ocBashLegacy); } catch { /* no legacy table */ }
298
+ for (const line of out.split('\n')) {
299
+ if (!line.trim()) continue;
300
+ const [id, sessionId, command] = line.split('\t');
301
+ if (typeof command !== 'string' || !command.trim()) continue;
302
+ rows.push({ id: id ?? '', sessionId: sessionId ?? '', command });
303
+ }
304
+ } catch { /* an unreadable db is a skip, not a failure */ }
305
+ return rows;
306
+ }
307
+
308
+ // Pre-v2 OpenCode stores one message per JSON file; the same tool-part shape holds the command.
309
+ function opencodeFileBash(file: string): { commands: string[]; sessionId: string } {
310
+ try {
311
+ const message = JSON.parse(fs.readFileSync(file, 'utf8')) as { sessionID?: unknown; content?: unknown };
312
+ const parts = Array.isArray(message.content) ? message.content : [];
313
+ const commands: string[] = [];
314
+ for (const part of parts) {
315
+ const p = part as { type?: unknown; name?: unknown; state?: { input?: { command?: unknown } } };
316
+ if (p?.type !== 'tool') continue;
317
+ if (p.name !== 'bash' && p.name !== 'shell') continue;
318
+ const command = p.state?.input?.command;
319
+ if (typeof command === 'string' && command) commands.push(command);
320
+ }
321
+ return { commands, sessionId: typeof message.sessionID === 'string' ? message.sessionID : '' };
322
+ } catch {
323
+ return { commands: [], sessionId: '' };
324
+ }
325
+ }
273
326
  export function walkJsonl(dir: string, out: string[], cap: number, ext = '.jsonl'): void {
274
327
  let entries: fs.Dirent[];
275
328
  try {
@@ -287,11 +340,6 @@ export function walkJsonl(dir: string, out: string[], cap: number, ext = '.jsonl
287
340
  // Recent rows are a newest-first window over lifetime history; aggregates keep everything.
288
341
  export const RECENT_LIMIT = 2000;
289
342
  const FREE_SUFFIX = /(?::free|-free)$/i;
290
- const RTK_ELIGIBLE_HEADS = new Set([
291
- 'rtk', 'git', 'grep', 'rg', 'find', 'cat', 'ls', 'tree', 'diff', 'log',
292
- 'npm', 'npx', 'pnpm', 'bun', 'bunx', 'cargo', 'go', 'python', 'pytest',
293
- 'ruff', 'mypy', 'docker', 'kubectl', 'psql', 'aws', 'gh', 'glab', 'wc',
294
- ]);
295
343
  // Parse buffer shared by live reads and usage.db syncs, so the store is a cache and never a fork.
296
344
  export interface SessionAccum {
297
345
  byModel: Record<string, TokenBreakdown>;
@@ -318,7 +366,7 @@ export interface SessionAccum {
318
366
  export function newSessionAccum(): SessionAccum {
319
367
  return { byModel: {}, byDay: {}, byDayModel: {}, byTool: {}, byModelMessages: {}, byHost: {}, byAgent: {}, byAgentMessages: {}, byModelMeasured: {}, byModelCostMessages: {}, totals: zeroBreakdown(), messages: 0, costMeasured: 0, recent: [], recentFloor: Number.POSITIVE_INFINITY };
320
368
  }
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 {
369
+ 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
370
  const watermark = readResetWatermark();
323
371
  const ms = ts === undefined ? NaN : typeof ts === 'number' ? ts : Date.parse(ts);
324
372
  if (watermark > 0 && (!Number.isFinite(ms) || ms < watermark)) return false;
@@ -342,11 +390,9 @@ export function ingestSessionRow(accum: SessionAccum, model: string, usage: Reco
342
390
  const num = (v: unknown): number => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? Math.floor(v) : 0);
343
391
  if (Number.isFinite(ms)) {
344
392
  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.
393
+ // Only the newest rows are read: skip below the floor, and a 50k-row store costs 2000 rows of memory.
348
394
  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 });
395
+ 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
396
  if (accum.recent.length > RECENT_LIMIT) {
351
397
  let drop = 0;
352
398
  for (let k = 1; k < accum.recent.length; k += 1) if (accum.recent[k].t < accum.recent[drop].t) drop = k;
@@ -378,7 +424,7 @@ export function ingestSessionRow(accum: SessionAccum, model: string, usage: Reco
378
424
  return true;
379
425
  }
380
426
  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 } {
427
+ 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
428
  const payload = row.payload;
383
429
  if (payload && typeof payload === 'object') {
384
430
  if (row.type === 'session_meta' && typeof payload.model_provider === 'string') return { kind: 'codex_provider', provider: payload.model_provider };
@@ -412,7 +458,7 @@ export function classifySessionLine(row: { timestamp?: string | number; type?: u
412
458
  const command = (part.arguments as { command?: unknown } | null)?.command;
413
459
  tools.push(`bash:${leadBinary(command)}`);
414
460
  }
415
- return { kind: 'assistant_row', model, usage: msg.usage, durMs: typeof msg.duration === 'number' ? msg.duration : computedDur, run: statusOf(msg), tools };
461
+ 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
462
  }
417
463
  // True when at least one token bucket holds a positive finite count.
418
464
  export function hasPositiveUsage(usage: Record<string, unknown>): boolean {
@@ -428,13 +474,30 @@ export interface OpencodeMessage {
428
474
  tokens?: { input?: unknown; output?: unknown; reasoning?: unknown; cache?: { read?: unknown; write?: unknown } } | null;
429
475
  modelID?: unknown;
430
476
  providerID?: unknown;
431
- time?: { created?: unknown; completed?: unknown } | null;
477
+ time?: { created?: unknown; streamed?: unknown; completed?: unknown } | null;
432
478
  cost?: unknown;
433
479
  finish?: unknown;
480
+ /** The message's parts. Tool calls sit here, named under a part of type `tool`. */
481
+ content?: unknown;
482
+ }
483
+
484
+ /** Tool names an OpenCode message called; v2 passes SQL's comma-joined string as the blob holds newlines. */
485
+ export function opencodeToolNames(message: OpencodeMessage, joined?: string): string[] {
486
+ if (joined !== undefined) return joined === '' ? [] : joined.split(',');
487
+ const parts = message?.content;
488
+ if (!Array.isArray(parts)) return [];
489
+ const names: string[] = [];
490
+ for (const part of parts) {
491
+ if (!part || typeof part !== 'object') continue;
492
+ if ((part as Record<string, unknown>).type !== 'tool') continue;
493
+ const name = (part as Record<string, unknown>).name;
494
+ if (typeof name === 'string' && name) names.push(name);
495
+ }
496
+ return names;
434
497
  }
435
498
 
436
499
  // 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 {
500
+ export function classifyOpencodeMessage(obj: OpencodeMessage | null | undefined): { model: string; usage: Record<string, unknown>; ms: number | undefined; durMs: number | undefined; ttftMs: number | undefined } | null {
438
501
  if (!obj || typeof obj !== 'object' || obj.role !== 'assistant') return null;
439
502
  const t = obj.tokens;
440
503
  if (!t || typeof t !== 'object') return null;
@@ -451,15 +514,17 @@ export function classifyOpencodeMessage(obj: OpencodeMessage | null | undefined)
451
514
  const num = (v: unknown): number | undefined => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : undefined);
452
515
  const created = num(obj.time?.created);
453
516
  const completed = num(obj.time?.completed);
517
+ // `streamed` is when the first token arrived, so it is the host's own first-token latency.
518
+ const streamed = num(obj.time?.streamed);
454
519
  return {
455
520
  model: `opencode/${provider}/${model}`,
456
521
  usage,
457
522
  ms: created,
458
523
  durMs: created !== undefined && completed !== undefined && completed > created ? completed - created : undefined,
524
+ ttftMs: created !== undefined && streamed !== undefined && streamed > created ? streamed - created : undefined,
459
525
  };
460
526
  }
461
527
 
462
- // One OpenCode message file into the shared accum. The msg id is the file name.
463
528
  export function processOpencodeFile(accum: SessionAccum, text: string, file?: string): void {
464
529
  let obj: unknown;
465
530
  try {
@@ -467,17 +532,18 @@ export function processOpencodeFile(accum: SessionAccum, text: string, file?: st
467
532
  } catch {
468
533
  return;
469
534
  }
470
- const parsed = classifyOpencodeMessage(obj as OpencodeMessage);
535
+ const message = obj as OpencodeMessage;
536
+ const parsed = classifyOpencodeMessage(message);
471
537
  if (!parsed) return;
472
538
  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');
539
+ ingestSessionRow(accum, parsed.model, parsed.usage, parsed.ms, parsed.durMs, { st: 'completed' as RunStatus }, opencodeToolNames(message), 'opencode', id, 'main', parsed.ttftMs);
474
540
  }
475
541
 
476
542
  // One v2 message row into the shared accum. The msg id is the row primary key.
477
543
  export function ingestOpencodeDbRow(accum: SessionAccum, row: OpencodeDbRow): void {
478
544
  const parsed = classifyOpencodeMessage(row.row);
479
545
  if (!parsed) return;
480
- ingestSessionRow(accum, parsed.model, parsed.usage, parsed.ms, parsed.durMs, { st: 'completed' as RunStatus }, undefined, 'opencode', row.id, 'main');
546
+ 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
547
  }
482
548
 
483
549
  // Fold `:free`/`-free` suffixes and case variants into one chart key.
@@ -490,8 +556,7 @@ export function messageRowId(row: unknown): string | undefined {
490
556
  return typeof id === 'string' && id ? id : undefined;
491
557
  }
492
558
 
493
- // Vendor names mirror the dashboard map; CLI needs names only. Family first:
494
- // gateway segments shadow it (minimaxai holds xai, gemma rides nvidia).
559
+ // Vendor names mirror the dashboard map; family first, so gateway segments shadow it (minimaxai holds xai).
495
560
  const VENDOR_RES: Array<[RegExp, string]> = [
496
561
  [/inclusionai|ling-/i, 'InclusionAI'],
497
562
  [/minimax/i, 'MiniMax'],
@@ -617,9 +682,7 @@ export function hostOfSessionFile(file: string): string {
617
682
  return 'pi';
618
683
  }
619
684
 
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.
685
+ // A transcript with a parent session ran as a subagent; the host's own row, so a mirror answers the same.
623
686
  export function agentOfSessionText(text: string): string {
624
687
  if (!text.includes('parentSession')) return 'main';
625
688
  for (const line of text.split('\n')) {
@@ -632,17 +695,28 @@ export function agentOfSessionText(text: string): string {
632
695
  return 'main';
633
696
  }
634
697
 
635
- const adoptionFileCache = new Map<string, { size: number; mtimeMs: number; counts: { bashCalls: number; eligibleCalls: number; rtkCalls: number } }>();
698
+ const adoptionFileCache = new Map<string, { size: number; mtimeMs: number; heads: string; counts: BashCounts }>();
699
+
700
+ interface BashCounts { bashCalls: number; eligibleCalls: number; rtkCalls: number }
701
+
702
+ function countBashCommands(commands: readonly string[], eligible: ReadonlySet<string>, counts: BashCounts): void {
703
+ for (const command of commands) {
704
+ counts.bashCalls += 1;
705
+ const head = leadBinary(command);
706
+ if (eligible.has(head)) counts.eligibleCalls += 1;
707
+ if (head === 'rtk') counts.rtkCalls += 1;
708
+ }
709
+ }
636
710
 
637
- function parseAdoptionFile(file: string): { counts: { bashCalls: number; eligibleCalls: number; rtkCalls: number }; sessions: number } {
638
- const counts = { bashCalls: 0, eligibleCalls: 0, rtkCalls: 0 };
711
+ function parseAdoptionFile(file: string, eligible: ReadonlySet<string>): { counts: BashCounts; sessions: number } {
712
+ const counts: BashCounts = { bashCalls: 0, eligibleCalls: 0, rtkCalls: 0 };
639
713
  let text: string;
640
714
  try {
641
715
  text = fs.readFileSync(file, 'utf8');
642
716
  } catch {
643
717
  return { counts, sessions: 0 };
644
718
  }
645
- let sawBash = false;
719
+ const commands: string[] = [];
646
720
  for (const line of text.split('\n')) {
647
721
  if (!line.includes('tool_execution_start')) continue;
648
722
  try {
@@ -653,15 +727,11 @@ function parseAdoptionFile(file: string): { counts: { bashCalls: number; eligibl
653
727
  };
654
728
  if (row.type !== 'custom' || row.customType !== 'tool_execution_start' || row.data?.toolName !== 'bash') continue;
655
729
  const command = row.data.args?.command;
656
- if (typeof command !== 'string') continue;
657
- sawBash = true;
658
- counts.bashCalls += 1;
659
- const head = leadBinary(command);
660
- if (RTK_ELIGIBLE_HEADS.has(head)) counts.eligibleCalls += 1;
661
- if (head === 'rtk') counts.rtkCalls += 1;
730
+ if (typeof command === 'string') commands.push(command);
662
731
  } catch { /* skip corrupt line */ }
663
732
  }
664
- return { counts, sessions: sawBash ? 1 : 0 };
733
+ countBashCommands(commands, eligible, counts);
734
+ return { counts, sessions: commands.length > 0 ? 1 : 0 };
665
735
  }
666
736
 
667
737
  export function clearRtkAdoptionCache(): void {
@@ -669,6 +739,10 @@ export function clearRtkAdoptionCache(): void {
669
739
  }
670
740
 
671
741
  export function readRtkAdoption(): RtkAdoption {
742
+ // The eligible set comes from rtk's rewrite contract, cached per binary version.
743
+ const eligible = eligibleHeads(resolveRtkBinary());
744
+ // A cached count is only valid for the head set that produced it.
745
+ const headsKey = [...eligible].sort().join(',');
672
746
  const files: string[] = [];
673
747
  for (const dir of sessionsDirs()) walkJsonl(dir, files, 2000);
674
748
  const live = new Set(files);
@@ -687,9 +761,9 @@ export function readRtkAdoption(): RtkAdoption {
687
761
  continue;
688
762
  }
689
763
  let entry = adoptionFileCache.get(file);
690
- if (!entry || entry.size !== stat.size || entry.mtimeMs !== stat.mtimeMs) {
691
- const parsed = parseAdoptionFile(file);
692
- entry = { size: stat.size, mtimeMs: stat.mtimeMs, counts: parsed.counts };
764
+ if (!entry || entry.size !== stat.size || entry.mtimeMs !== stat.mtimeMs || entry.heads !== headsKey) {
765
+ const parsed = parseAdoptionFile(file, eligible);
766
+ entry = { size: stat.size, mtimeMs: stat.mtimeMs, heads: headsKey, counts: parsed.counts };
693
767
  adoptionFileCache.set(file, entry);
694
768
  sessions += parsed.sessions;
695
769
  } else if (entry.counts.bashCalls > 0) {
@@ -699,6 +773,28 @@ export function readRtkAdoption(): RtkAdoption {
699
773
  eligibleCalls += entry.counts.eligibleCalls;
700
774
  rtkCalls += entry.counts.rtkCalls;
701
775
  }
776
+ // OpenCode keeps its own store: a v2 sqlite db, or one JSON file per message before v2.
777
+ const opencode: BashCounts = { bashCalls: 0, eligibleCalls: 0, rtkCalls: 0 };
778
+ const opencodeSessions = new Set<string>();
779
+ if (process.env.TERSIO_SESSIONS_DIR === undefined || process.env.TERSIO_OPENCODE_DB !== undefined) {
780
+ for (const row of readOpencodeBashRows(opencodeDbPath())) {
781
+ countBashCommands([row.command], eligible, opencode);
782
+ if (row.sessionId) opencodeSessions.add(row.sessionId);
783
+ }
784
+ }
785
+ const ocFiles: string[] = [];
786
+ if (process.env.TERSIO_SESSIONS_DIR === undefined || process.env.TERSIO_OPENCODE_DIR !== undefined) {
787
+ walkJsonl(opencodeSessionsDir(), ocFiles, 5000, '.json');
788
+ for (const file of ocFiles) {
789
+ const parsed = opencodeFileBash(file);
790
+ countBashCommands(parsed.commands, eligible, opencode);
791
+ if (parsed.sessionId) opencodeSessions.add(parsed.sessionId);
792
+ }
793
+ }
794
+ bashCalls += opencode.bashCalls;
795
+ eligibleCalls += opencode.eligibleCalls;
796
+ rtkCalls += opencode.rtkCalls;
797
+ sessions += opencodeSessions.size;
702
798
  const missedCalls = Math.max(0, eligibleCalls - rtkCalls);
703
799
  return { sessions, bashCalls, eligibleCalls, rtkCalls, missedCalls, adoptionPct: eligibleCalls ? (rtkCalls / eligibleCalls) * 100 : 0 };
704
800
  }
@@ -725,7 +821,7 @@ export function processSessionText(accum: SessionAccum, state: { codexProvider:
725
821
  continue;
726
822
  }
727
823
  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);
824
+ ingestSessionRow(accum, parsed.model, parsed.usage, row.timestamp, parsed.durMs, parsed.run, parsed.tools, host, rid, agent, parsed.ttftMs);
729
825
  } catch { /* skip corrupt lines */ }
730
826
  }
731
827
  }
@@ -762,7 +858,7 @@ export function leadBinary(command: unknown): string {
762
858
  return 'bash';
763
859
  }
764
860
 
765
- // --- Pricing lives in ./pricing.ts (live LiteLLM cache + fallback table) ---
861
+ // Pricing lives in ./pricing.ts (live LiteLLM cache + fallback table).
766
862
  import { DEFAULT_PRICE, PRICE_TIERS_FIRM, canonicalPriceId, priceFor, pricesOverridePath, refreshPricesIfStale, type PriceHit, type PriceTier } from './pricing.ts';
767
863
  import { co2GramsFor, energyWhFor } from './carbon.ts';
768
864
  import type { ModelPrice } from './pricing.ts';
@@ -771,11 +867,7 @@ export { DEFAULT_PRICE, PRICE_TIERS_FIRM, priceFor, pricesOverridePath, refreshP
771
867
  export { co2GramsFor, energyWhFor };
772
868
  export type { PriceHit, PriceTier, ModelPrice };
773
869
 
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
- */
870
+ /** Price one model's tokens; `measured` outranks every rate card, so pass undefined when unreported. */
779
871
  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
872
  const m = 1 / 1_000_000;
781
873
  if (measured !== undefined) {
@@ -797,8 +889,7 @@ export function usdCost(t: TokenBreakdown, model?: string, measured?: number): {
797
889
  };
798
890
  return {
799
891
  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.
892
+ // `tail`/`default` are estimates; summing them beside measured cost reads as a bill it is not.
802
893
  priced: PRICE_TIERS_FIRM.includes(hit.tier),
803
894
  tier: hit.tier,
804
895
  buckets,
@@ -829,7 +920,7 @@ export function readUsage(): UsageRow[] {
829
920
  return rows;
830
921
  }
831
922
 
832
- // Delete the ledger file and return the rows cleared. Tersio-owned data only: transcripts and the RTK database are never touched.
923
+ // Delete the ledger file and return the rows cleared. Tersio-owned data only: transcripts and the RTK database stay.
833
924
  export function clearUsageLedger(): number {
834
925
  const rows = readUsage().length;
835
926
  try {