@ipv9/tokentracker-cli 0.39.46 → 0.39.48
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/dashboard/dist/assets/{Card-Bk5BMQ2P.js → Card-CQ0G1GlR.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-DPP44kwE.js → DashboardPage-B5aBShIc.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-CWvnEI0O.js → FadeIn-BG37Pb1J.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-BmSn2zKd.js → IpCheckPage-DD2nObWJ.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-CDCccsH_.js → LimitsPage-L55ZcbUp.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-D19MhLRF.js → LocalOnlyNotice-3-SG-oKl.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-D78fYtAs.js → PopoverPopup--Imnw4O7.js} +1 -1
- package/dashboard/dist/assets/{Select-CKV1Rvl0.js → Select-BNQ73B60.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-5eDkx616.js → SelectItemText-Dlv4NEpN.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-DIwe50pL.js → SettingsPage-BDo6aFgs.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-zUzRrWak.js → SkillsPage-CA2IaWI5.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-3yOTNqKD.js → WidgetsPage-DTVOLZEy.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-DPAnbLdi.js → WrappedPage-CYwBaHuQ.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-BGg2lx0L.js → arrow-up-right-3x5AJIbb.js} +1 -1
- package/dashboard/dist/assets/{download-BU4HPPvG.js → download-DSegD216.js} +1 -1
- package/dashboard/dist/assets/{format-vw28c71d.js → format-yTFTff0D.js} +1 -1
- package/dashboard/dist/assets/{limitDisplay-CY5Qlw5s.js → limitDisplay-DxvqWNgp.js} +1 -1
- package/dashboard/dist/assets/{main-D9Jo7Pt-.js → main-DBDTVhz8.js} +3 -3
- package/dashboard/dist/assets/{mock-data-B0oPPFnn.js → mock-data-oSiCcgVh.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-BAGMxrq7.js → use-limits-display-prefs-C2F0IlsU.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-C4lSH6qA.js → use-native-settings-DtQFof4g.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-Dvz0184_.js → useCurrency-5LxVaA8B.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +138 -24
package/src/lib/rollout.js
CHANGED
|
@@ -3237,21 +3237,48 @@ function isUncPath(p) {
|
|
|
3237
3237
|
return typeof p === "string" && (p.startsWith("\\\\") || p.startsWith("//"));
|
|
3238
3238
|
}
|
|
3239
3239
|
|
|
3240
|
+
function sqliteDbChangeFingerprint(dbPath) {
|
|
3241
|
+
const parts = [];
|
|
3242
|
+
for (const suffix of ["", "-wal", "-shm", "-journal"]) {
|
|
3243
|
+
const filePath = dbPath + suffix;
|
|
3244
|
+
const label = suffix || "main";
|
|
3245
|
+
try {
|
|
3246
|
+
const stat = fssync.statSync(filePath, { bigint: true });
|
|
3247
|
+
parts.push(`${label}:${stat.size}:${stat.mtimeNs}`);
|
|
3248
|
+
} catch (error) {
|
|
3249
|
+
if (error && error.code === "ENOENT") {
|
|
3250
|
+
parts.push(`${label}:missing`);
|
|
3251
|
+
continue;
|
|
3252
|
+
}
|
|
3253
|
+
// A partial fingerprint must never authorize the no-change fast path.
|
|
3254
|
+
// Retry the read instead of treating repeated stat failures as stable.
|
|
3255
|
+
return null;
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
return parts.join("|");
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3240
3261
|
function snapshotSqliteDb(dbPath) {
|
|
3241
3262
|
const tmpRoot = fssync.mkdtempSync(
|
|
3242
3263
|
path.join(require("node:os").tmpdir(), "tokentracker-hermes-snap-"),
|
|
3243
3264
|
);
|
|
3244
3265
|
const target = path.join(tmpRoot, path.basename(dbPath));
|
|
3245
3266
|
fssync.copyFileSync(dbPath, target);
|
|
3246
|
-
//
|
|
3267
|
+
// Copy SQLite sidecars as one logical snapshot. A missing sidecar is normal,
|
|
3268
|
+
// but any other copy failure makes the snapshot incomplete and must prevent
|
|
3269
|
+
// callers from advancing a change fingerprint based on stale main-DB data.
|
|
3270
|
+
let sidecarsComplete = true;
|
|
3247
3271
|
for (const suffix of ["-wal", "-shm", "-journal"]) {
|
|
3248
3272
|
const src = dbPath + suffix;
|
|
3249
3273
|
try {
|
|
3250
|
-
|
|
3251
|
-
} catch (
|
|
3274
|
+
fssync.copyFileSync(src, target + suffix);
|
|
3275
|
+
} catch (error) {
|
|
3276
|
+
if (!error || error.code !== "ENOENT") sidecarsComplete = false;
|
|
3277
|
+
}
|
|
3252
3278
|
}
|
|
3253
3279
|
return {
|
|
3254
3280
|
path: target,
|
|
3281
|
+
sidecarsComplete,
|
|
3255
3282
|
cleanup() {
|
|
3256
3283
|
try { fssync.rmSync(tmpRoot, { recursive: true, force: true }); } catch (_e) { }
|
|
3257
3284
|
},
|
|
@@ -3267,12 +3294,6 @@ function readHermesSessions(dbPath, lastCompletedEpoch, unfinishedSessionIds = [
|
|
|
3267
3294
|
const forceIncludeSql = forceIds.length > 0
|
|
3268
3295
|
? ` OR id IN (${forceIds.map(sqliteStringLiteral).join(",")})`
|
|
3269
3296
|
: "";
|
|
3270
|
-
// Fetch sessions that started at/after the cursor, sessions that are still
|
|
3271
|
-
// in-progress (ended_at IS NULL), OR sessions that were previously observed
|
|
3272
|
-
// unfinished. Hermes updates token counts in real-time, including a final
|
|
3273
|
-
// delta when an active session later gets ended_at set.
|
|
3274
|
-
const sql = `SELECT id, model, started_at, ended_at, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, reasoning_tokens, message_count FROM sessions WHERE (started_at >= ${since} OR ended_at IS NULL${forceIncludeSql}) AND (input_tokens > 0 OR output_tokens > 0 OR cache_read_tokens > 0 OR reasoning_tokens > 0) ORDER BY started_at ASC`;
|
|
3275
|
-
|
|
3276
3297
|
let snapshot = null;
|
|
3277
3298
|
let effectiveDbPath = dbPath;
|
|
3278
3299
|
if (isUncPath(dbPath)) {
|
|
@@ -3284,6 +3305,65 @@ function readHermesSessions(dbPath, lastCompletedEpoch, unfinishedSessionIds = [
|
|
|
3284
3305
|
// the non-locked case (e.g. permissions, transient I/O).
|
|
3285
3306
|
}
|
|
3286
3307
|
}
|
|
3308
|
+
const eligible = `(started_at >= ${since} OR ended_at IS NULL${forceIncludeSql})`;
|
|
3309
|
+
const modelUsageColumns = new Set(
|
|
3310
|
+
readSqliteJsonRows(
|
|
3311
|
+
effectiveDbPath,
|
|
3312
|
+
"PRAGMA table_info(session_model_usage)",
|
|
3313
|
+
{ label: "Hermes", maxBuffer: 1024 * 1024, timeout: 5_000, ...sqliteOptions },
|
|
3314
|
+
).map((column) => column?.name).filter(Boolean),
|
|
3315
|
+
);
|
|
3316
|
+
const hasModelUsage = [
|
|
3317
|
+
"session_id",
|
|
3318
|
+
"model",
|
|
3319
|
+
"api_call_count",
|
|
3320
|
+
"input_tokens",
|
|
3321
|
+
"output_tokens",
|
|
3322
|
+
"cache_read_tokens",
|
|
3323
|
+
"cache_write_tokens",
|
|
3324
|
+
"reasoning_tokens",
|
|
3325
|
+
"first_seen",
|
|
3326
|
+
"last_seen",
|
|
3327
|
+
].every((column) => modelUsageColumns.has(column));
|
|
3328
|
+
const sql = hasModelUsage
|
|
3329
|
+
? `WITH eligible_sessions AS (
|
|
3330
|
+
SELECT * FROM sessions WHERE ${eligible}
|
|
3331
|
+
)
|
|
3332
|
+
SELECT s.id, TRIM(u.model) AS model, s.model AS session_model, s.started_at, s.ended_at,
|
|
3333
|
+
SUM(u.input_tokens) AS input_tokens,
|
|
3334
|
+
SUM(u.output_tokens) AS output_tokens,
|
|
3335
|
+
SUM(u.cache_read_tokens) AS cache_read_tokens,
|
|
3336
|
+
SUM(u.cache_write_tokens) AS cache_write_tokens,
|
|
3337
|
+
SUM(u.reasoning_tokens) AS reasoning_tokens,
|
|
3338
|
+
SUM(u.api_call_count) AS message_count,
|
|
3339
|
+
MIN(u.first_seen) AS first_seen,
|
|
3340
|
+
MAX(u.last_seen) AS last_seen,
|
|
3341
|
+
1 AS per_model
|
|
3342
|
+
FROM eligible_sessions s
|
|
3343
|
+
JOIN session_model_usage u ON u.session_id = s.id
|
|
3344
|
+
GROUP BY s.id, TRIM(u.model)
|
|
3345
|
+
HAVING SUM(u.input_tokens) > 0 OR SUM(u.output_tokens) > 0 OR SUM(u.cache_read_tokens) > 0 OR SUM(u.cache_write_tokens) > 0 OR SUM(u.reasoning_tokens) > 0
|
|
3346
|
+
UNION ALL
|
|
3347
|
+
SELECT s.id, s.model, s.model AS session_model, s.started_at, s.ended_at,
|
|
3348
|
+
s.input_tokens, s.output_tokens, s.cache_read_tokens,
|
|
3349
|
+
s.cache_write_tokens, s.reasoning_tokens, s.message_count,
|
|
3350
|
+
NULL AS first_seen, NULL AS last_seen, 0 AS per_model
|
|
3351
|
+
FROM eligible_sessions s
|
|
3352
|
+
WHERE NOT EXISTS (
|
|
3353
|
+
SELECT 1 FROM session_model_usage u
|
|
3354
|
+
WHERE u.session_id = s.id
|
|
3355
|
+
AND (u.input_tokens > 0 OR u.output_tokens > 0 OR u.cache_read_tokens > 0 OR u.cache_write_tokens > 0 OR u.reasoning_tokens > 0)
|
|
3356
|
+
)
|
|
3357
|
+
AND (s.input_tokens > 0 OR s.output_tokens > 0 OR s.cache_read_tokens > 0 OR s.cache_write_tokens > 0 OR s.reasoning_tokens > 0)
|
|
3358
|
+
ORDER BY 4 ASC`
|
|
3359
|
+
: `SELECT id, model, model AS session_model, started_at, ended_at,
|
|
3360
|
+
input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
|
|
3361
|
+
reasoning_tokens, message_count, NULL AS first_seen,
|
|
3362
|
+
NULL AS last_seen, 0 AS per_model
|
|
3363
|
+
FROM sessions
|
|
3364
|
+
WHERE ${eligible}
|
|
3365
|
+
AND (input_tokens > 0 OR output_tokens > 0 OR cache_read_tokens > 0 OR cache_write_tokens > 0 OR reasoning_tokens > 0)
|
|
3366
|
+
ORDER BY started_at ASC`;
|
|
3287
3367
|
|
|
3288
3368
|
try {
|
|
3289
3369
|
return readSqliteJsonRows(effectiveDbPath, sql, {
|
|
@@ -3341,6 +3421,11 @@ async function parseHermesIncremental({ hermesPath, dbPath, cursors, queuePath,
|
|
|
3341
3421
|
// Per-session snapshot from the previous sync: { [sessionId]: { in, out, cacheRead, cacheWrite, reasoning } }
|
|
3342
3422
|
const prevSnapshots = (dbState.snapshots && typeof dbState.snapshots === "object")
|
|
3343
3423
|
? dbState.snapshots : {};
|
|
3424
|
+
const hasPerModelRows = rows.some((row) => Number(row.per_model) === 1);
|
|
3425
|
+
const adoptingPerModelState =
|
|
3426
|
+
hasPerModelRows &&
|
|
3427
|
+
dbState.modelUsageVersion !== 1 &&
|
|
3428
|
+
Object.keys(prevSnapshots).length > 0;
|
|
3344
3429
|
|
|
3345
3430
|
// Only advance past sessions that have fully ended. Active sessions
|
|
3346
3431
|
// (ended_at IS NULL) must be re-read every sync because Hermes updates
|
|
@@ -3361,10 +3446,22 @@ async function parseHermesIncremental({ hermesPath, dbPath, cursors, queuePath,
|
|
|
3361
3446
|
const cacheWrite = toNonNegativeInt(row.cache_write_tokens);
|
|
3362
3447
|
const reasoning = toNonNegativeInt(row.reasoning_tokens);
|
|
3363
3448
|
const messageCount = toNonNegativeInt(row.message_count);
|
|
3364
|
-
if (inputTokens === 0 && outputTokens === 0 && cacheRead === 0 && reasoning === 0) continue;
|
|
3449
|
+
if (inputTokens === 0 && outputTokens === 0 && cacheRead === 0 && cacheWrite === 0 && reasoning === 0) continue;
|
|
3365
3450
|
|
|
3366
|
-
|
|
3367
|
-
|
|
3451
|
+
const perModel = Number(row.per_model) === 1;
|
|
3452
|
+
const model = normalizeModelInput(row.model) || "hermes-agent";
|
|
3453
|
+
const snapshotKey = perModel ? JSON.stringify([String(row.id || ""), model]) : row.id;
|
|
3454
|
+
const currentSnapshot = {
|
|
3455
|
+
in: inputTokens,
|
|
3456
|
+
out: outputTokens,
|
|
3457
|
+
cacheRead,
|
|
3458
|
+
cacheWrite,
|
|
3459
|
+
reasoning,
|
|
3460
|
+
message_count: messageCount,
|
|
3461
|
+
};
|
|
3462
|
+
// Save current snapshot for next sync. Mixed-model sessions are keyed by
|
|
3463
|
+
// both session and model so later growth is attributed independently.
|
|
3464
|
+
nextSnapshots[snapshotKey] = currentSnapshot;
|
|
3368
3465
|
|
|
3369
3466
|
const startedAt = Number(row.started_at);
|
|
3370
3467
|
const endedAt = row.ended_at == null ? null : Number(row.ended_at);
|
|
@@ -3380,7 +3477,10 @@ async function parseHermesIncremental({ hermesPath, dbPath, cursors, queuePath,
|
|
|
3380
3477
|
// Compute delta from previous snapshot (if any) so that we only count
|
|
3381
3478
|
// new usage since the last sync. First time we see a session the
|
|
3382
3479
|
// previous snapshot is absent, so the full amount is the delta.
|
|
3383
|
-
const
|
|
3480
|
+
const legacySessionSnapshot = prevSnapshots[row.id];
|
|
3481
|
+
const prev = adoptingPerModelState && perModel && legacySessionSnapshot
|
|
3482
|
+
? currentSnapshot
|
|
3483
|
+
: prevSnapshots[snapshotKey];
|
|
3384
3484
|
let dInput = inputTokens;
|
|
3385
3485
|
let dOutput = outputTokens;
|
|
3386
3486
|
let dCacheRead = cacheRead;
|
|
@@ -3398,18 +3498,17 @@ async function parseHermesIncremental({ hermesPath, dbPath, cursors, queuePath,
|
|
|
3398
3498
|
// Skip if delta is zero (session unchanged since last sync)
|
|
3399
3499
|
if (dInput === 0 && dOutput === 0 && dCacheRead === 0 && dCacheWrite === 0 && dReasoning === 0) continue;
|
|
3400
3500
|
|
|
3401
|
-
//
|
|
3402
|
-
//
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3501
|
+
// Per-model rows carry the authoritative last API-call timestamp. Older
|
|
3502
|
+
// Hermes schemas fall back to the session-level start/end policy.
|
|
3503
|
+
const lastSeen = row.last_seen == null ? null : Number(row.last_seen);
|
|
3504
|
+
const epochSec = perModel && Number.isFinite(lastSeen) && lastSeen > 0
|
|
3505
|
+
? lastSeen
|
|
3506
|
+
: endedAt ?? (prev ? Date.parse(updatedAt) / 1000 : startedAt);
|
|
3406
3507
|
if (!epochSec || !Number.isFinite(epochSec)) continue;
|
|
3407
3508
|
const tsIso = new Date(epochSec * 1000).toISOString();
|
|
3408
3509
|
const bucketStart = toUtcHalfHourStart(tsIso);
|
|
3409
3510
|
if (!bucketStart) continue;
|
|
3410
3511
|
|
|
3411
|
-
const model = normalizeModelInput(row.model) || "hermes-agent";
|
|
3412
|
-
|
|
3413
3512
|
const delta = {
|
|
3414
3513
|
input_tokens: dInput,
|
|
3415
3514
|
cached_input_tokens: dCacheRead,
|
|
@@ -3445,6 +3544,7 @@ async function parseHermesIncremental({ hermesPath, dbPath, cursors, queuePath,
|
|
|
3445
3544
|
lastCompletedStartedAt: nextLastCompletedStartedAt,
|
|
3446
3545
|
unfinishedSessionIds: Array.from(nextUnfinishedSessionIds),
|
|
3447
3546
|
snapshots: nextSnapshots,
|
|
3547
|
+
...(hasPerModelRows ? { modelUsageVersion: 1 } : {}),
|
|
3448
3548
|
updatedAt,
|
|
3449
3549
|
});
|
|
3450
3550
|
}
|
|
@@ -6128,7 +6228,8 @@ async function parseGooseIncremental({
|
|
|
6128
6228
|
? { ...gooseState.sessionTotals }
|
|
6129
6229
|
: {};
|
|
6130
6230
|
|
|
6131
|
-
const
|
|
6231
|
+
const cursorDbFingerprint =
|
|
6232
|
+
typeof gooseState.lastDbFingerprint === "string" ? gooseState.lastDbFingerprint : null;
|
|
6132
6233
|
let currentMtime = 0;
|
|
6133
6234
|
try {
|
|
6134
6235
|
currentMtime = fssync.statSync(resolvedDb).mtimeMs;
|
|
@@ -6139,9 +6240,12 @@ async function parseGooseIncremental({
|
|
|
6139
6240
|
}
|
|
6140
6241
|
throw e;
|
|
6141
6242
|
}
|
|
6142
|
-
|
|
6143
|
-
//
|
|
6144
|
-
|
|
6243
|
+
const currentDbFingerprint = sqliteDbChangeFingerprint(resolvedDb);
|
|
6244
|
+
// Goose commonly commits active-session writes only to SQLite's WAL. Skip
|
|
6245
|
+
// the table scan only when the main DB and every sidecar are unchanged.
|
|
6246
|
+
// Legacy cursors without a fingerprint intentionally perform one read to
|
|
6247
|
+
// establish the sidecar-aware baseline.
|
|
6248
|
+
if (cursorDbFingerprint && currentDbFingerprint === cursorDbFingerprint) {
|
|
6145
6249
|
cursors.goose = { ...gooseState, sessionTotals, updatedAt: observedAt };
|
|
6146
6250
|
return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
6147
6251
|
}
|
|
@@ -6151,6 +6255,15 @@ async function parseGooseIncremental({
|
|
|
6151
6255
|
const snap = snapshotSqliteDb(resolvedDb);
|
|
6152
6256
|
let rows = [];
|
|
6153
6257
|
try {
|
|
6258
|
+
if (!snap.sidecarsComplete) {
|
|
6259
|
+
cursors.goose = {
|
|
6260
|
+
...gooseState,
|
|
6261
|
+
sessionTotals,
|
|
6262
|
+
lastDbFingerprint: null,
|
|
6263
|
+
updatedAt: observedAt,
|
|
6264
|
+
};
|
|
6265
|
+
return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
6266
|
+
}
|
|
6154
6267
|
rows = readGooseSessionsFromSqlite(snap.path, sqliteOptions);
|
|
6155
6268
|
} finally {
|
|
6156
6269
|
snap.cleanup();
|
|
@@ -6271,6 +6384,7 @@ async function parseGooseIncremental({
|
|
|
6271
6384
|
...gooseState,
|
|
6272
6385
|
sessionTotals,
|
|
6273
6386
|
lastDbMtimeMs: currentMtime,
|
|
6387
|
+
lastDbFingerprint: currentDbFingerprint,
|
|
6274
6388
|
updatedAt: observedAt,
|
|
6275
6389
|
};
|
|
6276
6390
|
|