tokentracker-cli 0.77.1 → 0.78.0

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 (83) hide show
  1. package/README.de.md +24 -3
  2. package/README.ja.md +3 -3
  3. package/README.ko.md +3 -3
  4. package/README.md +24 -3
  5. package/README.zh-CN.md +3 -3
  6. package/dashboard/dist/assets/ActivityHeatmap-5352XxLR.js +42 -0
  7. package/dashboard/dist/assets/{Card-DP2HT77t.js → Card-Ca_hYHhx.js} +1 -1
  8. package/dashboard/dist/assets/{ClawdAnimated-ScYRJSG0.js → ClawdAnimated-DSgOS5ND.js} +1 -1
  9. package/dashboard/dist/assets/{CommunityStatsModal-CU46JMkg.js → CommunityStatsModal-o8uQVQxu.js} +1 -1
  10. package/dashboard/dist/assets/ConfirmModal-D1hi9XUf.js +1 -0
  11. package/dashboard/dist/assets/Controls-CwnKP56V.js +1 -0
  12. package/dashboard/dist/assets/DashboardPage-CDv0TIOL.js +19 -0
  13. package/dashboard/dist/assets/{DevicePage-BL9ew9-m.js → DevicePage-BlfIHcZX.js} +1 -1
  14. package/dashboard/dist/assets/DialogClose-CismfYwe.js +1 -0
  15. package/dashboard/dist/assets/DialogDescription-DWKp1yez.js +1 -0
  16. package/dashboard/dist/assets/{DialogTitle-Oqnn_dIL.js → DialogTitle-DuuTVbUb.js} +1 -1
  17. package/dashboard/dist/assets/{FadeIn-CFLwnHqt.js → FadeIn-DpnitRKo.js} +1 -1
  18. package/dashboard/dist/assets/{HeaderGithubStar-B397ELF4.js → HeaderGithubStar-D6zy-ADl.js} +1 -1
  19. package/dashboard/dist/assets/{IpCheckPage-CamsHe6o.js → IpCheckPage-yUNp0VNe.js} +1 -1
  20. package/dashboard/dist/assets/LandingPage-BvmpAaVn.js +4314 -0
  21. package/dashboard/dist/assets/LeaderboardAvatar-2WTIeXye.js +1 -0
  22. package/dashboard/dist/assets/LeaderboardPage-Bq3-VO7w.js +2 -0
  23. package/dashboard/dist/assets/{LeaderboardProfileModal-BIe6gBNu.js → LeaderboardProfileModal-DH0EEK6q.js} +3 -3
  24. package/dashboard/dist/assets/{LeaderboardProfilePage-DeVBEYtg.js → LeaderboardProfilePage-uJAng2JE.js} +3 -3
  25. package/dashboard/dist/assets/{LimitsPage-6I5EU7qe.js → LimitsPage-D8UohgJm.js} +2 -2
  26. package/dashboard/dist/assets/{LocalOnlyNotice-AyYKZloH.js → LocalOnlyNotice-Dv_BnDVV.js} +1 -1
  27. package/dashboard/dist/assets/{LoginPage-D8smF1gU.js → LoginPage-D6b6DZU3.js} +1 -1
  28. package/dashboard/dist/assets/PetPage-DfpBkuCa.js +1 -0
  29. package/dashboard/dist/assets/{PopoverPopup-DRWhz-bY.js → PopoverPopup-CjzdZn-9.js} +1 -1
  30. package/dashboard/dist/assets/{ResetPasswordPage-CzSqJfuA.js → ResetPasswordPage-BnOCFkwe.js} +1 -1
  31. package/dashboard/dist/assets/Select-CHtQAiLD.js +1 -0
  32. package/dashboard/dist/assets/{SelectItemText-DEYadcIK.js → SelectItemText-Vjqvf8wO.js} +1 -1
  33. package/dashboard/dist/assets/SettingsPage-B-iHGL4q.js +1 -0
  34. package/dashboard/dist/assets/SkillsPage-DtG2aV-q.js +1 -0
  35. package/dashboard/dist/assets/WidgetsPage-4kqTtWfK.js +1 -0
  36. package/dashboard/dist/assets/{WrappedPage-BuP9KXqF.js → WrappedPage-DMMRpani.js} +1 -1
  37. package/dashboard/dist/assets/agent-logos-D0vYkEpX.js +1 -0
  38. package/dashboard/dist/assets/{arrow-up-right-cFMBPEVS.js → arrow-up-right-BAQYGZkR.js} +1 -1
  39. package/dashboard/dist/assets/check-C3RPBZiS.js +1 -0
  40. package/dashboard/dist/assets/{copy-DYx8X3ZO.js → copy-gG_BLBs8.js} +1 -1
  41. package/dashboard/dist/assets/{download-B-hXZWKj.js → download-BQOcg3gT.js} +1 -1
  42. package/dashboard/dist/assets/{format-tokens-CngV51wN.js → format-tokens-CGK6lQAf.js} +1 -1
  43. package/dashboard/dist/assets/{info-qVF2hV50.js → info-C_BrDrcq.js} +1 -1
  44. package/dashboard/dist/assets/{link-2-DR-yGZFS.js → link-2-DFwCBbIZ.js} +1 -1
  45. package/dashboard/dist/assets/{loader-circle-0k9uueb5.js → loader-circle-BMFI_BJ8.js} +1 -1
  46. package/dashboard/dist/assets/{main-B8lT0e78.css → main-Dy8Y_qst.css} +1 -1
  47. package/dashboard/dist/assets/main-w0hAaCsy.js +1190 -0
  48. package/dashboard/dist/assets/{trash-2-DpxBaV_1.js → trash-2-GeZ6IzgV.js} +1 -1
  49. package/dashboard/dist/assets/{use-community-stats-Cbij-V8j.js → use-community-stats-pC8cmVof.js} +1 -1
  50. package/dashboard/dist/assets/{use-limits-display-prefs-q0TuryM0.js → use-limits-display-prefs-DnFpPcA3.js} +1 -1
  51. package/dashboard/dist/assets/use-native-settings-CvcOSUhn.js +1 -0
  52. package/dashboard/dist/assets/{use-ordered-list-CH-J7c04.js → use-ordered-list-CbMroVZ8.js} +2 -2
  53. package/dashboard/dist/assets/{use-quality-per-dollar-pref-DKLmkwxe.js → use-quality-per-dollar-pref-Bn3Al6yX.js} +1 -1
  54. package/dashboard/dist/assets/{use-usage-limits-DrmF6I_t.js → use-usage-limits-D08q_Dr8.js} +1 -1
  55. package/dashboard/dist/assets/{useCurrency-DwygZqrs.js → useCurrency-h3kqF52v.js} +1 -1
  56. package/dashboard/dist/assets/{useScrollLock-DVCKuErg.js → useScrollLock-9au2731u.js} +1 -1
  57. package/dashboard/dist/index.html +2 -2
  58. package/dashboard/dist/share.html +2 -2
  59. package/package.json +1 -1
  60. package/src/commands/status.js +293 -30
  61. package/src/commands/sync.js +494 -104
  62. package/src/lib/install-resolver.js +1 -0
  63. package/src/lib/local-api.js +202 -56
  64. package/src/lib/pricing/seed-snapshot.json +1 -1
  65. package/src/lib/rollout.js +1329 -40
  66. package/dashboard/dist/assets/ActivityHeatmap-DTZh551Z.js +0 -42
  67. package/dashboard/dist/assets/ConfirmModal-BYvz0dzg.js +0 -1
  68. package/dashboard/dist/assets/Controls-DPElSq0S.js +0 -1
  69. package/dashboard/dist/assets/DashboardPage-CN0buIso.js +0 -19
  70. package/dashboard/dist/assets/DialogClose-cqXrG903.js +0 -1
  71. package/dashboard/dist/assets/DialogDescription-Bo0iWtYz.js +0 -1
  72. package/dashboard/dist/assets/LandingPage-D4ygTHwO.js +0 -4312
  73. package/dashboard/dist/assets/LeaderboardAvatar-B39rNGCV.js +0 -1
  74. package/dashboard/dist/assets/LeaderboardPage-BFUOxX6P.js +0 -2
  75. package/dashboard/dist/assets/PetPage-BlMYvJn2.js +0 -1
  76. package/dashboard/dist/assets/Select-Cb8n7kQO.js +0 -1
  77. package/dashboard/dist/assets/SettingsPage-DlHXcFdJ.js +0 -1
  78. package/dashboard/dist/assets/SkillsPage--Kcv1pxG.js +0 -1
  79. package/dashboard/dist/assets/WidgetsPage-DwtJ9CKU.js +0 -1
  80. package/dashboard/dist/assets/agent-logos-DbwDXgg1.js +0 -1
  81. package/dashboard/dist/assets/check-t_ltRp2A.js +0 -1
  82. package/dashboard/dist/assets/main-CU04bNYH.js +0 -1172
  83. package/dashboard/dist/assets/use-native-settings-BCzogs1L.js +0 -1
@@ -3729,7 +3729,11 @@ async function parseKiroIncremental({ dbPath, jsonlPath, cursors, queuePath, onP
3729
3729
  jsonl: { lastLine: jsonlResult.lineCount, updatedAt: new Date().toISOString() },
3730
3730
  updatedAt: new Date().toISOString(),
3731
3731
  };
3732
- return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
3732
+ return {
3733
+ recordsProcessed: 0,
3734
+ eventsAggregated: 0,
3735
+ bucketsQueued: 0,
3736
+ };
3733
3737
  }
3734
3738
  } else {
3735
3739
  return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
@@ -4212,7 +4216,11 @@ async function parseHermesIncremental({ hermesPath, dbPath, cursors, queuePath,
4212
4216
  updatedAt, // Update the overall profile state timestamp even if the DB doesn't exist for the fast-path check
4213
4217
  };
4214
4218
 
4215
- return { recordsProcessed, eventsAggregated, bucketsQueued };
4219
+ return {
4220
+ recordsProcessed,
4221
+ eventsAggregated,
4222
+ bucketsQueued,
4223
+ };
4216
4224
  }
4217
4225
 
4218
4226
  // ─────────────────────────────────────────────────────────────────────────────
@@ -4993,7 +5001,11 @@ async function parseKiroCliFromSessionFiles({
4993
5001
  fileOffsets,
4994
5002
  updatedAt: new Date().toISOString(),
4995
5003
  };
4996
- return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
5004
+ return {
5005
+ recordsProcessed: 0,
5006
+ eventsAggregated: 0,
5007
+ bucketsQueued: 0,
5008
+ };
4997
5009
  }
4998
5010
 
4999
5011
  const hourlyState = normalizeHourlyState(cursors?.hourly);
@@ -5124,14 +5136,32 @@ async function parseKiroCliFromSessionFiles({
5124
5136
  cursors.hourly = hourlyState;
5125
5137
  cursors.kiroCli = { ...kiroCliState, seenIds: cappedSeen, fileOffsets, updatedAt };
5126
5138
 
5127
- return { recordsProcessed, eventsAggregated, bucketsQueued };
5139
+ return {
5140
+ recordsProcessed,
5141
+ eventsAggregated,
5142
+ bucketsQueued,
5143
+ };
5128
5144
  }
5129
5145
 
5130
5146
  // ─────────────────────────────────────────────────────────────────────────────
5131
5147
 
5132
- function resolveKimiWireFiles(env = process.env) {
5148
+ function resolveKimiHome(env = process.env) {
5133
5149
  const home = require("node:os").homedir();
5134
- const kimiHome = env.KIMI_HOME || path.join(home, ".kimi");
5150
+ const explicit = typeof env?.KIMI_HOME === "string" ? env.KIMI_HOME.trim() : "";
5151
+ if (explicit) return path.resolve(explicit);
5152
+ if (process.platform === "win32") {
5153
+ return pickWin32ProviderPath({
5154
+ env,
5155
+ nativeValue: path.join(home, ".kimi"),
5156
+ wslProviderDir: ".kimi",
5157
+ });
5158
+ }
5159
+ return path.join(home, ".kimi");
5160
+ }
5161
+
5162
+ function resolveKimiWireFiles(env = process.env) {
5163
+ const kimiHome = resolveKimiHome(env);
5164
+ if (!kimiHome) return [];
5135
5165
  const sessionsDir = path.join(kimiHome, "sessions");
5136
5166
  if (!fssync.existsSync(sessionsDir)) return [];
5137
5167
  const files = [];
@@ -9264,6 +9294,46 @@ function pickCopilotModel(attrs) {
9264
9294
  }
9265
9295
 
9266
9296
  const COPILOT_PARSER_VERSION = 2;
9297
+ const COPILOT_USAGE_CLAIM_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
9298
+ const COPILOT_USAGE_CLAIM_MAX_ENTRIES = 10_000;
9299
+
9300
+ function pruneCopilotUsageClaims(
9301
+ events,
9302
+ nowMs = Date.now(),
9303
+ maxEntries = COPILOT_USAGE_CLAIM_MAX_ENTRIES,
9304
+ ) {
9305
+ const cutoffMs = nowMs - COPILOT_USAGE_CLAIM_RETENTION_MS;
9306
+ const retained = [];
9307
+ for (const event of Array.isArray(events) ? events : []) {
9308
+ if (!event || typeof event !== "object" || event.consumed === true) {
9309
+ continue;
9310
+ }
9311
+ const storedFirstSeenAtMs = Number(event.firstSeenAtMs);
9312
+ const firstSeenAtMs =
9313
+ Number.isFinite(storedFirstSeenAtMs) && storedFirstSeenAtMs > 0
9314
+ ? storedFirstSeenAtMs
9315
+ : nowMs;
9316
+ if (firstSeenAtMs < cutoffMs) continue;
9317
+ retained.push(
9318
+ firstSeenAtMs === storedFirstSeenAtMs
9319
+ ? event
9320
+ : { ...event, firstSeenAtMs },
9321
+ );
9322
+ }
9323
+ return retained.length > maxEntries
9324
+ ? retained.slice(retained.length - maxEntries)
9325
+ : retained;
9326
+ }
9327
+
9328
+ function replaceCopilotUsageClaims(
9329
+ target,
9330
+ nowMs = Date.now(),
9331
+ maxEntries = COPILOT_USAGE_CLAIM_MAX_ENTRIES,
9332
+ ) {
9333
+ if (!Array.isArray(target)) return;
9334
+ const retained = pruneCopilotUsageClaims(target, nowMs, maxEntries);
9335
+ target.splice(0, target.length, ...retained);
9336
+ }
9267
9337
 
9268
9338
  function isCopilotV1ChatSpan(record) {
9269
9339
  if (!record || record.type !== "span") return false;
@@ -9288,6 +9358,65 @@ function getCopilotDedupKey(record, attrs = record?.attributes || {}) {
9288
9358
  return traceId && spanId ? `${traceId}:${spanId}` : responseId ? `resp:${responseId}` : null;
9289
9359
  }
9290
9360
 
9361
+ function copilotUsageMatchKey({
9362
+ sessionId,
9363
+ model,
9364
+ input,
9365
+ output,
9366
+ cacheRead,
9367
+ cacheWrite,
9368
+ reasoning,
9369
+ }) {
9370
+ return [
9371
+ sessionId || "",
9372
+ normalizeCopilotAppModel(model) || COPILOT_APP_DEFAULT_MODEL,
9373
+ toNonNegativeInt(input),
9374
+ toNonNegativeInt(output),
9375
+ toNonNegativeInt(cacheRead),
9376
+ toNonNegativeInt(cacheWrite),
9377
+ toNonNegativeInt(reasoning),
9378
+ ].join("|");
9379
+ }
9380
+
9381
+ function createCopilotStoreUsageMatcher(events) {
9382
+ const candidates = new Map();
9383
+ for (const event of Array.isArray(events) ? events : []) {
9384
+ if (!event || typeof event !== "object") continue;
9385
+ if (event.consumed === true) continue;
9386
+ const tsMs = Number(event.tsMs);
9387
+ if (!Number.isFinite(tsMs) || tsMs <= 0) continue;
9388
+ const key = copilotUsageMatchKey(event);
9389
+ const list = candidates.get(key) || [];
9390
+ list.push({ tsMs, event });
9391
+ candidates.set(key, list);
9392
+ }
9393
+ for (const list of candidates.values()) {
9394
+ list.sort((a, b) => a.tsMs - b.tsMs);
9395
+ }
9396
+ return {
9397
+ consume(event, toleranceMs = 2_000) {
9398
+ const tsMs = Number(event?.tsMs);
9399
+ if (!Number.isFinite(tsMs) || tsMs <= 0) return false;
9400
+ const key = copilotUsageMatchKey(event || {});
9401
+ const list = candidates.get(key);
9402
+ if (!list || list.length === 0) return false;
9403
+ let bestIndex = -1;
9404
+ let bestDistance = Infinity;
9405
+ for (let i = 0; i < list.length; i++) {
9406
+ const distance = Math.abs(list[i].tsMs - tsMs);
9407
+ if (distance <= toleranceMs && distance < bestDistance) {
9408
+ bestIndex = i;
9409
+ bestDistance = distance;
9410
+ }
9411
+ }
9412
+ if (bestIndex < 0) return false;
9413
+ const [matched] = list.splice(bestIndex, 1);
9414
+ matched.event.consumed = true;
9415
+ return true;
9416
+ },
9417
+ };
9418
+ }
9419
+
9291
9420
  // Migration helper: stream the bytes v1 already saw (0 -> prevSize), classify
9292
9421
  // whether the file contains old CLI spans v1 processed, and whether it also
9293
9422
  // contains v2-only chat records v1 skipped. Mixed files must be replayed, but
@@ -9334,7 +9463,54 @@ async function scanCopilotV1MigrationFile(filePath, maxBytes) {
9334
9463
  return result;
9335
9464
  }
9336
9465
 
9337
- async function parseCopilotIncremental({ otelPaths, cursors, queuePath, onProgress, env } = {}) {
9466
+ async function copilotOtelCursorHasLegacyCliUsage(cursors) {
9467
+ const state =
9468
+ cursors?.copilot && typeof cursors.copilot === "object"
9469
+ ? cursors.copilot
9470
+ : {};
9471
+ if (state.legacyCliHistory === true) return true;
9472
+ if (state.usageClaimsComplete === true) return false;
9473
+ const offsets =
9474
+ state.fileOffsets && typeof state.fileOffsets === "object"
9475
+ ? state.fileOffsets
9476
+ : {};
9477
+ const entries = Object.entries(offsets).filter(
9478
+ ([, entry]) => Number(entry?.size) > 0,
9479
+ );
9480
+ const hasSeenIds =
9481
+ Array.isArray(state.seenIds) && state.seenIds.length > 0;
9482
+ if (entries.length === 0) return hasSeenIds;
9483
+ for (const [filePath, entry] of entries) {
9484
+ let stat;
9485
+ try {
9486
+ stat = fssync.statSync(filePath);
9487
+ } catch (_e) {
9488
+ return true;
9489
+ }
9490
+ if (
9491
+ stat.size < Number(entry?.size) ||
9492
+ (typeof entry?.ino === "number" && stat.ino !== entry.ino)
9493
+ ) {
9494
+ return true;
9495
+ }
9496
+ const scan = await scanCopilotV1MigrationFile(
9497
+ filePath,
9498
+ Number(entry?.size) || 0,
9499
+ );
9500
+ if (scan.v1Processed) return true;
9501
+ }
9502
+ return false;
9503
+ }
9504
+
9505
+ async function parseCopilotIncremental({
9506
+ otelPaths,
9507
+ cursors,
9508
+ queuePath,
9509
+ onProgress,
9510
+ env,
9511
+ skipCliSpans = false,
9512
+ storeUsageEvents,
9513
+ } = {}) {
9338
9514
  await ensureDir(path.dirname(queuePath));
9339
9515
  const copilotState = cursors.copilot && typeof cursors.copilot === "object" ? cursors.copilot : {};
9340
9516
  const seenIds = new Set(Array.isArray(copilotState.seenIds) ? copilotState.seenIds : []);
@@ -9344,6 +9520,16 @@ async function parseCopilotIncremental({ otelPaths, cursors, queuePath, onProgre
9344
9520
  ? copilotState.fileOffsets
9345
9521
  : {};
9346
9522
  const fileOffsets = { ...fileOffsetsRaw };
9523
+ const claimNowMs = Date.now();
9524
+ const recentOtelUsageEvents = pruneCopilotUsageClaims(
9525
+ copilotState.recentUsageEvents,
9526
+ claimNowMs,
9527
+ );
9528
+ replaceCopilotUsageClaims(storeUsageEvents, claimNowMs, Infinity);
9529
+ const hadPriorUsageHistory =
9530
+ seenIds.size > 0 || Object.keys(fileOffsetsRaw).length > 0;
9531
+ let usageClaimsComplete =
9532
+ copilotState.usageClaimsComplete === true || !hadPriorUsageHistory;
9347
9533
  const migrationSkipLineHashes = new Map();
9348
9534
  // One-shot v1->v2 migration:
9349
9535
  // - pure v2-only files: clear offset and re-read all skipped Chat records
@@ -9371,9 +9557,16 @@ async function parseCopilotIncremental({ otelPaths, cursors, queuePath, onProgre
9371
9557
  version: COPILOT_PARSER_VERSION,
9372
9558
  seenIds: Array.from(seenIds),
9373
9559
  fileOffsets,
9560
+ recentUsageEvents: recentOtelUsageEvents,
9561
+ usageClaimsComplete,
9374
9562
  updatedAt: new Date().toISOString(),
9375
9563
  };
9376
- return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
9564
+ return {
9565
+ recordsProcessed: 0,
9566
+ eventsAggregated: 0,
9567
+ bucketsQueued: 0,
9568
+ usageClaims: recentOtelUsageEvents,
9569
+ };
9377
9570
  }
9378
9571
 
9379
9572
  const hourlyState = normalizeHourlyState(cursors?.hourly);
@@ -9381,6 +9574,7 @@ async function parseCopilotIncremental({ otelPaths, cursors, queuePath, onProgre
9381
9574
  const cb = typeof onProgress === "function" ? onProgress : null;
9382
9575
  let recordsProcessed = 0;
9383
9576
  let eventsAggregated = 0;
9577
+ const storeUsageMatcher = createCopilotStoreUsageMatcher(storeUsageEvents);
9384
9578
 
9385
9579
  for (let fileIdx = 0; fileIdx < files.length; fileIdx++) {
9386
9580
  const filePath = files[fileIdx];
@@ -9436,6 +9630,10 @@ async function parseCopilotIncremental({ otelPaths, cursors, queuePath, onProgre
9436
9630
  // gen_ai.response.id is per-LLM-call unique.
9437
9631
  const dedupKey = getCopilotDedupKey(record, attrs);
9438
9632
  if (dedupKey && seenIds.has(dedupKey)) continue;
9633
+ if (skipCliSpans && isCopilotV1ChatSpan(record)) {
9634
+ if (dedupKey) seenIds.add(dedupKey);
9635
+ continue;
9636
+ }
9439
9637
 
9440
9638
  const inputRaw = toNonNegativeInt(attrs["gen_ai.usage.input_tokens"]);
9441
9639
  const output = toNonNegativeInt(attrs["gen_ai.usage.output_tokens"]);
@@ -9495,6 +9693,30 @@ async function parseCopilotIncremental({ otelPaths, cursors, queuePath, onProgre
9495
9693
  const model =
9496
9694
  normalizeCopilotAppModel(pickCopilotModel(attrs)) ||
9497
9695
  COPILOT_APP_DEFAULT_MODEL;
9696
+ const cliSessionId =
9697
+ typeof attrs["gen_ai.conversation.id"] === "string"
9698
+ ? attrs["gen_ai.conversation.id"].trim()
9699
+ : "";
9700
+ if (cliSpan && !cliSessionId) usageClaimsComplete = false;
9701
+ const matchBase = {
9702
+ sessionId: cliSessionId,
9703
+ model,
9704
+ output: outputWithoutReasoning,
9705
+ cacheRead: cacheReadClamped,
9706
+ cacheWrite: cacheWriteClamped,
9707
+ reasoning: reasoningClamped,
9708
+ tsMs,
9709
+ };
9710
+ const matchedStoreUsage =
9711
+ isCopilotV1ChatSpan(record) &&
9712
+ storeUsageMatcher.consume({
9713
+ ...matchBase,
9714
+ input,
9715
+ });
9716
+ if (matchedStoreUsage) {
9717
+ if (dedupKey) seenIds.add(dedupKey);
9718
+ continue;
9719
+ }
9498
9720
 
9499
9721
  const delta = {
9500
9722
  input_tokens: input,
@@ -9515,6 +9737,13 @@ async function parseCopilotIncremental({ otelPaths, cursors, queuePath, onProgre
9515
9737
  addTotals(bucket.totals, delta);
9516
9738
  touchedBuckets.add(bucketKey("copilot", model, bucketStart));
9517
9739
  eventsAggregated++;
9740
+ if (cliSpan && cliSessionId) {
9741
+ recentOtelUsageEvents.push({
9742
+ ...matchBase,
9743
+ input,
9744
+ firstSeenAtMs: claimNowMs,
9745
+ });
9746
+ }
9518
9747
  if (dedupKey) seenIds.add(dedupKey);
9519
9748
 
9520
9749
  if (cb) {
@@ -9542,6 +9771,10 @@ async function parseCopilotIncremental({ otelPaths, cursors, queuePath, onProgre
9542
9771
  // Cap dedup set to last 10k IDs to bound state size
9543
9772
  const seenArr = Array.from(seenIds);
9544
9773
  const cappedSeen = seenArr.length > 10_000 ? seenArr.slice(seenArr.length - 10_000) : seenArr;
9774
+ const retainedRecentOtelUsageEvents = pruneCopilotUsageClaims(
9775
+ recentOtelUsageEvents,
9776
+ );
9777
+ replaceCopilotUsageClaims(storeUsageEvents, Date.now(), Infinity);
9545
9778
 
9546
9779
  const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
9547
9780
  const updatedAt = new Date().toISOString();
@@ -9552,10 +9785,914 @@ async function parseCopilotIncremental({ otelPaths, cursors, queuePath, onProgre
9552
9785
  version: COPILOT_PARSER_VERSION,
9553
9786
  seenIds: cappedSeen,
9554
9787
  fileOffsets,
9788
+ recentUsageEvents: retainedRecentOtelUsageEvents,
9789
+ usageClaimsComplete,
9555
9790
  updatedAt,
9556
9791
  };
9557
9792
 
9558
- return { recordsProcessed, eventsAggregated, bucketsQueued };
9793
+ return {
9794
+ recordsProcessed,
9795
+ eventsAggregated,
9796
+ bucketsQueued,
9797
+ usageClaims: recentOtelUsageEvents,
9798
+ };
9799
+ }
9800
+
9801
+ // ─────────────────────────────────────────────────────────────────────────────
9802
+ // GitHub Copilot local runtime — session-store.db assistant_usage_events
9803
+ //
9804
+ // Copilot CLI 1.0.70+ persists one row per LLM request here. The Copilot App
9805
+ // uses the same runtime, so this is the only local source that remains complete
9806
+ // when one session moves App -> CLI -> App (or the reverse). data.db only keeps
9807
+ // the App-owned portion of those sessions.
9808
+ // ─────────────────────────────────────────────────────────────────────────────
9809
+
9810
+ const COPILOT_STORE_CURSOR_VERSION = 2;
9811
+ const COPILOT_STORE_LEGACY_MODEL = "github-copilot-legacy";
9812
+
9813
+ function resolveCopilotDbPaths({ fileName, overrideEnvKey }, env = process.env) {
9814
+ const home =
9815
+ process.platform === "win32"
9816
+ ? env.USERPROFILE || env.HOME || require("node:os").homedir()
9817
+ : env.HOME || require("node:os").homedir();
9818
+ const paths = new Set();
9819
+ const addDbPath = (dbPath) => {
9820
+ const normalized = normalizeCopilotDbPath(dbPath, env);
9821
+ if (normalized) paths.add(normalized);
9822
+ };
9823
+ const addHome = (homePath) => {
9824
+ const normalizedHome = normalizeCopilotDbPath(homePath, env);
9825
+ if (normalizedHome) addDbPath(path.join(normalizedHome, fileName));
9826
+ };
9827
+
9828
+ if (typeof env[overrideEnvKey] === "string" && env[overrideEnvKey].trim()) {
9829
+ addDbPath(env[overrideEnvKey]);
9830
+ }
9831
+ if (typeof env.COPILOT_HOME === "string" && env.COPILOT_HOME.trim()) {
9832
+ addHome(env.COPILOT_HOME);
9833
+ }
9834
+
9835
+ addDbPath(path.join(home, ".copilot", fileName));
9836
+
9837
+ return Array.from(paths).sort();
9838
+ }
9839
+
9840
+ function resolveCopilotSessionStorePaths(env = process.env) {
9841
+ return resolveCopilotDbPaths(
9842
+ {
9843
+ fileName: "session-store.db",
9844
+ overrideEnvKey: "TOKENTRACKER_COPILOT_SESSION_STORE_DB",
9845
+ },
9846
+ env,
9847
+ );
9848
+ }
9849
+
9850
+ function copilotStoreRowKey(row) {
9851
+ if (!row) return null;
9852
+ return crypto
9853
+ .createHash("sha256")
9854
+ .update(
9855
+ JSON.stringify([
9856
+ row.max_id ?? row.id,
9857
+ row.session_id,
9858
+ row.model,
9859
+ row.input_tokens,
9860
+ row.output_tokens,
9861
+ row.cache_read_tokens,
9862
+ row.cache_write_tokens,
9863
+ row.reasoning_tokens,
9864
+ row.created_at,
9865
+ ]),
9866
+ )
9867
+ .digest("hex");
9868
+ }
9869
+
9870
+ function copilotStoreEventFingerprint(row) {
9871
+ const sessionId =
9872
+ typeof row?.session_id === "string" ? row.session_id.trim() : "";
9873
+ if (!sessionId) return null;
9874
+ const normalized = normalizeCopilotSessionStoreUsage(row);
9875
+ const timestamp =
9876
+ parseCopilotAppTimestamp(row?.created_at) ||
9877
+ `invalid:${String(row?.created_at ?? "")}`;
9878
+ return crypto
9879
+ .createHash("sha256")
9880
+ .update(
9881
+ JSON.stringify([
9882
+ sessionId,
9883
+ normalizeCopilotAppModel(row?.model) || COPILOT_APP_DEFAULT_MODEL,
9884
+ normalized.input_tokens,
9885
+ normalized.output_tokens,
9886
+ normalized.cached_input_tokens,
9887
+ normalized.cache_creation_input_tokens,
9888
+ normalized.reasoning_output_tokens,
9889
+ timestamp,
9890
+ ]),
9891
+ )
9892
+ .digest("hex");
9893
+ }
9894
+
9895
+ function normalizeCopilotFingerprintCounts(value) {
9896
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
9897
+ const result = {};
9898
+ for (const [fingerprint, count] of Object.entries(value)) {
9899
+ const normalized = toNonNegativeInt(count);
9900
+ if (/^[a-f0-9]{64}$/.test(fingerprint) && normalized > 0) {
9901
+ result[fingerprint] = normalized;
9902
+ }
9903
+ }
9904
+ return result;
9905
+ }
9906
+
9907
+ function normalizeCopilotPendingMalformedEvents(value) {
9908
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
9909
+ const result = {};
9910
+ for (const [id, fingerprint] of Object.entries(value)) {
9911
+ const normalizedId = toNonNegativeInt(id);
9912
+ if (normalizedId > 0) {
9913
+ result[normalizedId] =
9914
+ typeof fingerprint === "string" ? fingerprint : "";
9915
+ }
9916
+ }
9917
+ return result;
9918
+ }
9919
+
9920
+ function incrementCopilotFingerprintCount(counts, fingerprint) {
9921
+ if (!fingerprint) return;
9922
+ counts[fingerprint] = toNonNegativeInt(counts[fingerprint]) + 1;
9923
+ }
9924
+
9925
+ function latestCopilotTimestamp(first, second) {
9926
+ const values = [first, second]
9927
+ .map((value) => parseCopilotAppTimestamp(value))
9928
+ .filter(Boolean);
9929
+ if (values.length === 0) return null;
9930
+ return values.reduce((latest, value) => (value > latest ? value : latest));
9931
+ }
9932
+
9933
+ function omitCopilotLegacyCatchupState(state) {
9934
+ const clean = { ...(state || {}) };
9935
+ delete clean.pendingCatchupMaxId;
9936
+ delete clean.pendingCatchupFingerprint;
9937
+ delete clean.pendingLegacyCatchup;
9938
+ return clean;
9939
+ }
9940
+
9941
+ function readCopilotSessionStoreMetadata(
9942
+ dbPath,
9943
+ sqliteOptions = {},
9944
+ cursorId = 0,
9945
+ ) {
9946
+ const tables = readSqliteJsonRows(
9947
+ dbPath,
9948
+ "SELECT name FROM sqlite_master WHERE type='table' AND name IN ('assistant_usage_events', 'schema_version')",
9949
+ {
9950
+ label: "GitHub Copilot session store",
9951
+ maxBuffer: 4 * 1024 * 1024,
9952
+ timeout: 10_000,
9953
+ readOnly: true,
9954
+ throwOnReadFailure: true,
9955
+ ...sqliteOptions,
9956
+ },
9957
+ );
9958
+ const names = new Set(tables.map((row) => row?.name).filter(Boolean));
9959
+ if (!names.has("assistant_usage_events")) {
9960
+ return { active: false, schemaVersion: null, maxId: 0 };
9961
+ }
9962
+ const maxRows = readSqliteJsonRows(
9963
+ dbPath,
9964
+ `
9965
+ SELECT
9966
+ id AS max_id,
9967
+ session_id,
9968
+ model,
9969
+ input_tokens,
9970
+ output_tokens,
9971
+ cache_read_tokens,
9972
+ cache_write_tokens,
9973
+ reasoning_tokens,
9974
+ created_at
9975
+ FROM assistant_usage_events
9976
+ ORDER BY id DESC
9977
+ LIMIT 1
9978
+ `.trim(),
9979
+ {
9980
+ label: "GitHub Copilot session store",
9981
+ maxBuffer: 4 * 1024 * 1024,
9982
+ timeout: 10_000,
9983
+ readOnly: true,
9984
+ throwOnReadFailure: true,
9985
+ ...sqliteOptions,
9986
+ },
9987
+ );
9988
+ let schemaVersion = null;
9989
+ if (names.has("schema_version")) {
9990
+ const versionRows = readSqliteJsonRows(
9991
+ dbPath,
9992
+ "SELECT version FROM schema_version LIMIT 1",
9993
+ {
9994
+ label: "GitHub Copilot session store",
9995
+ maxBuffer: 4 * 1024 * 1024,
9996
+ timeout: 10_000,
9997
+ readOnly: true,
9998
+ throwOnReadFailure: true,
9999
+ ...sqliteOptions,
10000
+ },
10001
+ );
10002
+ schemaVersion = toNonNegativeInt(versionRows[0]?.version);
10003
+ }
10004
+ let cursorRowKey = null;
10005
+ const normalizedCursorId = toNonNegativeInt(cursorId);
10006
+ if (normalizedCursorId > 0) {
10007
+ if (normalizedCursorId === toNonNegativeInt(maxRows[0]?.max_id)) {
10008
+ cursorRowKey = copilotStoreRowKey(maxRows[0]);
10009
+ } else {
10010
+ const cursorRows = readSqliteJsonRows(
10011
+ dbPath,
10012
+ `
10013
+ SELECT
10014
+ id,
10015
+ session_id,
10016
+ model,
10017
+ input_tokens,
10018
+ output_tokens,
10019
+ cache_read_tokens,
10020
+ cache_write_tokens,
10021
+ reasoning_tokens,
10022
+ created_at
10023
+ FROM assistant_usage_events
10024
+ WHERE id = ${normalizedCursorId}
10025
+ LIMIT 1
10026
+ `.trim(),
10027
+ {
10028
+ label: "GitHub Copilot session store",
10029
+ maxBuffer: 4 * 1024 * 1024,
10030
+ timeout: 10_000,
10031
+ readOnly: true,
10032
+ throwOnReadFailure: true,
10033
+ ...sqliteOptions,
10034
+ },
10035
+ );
10036
+ cursorRowKey = copilotStoreRowKey(cursorRows[0]);
10037
+ }
10038
+ }
10039
+ return {
10040
+ active: true,
10041
+ schemaVersion,
10042
+ maxId: toNonNegativeInt(maxRows[0]?.max_id),
10043
+ lastRowKey: copilotStoreRowKey(maxRows[0]),
10044
+ lastEventAt: parseCopilotAppTimestamp(maxRows[0]?.created_at),
10045
+ cursorRowKey,
10046
+ };
10047
+ }
10048
+
10049
+ function describeCopilotSessionStoreDb(dbPath, sqliteOptions = {}) {
10050
+ let snap = null;
10051
+ try {
10052
+ snap = snapshotSqliteDb(dbPath);
10053
+ const metadata = readCopilotSessionStoreMetadata(snap.path, sqliteOptions);
10054
+ if (!metadata.active) {
10055
+ return {
10056
+ path: dbPath,
10057
+ active: false,
10058
+ schemaVersion: metadata.schemaVersion,
10059
+ eventCount: 0,
10060
+ lastEventId: 0,
10061
+ lastEventAt: null,
10062
+ };
10063
+ }
10064
+ const countRows = readSqliteJsonRows(
10065
+ snap.path,
10066
+ "SELECT COUNT(*) AS event_count FROM assistant_usage_events",
10067
+ {
10068
+ label: "GitHub Copilot session store",
10069
+ maxBuffer: 4 * 1024 * 1024,
10070
+ timeout: 10_000,
10071
+ readOnly: true,
10072
+ throwOnReadFailure: true,
10073
+ ...sqliteOptions,
10074
+ },
10075
+ );
10076
+ return {
10077
+ path: dbPath,
10078
+ active: true,
10079
+ schemaVersion: metadata.schemaVersion,
10080
+ eventCount: toNonNegativeInt(countRows[0]?.event_count),
10081
+ lastEventId: metadata.maxId,
10082
+ lastEventAt: metadata.lastEventAt,
10083
+ };
10084
+ } finally {
10085
+ if (snap) snap.cleanup();
10086
+ }
10087
+ }
10088
+
10089
+ function readCopilotSessionStoreUsageRowsWhere(
10090
+ dbPath,
10091
+ whereClause,
10092
+ sqliteOptions = {},
10093
+ ) {
10094
+ return readSqliteJsonRows(
10095
+ dbPath,
10096
+ `
10097
+ SELECT
10098
+ id,
10099
+ session_id,
10100
+ model,
10101
+ input_tokens,
10102
+ output_tokens,
10103
+ cache_read_tokens,
10104
+ cache_write_tokens,
10105
+ reasoning_tokens,
10106
+ token_details_json,
10107
+ created_at
10108
+ FROM assistant_usage_events
10109
+ WHERE ${whereClause}
10110
+ ORDER BY id ASC
10111
+ `.trim(),
10112
+ {
10113
+ label: "GitHub Copilot session store",
10114
+ maxBuffer: 64 * 1024 * 1024,
10115
+ timeout: 60_000,
10116
+ readOnly: true,
10117
+ throwOnReadFailure: true,
10118
+ ...sqliteOptions,
10119
+ },
10120
+ );
10121
+ }
10122
+
10123
+ function readCopilotSessionStoreUsageRows(dbPath, lastId, sqliteOptions = {}) {
10124
+ return readCopilotSessionStoreUsageRowsWhere(
10125
+ dbPath,
10126
+ `id > ${toNonNegativeInt(lastId)}`,
10127
+ sqliteOptions,
10128
+ );
10129
+ }
10130
+
10131
+ function readCopilotSessionStoreUsageRowsByIds(
10132
+ dbPath,
10133
+ ids,
10134
+ sqliteOptions = {},
10135
+ ) {
10136
+ const normalizedIds = [
10137
+ ...new Set(
10138
+ (Array.isArray(ids) ? ids : [])
10139
+ .map(toNonNegativeInt)
10140
+ .filter((id) => id > 0),
10141
+ ),
10142
+ ];
10143
+ const rows = [];
10144
+ for (let start = 0; start < normalizedIds.length; start += 500) {
10145
+ const chunk = normalizedIds.slice(start, start + 500);
10146
+ rows.push(
10147
+ ...readCopilotSessionStoreUsageRowsWhere(
10148
+ dbPath,
10149
+ `id IN (${chunk.join(",")})`,
10150
+ sqliteOptions,
10151
+ ),
10152
+ );
10153
+ }
10154
+ return rows.sort((a, b) => toNonNegativeInt(a?.id) - toNonNegativeInt(b?.id));
10155
+ }
10156
+
10157
+ function parseCopilotStoreTokenDetails(raw) {
10158
+ if (typeof raw !== "string" || !raw.trim()) return null;
10159
+ let entries;
10160
+ try {
10161
+ entries = JSON.parse(raw);
10162
+ } catch (_e) {
10163
+ return null;
10164
+ }
10165
+ if (!Array.isArray(entries)) return null;
10166
+ const totals = { input: 0, cacheRead: 0, cacheWrite: 0, output: 0 };
10167
+ let recognized = 0;
10168
+ for (const entry of entries) {
10169
+ if (!entry || typeof entry !== "object") continue;
10170
+ const count = toNonNegativeInt(entry.tokenCount);
10171
+ switch (entry.tokenType) {
10172
+ case "input":
10173
+ totals.input += count;
10174
+ recognized++;
10175
+ break;
10176
+ case "cache_read":
10177
+ totals.cacheRead += count;
10178
+ recognized++;
10179
+ break;
10180
+ case "cache_write":
10181
+ totals.cacheWrite += count;
10182
+ recognized++;
10183
+ break;
10184
+ case "output":
10185
+ totals.output += count;
10186
+ recognized++;
10187
+ break;
10188
+ default:
10189
+ break;
10190
+ }
10191
+ }
10192
+ return recognized > 0 ? totals : null;
10193
+ }
10194
+
10195
+ function copilotStoreRecentEvent(row, firstSeenAtMs = Date.now()) {
10196
+ const tsIso = parseCopilotAppTimestamp(row?.created_at);
10197
+ const tsMs = tsIso ? Date.parse(tsIso) : NaN;
10198
+ const sessionId =
10199
+ typeof row?.session_id === "string" ? row.session_id.trim() : "";
10200
+ if (!sessionId || !Number.isFinite(tsMs) || tsMs <= 0) return null;
10201
+ const normalized = normalizeCopilotSessionStoreUsage(row);
10202
+ return {
10203
+ id: toNonNegativeInt(row?.id),
10204
+ sessionId,
10205
+ model: normalizeCopilotAppModel(row?.model) || COPILOT_APP_DEFAULT_MODEL,
10206
+ input: normalized.input_tokens,
10207
+ output: normalized.output_tokens,
10208
+ cacheRead: normalized.cached_input_tokens,
10209
+ cacheWrite: normalized.cache_creation_input_tokens,
10210
+ reasoning: normalized.reasoning_output_tokens,
10211
+ tsMs,
10212
+ firstSeenAtMs,
10213
+ };
10214
+ }
10215
+
10216
+ function normalizeCopilotSessionStoreUsage(row) {
10217
+ const inputRaw = toNonNegativeInt(row?.input_tokens);
10218
+ const outputRaw = toNonNegativeInt(row?.output_tokens);
10219
+ const reasoning = toNonNegativeInt(row?.reasoning_tokens);
10220
+ const details = parseCopilotStoreTokenDetails(row?.token_details_json);
10221
+ let input;
10222
+ let cacheRead;
10223
+ let cacheWrite;
10224
+ let output;
10225
+ let precision = "fallback";
10226
+
10227
+ if (
10228
+ details &&
10229
+ details.input + details.cacheRead + details.cacheWrite === inputRaw &&
10230
+ details.output === outputRaw
10231
+ ) {
10232
+ input = details.input;
10233
+ cacheRead = details.cacheRead;
10234
+ cacheWrite = details.cacheWrite;
10235
+ output = details.output;
10236
+ precision = "exact";
10237
+ } else {
10238
+ cacheRead = Math.min(toNonNegativeInt(row?.cache_read_tokens), inputRaw);
10239
+ cacheWrite = Math.min(
10240
+ toNonNegativeInt(row?.cache_write_tokens),
10241
+ Math.max(0, inputRaw - cacheRead),
10242
+ );
10243
+ input = Math.max(0, inputRaw - cacheRead - cacheWrite);
10244
+ output = outputRaw;
10245
+ }
10246
+
10247
+ const reasoningClamped = Math.min(reasoning, output);
10248
+ const outputWithoutReasoning = Math.max(0, output - reasoningClamped);
10249
+ return {
10250
+ input_tokens: input,
10251
+ cached_input_tokens: cacheRead,
10252
+ cache_creation_input_tokens: cacheWrite,
10253
+ output_tokens: outputWithoutReasoning,
10254
+ reasoning_output_tokens: reasoningClamped,
10255
+ total_tokens:
10256
+ input + cacheRead + cacheWrite + outputWithoutReasoning + reasoningClamped,
10257
+ precision,
10258
+ };
10259
+ }
10260
+
10261
+ async function parseCopilotSessionStoreIncremental({
10262
+ dbPath,
10263
+ dbPaths,
10264
+ cursors,
10265
+ queuePath,
10266
+ onProgress,
10267
+ env,
10268
+ sqliteOptions,
10269
+ backfillOnFirstRun = false,
10270
+ excludeSessionIdsOnFirstRun,
10271
+ excludeSessionTotalsOnFirstRun,
10272
+ expectedFingerprints,
10273
+ otelUsageEvents,
10274
+ } = {}) {
10275
+ await ensureDir(path.dirname(queuePath));
10276
+ const paths = Array.isArray(dbPaths)
10277
+ ? dbPaths
10278
+ : dbPath
10279
+ ? [dbPath]
10280
+ : resolveCopilotSessionStorePaths(env || process.env);
10281
+ const storeState =
10282
+ cursors.copilotStore && typeof cursors.copilotStore === "object"
10283
+ ? cursors.copilotStore
10284
+ : {};
10285
+ const dbStates =
10286
+ storeState.dbs && typeof storeState.dbs === "object" ? { ...storeState.dbs } : {};
10287
+ let uniquePaths = uniqueCopilotDbPaths(
10288
+ paths,
10289
+ env || process.env,
10290
+ Object.keys(dbStates),
10291
+ );
10292
+ const migratedAliasPaths = coalesceCopilotDbStatesByIdentity(
10293
+ dbStates,
10294
+ uniquePaths,
10295
+ mergeCopilotStoreAliasStates,
10296
+ );
10297
+ uniquePaths = uniquePaths.filter(
10298
+ (dbPath) => !migratedAliasPaths.has(dbPath),
10299
+ );
10300
+ const seenSessions = new Set(
10301
+ Array.isArray(storeState.seenSessions) ? storeState.seenSessions : [],
10302
+ );
10303
+ const excludedFirstRunSessions = new Set(
10304
+ Array.isArray(excludeSessionIdsOnFirstRun)
10305
+ ? excludeSessionIdsOnFirstRun.filter((value) => typeof value === "string" && value)
10306
+ : [],
10307
+ );
10308
+ const excludedFirstRunTotals =
10309
+ excludeSessionTotalsOnFirstRun &&
10310
+ typeof excludeSessionTotalsOnFirstRun === "object" &&
10311
+ !Array.isArray(excludeSessionTotalsOnFirstRun)
10312
+ ? excludeSessionTotalsOnFirstRun
10313
+ : {};
10314
+ const legacyResiduals = new Map();
10315
+ const claimNowMs = Date.now();
10316
+ const recentEvents = pruneCopilotUsageClaims(
10317
+ storeState.recentEvents,
10318
+ claimNowMs,
10319
+ );
10320
+ replaceCopilotUsageClaims(otelUsageEvents, claimNowMs, Infinity);
10321
+ const otelUsageMatcher = createCopilotStoreUsageMatcher(otelUsageEvents);
10322
+ const hourlyState = normalizeHourlyState(cursors?.hourly);
10323
+ const touchedBuckets = new Set();
10324
+ const cb = typeof onProgress === "function" ? onProgress : null;
10325
+ const updatedAt = new Date(claimNowMs).toISOString();
10326
+ let activeDbCount = 0;
10327
+ let adoptedThisRun = false;
10328
+ let recordsProcessed = 0;
10329
+ let eventsAggregated = 0;
10330
+ let dbErrors = 0;
10331
+ const canonicalDbPaths = [];
10332
+ const globalAdoptionPending = storeState.active !== true;
10333
+
10334
+ for (const resolvedDb of uniquePaths) {
10335
+ const dbState =
10336
+ dbStates[resolvedDb] && typeof dbStates[resolvedDb] === "object"
10337
+ ? dbStates[resolvedDb]
10338
+ : {};
10339
+ const seenEventCounts = normalizeCopilotFingerprintCounts(
10340
+ dbState.seenEventCounts,
10341
+ );
10342
+ let pendingMalformedEvents = normalizeCopilotPendingMalformedEvents(
10343
+ dbState.pendingMalformedEvents,
10344
+ );
10345
+ let fingerprint = null;
10346
+ let snap = null;
10347
+ try {
10348
+ fingerprint = sqliteSidecarFingerprint(resolvedDb);
10349
+ const expectedFingerprint = expectedFingerprints?.[resolvedDb];
10350
+ if (
10351
+ expectedFingerprint &&
10352
+ !sameSqliteFingerprint(fingerprint, expectedFingerprint)
10353
+ ) {
10354
+ dbStates[resolvedDb] = {
10355
+ ...omitCopilotLegacyCatchupState(dbState),
10356
+ adoptionDeferredAt: updatedAt,
10357
+ updatedAt,
10358
+ };
10359
+ continue;
10360
+ }
10361
+ const currentIno = fingerprint?.db?.ino;
10362
+ const priorLastId = toNonNegativeInt(dbState.lastId);
10363
+ snap = snapshotSqliteDb(resolvedDb);
10364
+ const metadata = readCopilotSessionStoreMetadata(
10365
+ snap.path,
10366
+ sqliteOptions,
10367
+ priorLastId,
10368
+ );
10369
+ if (!metadata.active) {
10370
+ dbStates[resolvedDb] = {
10371
+ ...omitCopilotLegacyCatchupState(dbState),
10372
+ schemaVersion: metadata.schemaVersion,
10373
+ lastError: null,
10374
+ lastErrorAt: null,
10375
+ updatedAt,
10376
+ };
10377
+ continue;
10378
+ }
10379
+
10380
+ const inodeChanged =
10381
+ typeof dbState.dbIno === "number" &&
10382
+ typeof currentIno === "number" &&
10383
+ dbState.dbIno !== currentIno;
10384
+ const cursorRowChanged =
10385
+ priorLastId > 0 &&
10386
+ typeof dbState.lastRowKey === "string" &&
10387
+ dbState.lastRowKey !== metadata.cursorRowKey;
10388
+ const idRegressed = metadata.maxId < priorLastId;
10389
+ const resetDetected =
10390
+ Boolean(dbState.adoptedAt) && (idRegressed || cursorRowChanged);
10391
+ const needsFingerprintSeed =
10392
+ Boolean(dbState.adoptedAt) &&
10393
+ priorLastId > 0 &&
10394
+ Object.keys(seenEventCounts).length === 0;
10395
+ const resetWithoutFingerprintHistory =
10396
+ resetDetected && needsFingerprintSeed;
10397
+ const reconcileAllRows =
10398
+ (resetDetected || inodeChanged) && !resetWithoutFingerprintHistory;
10399
+ const needsAdoption =
10400
+ globalAdoptionPending ||
10401
+ !dbState.adoptedAt ||
10402
+ resetDetected;
10403
+ const baselineOnly =
10404
+ resetWithoutFingerprintHistory ||
10405
+ (needsAdoption && !resetDetected && !backfillOnFirstRun);
10406
+ const incrementalRows = readCopilotSessionStoreUsageRows(
10407
+ snap.path,
10408
+ reconcileAllRows || baselineOnly || needsFingerprintSeed ? 0 : priorLastId,
10409
+ sqliteOptions,
10410
+ );
10411
+ const pendingRetryRows = resetDetected || reconcileAllRows
10412
+ ? []
10413
+ : readCopilotSessionStoreUsageRowsByIds(
10414
+ snap.path,
10415
+ Object.keys(pendingMalformedEvents),
10416
+ sqliteOptions,
10417
+ );
10418
+ const pendingRetryIds = new Set(
10419
+ pendingRetryRows.map((row) => toNonNegativeInt(row?.id)),
10420
+ );
10421
+ if (resetDetected || reconcileAllRows) {
10422
+ pendingMalformedEvents = {};
10423
+ } else {
10424
+ for (const pendingId of Object.keys(pendingMalformedEvents)) {
10425
+ if (!pendingRetryIds.has(toNonNegativeInt(pendingId))) {
10426
+ delete pendingMalformedEvents[pendingId];
10427
+ }
10428
+ }
10429
+ }
10430
+ const rowsById = new Map();
10431
+ for (const row of [...pendingRetryRows, ...incrementalRows]) {
10432
+ rowsById.set(toNonNegativeInt(row?.id), row);
10433
+ }
10434
+ const rows = Array.from(rowsById.values()).sort(
10435
+ (a, b) => toNonNegativeInt(a?.id) - toNonNegativeInt(b?.id),
10436
+ );
10437
+ // Reindex/reset can rewrite every SQLite id. Subtract the exact metadata
10438
+ // multiset already observed so only genuinely new requests are emitted.
10439
+ const historicalFingerprintCounts = reconcileAllRows
10440
+ ? new Map(
10441
+ Object.entries(seenEventCounts).map(([eventFingerprint, count]) => [
10442
+ eventFingerprint,
10443
+ toNonNegativeInt(count),
10444
+ ]),
10445
+ )
10446
+ : null;
10447
+ if (needsAdoption) adoptedThisRun = true;
10448
+ if (expectedFingerprint) {
10449
+ const finalFingerprint = sqliteSidecarFingerprint(resolvedDb);
10450
+ if (!sameSqliteFingerprint(finalFingerprint, expectedFingerprint)) {
10451
+ dbStates[resolvedDb] = {
10452
+ ...omitCopilotLegacyCatchupState(dbState),
10453
+ adoptionDeferredAt: updatedAt,
10454
+ updatedAt,
10455
+ };
10456
+ continue;
10457
+ }
10458
+ fingerprint = finalFingerprint;
10459
+ }
10460
+ let lastId =
10461
+ reconcileAllRows || baselineOnly ? metadata.maxId : priorLastId;
10462
+ for (const row of rows) {
10463
+ recordsProcessed++;
10464
+ const rowId = toNonNegativeInt(row?.id);
10465
+ lastId = Math.max(lastId, rowId);
10466
+ const sessionId =
10467
+ typeof row?.session_id === "string" ? row.session_id.trim() : "";
10468
+ if (!sessionId) {
10469
+ if (rowId > 0) pendingMalformedEvents[rowId] = "";
10470
+ continue;
10471
+ }
10472
+ const eventFingerprint = copilotStoreEventFingerprint(row);
10473
+ if (historicalFingerprintCounts && eventFingerprint) {
10474
+ const historicalCount = toNonNegativeInt(
10475
+ historicalFingerprintCounts.get(eventFingerprint),
10476
+ );
10477
+ if (historicalCount > 0) {
10478
+ historicalFingerprintCounts.set(
10479
+ eventFingerprint,
10480
+ historicalCount - 1,
10481
+ );
10482
+ if (!parseCopilotAppTimestamp(row?.created_at) && rowId > 0) {
10483
+ pendingMalformedEvents[rowId] = eventFingerprint;
10484
+ }
10485
+ seenSessions.add(sessionId);
10486
+ continue;
10487
+ }
10488
+ }
10489
+ const seedOnly =
10490
+ needsFingerprintSeed && rowId <= priorLastId;
10491
+ const recentEvent = copilotStoreRecentEvent(row, claimNowMs);
10492
+ if (pendingRetryIds.has(rowId) && !recentEvent) {
10493
+ if (seedOnly) {
10494
+ incrementCopilotFingerprintCount(
10495
+ seenEventCounts,
10496
+ eventFingerprint,
10497
+ );
10498
+ }
10499
+ pendingMalformedEvents[rowId] = eventFingerprint || "";
10500
+ seenSessions.add(sessionId);
10501
+ continue;
10502
+ }
10503
+ if (rowId > 0) delete pendingMalformedEvents[rowId];
10504
+ const matchedOtelUsage =
10505
+ recentEvent && otelUsageMatcher.consume(recentEvent);
10506
+ if (matchedOtelUsage) recentEvent.consumed = true;
10507
+ if (baselineOnly || seedOnly) {
10508
+ incrementCopilotFingerprintCount(seenEventCounts, eventFingerprint);
10509
+ if (!recentEvent && rowId > 0) {
10510
+ pendingMalformedEvents[rowId] = eventFingerprint || "";
10511
+ }
10512
+ seenSessions.add(sessionId);
10513
+ continue;
10514
+ }
10515
+ incrementCopilotFingerprintCount(seenEventCounts, eventFingerprint);
10516
+ if (needsAdoption && backfillOnFirstRun && excludedFirstRunSessions.has(sessionId)) {
10517
+ if (
10518
+ !matchedOtelUsage &&
10519
+ Object.prototype.hasOwnProperty.call(
10520
+ excludedFirstRunTotals,
10521
+ sessionId,
10522
+ )
10523
+ ) {
10524
+ const normalized = normalizeCopilotSessionStoreUsage(row);
10525
+ const priorResidual = legacyResiduals.get(sessionId) || {
10526
+ storeTokens: 0,
10527
+ lastEventAt: null,
10528
+ };
10529
+ priorResidual.storeTokens += normalized.total_tokens;
10530
+ priorResidual.lastEventAt = latestCopilotTimestamp(
10531
+ priorResidual.lastEventAt,
10532
+ row?.created_at,
10533
+ );
10534
+ legacyResiduals.set(sessionId, priorResidual);
10535
+ }
10536
+ if (recentEvent && !matchedOtelUsage) recentEvents.push(recentEvent);
10537
+ seenSessions.add(sessionId);
10538
+ continue;
10539
+ }
10540
+ if (matchedOtelUsage) {
10541
+ seenSessions.add(sessionId);
10542
+ continue;
10543
+ }
10544
+ if (recentEvent) recentEvents.push(recentEvent);
10545
+ const normalized = normalizeCopilotSessionStoreUsage(row);
10546
+ if (normalized.total_tokens <= 0) {
10547
+ seenSessions.add(sessionId);
10548
+ continue;
10549
+ }
10550
+ const tsIso = parseCopilotAppTimestamp(row.created_at);
10551
+ const bucketStart = tsIso ? toUtcHalfHourStart(tsIso) : null;
10552
+ if (!bucketStart) {
10553
+ if (rowId > 0) {
10554
+ pendingMalformedEvents[rowId] = eventFingerprint || "";
10555
+ }
10556
+ seenSessions.add(sessionId);
10557
+ continue;
10558
+ }
10559
+ const model =
10560
+ normalizeCopilotAppModel(row.model) || COPILOT_APP_DEFAULT_MODEL;
10561
+ const delta = {
10562
+ input_tokens: normalized.input_tokens,
10563
+ cached_input_tokens: normalized.cached_input_tokens,
10564
+ cache_creation_input_tokens: normalized.cache_creation_input_tokens,
10565
+ output_tokens: normalized.output_tokens,
10566
+ reasoning_output_tokens: normalized.reasoning_output_tokens,
10567
+ total_tokens: normalized.total_tokens,
10568
+ conversation_count: seenSessions.has(sessionId) ? 0 : 1,
10569
+ };
10570
+ const bucket = getHourlyBucket(hourlyState, "copilot", model, bucketStart);
10571
+ addTotals(bucket.totals, delta);
10572
+ touchedBuckets.add(bucketKey("copilot", model, bucketStart));
10573
+ seenSessions.add(sessionId);
10574
+ eventsAggregated++;
10575
+ if (cb) {
10576
+ cb({
10577
+ index: recordsProcessed,
10578
+ total: rows.length,
10579
+ recordsProcessed,
10580
+ eventsAggregated,
10581
+ bucketsQueued: touchedBuckets.size,
10582
+ });
10583
+ }
10584
+ }
10585
+
10586
+ dbStates[resolvedDb] = {
10587
+ ...omitCopilotLegacyCatchupState(dbState),
10588
+ adoptedAt: dbState.adoptedAt || updatedAt,
10589
+ resetAt: resetDetected ? updatedAt : dbState.resetAt || null,
10590
+ schemaVersion: metadata.schemaVersion,
10591
+ lastRowKey: metadata.lastRowKey,
10592
+ lastId: Math.max(lastId, metadata.maxId),
10593
+ lastEventAt: latestCopilotTimestamp(
10594
+ dbState.lastEventAt,
10595
+ metadata.lastEventAt,
10596
+ ),
10597
+ seenEventCounts,
10598
+ pendingMalformedEvents,
10599
+ malformedEventCount: Object.keys(pendingMalformedEvents).length,
10600
+ resetGapEventCount:
10601
+ toNonNegativeInt(dbState.resetGapEventCount) +
10602
+ (resetWithoutFingerprintHistory ? rows.length : 0),
10603
+ dbIno: typeof currentIno === "number" ? currentIno : null,
10604
+ lastDbFingerprint: fingerprint,
10605
+ lastError: null,
10606
+ lastErrorAt: null,
10607
+ updatedAt,
10608
+ };
10609
+ canonicalDbPaths.push(resolvedDb);
10610
+ activeDbCount++;
10611
+ } catch (err) {
10612
+ if (err?.code !== "ENOENT") dbErrors++;
10613
+ dbStates[resolvedDb] = {
10614
+ ...omitCopilotLegacyCatchupState(dbState),
10615
+ lastError: err && err.message ? err.message : String(err),
10616
+ lastErrorAt: updatedAt,
10617
+ updatedAt,
10618
+ };
10619
+ } finally {
10620
+ if (snap) snap.cleanup();
10621
+ }
10622
+ }
10623
+
10624
+ for (const [sessionId, residual] of legacyResiduals) {
10625
+ const baseline = excludedFirstRunTotals[sessionId] || {};
10626
+ const appTotals = normalizeCopilotAppTokenDelta(baseline, {
10627
+ input: 0,
10628
+ output: 0,
10629
+ cached: 0,
10630
+ reasoning: 0,
10631
+ });
10632
+ const residualTokens = Math.max(
10633
+ 0,
10634
+ toNonNegativeInt(residual.storeTokens) - appTotals.total_tokens,
10635
+ );
10636
+ if (residualTokens <= 0) continue;
10637
+ const timestamp = latestCopilotTimestamp(
10638
+ residual.lastEventAt,
10639
+ baseline.updatedAt,
10640
+ );
10641
+ const bucketStart = timestamp ? toUtcHalfHourStart(timestamp) : null;
10642
+ if (!bucketStart) continue;
10643
+ const bucket = getHourlyBucket(
10644
+ hourlyState,
10645
+ "copilot",
10646
+ COPILOT_STORE_LEGACY_MODEL,
10647
+ bucketStart,
10648
+ );
10649
+ addTotals(bucket.totals, {
10650
+ input_tokens: residualTokens,
10651
+ cached_input_tokens: 0,
10652
+ cache_creation_input_tokens: 0,
10653
+ output_tokens: 0,
10654
+ reasoning_output_tokens: 0,
10655
+ total_tokens: residualTokens,
10656
+ conversation_count: 0,
10657
+ });
10658
+ touchedBuckets.add(
10659
+ bucketKey("copilot", COPILOT_STORE_LEGACY_MODEL, bucketStart),
10660
+ );
10661
+ eventsAggregated++;
10662
+ }
10663
+
10664
+ const bucketsQueued = await enqueueTouchedBuckets({
10665
+ queuePath,
10666
+ hourlyState,
10667
+ touchedBuckets,
10668
+ });
10669
+ const allCurrentPathsHealthy =
10670
+ uniquePaths.length > 0 &&
10671
+ activeDbCount === uniquePaths.length &&
10672
+ dbErrors === 0;
10673
+ const canonicalActive = storeState.active === true || allCurrentPathsHealthy;
10674
+ hourlyState.updatedAt = updatedAt;
10675
+ cursors.hourly = hourlyState;
10676
+ cursors.copilotStore = {
10677
+ ...omitCopilotLegacyCatchupState(storeState),
10678
+ version: COPILOT_STORE_CURSOR_VERSION,
10679
+ active: canonicalActive,
10680
+ dbs: dbStates,
10681
+ seenSessions: Array.from(seenSessions),
10682
+ recentEvents: pruneCopilotUsageClaims(recentEvents),
10683
+ updatedAt,
10684
+ };
10685
+ return {
10686
+ active: canonicalActive,
10687
+ healthy: allCurrentPathsHealthy,
10688
+ adoptedThisRun,
10689
+ canonicalDbPaths,
10690
+ recordsProcessed,
10691
+ eventsAggregated,
10692
+ bucketsQueued,
10693
+ dbErrors,
10694
+ usageClaims: recentEvents,
10695
+ };
9559
10696
  }
9560
10697
 
9561
10698
  // ─────────────────────────────────────────────────────────────────────────────
@@ -9567,9 +10704,9 @@ async function parseCopilotIncremental({ otelPaths, cursors, queuePath, onProgre
9567
10704
  // there before the child has produced any tokens, so parsing event history would
9568
10705
  // double-count parent usage. The only accounting source here is positive deltas
9569
10706
  // from the `sessions.total_*` counters. Those deltas feed the same aggregate
9570
- // `source="copilot"` pipeline as existing OTEL data (dashboard totals, trends,
9571
- // model breakdown, cost), but the cursor is separate (`cursors.copilotApp`) so
9572
- // Copilot CLI / Chat extension OTEL support keeps its existing behavior.
10707
+ // `source="copilot"` pipeline during legacy adoption. Once session-store is
10708
+ // canonical, this parser runs observe-only so a temporary store outage cannot
10709
+ // create duplicate fallback writes when the store returns.
9573
10710
  // Project attribution and per-conversation detail are intentionally absent:
9574
10711
  // data.db's summary columns do not expose privacy-safe project/message metadata.
9575
10712
  // ─────────────────────────────────────────────────────────────────────────────
@@ -9577,44 +10714,169 @@ async function parseCopilotIncremental({ otelPaths, cursors, queuePath, onProgre
9577
10714
  const COPILOT_APP_CURSOR_VERSION = 1;
9578
10715
  const COPILOT_APP_DEFAULT_MODEL = "github-copilot";
9579
10716
 
9580
- function normalizeCopilotAppDbPath(dbPath, env = process.env) {
10717
+ function normalizeCopilotDbPath(dbPath, env = process.env) {
9581
10718
  if (typeof dbPath !== "string" || !dbPath.trim()) return null;
9582
- const expanded = expandHomePath(dbPath.trim(), env);
9583
- return path.isAbsolute(expanded) ? path.normalize(expanded) : path.resolve(expanded);
10719
+ const raw = dbPath.trim();
10720
+ const home =
10721
+ process.platform === "win32"
10722
+ ? env.USERPROFILE || env.HOME || require("node:os").homedir()
10723
+ : env.HOME || require("node:os").homedir();
10724
+ const expanded =
10725
+ raw === "~"
10726
+ ? home
10727
+ : raw.startsWith("~/") || raw.startsWith("~\\")
10728
+ ? path.join(home, raw.slice(2))
10729
+ : raw;
10730
+ const normalized =
10731
+ path.isAbsolute(expanded) ? path.normalize(expanded) : path.resolve(expanded);
10732
+ return normalized;
9584
10733
  }
9585
10734
 
9586
- function resolveCopilotAppDbPaths(env = process.env) {
9587
- const home = env.HOME || require("node:os").homedir();
9588
- const paths = new Set();
9589
- const addDbPath = (dbPath) => {
9590
- const normalized = normalizeCopilotAppDbPath(dbPath, env);
9591
- if (normalized) paths.add(normalized);
10735
+ function mergeCopilotStoreAliasStates(primary = {}, alias = {}) {
10736
+ const primaryLastId = toNonNegativeInt(primary?.lastId);
10737
+ const aliasLastId = toNonNegativeInt(alias?.lastId);
10738
+ const primaryUpdatedAt = Date.parse(primary?.updatedAt || "") || 0;
10739
+ const aliasUpdatedAt = Date.parse(alias?.updatedAt || "") || 0;
10740
+ const newer =
10741
+ aliasLastId > primaryLastId ||
10742
+ (aliasLastId === primaryLastId && aliasUpdatedAt > primaryUpdatedAt)
10743
+ ? alias
10744
+ : primary;
10745
+ const seenEventCounts = normalizeCopilotFingerprintCounts(
10746
+ primary?.seenEventCounts,
10747
+ );
10748
+ for (const [fingerprint, count] of Object.entries(
10749
+ normalizeCopilotFingerprintCounts(alias?.seenEventCounts),
10750
+ )) {
10751
+ seenEventCounts[fingerprint] = Math.max(
10752
+ toNonNegativeInt(seenEventCounts[fingerprint]),
10753
+ count,
10754
+ );
10755
+ }
10756
+ const pendingMalformedEvents = {
10757
+ ...normalizeCopilotPendingMalformedEvents(
10758
+ primary?.pendingMalformedEvents,
10759
+ ),
10760
+ ...normalizeCopilotPendingMalformedEvents(
10761
+ alias?.pendingMalformedEvents,
10762
+ ),
9592
10763
  };
9593
- const addHome = (homePath) => {
9594
- const normalizedHome = normalizeCopilotAppDbPath(homePath, env);
9595
- if (normalizedHome) addDbPath(path.join(normalizedHome, "data.db"));
10764
+ return {
10765
+ ...primary,
10766
+ ...newer,
10767
+ seenEventCounts,
10768
+ pendingMalformedEvents,
10769
+ malformedEventCount: Object.keys(pendingMalformedEvents).length,
10770
+ resetGapEventCount: Math.max(
10771
+ toNonNegativeInt(primary?.resetGapEventCount),
10772
+ toNonNegativeInt(alias?.resetGapEventCount),
10773
+ ),
9596
10774
  };
10775
+ }
9597
10776
 
9598
- if (typeof env.TOKENTRACKER_COPILOT_APP_DB === "string" && env.TOKENTRACKER_COPILOT_APP_DB.trim()) {
9599
- addDbPath(env.TOKENTRACKER_COPILOT_APP_DB);
10777
+ function coalesceCopilotDbStatesByIdentity(
10778
+ dbStates,
10779
+ selectedPaths,
10780
+ mergeStates,
10781
+ ) {
10782
+ if (!dbStates || typeof dbStates !== "object") return new Set();
10783
+ const selectedByIdentity = new Map();
10784
+ const selectedByInode = new Map();
10785
+ for (const selectedPath of selectedPaths) {
10786
+ try {
10787
+ const stat = fssync.statSync(selectedPath);
10788
+ selectedByIdentity.set(`${stat.dev}:${stat.ino}`, selectedPath);
10789
+ if (selectedByInode.has(stat.ino)) selectedByInode.set(stat.ino, null);
10790
+ else selectedByInode.set(stat.ino, selectedPath);
10791
+ } catch (_e) {}
9600
10792
  }
9601
- if (typeof env.COPILOT_HOME === "string" && env.COPILOT_HOME.trim()) {
9602
- addHome(env.COPILOT_HOME);
10793
+ const migratedPaths = new Set();
10794
+ for (const [statePath, state] of Object.entries({ ...dbStates })) {
10795
+ let dev = null;
10796
+ let ino = null;
10797
+ try {
10798
+ const stat = fssync.statSync(statePath);
10799
+ dev = stat.dev;
10800
+ ino = stat.ino;
10801
+ } catch (_e) {
10802
+ dev = state?.lastDbFingerprint?.db?.dev;
10803
+ ino = state?.lastDbFingerprint?.db?.ino;
10804
+ }
10805
+ const selectedPath =
10806
+ Number.isFinite(dev) && Number.isFinite(ino)
10807
+ ? selectedByIdentity.get(`${dev}:${ino}`)
10808
+ : Number.isFinite(ino)
10809
+ ? selectedByInode.get(ino)
10810
+ : null;
10811
+ if (!selectedPath || selectedPath === statePath) continue;
10812
+ dbStates[selectedPath] = mergeStates(
10813
+ dbStates[selectedPath],
10814
+ state,
10815
+ );
10816
+ delete dbStates[statePath];
10817
+ migratedPaths.add(statePath);
9603
10818
  }
10819
+ return migratedPaths;
10820
+ }
9604
10821
 
9605
- const defaultDb = path.join(home, ".copilot", "data.db");
9606
- if (process.platform === "win32") {
9607
- const wslHome = wsl.shouldProbeWsl(env) ? wsl.discoverWslHome(".copilot", { env }) : null;
9608
- const wslDb = wslHome ? path.join(wslHome, "data.db") : null;
9609
- const resolved = resolveInstallPaths({ nativeValue: defaultDb, wslValue: wslDb }, env);
9610
- if (resolved.native) addDbPath(resolved.native);
9611
- if (resolved.wsl) addDbPath(resolved.wsl);
9612
- if (paths.size === 0 && wsl.shouldProbeNative(env)) addDbPath(defaultDb);
9613
- } else {
9614
- addDbPath(defaultDb);
10822
+ function uniqueCopilotDbPaths(
10823
+ paths,
10824
+ env = process.env,
10825
+ preferredPaths = [],
10826
+ ) {
10827
+ const pathKey = (dbPath) =>
10828
+ process.platform === "win32" ? dbPath.toLowerCase() : dbPath;
10829
+ const preferredKeys = new Set(
10830
+ (Array.isArray(preferredPaths) ? preferredPaths : [])
10831
+ .map((candidate) => normalizeCopilotDbPath(candidate, env))
10832
+ .filter(Boolean)
10833
+ .map(pathKey),
10834
+ );
10835
+ const candidates = (Array.isArray(paths) ? paths : [])
10836
+ .map((candidate, index) => ({
10837
+ index,
10838
+ path: normalizeCopilotDbPath(candidate, env),
10839
+ }))
10840
+ .filter((candidate) => candidate.path)
10841
+ .sort((a, b) => {
10842
+ const preferredDiff =
10843
+ Number(preferredKeys.has(pathKey(b.path))) -
10844
+ Number(preferredKeys.has(pathKey(a.path)));
10845
+ return preferredDiff || a.index - b.index;
10846
+ });
10847
+ const result = [];
10848
+ const identities = new Set();
10849
+ for (const candidate of candidates) {
10850
+ const normalized = candidate.path;
10851
+ let identity =
10852
+ process.platform === "win32"
10853
+ ? `path:${normalized.toLowerCase()}`
10854
+ : `path:${normalized}`;
10855
+ try {
10856
+ const stat = fssync.statSync(normalized);
10857
+ if (
10858
+ Number.isFinite(stat.dev) &&
10859
+ Number.isFinite(stat.ino) &&
10860
+ stat.ino > 0
10861
+ ) {
10862
+ identity = `inode:${stat.dev}:${stat.ino}`;
10863
+ }
10864
+ } catch (_e) {}
10865
+ if (identities.has(identity)) continue;
10866
+ identities.add(identity);
10867
+ result.push(normalized);
9615
10868
  }
10869
+ return result;
10870
+ }
9616
10871
 
9617
- return Array.from(paths).sort();
10872
+ function resolveCopilotAppDbPaths(env = process.env) {
10873
+ return resolveCopilotDbPaths(
10874
+ {
10875
+ fileName: "data.db",
10876
+ overrideEnvKey: "TOKENTRACKER_COPILOT_APP_DB",
10877
+ },
10878
+ env,
10879
+ );
9618
10880
  }
9619
10881
 
9620
10882
  function resolveCopilotAppDbPath(env = process.env) {
@@ -9723,6 +10985,7 @@ function sqliteSidecarFingerprint(dbPath) {
9723
10985
  out[suffix || "db"] = {
9724
10986
  size: stat.size,
9725
10987
  mtimeMs: stat.mtimeMs,
10988
+ dev: stat.dev,
9726
10989
  ino: stat.ino,
9727
10990
  };
9728
10991
  } catch (e) {
@@ -9736,6 +10999,10 @@ function sameSqliteFingerprint(a, b) {
9736
10999
  return JSON.stringify(a || {}) === JSON.stringify(b || {});
9737
11000
  }
9738
11001
 
11002
+ function getCopilotSqliteFingerprint(dbPath) {
11003
+ return sqliteSidecarFingerprint(dbPath);
11004
+ }
11005
+
9739
11006
  function copilotAppBaselineTotal(value) {
9740
11007
  if (!value || typeof value !== "object") return 0;
9741
11008
  return (
@@ -9801,6 +11068,7 @@ async function parseCopilotAppDbIncremental({
9801
11068
  onProgress,
9802
11069
  env,
9803
11070
  sqliteOptions,
11071
+ observeOnly = false,
9804
11072
  } = {}) {
9805
11073
  await ensureDir(path.dirname(queuePath));
9806
11074
  const paths = Array.isArray(dbPaths) && dbPaths.length > 0
@@ -9808,11 +11076,15 @@ async function parseCopilotAppDbIncremental({
9808
11076
  : dbPath
9809
11077
  ? [dbPath]
9810
11078
  : resolveCopilotAppDbPaths(env || process.env);
9811
- const uniquePaths = [...new Set(paths.map((p) => normalizeCopilotAppDbPath(p, env || process.env)).filter(Boolean))];
9812
11079
  const appState =
9813
11080
  cursors.copilotApp && typeof cursors.copilotApp === "object" ? cursors.copilotApp : {};
9814
11081
  const dbStates =
9815
11082
  appState.dbs && typeof appState.dbs === "object" ? { ...appState.dbs } : {};
11083
+ const uniquePaths = uniqueCopilotDbPaths(
11084
+ paths,
11085
+ env || process.env,
11086
+ Object.keys(dbStates),
11087
+ );
9816
11088
  const hourlyState = normalizeHourlyState(cursors?.hourly);
9817
11089
  const touchedBuckets = new Set();
9818
11090
  const cb = typeof onProgress === "function" ? onProgress : null;
@@ -9895,6 +11167,10 @@ async function parseCopilotAppDbIncremental({
9895
11167
  sessionTotals[sessionId] = { ...curr, model, updatedAt: rowUpdatedAt };
9896
11168
  continue;
9897
11169
  }
11170
+ if (observeOnly) {
11171
+ sessionTotals[sessionId] = { ...curr, model, updatedAt: rowUpdatedAt };
11172
+ continue;
11173
+ }
9898
11174
 
9899
11175
  const bucketStart = toUtcHalfHourStart(rowUpdatedAt);
9900
11176
  if (!bucketStart) {
@@ -10859,8 +12135,16 @@ module.exports = {
10859
12135
  probeWslDistros,
10860
12136
  discoverWslHermesHome,
10861
12137
  resolveCopilotOtelPaths,
12138
+ normalizeCopilotDbPath,
12139
+ uniqueCopilotDbPaths,
12140
+ coalesceCopilotDbStatesByIdentity,
12141
+ resolveCopilotSessionStorePaths,
12142
+ getCopilotSqliteFingerprint,
12143
+ describeCopilotSessionStoreDb,
10862
12144
  resolveCopilotAppDbPath,
10863
12145
  resolveCopilotAppDbPaths,
12146
+ readCopilotSessionStoreUsageRows,
12147
+ normalizeCopilotSessionStoreUsage,
10864
12148
  readCopilotAppSessionsFromSqlite,
10865
12149
  parseRolloutIncremental,
10866
12150
  parseClaudeIncremental,
@@ -10874,8 +12158,12 @@ module.exports = {
10874
12158
  gooseInstallOwnsCursor,
10875
12159
  zedInstallOwnsCursor,
10876
12160
  hermesInstallOwnsCursor,
12161
+ copilotOtelCursorHasLegacyCliUsage,
12162
+ pruneCopilotUsageClaims,
10877
12163
  parseCopilotIncremental,
12164
+ parseCopilotSessionStoreIncremental,
10878
12165
  parseCopilotAppDbIncremental,
12166
+ resolveKimiHome,
10879
12167
  resolveKimiWireFiles,
10880
12168
  resolveKimiDefaultModel,
10881
12169
  parseKimiIncremental,
@@ -10957,6 +12245,7 @@ module.exports = {
10957
12245
  // Exposed for regression tests covering nested-group remote URLs.
10958
12246
  canonicalizeProjectRef,
10959
12247
  deriveProjectKeyFromRef,
12248
+ CLAUDE_MEM_OBSERVER_PROJECT_REF,
10960
12249
 
10961
12250
  // Grok Build (xAI) — SessionEnd hook + passive updates.jsonl/signals.json reader
10962
12251
  resolveGrokBuildHome,