@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.
- package/README.md +42 -38
- package/dashboard/dist/index.html +15 -15
- package/dist/bench/code-out/pfull-0/debounce.js +13 -0
- package/dist/bench/code-out/pfull-0/deepClone.js +18 -0
- package/dist/bench/code-out/pfull-0/groupBy.js +15 -0
- package/dist/bench/code-out/pfull-1/debounce.js +13 -0
- package/dist/bench/code-out/pfull-1/deepClone.js +17 -0
- package/dist/bench/code-out/pfull-1/groupBy.js +14 -0
- package/dist/bench/code-out/pfull-2/debounce.js +13 -0
- package/dist/bench/code-out/pfull-2/deepClone.js +18 -0
- package/dist/bench/code-out/pfull-2/groupBy.js +15 -0
- package/dist/bench/code-out/pfull-3/debounce.js +12 -0
- package/dist/bench/code-out/pfull-3/deepClone.js +17 -0
- package/dist/bench/code-out/pfull-3/groupBy.js +7 -0
- package/dist/bench/code-out/pfull-4/debounce.js +10 -0
- package/dist/bench/code-out/pfull-4/deepClone.js +17 -0
- package/dist/bench/code-out/pfull-4/groupBy.js +14 -0
- package/dist/bench/code-out/plite-0/debounce.js +13 -0
- package/dist/bench/code-out/plite-0/deepClone.js +18 -0
- package/dist/bench/code-out/plite-0/groupBy.js +15 -0
- package/dist/bench/code-out/plite-1/debounce.js +13 -0
- package/dist/bench/code-out/plite-1/deepClone.js +17 -0
- package/dist/bench/code-out/plite-1/groupBy.js +11 -0
- package/dist/bench/code-out/plite-2/debounce.js +12 -0
- package/dist/bench/code-out/plite-2/deepClone.js +17 -0
- package/dist/bench/code-out/plite-2/groupBy.js +14 -0
- package/dist/bench/code-out/plite-3/debounce.js +12 -0
- package/dist/bench/code-out/plite-3/deepClone.js +17 -0
- package/dist/bench/code-out/plite-3/groupBy.js +14 -0
- package/dist/bench/code-out/plite-4/debounce.js +12 -0
- package/dist/bench/code-out/plite-4/deepClone.js +17 -0
- package/dist/bench/code-out/plite-4/groupBy.js +14 -0
- package/dist/bench/code-out/pultra-0/debounce.js +12 -0
- package/dist/bench/code-out/pultra-0/deepClone.js +20 -0
- package/dist/bench/code-out/pultra-0/groupBy.js +14 -0
- package/dist/bench/code-out/pultra-1/debounce.js +10 -0
- package/dist/bench/code-out/pultra-1/deepClone.js +17 -0
- package/dist/bench/code-out/pultra-1/groupBy.js +7 -0
- package/dist/bench/code-out/pultra-2/debounce.js +13 -0
- package/dist/bench/code-out/pultra-2/deepClone.js +18 -0
- package/dist/bench/code-out/pultra-2/groupBy.js +15 -0
- package/dist/bench/code-out/pultra-3/debounce.js +13 -0
- package/dist/bench/code-out/pultra-3/deepClone.js +18 -0
- package/dist/bench/code-out/pultra-3/groupBy.js +15 -0
- package/dist/bench/code-out/pultra-4/debounce.js +13 -0
- package/dist/bench/code-out/pultra-4/deepClone.js +18 -0
- package/dist/bench/code-out/pultra-4/groupBy.js +15 -0
- package/dist/bench/code-out/run-0/debounce.js +12 -0
- package/dist/bench/code-out/run-0/deepClone.js +17 -0
- package/dist/bench/code-out/run-0/groupBy.js +14 -0
- package/dist/bench/code-out/run-1/debounce.js +13 -0
- package/dist/bench/code-out/run-1/deepClone.js +15 -0
- package/dist/bench/code-out/run-1/groupBy.js +13 -0
- package/dist/bench/code-out/run-2/debounce.js +12 -0
- package/dist/bench/code-out/run-2/deepClone.js +18 -0
- package/dist/bench/code-out/run-2/groupBy.js +13 -0
- package/dist/bench/code-out/run-3/debounce.js +12 -0
- package/dist/bench/code-out/run-3/deepClone.js +18 -0
- package/dist/bench/code-out/run-3/groupBy.js +14 -0
- package/dist/bench/code-out/run-4/debounce.js +12 -0
- package/dist/bench/code-out/run-4/deepClone.js +16 -0
- package/dist/bench/code-out/run-4/groupBy.js +13 -0
- package/dist/cli/backups.js +7 -14
- package/dist/cli/banner.js +3 -3
- package/dist/cli/common.js +29 -13
- package/dist/cli/currency.js +1 -1
- package/dist/cli/dashboard.js +66 -35
- package/dist/cli/doctor-fix.js +204 -19
- package/dist/cli/doctor.js +263 -40
- package/dist/cli/hosts.js +25 -6
- package/dist/cli/install.js +150 -49
- package/dist/cli/interactive.js +3 -3
- package/dist/cli/manifest.js +109 -9
- package/dist/cli/profile.js +13 -8
- package/dist/cli/rtk-wiring.js +1 -1
- package/dist/cli/settings.js +20 -5
- package/dist/cli/uninstall.js +4 -4
- package/dist/cli/update.js +6 -7
- package/dist/cli/usage.js +6 -9
- package/dist/extensions/caveman/index.js +10 -4
- package/dist/extensions/caveman/rule-lite.md +77 -0
- package/dist/extensions/caveman/rule-megacave-lite.md +59 -0
- package/dist/extensions/caveman/rule-megacave-ultra.md +59 -0
- package/dist/extensions/caveman/rule-megacave.md +5 -3
- package/dist/extensions/caveman/rule-ultra.md +4 -4
- package/dist/extensions/caveman/rule.md +55 -45
- package/dist/extensions/combo/index.js +85 -25
- package/dist/extensions/commands/index.js +1 -1
- package/dist/extensions/pstack/index.js +121 -0
- package/dist/extensions/pstack/rule.md +141 -0
- package/dist/extensions/rtk/filter.js +0 -1
- package/dist/extensions/rtk/session.js +20 -12
- package/dist/extensions/server.js +90 -6
- package/dist/extensions/shared/agent-stats.js +214 -278
- package/dist/extensions/shared/brand-marks.js +33 -34
- package/dist/extensions/shared/carbon.js +1 -1
- package/dist/extensions/shared/plugin-settings.js +11 -6
- package/dist/extensions/shared/pricing.js +8 -16
- package/dist/extensions/shared/rtk-eligible.js +84 -0
- package/dist/extensions/shared/rtk-gain.js +5 -10
- package/dist/extensions/shared/session-state.js +15 -10
- package/dist/extensions/shared/sqlite.js +162 -0
- package/dist/extensions/shared/status.js +8 -9
- package/dist/extensions/shared/types.js +1 -1
- package/dist/extensions/shared/usage-ledger.js +155 -61
- package/dist/extensions/shared/usage-store.js +111 -52
- package/dist/extensions/shared/utils.js +5 -5
- package/dist/extensions/timing/index.js +31 -0
- package/dist/extensions/updater/index.js +3 -6
- package/dist/tersio.js +8 -3
- package/extensions/caveman/index.ts +10 -4
- package/extensions/caveman/rule-lite.md +77 -0
- package/extensions/caveman/rule-megacave-lite.md +59 -0
- package/extensions/caveman/rule-megacave-ultra.md +59 -0
- package/extensions/caveman/rule-megacave.md +5 -3
- package/extensions/caveman/rule-ultra.md +4 -4
- package/extensions/caveman/rule.md +55 -45
- package/extensions/combo/index.ts +86 -23
- package/extensions/commands/index.ts +1 -1
- package/extensions/pstack/index.ts +130 -0
- package/extensions/pstack/playbooks/authoring-a-skill.md +12 -0
- package/extensions/pstack/playbooks/autonomous-run.md +13 -0
- package/extensions/pstack/playbooks/autopilot-full.md +13 -0
- package/extensions/pstack/playbooks/autopilot-stack.md +16 -0
- package/extensions/pstack/playbooks/babysit.md +23 -0
- package/extensions/pstack/playbooks/bug-fix.md +15 -0
- package/extensions/pstack/playbooks/eval.md +25 -0
- package/extensions/pstack/playbooks/feature.md +21 -0
- package/extensions/pstack/playbooks/hillclimb.md +21 -0
- package/extensions/pstack/playbooks/investigation.md +14 -0
- package/extensions/pstack/playbooks/multi-phase-plan.md +155 -0
- package/extensions/pstack/playbooks/opening-a-pr.md +34 -0
- package/extensions/pstack/playbooks/orchestrate.md +111 -0
- package/extensions/pstack/playbooks/pause-safely.md +10 -0
- package/extensions/pstack/playbooks/perf-issue.md +25 -0
- package/extensions/pstack/playbooks/prototype.md +14 -0
- package/extensions/pstack/playbooks/refactoring.md +16 -0
- package/extensions/pstack/playbooks/runtime-forensics.md +11 -0
- package/extensions/pstack/playbooks/session-pickup.md +11 -0
- package/extensions/pstack/playbooks/shipping.md +17 -0
- package/extensions/pstack/playbooks/trace-forensics.md +14 -0
- package/extensions/pstack/playbooks/visual-parity.md +11 -0
- package/extensions/pstack/playbooks/worktree-cleanup.md +12 -0
- package/extensions/pstack/principles/attack-the-premise.md +22 -0
- package/extensions/pstack/principles/boundary-discipline.md +33 -0
- package/extensions/pstack/principles/build-the-lever.md +22 -0
- package/extensions/pstack/principles/encode-lessons-in-structure.md +10 -0
- package/extensions/pstack/principles/exhaust-the-design-space.md +8 -0
- package/extensions/pstack/principles/experience-first.md +18 -0
- package/extensions/pstack/principles/explain-the-number.md +16 -0
- package/extensions/pstack/principles/fix-root-causes.md +23 -0
- package/extensions/pstack/principles/foundational-thinking.md +20 -0
- package/extensions/pstack/principles/guard-the-context-window.md +11 -0
- package/extensions/pstack/principles/laziness-protocol.md +9 -0
- package/extensions/pstack/principles/make-operations-idempotent.md +23 -0
- package/extensions/pstack/principles/migrate-callers-then-delete-legacy-apis.md +21 -0
- package/extensions/pstack/principles/minimize-reader-load.md +17 -0
- package/extensions/pstack/principles/model-the-domain.md +23 -0
- package/extensions/pstack/principles/never-block-on-the-human.md +14 -0
- package/extensions/pstack/principles/outcome-oriented-execution.md +20 -0
- package/extensions/pstack/principles/prove-it-works.md +21 -0
- package/extensions/pstack/principles/redesign-from-first-principles.md +15 -0
- package/extensions/pstack/principles/separate-before-serializing-shared-state.md +15 -0
- package/extensions/pstack/principles/sequence-verifiable-units.md +16 -0
- package/extensions/pstack/principles/subtract-before-you-add.md +17 -0
- package/extensions/pstack/principles/test-behavior-not-implementation.md +20 -0
- package/extensions/pstack/principles/type-system-discipline.md +17 -0
- package/extensions/pstack/references/bugbot-triage.md +142 -0
- package/extensions/pstack/rule.md +141 -0
- package/extensions/pstack/skills/architect/SKILL.md +78 -0
- package/extensions/pstack/skills/architect/references/design-red-flags.md +57 -0
- package/extensions/pstack/skills/architect/references/rationale-template.md +35 -0
- package/extensions/pstack/skills/architect/references/runner-prompt.md +20 -0
- package/extensions/pstack/skills/arena/SKILL.md +47 -0
- package/extensions/pstack/skills/benchmark-checklist/SKILL.md +38 -0
- package/extensions/pstack/skills/blast-radius/SKILL.md +49 -0
- package/extensions/pstack/skills/bro/SKILL.md +6 -0
- package/extensions/pstack/skills/correct/SKILL.md +31 -0
- package/extensions/pstack/skills/figure-it-out/SKILL.md +51 -0
- package/extensions/pstack/skills/how/SKILL.md +35 -0
- package/extensions/pstack/skills/how/references/explainer-prompt.md +55 -0
- package/extensions/pstack/skills/how/references/explorer-prompt.md +35 -0
- package/extensions/pstack/skills/interrogate/SKILL.md +73 -0
- package/extensions/pstack/skills/interrogate/references/code-quality-review.md +47 -0
- package/extensions/pstack/skills/interrogate/references/lead-judgment.md +58 -0
- package/extensions/pstack/skills/interrogate/references/reviewer-prompt.md +62 -0
- package/extensions/pstack/skills/interrogate/references/rubric.md +77 -0
- package/extensions/pstack/skills/no-comments/SKILL.md +23 -0
- package/extensions/pstack/skills/reflect/SKILL.md +66 -0
- package/extensions/pstack/skills/reflect/references/divergent-reviewer.md +38 -0
- package/extensions/pstack/skills/reflect/references/judgment-reviewer.md +37 -0
- package/extensions/pstack/skills/reflect/references/synthesizer.md +56 -0
- package/extensions/pstack/skills/reflect/references/tooling-reviewer.md +48 -0
- package/extensions/pstack/skills/show-me-your-work/SKILL.md +81 -0
- package/extensions/pstack/skills/swarm/SKILL.md +45 -0
- package/extensions/pstack/skills/tdd/SKILL.md +41 -0
- package/extensions/pstack/skills/technical-writing/SKILL.md +105 -0
- package/extensions/pstack/skills/unslop/SKILL.md +61 -0
- package/extensions/pstack/skills/why/SKILL.md +97 -0
- package/extensions/pstack/skills/why/references/epistemics.md +71 -0
- package/extensions/pstack/skills/why/references/investigator-prompt.md +84 -0
- package/extensions/pstack/skills/why/references/source-playbook.md +17 -0
- package/extensions/pstack/skills/why/references/sources/code-archaeology.md +51 -0
- package/extensions/pstack/skills/why/references/sources/databricks.md +70 -0
- package/extensions/pstack/skills/why/references/sources/datadog.md +97 -0
- package/extensions/pstack/skills/why/references/sources/incident-postmortem.md +5 -0
- package/extensions/pstack/skills/why/references/sources/linear.md +48 -0
- package/extensions/pstack/skills/why/references/sources/notion.md +55 -0
- package/extensions/pstack/skills/why/references/sources/sentry.md +66 -0
- package/extensions/pstack/skills/why/references/sources/slack.md +17 -0
- package/extensions/pstack/skills/why/references/synthesizer-prompt.md +129 -0
- package/extensions/rtk/filter.ts +0 -1
- package/extensions/rtk/session.ts +19 -11
- package/extensions/server.ts +72 -7
- package/extensions/shared/agent-stats.ts +226 -278
- package/extensions/shared/brand-marks.ts +34 -34
- package/extensions/shared/carbon.ts +1 -1
- package/extensions/shared/plugin-settings.ts +12 -6
- package/extensions/shared/pricing.ts +10 -26
- package/extensions/shared/rtk-eligible.ts +83 -0
- package/extensions/shared/rtk-gain.ts +5 -10
- package/extensions/shared/session-state.ts +18 -13
- package/extensions/shared/sqlite.ts +162 -0
- package/extensions/shared/status.ts +9 -9
- package/extensions/shared/types.ts +2 -1
- package/extensions/shared/usage-ledger.ts +157 -66
- package/extensions/shared/usage-store.ts +120 -52
- package/extensions/shared/utils.ts +5 -5
- package/extensions/timing/index.ts +44 -0
- package/extensions/updater/index.ts +3 -8
- package/package.json +21 -9
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
//
|
|
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
|
-
//
|
|
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,
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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 },
|
|
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 },
|
|
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;
|
|
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
|
-
//
|
|
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;
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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 {
|