@ipv9/tokentracker-cli 0.39.45 → 0.39.47

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 (26) hide show
  1. package/dashboard/dist/assets/{Card-2-TQg7P7.js → Card-BMWFZ5k5.js} +1 -1
  2. package/dashboard/dist/assets/{DashboardPage-yb9ss9uE.js → DashboardPage-HjWNRKq7.js} +1 -1
  3. package/dashboard/dist/assets/{FadeIn-BOc6XtOK.js → FadeIn-BgLVBi0I.js} +1 -1
  4. package/dashboard/dist/assets/{IpCheckPage-C2tIb68P.js → IpCheckPage-CsS9rEbM.js} +1 -1
  5. package/dashboard/dist/assets/{LimitsPage-BsuLQ9co.js → LimitsPage-BifpRDJ6.js} +1 -1
  6. package/dashboard/dist/assets/{LocalOnlyNotice-C8R9KLef.js → LocalOnlyNotice-D7tdrheS.js} +1 -1
  7. package/dashboard/dist/assets/{PopoverPopup-BpfseoI7.js → PopoverPopup-DjMB9uLV.js} +1 -1
  8. package/dashboard/dist/assets/{Select-Ctk8zze5.js → Select-DeCgT4YH.js} +1 -1
  9. package/dashboard/dist/assets/{SelectItemText-BKZlFyFs.js → SelectItemText-DwZRVpPE.js} +1 -1
  10. package/dashboard/dist/assets/{SettingsPage-CQXM8qGU.js → SettingsPage-B1l6jasL.js} +1 -1
  11. package/dashboard/dist/assets/{SkillsPage-4EMm0eBX.js → SkillsPage-DP5bdeUE.js} +1 -1
  12. package/dashboard/dist/assets/{WidgetsPage-C2sdX5g6.js → WidgetsPage-BVOQlozx.js} +1 -1
  13. package/dashboard/dist/assets/{WrappedPage-CLiuEcQZ.js → WrappedPage-ixZYG73C.js} +1 -1
  14. package/dashboard/dist/assets/{arrow-up-right-BDkp93DX.js → arrow-up-right-D74FAyJC.js} +1 -1
  15. package/dashboard/dist/assets/{download-DZ6SoCSn.js → download-sKFNy5AQ.js} +1 -1
  16. package/dashboard/dist/assets/{format-CaW9kvsA.js → format-Bl1W1y2T.js} +1 -1
  17. package/dashboard/dist/assets/{limitDisplay-DNU_w4O7.js → limitDisplay-C2rw2OOb.js} +1 -1
  18. package/dashboard/dist/assets/{main-DgyymGht.js → main-NjvOmlYA.js} +3 -3
  19. package/dashboard/dist/assets/{mock-data-D6C7Fba3.js → mock-data-DJ2XGAUP.js} +1 -1
  20. package/dashboard/dist/assets/{use-limits-display-prefs-B7cHBa7Y.js → use-limits-display-prefs-C7BBDoNV.js} +1 -1
  21. package/dashboard/dist/assets/{use-native-settings-BKAzGuxw.js → use-native-settings-BYIme-b6.js} +1 -1
  22. package/dashboard/dist/assets/{useCurrency-BVr6Ajuu.js → useCurrency-CJzAqnSI.js} +1 -1
  23. package/dashboard/dist/index.html +1 -1
  24. package/package.json +1 -1
  25. package/src/lib/pricing/seed-snapshot.json +1 -1
  26. package/src/lib/rollout.js +62 -14
@@ -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
- // Best-effort copy of SQLite sidecars; missing -wal/-shm/-journal is fine.
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
- if (fssync.existsSync(src)) fssync.copyFileSync(src, target + suffix);
3251
- } catch (_e) { }
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
  },
@@ -6116,8 +6143,10 @@ async function parseGooseIncremental({
6116
6143
  onProgress,
6117
6144
  env,
6118
6145
  sqliteOptions,
6146
+ now = () => new Date(),
6119
6147
  } = {}) {
6120
6148
  await ensureDir(path.dirname(queuePath));
6149
+ const observedAt = now().toISOString();
6121
6150
  const resolvedDb = dbPath || resolveGooseDbPath(env || process.env);
6122
6151
  const gooseState =
6123
6152
  cursors.goose && typeof cursors.goose === "object" ? cursors.goose : {};
@@ -6126,21 +6155,25 @@ async function parseGooseIncremental({
6126
6155
  ? { ...gooseState.sessionTotals }
6127
6156
  : {};
6128
6157
 
6129
- const cursorDbMtime = Number.isFinite(gooseState.lastDbMtimeMs) ? gooseState.lastDbMtimeMs : 0;
6158
+ const cursorDbFingerprint =
6159
+ typeof gooseState.lastDbFingerprint === "string" ? gooseState.lastDbFingerprint : null;
6130
6160
  let currentMtime = 0;
6131
6161
  try {
6132
6162
  currentMtime = fssync.statSync(resolvedDb).mtimeMs;
6133
6163
  } catch (e) {
6134
6164
  if (e && e.code === "ENOENT") {
6135
- cursors.goose = { ...gooseState, sessionTotals, updatedAt: new Date().toISOString() };
6165
+ cursors.goose = { ...gooseState, sessionTotals, updatedAt: observedAt };
6136
6166
  return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
6137
6167
  }
6138
6168
  throw e;
6139
6169
  }
6140
- // mtime short-circuit: skip the full sessions table scan when the DB
6141
- // hasn't been touched since the last sync.
6142
- if (currentMtime > 0 && currentMtime === cursorDbMtime) {
6143
- cursors.goose = { ...gooseState, sessionTotals, updatedAt: new Date().toISOString() };
6170
+ const currentDbFingerprint = sqliteDbChangeFingerprint(resolvedDb);
6171
+ // Goose commonly commits active-session writes only to SQLite's WAL. Skip
6172
+ // the table scan only when the main DB and every sidecar are unchanged.
6173
+ // Legacy cursors without a fingerprint intentionally perform one read to
6174
+ // establish the sidecar-aware baseline.
6175
+ if (cursorDbFingerprint && currentDbFingerprint === cursorDbFingerprint) {
6176
+ cursors.goose = { ...gooseState, sessionTotals, updatedAt: observedAt };
6144
6177
  return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
6145
6178
  }
6146
6179
 
@@ -6149,13 +6182,22 @@ async function parseGooseIncremental({
6149
6182
  const snap = snapshotSqliteDb(resolvedDb);
6150
6183
  let rows = [];
6151
6184
  try {
6185
+ if (!snap.sidecarsComplete) {
6186
+ cursors.goose = {
6187
+ ...gooseState,
6188
+ sessionTotals,
6189
+ lastDbFingerprint: null,
6190
+ updatedAt: observedAt,
6191
+ };
6192
+ return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
6193
+ }
6152
6194
  rows = readGooseSessionsFromSqlite(snap.path, sqliteOptions);
6153
6195
  } finally {
6154
6196
  snap.cleanup();
6155
6197
  }
6156
6198
 
6157
6199
  if (rows.length === 0) {
6158
- cursors.goose = { ...gooseState, sessionTotals, updatedAt: new Date().toISOString() };
6200
+ cursors.goose = { ...gooseState, sessionTotals, updatedAt: observedAt };
6159
6201
  return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
6160
6202
  }
6161
6203
 
@@ -6188,6 +6230,7 @@ async function parseGooseIncremental({
6188
6230
  );
6189
6231
  if (totalNow === 0 && inputNow === 0 && outputNow === 0) continue;
6190
6232
 
6233
+ const hadPreviousSnapshot = Object.prototype.hasOwnProperty.call(sessionTotals, row.id);
6191
6234
  const prev = sessionTotals[row.id] || { input: 0, output: 0, total: 0 };
6192
6235
  // Goose can wipe a session and re-create with the same id during
6193
6236
  // database migration. Treat shrinking cumulative as a reset and emit
@@ -6213,7 +6256,12 @@ async function parseGooseIncremental({
6213
6256
  const accountedDelta = dInput + dOutput;
6214
6257
  const reasoningDelta = Math.max(0, dTotal - accountedDelta);
6215
6258
 
6216
- const tsIso = parseGooseCreatedAt(row.created_at) || new Date().toISOString();
6259
+ // The first observation has only created_at as a trustworthy timestamp.
6260
+ // Later cumulative deltas belong to this sync's observation bucket so a
6261
+ // long-lived session does not keep growing the day on which it started.
6262
+ const tsIso = hadPreviousSnapshot
6263
+ ? observedAt
6264
+ : parseGooseCreatedAt(row.created_at) || observedAt;
6217
6265
  const bucketStart = toUtcHalfHourStart(tsIso);
6218
6266
  if (!bucketStart) continue;
6219
6267
 
@@ -6257,14 +6305,14 @@ async function parseGooseIncremental({
6257
6305
  }
6258
6306
 
6259
6307
  const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
6260
- const updatedAt = new Date().toISOString();
6261
- hourlyState.updatedAt = updatedAt;
6308
+ hourlyState.updatedAt = observedAt;
6262
6309
  cursors.hourly = hourlyState;
6263
6310
  cursors.goose = {
6264
6311
  ...gooseState,
6265
6312
  sessionTotals,
6266
6313
  lastDbMtimeMs: currentMtime,
6267
- updatedAt,
6314
+ lastDbFingerprint: currentDbFingerprint,
6315
+ updatedAt: observedAt,
6268
6316
  };
6269
6317
 
6270
6318
  return { recordsProcessed, eventsAggregated, bucketsQueued };