tokentracker-cli 0.69.0 → 0.71.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 (56) hide show
  1. package/dashboard/dist/assets/{ActivityHeatmap-Bqy-meR1.js → ActivityHeatmap-Dk9JY1ug.js} +1 -1
  2. package/dashboard/dist/assets/{Card-DbzltTXg.js → Card-CNWjr8mp.js} +1 -1
  3. package/dashboard/dist/assets/{CommunityStatsModal-CumafhRo.js → CommunityStatsModal-C2qyGffN.js} +1 -1
  4. package/dashboard/dist/assets/{ConfirmModal-CfmWYFAl.js → ConfirmModal-DSu8Tu1r.js} +1 -1
  5. package/dashboard/dist/assets/{DashboardPage-beIJMVvO.js → DashboardPage-DIf7YTQ9.js} +1 -1
  6. package/dashboard/dist/assets/{DevicePage-Cb_cMnJj.js → DevicePage-CXQPPyMG.js} +1 -1
  7. package/dashboard/dist/assets/{DialogClose-245t1wj3.js → DialogClose-Cl6zG-Oy.js} +1 -1
  8. package/dashboard/dist/assets/DialogDescription-Bg5Mv-UG.js +1 -0
  9. package/dashboard/dist/assets/{DialogTitle-C_S_W-qN.js → DialogTitle-BRCsGR2a.js} +1 -1
  10. package/dashboard/dist/assets/{FadeIn-9qSEABKW.js → FadeIn-B3oaxBze.js} +1 -1
  11. package/dashboard/dist/assets/{HeaderGithubStar-C42NExcO.js → HeaderGithubStar-Dl3VFKq_.js} +1 -1
  12. package/dashboard/dist/assets/{IpCheckPage-C_LVvRK3.js → IpCheckPage-BJGOP6DZ.js} +1 -1
  13. package/dashboard/dist/assets/{LandingPage-1ng5HTtU.js → LandingPage-CM30tqFJ.js} +2 -2
  14. package/dashboard/dist/assets/{LeaderboardAvatar-Dud2isij.js → LeaderboardAvatar-P3H4JJH7.js} +1 -1
  15. package/dashboard/dist/assets/{LeaderboardPage-BVLHknpa.js → LeaderboardPage-CMgku-JV.js} +2 -2
  16. package/dashboard/dist/assets/{LeaderboardProfileModal-creqSCBC.js → LeaderboardProfileModal-1pDzkqlc.js} +1 -1
  17. package/dashboard/dist/assets/{LeaderboardProfilePage-CtCiW1ZR.js → LeaderboardProfilePage-CXv6v78X.js} +1 -1
  18. package/dashboard/dist/assets/{LimitsPage-DXhNfD6c.js → LimitsPage-DPmTlaMP.js} +1 -1
  19. package/dashboard/dist/assets/{LocalOnlyNotice-CupEXgLA.js → LocalOnlyNotice-DZ2JWpUi.js} +1 -1
  20. package/dashboard/dist/assets/{LoginPage-Bvb6khA_.js → LoginPage-yZfMo0Bn.js} +1 -1
  21. package/dashboard/dist/assets/{PopoverPopup-1RwMOuZR.js → PopoverPopup-C7ClU-a4.js} +1 -1
  22. package/dashboard/dist/assets/{ResetPasswordPage-C1t2TVSo.js → ResetPasswordPage-DRxzMEet.js} +1 -1
  23. package/dashboard/dist/assets/{Select-35dakfdf.js → Select-ShDjR08e.js} +1 -1
  24. package/dashboard/dist/assets/{SelectItemText-Bn80bCzm.js → SelectItemText-Cdb3AE2d.js} +1 -1
  25. package/dashboard/dist/assets/{SettingsPage-BtNI2EB5.js → SettingsPage-KhtHkiXb.js} +1 -1
  26. package/dashboard/dist/assets/{SkillsPage-DrtaOCt8.js → SkillsPage-BfvTyaan.js} +1 -1
  27. package/dashboard/dist/assets/{WidgetsPage-DBP3CcAv.js → WidgetsPage-B9WHyvqX.js} +1 -1
  28. package/dashboard/dist/assets/{WrappedPage-Dxzewd0b.js → WrappedPage-BVY8Z7e8.js} +1 -1
  29. package/dashboard/dist/assets/{agent-logos-DXWdLSyr.js → agent-logos-w8DphLG5.js} +1 -1
  30. package/dashboard/dist/assets/{arrow-up-right-CAv_ofGl.js → arrow-up-right-CCe_6-J8.js} +1 -1
  31. package/dashboard/dist/assets/check-BuuQsfIt.js +1 -0
  32. package/dashboard/dist/assets/{copy-CkAl55Gc.js → copy-B6Uad-NU.js} +1 -1
  33. package/dashboard/dist/assets/{download-CHuHPQRG.js → download-BYcyu1Bg.js} +1 -1
  34. package/dashboard/dist/assets/{format-tokens-BhF7ktma.js → format-tokens-BFn66jct.js} +1 -1
  35. package/dashboard/dist/assets/{info-4RqgE-Sk.js → info-CTKpV2vu.js} +1 -1
  36. package/dashboard/dist/assets/main-DXM2UPde.css +1 -0
  37. package/dashboard/dist/assets/{main-Cy1-zTjI.js → main-DbLbrnIE.js} +3 -3
  38. package/dashboard/dist/assets/{use-community-stats-BHARwgjJ.js → use-community-stats-Bumdx05F.js} +1 -1
  39. package/dashboard/dist/assets/{use-limits-display-prefs-D6TuBuXc.js → use-limits-display-prefs-DeSmHt-l.js} +1 -1
  40. package/dashboard/dist/assets/{use-native-settings-8aQ6BEe6.js → use-native-settings-CdTOidJ4.js} +1 -1
  41. package/dashboard/dist/assets/{use-ordered-list-B0XApham.js → use-ordered-list-DQoW_9df.js} +1 -1
  42. package/dashboard/dist/assets/{use-quality-per-dollar-pref-DNa5uaGP.js → use-quality-per-dollar-pref-CwFs7F06.js} +1 -1
  43. package/dashboard/dist/assets/{use-usage-limits-C1V3apqc.js → use-usage-limits-CKuCunJc.js} +1 -1
  44. package/dashboard/dist/assets/{useCurrency-BEkNDMgc.js → useCurrency-CG5fqlMR.js} +1 -1
  45. package/dashboard/dist/assets/{useScrollLock-BRpWM_0H.js → useScrollLock-DsnK0M2M.js} +1 -1
  46. package/dashboard/dist/index.html +2 -2
  47. package/dashboard/dist/share.html +2 -2
  48. package/package.json +1 -1
  49. package/src/commands/status.js +8 -5
  50. package/src/commands/sync.js +1 -1
  51. package/src/lib/pricing/seed-snapshot.json +1 -1
  52. package/src/lib/rollout.js +502 -110
  53. package/src/lib/usage-limits.js +80 -14
  54. package/dashboard/dist/assets/DialogDescription-CMG2f0Xr.js +0 -1
  55. package/dashboard/dist/assets/check-BzPtb9x7.js +0 -1
  56. package/dashboard/dist/assets/main-CjU-7igC.css +0 -1
@@ -5,7 +5,7 @@ const readline = require("node:readline");
5
5
 
6
6
  const crypto = require("node:crypto");
7
7
  const { ensureDir } = require("./fs");
8
- const { readSqliteJsonRows } = require("./sqlite-reader");
8
+ const { readSqliteJsonRows, readSqliteJsonRowsAsync } = require("./sqlite-reader");
9
9
  const wsl = require("./wsl-probe");
10
10
  const { resolveInstallPaths } = require("./install-resolver");
11
11
 
@@ -3049,6 +3049,26 @@ function toNonNegativeInt(v) {
3049
3049
  return Math.floor(n);
3050
3050
  }
3051
3051
 
3052
+ function firstPresentNonNegativeInt(values) {
3053
+ for (const value of values) {
3054
+ if (value !== undefined && value !== null) {
3055
+ return toNonNegativeInt(value);
3056
+ }
3057
+ }
3058
+ return 0;
3059
+ }
3060
+
3061
+ function firstPositiveOrPresentNonNegativeInt(values) {
3062
+ let firstPresent = null;
3063
+ for (const value of values) {
3064
+ if (value === undefined || value === null) continue;
3065
+ const n = toNonNegativeInt(value);
3066
+ if (firstPresent === null) firstPresent = n;
3067
+ if (n > 0) return n;
3068
+ }
3069
+ return firstPresent === null ? 0 : firstPresent;
3070
+ }
3071
+
3052
3072
  function coerceEpochMs(v) {
3053
3073
  const n = Number(v);
3054
3074
  if (!Number.isFinite(n) || n <= 0) return 0;
@@ -5530,30 +5550,131 @@ function resolveCodebuddyDefaultModel(env = process.env) {
5530
5550
 
5531
5551
  function resolveCodebuddyProjectFiles(env = process.env) {
5532
5552
  const codebuddyHome = resolveCodebuddyHome(env);
5533
- if (!codebuddyHome) return [];
5534
- const projectsDir = path.join(codebuddyHome, "projects");
5535
- if (!fssync.existsSync(projectsDir)) return [];
5536
5553
  const files = [];
5537
- try {
5538
- for (const cwd of fssync.readdirSync(projectsDir)) {
5539
- const cwdDir = path.join(projectsDir, cwd);
5540
- let stat;
5541
- try { stat = fssync.statSync(cwdDir); } catch { continue; }
5542
- if (!stat.isDirectory()) continue;
5554
+
5555
+ // 1. Recursive JSONL scan in codebuddyHome/projects
5556
+ if (codebuddyHome) {
5557
+ const projectsDir = path.join(codebuddyHome, "projects");
5558
+ if (fssync.existsSync(projectsDir)) {
5559
+ const walkJsonl = (dir) => {
5560
+ let entries;
5561
+ try { entries = fssync.readdirSync(dir, { withFileTypes: true }); } catch { return; }
5562
+ for (const entry of entries) {
5563
+ const full = path.join(dir, entry.name);
5564
+ let isDir = entry.isDirectory();
5565
+ let isFile = entry.isFile();
5566
+ if (!isDir && !isFile) {
5567
+ try {
5568
+ const st = fssync.statSync(full);
5569
+ isDir = st.isDirectory();
5570
+ isFile = st.isFile();
5571
+ } catch { continue; }
5572
+ }
5573
+ if (isDir) walkJsonl(full);
5574
+ else if (isFile && entry.name.endsWith(".jsonl")) files.push(full);
5575
+ }
5576
+ };
5577
+ walkJsonl(projectsDir);
5578
+ }
5579
+ }
5580
+
5581
+ // 2. Active IDE extension logs scan
5582
+ const logRoots = [];
5583
+ const home = env.HOME || require("node:os").homedir();
5584
+
5585
+ if (process.platform === "darwin") {
5586
+ const appSupport = path.join(home, "Library", "Application Support");
5587
+ logRoots.push({ dir: path.join(appSupport, "CodeBuddy CN", "logs"), pattern: "codebuddy-extension-log" });
5588
+ logRoots.push({ dir: path.join(appSupport, "Code", "logs"), pattern: "codebuddy-extension-log" });
5589
+ logRoots.push({ dir: path.join(appSupport, "CodeBuddyExtension", "Logs", "CodeBuddyIDE"), pattern: "*.log" });
5590
+ logRoots.push({ dir: path.join(appSupport, "CodeBuddyExtension", "Logs", "VSCode"), pattern: "*.log" });
5591
+ } else if (process.platform === "win32") {
5592
+ const appData = env.APPDATA || path.join(home, "AppData", "Roaming");
5593
+ const localAppData = env.LOCALAPPDATA || path.join(home, "AppData", "Local");
5594
+ logRoots.push({ dir: path.join(appData, "CodeBuddy CN", "logs"), pattern: "codebuddy-extension-log" });
5595
+ logRoots.push({ dir: path.join(appData, "Code", "logs"), pattern: "codebuddy-extension-log" });
5596
+ logRoots.push({ dir: path.join(localAppData, "CodeBuddyExtension", "Logs", "CodeBuddyIDE"), pattern: "*.log" });
5597
+ logRoots.push({ dir: path.join(localAppData, "CodeBuddyExtension", "Logs", "VSCode"), pattern: "*.log" });
5598
+ } else {
5599
+ const xdgConfig = env.XDG_CONFIG_HOME || path.join(home, ".config");
5600
+ const xdgData = env.XDG_DATA_HOME || path.join(home, ".local", "share");
5601
+ logRoots.push({ dir: path.join(xdgConfig, "CodeBuddy CN", "logs"), pattern: "codebuddy-extension-log" });
5602
+ logRoots.push({ dir: path.join(xdgConfig, "Code", "logs"), pattern: "codebuddy-extension-log" });
5603
+ logRoots.push({ dir: path.join(xdgData, "CodeBuddyExtension", "Logs", "CodeBuddyIDE"), pattern: "*.log" });
5604
+ logRoots.push({ dir: path.join(xdgData, "CodeBuddyExtension", "Logs", "VSCode"), pattern: "*.log" });
5605
+ }
5606
+
5607
+ for (const root of logRoots) {
5608
+ if (!fssync.existsSync(root.dir)) continue;
5609
+ const walkLogs = (dir) => {
5543
5610
  let entries;
5544
- try { entries = fssync.readdirSync(cwdDir); } catch { continue; }
5611
+ try { entries = fssync.readdirSync(dir, { withFileTypes: true }); } catch { return; }
5545
5612
  for (const entry of entries) {
5546
- if (!entry.endsWith(".jsonl")) continue;
5547
- files.push(path.join(cwdDir, entry));
5613
+ const full = path.join(dir, entry.name);
5614
+ let isDir = entry.isDirectory();
5615
+ let isFile = entry.isFile();
5616
+ if (!isDir && !isFile) {
5617
+ try {
5618
+ const st = fssync.statSync(full);
5619
+ isDir = st.isDirectory();
5620
+ isFile = st.isFile();
5621
+ } catch { continue; }
5622
+ }
5623
+ if (isDir) {
5624
+ walkLogs(full);
5625
+ } else if (isFile && entry.name.endsWith(".log")) {
5626
+ if (root.pattern === "*.log") {
5627
+ files.push(full);
5628
+ } else if (root.pattern === "codebuddy-extension-log") {
5629
+ if (full.toLowerCase().includes("tencent-cloud.coding-copilot")) {
5630
+ files.push(full);
5631
+ }
5632
+ }
5633
+ }
5548
5634
  }
5549
- }
5550
- } catch {
5551
- // ignore — return what we have
5635
+ };
5636
+ walkLogs(root.dir);
5552
5637
  }
5638
+
5553
5639
  files.sort((a, b) => a.localeCompare(b));
5554
5640
  return files;
5555
5641
  }
5556
5642
 
5643
+ function getBracketValueAfter(line, marker) {
5644
+ const parts = line.split(marker);
5645
+ if (parts.length < 2) return null;
5646
+ const after = parts[1];
5647
+ const start = after.indexOf("[");
5648
+ if (start === -1) return null;
5649
+ const afterOpen = after.slice(start + 1);
5650
+ const end = afterOpen.indexOf("]");
5651
+ if (end === -1) return null;
5652
+ return afterOpen.slice(0, end).trim();
5653
+ }
5654
+
5655
+ function parseLogTimestampMs(line, fallbackTs) {
5656
+ let raw = "";
5657
+ if (line.startsWith("[")) {
5658
+ const endIdx = line.indexOf("]");
5659
+ if (endIdx !== -1) {
5660
+ raw = line.slice(1, endIdx).trim();
5661
+ }
5662
+ } else {
5663
+ const idx = line.indexOf(" [");
5664
+ if (idx !== -1) {
5665
+ raw = line.slice(0, idx).trim();
5666
+ } else {
5667
+ raw = line.slice(0, 23).trim();
5668
+ }
5669
+ }
5670
+ let ts = Date.parse(raw);
5671
+ if (isNaN(ts)) {
5672
+ const normalized = raw.replace(/\//g, '-');
5673
+ ts = Date.parse(normalized);
5674
+ }
5675
+ return isNaN(ts) ? fallbackTs : ts;
5676
+ }
5677
+
5557
5678
  async function parseCodebuddyIncremental({
5558
5679
  projectFiles,
5559
5680
  cursors,
@@ -5572,6 +5693,10 @@ async function parseCodebuddyIncremental({
5572
5693
  codebuddyState.fileOffsets && typeof codebuddyState.fileOffsets === "object"
5573
5694
  ? { ...codebuddyState.fileOffsets }
5574
5695
  : {};
5696
+ const logModelsByAgent =
5697
+ codebuddyState.logModelsByAgent && typeof codebuddyState.logModelsByAgent === "object"
5698
+ ? { ...codebuddyState.logModelsByAgent }
5699
+ : {};
5575
5700
 
5576
5701
  const files = Array.isArray(projectFiles)
5577
5702
  ? projectFiles
@@ -5602,8 +5727,6 @@ async function parseCodebuddyIncremental({
5602
5727
  const prevEntry = fileOffsets[filePath] || {};
5603
5728
  const prevSize = Number(prevEntry.size) || 0;
5604
5729
  const prevIno = prevEntry.ino;
5605
- // Re-read from start if file shrunk (truncate/rewrite) or inode changed
5606
- // (file deleted + recreated). Otherwise pick up after the last read offset.
5607
5730
  const inodeChanged = typeof prevIno === "number" && prevIno !== stat.ino;
5608
5731
  const startOffset = stat.size < prevSize || inodeChanged ? 0 : prevSize;
5609
5732
  if (stat.size <= startOffset) continue;
@@ -5617,111 +5740,244 @@ async function parseCodebuddyIncremental({
5617
5740
  } catch { continue; }
5618
5741
  const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
5619
5742
 
5743
+ const isLogFile = filePath.endsWith(".log");
5744
+ const modelsByAgent = new Map();
5745
+ const agentModelKey = (agentId) => `${filePath}::${agentId}`;
5746
+
5620
5747
  for await (const line of rl) {
5621
5748
  if (!line || !line.trim()) continue;
5622
- let entry;
5623
- try { entry = JSON.parse(line); } catch { continue; }
5624
5749
 
5625
- // Only assistant message events carry token usage.
5626
- if (!entry || entry.type !== "message" || entry.role !== "assistant") continue;
5750
+ if (isLogFile) {
5751
+ if (line.includes("[CraftInvokableAgent]") && line.includes("Model prepared:")) {
5752
+ const agentId = getBracketValueAfter(line, "[CraftInvokableAgent]");
5753
+ const marker = "Model prepared:";
5754
+ const idx = line.indexOf(marker);
5755
+ if (agentId && idx !== -1) {
5756
+ const afterMarker = line.slice(idx + marker.length).trim();
5757
+ let modelId = afterMarker;
5758
+ const openParen = afterMarker.lastIndexOf("(");
5759
+ if (openParen !== -1) {
5760
+ const tail = afterMarker.slice(openParen + 1);
5761
+ const closeParen = tail.indexOf(")");
5762
+ if (closeParen !== -1) {
5763
+ const inner = tail.slice(0, closeParen).trim();
5764
+ if (inner) modelId = inner;
5765
+ }
5766
+ }
5767
+ modelsByAgent.set(agentId, modelId);
5768
+ logModelsByAgent[agentModelKey(agentId)] = modelId;
5769
+ }
5770
+ continue;
5771
+ }
5627
5772
 
5628
- const provider = entry.providerData;
5629
- const rawUsage = provider && typeof provider === "object" ? provider.rawUsage : null;
5630
- if (!rawUsage || typeof rawUsage !== "object") continue;
5773
+ if (!line.includes("[AgentReporter]") || !line.includes("Agent execution successful with usage:")) {
5774
+ continue;
5775
+ }
5631
5776
 
5632
- // Dedup per-message — message id (uuid) is most stable, then session +
5633
- // timestamp as fallback.
5634
- const sessionId =
5635
- typeof entry.sessionId === "string" && entry.sessionId
5636
- ? entry.sessionId
5637
- : path.basename(filePath, ".jsonl");
5638
- const tsMs =
5639
- Number.isFinite(Number(entry.timestamp)) && Number(entry.timestamp) > 0
5640
- ? Number(entry.timestamp)
5641
- : null;
5642
- const messageId =
5643
- typeof entry.uuid === "string" && entry.uuid
5644
- ? entry.uuid
5645
- : typeof entry.id === "string" && entry.id
5646
- ? entry.id
5647
- : tsMs != null
5648
- ? `${sessionId}:${tsMs}`
5649
- : null;
5650
- if (!messageId) continue;
5651
- if (seenIds.has(messageId)) continue;
5777
+ const agentId = getBracketValueAfter(line, "[AgentReporter]");
5778
+ if (!agentId) continue;
5652
5779
 
5653
- recordsProcessed++;
5780
+ const marker = "Agent execution successful with usage:";
5781
+ const usageParts = line.split(marker);
5782
+ if (usageParts.length < 2) continue;
5654
5783
 
5655
- const promptTokens = toNonNegativeInt(rawUsage.prompt_tokens);
5656
- const completionTokens = toNonNegativeInt(rawUsage.completion_tokens);
5657
- const details =
5658
- rawUsage.prompt_tokens_details && typeof rawUsage.prompt_tokens_details === "object"
5659
- ? rawUsage.prompt_tokens_details
5660
- : {};
5661
- const cachedTokens = toNonNegativeInt(details.cached_tokens);
5662
- // Anthropic-style mirror; CodeBuddy emits these too even if usually 0.
5663
- const cacheReadAlt = toNonNegativeInt(rawUsage.cache_read_input_tokens);
5664
- const cacheCreation = toNonNegativeInt(rawUsage.cache_creation_input_tokens);
5665
- const reasoningTokens = toNonNegativeInt(details.reasoning_tokens);
5666
-
5667
- // CRITICAL: prompt_tokens is OpenAI-style and INCLUDES cached.
5668
- // Subtract cached so input_tokens is pure non-cached input — matches
5669
- // the repo's normalization convention (see CLAUDE.md "Token
5670
- // Normalization Convention"). cache_read takes the larger of the two
5671
- // mirrored fields (rawUsage.cache_read_input_tokens vs
5672
- // prompt_tokens_details.cached_tokens) since CodeBuddy populates one
5673
- // or the other depending on upstream provider.
5674
- const cacheRead = Math.max(cachedTokens, cacheReadAlt);
5675
- const inputTokens = Math.max(0, promptTokens - cacheRead);
5784
+ const usageJsonRaw = usageParts[1].trim();
5785
+ const endBrace = usageJsonRaw.lastIndexOf("}");
5786
+ if (endBrace === -1) continue;
5676
5787
 
5677
- if (
5678
- inputTokens === 0 &&
5679
- completionTokens === 0 &&
5680
- cacheRead === 0 &&
5681
- cacheCreation === 0
5682
- ) {
5683
- seenIds.add(messageId);
5684
- continue;
5685
- }
5788
+ let usage;
5789
+ try {
5790
+ usage = JSON.parse(usageJsonRaw.slice(0, endBrace + 1));
5791
+ } catch { continue; }
5686
5792
 
5687
- if (tsMs == null) {
5793
+ if (!usage || typeof usage !== "object") continue;
5794
+
5795
+ const inputRaw = firstPresentNonNegativeInt([
5796
+ usage.cachedMissTokens,
5797
+ usage.cacheMissTokens,
5798
+ usage.input_tokens,
5799
+ usage.inputTokens,
5800
+ usage.prompt_tokens,
5801
+ ]);
5802
+ const outputRaw = firstPresentNonNegativeInt([
5803
+ usage.output_tokens,
5804
+ usage.outputTokens,
5805
+ usage.completion_tokens,
5806
+ ]);
5807
+ const cacheReadRaw = firstPositiveOrPresentNonNegativeInt([
5808
+ usage.cache_read_input_tokens,
5809
+ usage.cacheReadInputTokens,
5810
+ usage.cacheTokens,
5811
+ usage.prompt_cache_hit_tokens,
5812
+ usage.cached_tokens,
5813
+ ]);
5814
+ const cacheCreationRaw = firstPositiveOrPresentNonNegativeInt([
5815
+ usage.cache_creation_input_tokens,
5816
+ usage.cacheCreationInputTokens,
5817
+ usage.cachedWriteTokens,
5818
+ usage.prompt_cache_write_tokens,
5819
+ ]);
5820
+ const reasoningRaw = firstPresentNonNegativeInt([
5821
+ usage.completion_thinking_tokens,
5822
+ usage.completionThinkingTokens,
5823
+ usage.reasoningTokens,
5824
+ ]);
5825
+
5826
+ const tsMs = parseLogTimestampMs(line, stat.mtimeMs);
5827
+ const dedupSecond = Math.floor(tsMs / 1000);
5828
+ const messageId = `codebuddy:extension-log:${agentId}:${dedupSecond}:${inputRaw}:${outputRaw}:${cacheReadRaw}:${cacheCreationRaw}:${reasoningRaw}`;
5829
+
5830
+ if (seenIds.has(messageId)) continue;
5831
+ recordsProcessed++;
5832
+
5833
+ let inputTokens = toNonNegativeInt(inputRaw);
5834
+ const completionTokens = toNonNegativeInt(outputRaw);
5835
+ const cacheRead = toNonNegativeInt(cacheReadRaw);
5836
+ const cacheCreation = toNonNegativeInt(cacheCreationRaw);
5837
+ const reasoningTokens = toNonNegativeInt(reasoningRaw);
5838
+
5839
+ const isFromTotalField =
5840
+ usage.cachedMissTokens === undefined &&
5841
+ usage.cacheMissTokens === undefined &&
5842
+ usage.prompt_cache_miss_tokens === undefined;
5843
+ if (isFromTotalField && cacheRead > 0) {
5844
+ inputTokens = Math.max(0, inputTokens - cacheRead);
5845
+ }
5846
+
5847
+ if (inputTokens === 0 && completionTokens === 0 && cacheRead === 0 && cacheCreation === 0) {
5848
+ seenIds.add(messageId);
5849
+ continue;
5850
+ }
5851
+
5852
+ const tsIso = new Date(tsMs).toISOString();
5853
+ const bucketStart = toUtcHalfHourStart(tsIso);
5854
+ if (!bucketStart) continue;
5855
+
5856
+ const rawModel =
5857
+ modelsByAgent.get(agentId) ||
5858
+ logModelsByAgent[agentModelKey(agentId)] ||
5859
+ fallbackModel;
5860
+ const model = normalizeModelInput(rawModel);
5861
+
5862
+ const delta = {
5863
+ input_tokens: inputTokens,
5864
+ cached_input_tokens: cacheRead,
5865
+ cache_creation_input_tokens: cacheCreation,
5866
+ output_tokens: completionTokens,
5867
+ reasoning_output_tokens: reasoningTokens,
5868
+ total_tokens: inputTokens + completionTokens + cacheRead + cacheCreation + reasoningTokens,
5869
+ conversation_count: 1,
5870
+ };
5871
+
5872
+ const bucket = getHourlyBucket(hourlyState, "codebuddy", model, bucketStart);
5873
+ addTotals(bucket.totals, delta);
5874
+ touchedBuckets.add(bucketKey("codebuddy", model, bucketStart));
5688
5875
  seenIds.add(messageId);
5689
- continue;
5690
- }
5691
- const tsIso = new Date(tsMs).toISOString();
5692
- const bucketStart = toUtcHalfHourStart(tsIso);
5693
- if (!bucketStart) continue;
5876
+ eventsAggregated++;
5694
5877
 
5695
- const model =
5696
- normalizeModelInput(provider?.model) ||
5697
- normalizeModelInput(entry.model) ||
5698
- fallbackModel;
5878
+ if (cb) {
5879
+ cb({
5880
+ index: fileIdx + 1,
5881
+ total: files.length,
5882
+ recordsProcessed,
5883
+ eventsAggregated,
5884
+ bucketsQueued: touchedBuckets.size,
5885
+ });
5886
+ }
5887
+ } else {
5888
+ let entry;
5889
+ try { entry = JSON.parse(line); } catch { continue; }
5890
+
5891
+ if (!entry || entry.type !== "message" || entry.role !== "assistant") continue;
5892
+
5893
+ const provider = entry.providerData;
5894
+ const rawUsage = provider && typeof provider === "object" ? provider.rawUsage : null;
5895
+ if (!rawUsage || typeof rawUsage !== "object") continue;
5896
+
5897
+ const sessionId =
5898
+ typeof entry.sessionId === "string" && entry.sessionId
5899
+ ? entry.sessionId
5900
+ : path.basename(filePath, ".jsonl");
5901
+ const tsMs =
5902
+ Number.isFinite(Number(entry.timestamp)) && Number(entry.timestamp) > 0
5903
+ ? Number(entry.timestamp)
5904
+ : null;
5905
+ const messageId =
5906
+ typeof entry.uuid === "string" && entry.uuid
5907
+ ? entry.uuid
5908
+ : typeof entry.id === "string" && entry.id
5909
+ ? entry.id
5910
+ : tsMs != null
5911
+ ? `${sessionId}:${tsMs}`
5912
+ : null;
5913
+ if (!messageId) continue;
5914
+ if (seenIds.has(messageId)) continue;
5915
+
5916
+ recordsProcessed++;
5917
+
5918
+ const promptTokens = toNonNegativeInt(rawUsage.prompt_tokens);
5919
+ const completionTokens = toNonNegativeInt(rawUsage.completion_tokens);
5920
+ const details =
5921
+ rawUsage.prompt_tokens_details && typeof rawUsage.prompt_tokens_details === "object"
5922
+ ? rawUsage.prompt_tokens_details
5923
+ : {};
5924
+ const cachedTokens = toNonNegativeInt(details.cached_tokens);
5925
+ const cacheReadAlt = toNonNegativeInt(rawUsage.cache_read_input_tokens);
5926
+ const cacheCreation = toNonNegativeInt(rawUsage.cache_creation_input_tokens);
5927
+ const reasoningTokens = toNonNegativeInt(details.reasoning_tokens);
5928
+
5929
+ const cacheRead = Math.max(cachedTokens, cacheReadAlt);
5930
+ const inputTokens = Math.max(0, promptTokens - cacheRead);
5931
+
5932
+ if (
5933
+ inputTokens === 0 &&
5934
+ completionTokens === 0 &&
5935
+ cacheRead === 0 &&
5936
+ cacheCreation === 0
5937
+ ) {
5938
+ seenIds.add(messageId);
5939
+ continue;
5940
+ }
5699
5941
 
5700
- const delta = {
5701
- input_tokens: inputTokens,
5702
- cached_input_tokens: cacheRead,
5703
- cache_creation_input_tokens: cacheCreation,
5704
- output_tokens: completionTokens,
5705
- reasoning_output_tokens: reasoningTokens,
5706
- total_tokens:
5707
- inputTokens + completionTokens + cacheRead + cacheCreation + reasoningTokens,
5708
- conversation_count: 1,
5709
- };
5942
+ if (tsMs == null) {
5943
+ seenIds.add(messageId);
5944
+ continue;
5945
+ }
5946
+ const tsIso = new Date(tsMs).toISOString();
5947
+ const bucketStart = toUtcHalfHourStart(tsIso);
5948
+ if (!bucketStart) continue;
5949
+
5950
+ const model =
5951
+ normalizeModelInput(provider?.model) ||
5952
+ normalizeModelInput(entry.model) ||
5953
+ fallbackModel;
5954
+
5955
+ const delta = {
5956
+ input_tokens: inputTokens,
5957
+ cached_input_tokens: cacheRead,
5958
+ cache_creation_input_tokens: cacheCreation,
5959
+ output_tokens: completionTokens,
5960
+ reasoning_output_tokens: reasoningTokens,
5961
+ total_tokens:
5962
+ inputTokens + completionTokens + cacheRead + cacheCreation + reasoningTokens,
5963
+ conversation_count: 1,
5964
+ };
5710
5965
 
5711
- const bucket = getHourlyBucket(hourlyState, "codebuddy", model, bucketStart);
5712
- addTotals(bucket.totals, delta);
5713
- touchedBuckets.add(bucketKey("codebuddy", model, bucketStart));
5714
- seenIds.add(messageId);
5715
- eventsAggregated++;
5966
+ const bucket = getHourlyBucket(hourlyState, "codebuddy", model, bucketStart);
5967
+ addTotals(bucket.totals, delta);
5968
+ touchedBuckets.add(bucketKey("codebuddy", model, bucketStart));
5969
+ seenIds.add(messageId);
5970
+ eventsAggregated++;
5716
5971
 
5717
- if (cb) {
5718
- cb({
5719
- index: fileIdx + 1,
5720
- total: files.length,
5721
- recordsProcessed,
5722
- eventsAggregated,
5723
- bucketsQueued: touchedBuckets.size,
5724
- });
5972
+ if (cb) {
5973
+ cb({
5974
+ index: fileIdx + 1,
5975
+ total: files.length,
5976
+ recordsProcessed,
5977
+ eventsAggregated,
5978
+ bucketsQueued: touchedBuckets.size,
5979
+ });
5980
+ }
5725
5981
  }
5726
5982
  }
5727
5983
 
@@ -5739,6 +5995,11 @@ async function parseCodebuddyIncremental({
5739
5995
  const seenArr = Array.from(seenIds);
5740
5996
  const cappedSeen =
5741
5997
  seenArr.length > 10_000 ? seenArr.slice(seenArr.length - 10_000) : seenArr;
5998
+ const logModelEntries = Object.entries(logModelsByAgent);
5999
+ const cappedLogModelsByAgent =
6000
+ logModelEntries.length > 10_000
6001
+ ? Object.fromEntries(logModelEntries.slice(logModelEntries.length - 10_000))
6002
+ : logModelsByAgent;
5742
6003
 
5743
6004
  const bucketsQueued = await enqueueTouchedBuckets({
5744
6005
  queuePath,
@@ -5752,6 +6013,7 @@ async function parseCodebuddyIncremental({
5752
6013
  ...codebuddyState,
5753
6014
  seenIds: cappedSeen,
5754
6015
  fileOffsets,
6016
+ logModelsByAgent: cappedLogModelsByAgent,
5755
6017
  updatedAt,
5756
6018
  };
5757
6019
 
@@ -5885,17 +6147,32 @@ async function parseWorkbuddyIncremental({
5885
6147
  workbuddyState.fileOffsets && typeof workbuddyState.fileOffsets === "object"
5886
6148
  ? { ...workbuddyState.fileOffsets }
5887
6149
  : {};
6150
+ const sqliteSessions =
6151
+ workbuddyState.sqliteSessions && typeof workbuddyState.sqliteSessions === "object"
6152
+ ? { ...workbuddyState.sqliteSessions }
6153
+ : {};
6154
+ const detailedSessions =
6155
+ workbuddyState.detailedSessions && typeof workbuddyState.detailedSessions === "object"
6156
+ ? { ...workbuddyState.detailedSessions }
6157
+ : {};
6158
+ const detailedSessionsWithUsage = new Set();
5888
6159
 
5889
6160
  const files = Array.isArray(projectFiles)
5890
6161
  ? projectFiles
5891
6162
  : resolveWorkbuddyProjectFiles(env || process.env);
5892
6163
  const fallbackModel = defaultModel || resolveWorkbuddyDefaultModel(env || process.env);
5893
6164
 
5894
- if (files.length === 0) {
6165
+ const workbuddyHome = resolveWorkbuddyHome(env || process.env);
6166
+ const dbPath = workbuddyHome ? path.join(workbuddyHome, "workbuddy.db") : null;
6167
+ const dbExists = Boolean(dbPath && fssync.existsSync(dbPath));
6168
+
6169
+ if (files.length === 0 && !dbExists) {
5895
6170
  cursors.workbuddy = {
5896
6171
  ...workbuddyState,
5897
6172
  seenIds: Array.from(seenIds),
5898
6173
  fileOffsets,
6174
+ sqliteSessions,
6175
+ detailedSessions,
5899
6176
  updatedAt: new Date().toISOString(),
5900
6177
  };
5901
6178
  return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
@@ -5946,6 +6223,9 @@ async function parseWorkbuddyIncremental({
5946
6223
  typeof entry.sessionId === "string" && entry.sessionId
5947
6224
  ? entry.sessionId
5948
6225
  : path.basename(filePath, ".jsonl");
6226
+ if (Object.prototype.hasOwnProperty.call(sqliteSessions, sessionId)) {
6227
+ continue;
6228
+ }
5949
6229
  const tsMs =
5950
6230
  Number.isFinite(Number(entry.timestamp)) && Number(entry.timestamp) > 0
5951
6231
  ? Number(entry.timestamp)
@@ -6031,6 +6311,8 @@ async function parseWorkbuddyIncremental({
6031
6311
  addTotals(bucket.totals, delta);
6032
6312
  touchedBuckets.add(bucketKey("workbuddy", model, bucketStart));
6033
6313
  seenIds.add(messageId);
6314
+ detailedSessions[sessionId] = true;
6315
+ detailedSessionsWithUsage.add(sessionId);
6034
6316
  eventsAggregated++;
6035
6317
 
6036
6318
  if (cb) {
@@ -6053,11 +6335,119 @@ async function parseWorkbuddyIncremental({
6053
6335
  };
6054
6336
  }
6055
6337
 
6338
+ if (dbExists) {
6339
+ const query = `
6340
+ SELECT
6341
+ su.session_id,
6342
+ su.used,
6343
+ su.updated_at,
6344
+ s.model,
6345
+ s.cwd
6346
+ FROM session_usage su
6347
+ LEFT JOIN sessions s ON s.id = su.session_id
6348
+ WHERE su.used IS NOT NULL
6349
+ AND su.used > 0
6350
+ AND su.updated_at IS NOT NULL
6351
+ AND su.updated_at > 0
6352
+ `.trim();
6353
+ let rows = [];
6354
+ const snap = snapshotSqliteDb(dbPath);
6355
+ try {
6356
+ rows = await readSqliteJsonRowsAsync(snap.path, query, {
6357
+ label: "WorkBuddy",
6358
+ timeout: 10_000,
6359
+ maxBuffer: 16 * 1024 * 1024,
6360
+ });
6361
+ } finally {
6362
+ snap.cleanup();
6363
+ }
6364
+
6365
+ try {
6366
+ for (const row of rows) {
6367
+ if (!row || typeof row !== "object") continue;
6368
+ const sessionId = typeof row.session_id === "string" ? row.session_id.trim() : "";
6369
+ if (!sessionId) continue;
6370
+ if (detailedSessions[sessionId] || detailedSessionsWithUsage.has(sessionId)) continue;
6371
+
6372
+ const usedNow = toNonNegativeInt(row.used);
6373
+ const updatedAtRaw = toNonNegativeInt(row.updated_at);
6374
+ const rawModel = typeof row.model === "string" ? row.model.trim() : "";
6375
+
6376
+ if (usedNow <= 0 || updatedAtRaw <= 0) continue;
6377
+
6378
+ const prev = sqliteSessions[sessionId] || { used: 0 };
6379
+ const prevUsed = toNonNegativeInt(prev.used);
6380
+ const isReset = usedNow > 0 && prevUsed > 0 && usedNow < prevUsed;
6381
+ const inputDelta = isReset ? usedNow : Math.max(0, usedNow - prevUsed);
6382
+ if (inputDelta === 0) {
6383
+ sqliteSessions[sessionId] = {
6384
+ ...prev,
6385
+ used: usedNow,
6386
+ updatedAt: updatedAtRaw,
6387
+ model: rawModel || prev.model || fallbackModel,
6388
+ };
6389
+ continue;
6390
+ }
6391
+
6392
+ recordsProcessed++;
6393
+
6394
+ const inputTokens = inputDelta;
6395
+ const completionTokens = 0;
6396
+ const cacheRead = 0;
6397
+ const cacheCreation = 0;
6398
+ const reasoningTokens = 0;
6399
+
6400
+ const tsMs = updatedAtRaw > 10000000000 ? updatedAtRaw : updatedAtRaw * 1000;
6401
+ const tsIso = new Date(tsMs).toISOString();
6402
+ const bucketStart = toUtcHalfHourStart(tsIso);
6403
+ if (!bucketStart) continue;
6404
+
6405
+ const model = normalizeModelInput(rawModel) || fallbackModel;
6406
+
6407
+ const delta = {
6408
+ input_tokens: inputTokens,
6409
+ cached_input_tokens: cacheRead,
6410
+ cache_creation_input_tokens: cacheCreation,
6411
+ output_tokens: completionTokens,
6412
+ reasoning_output_tokens: reasoningTokens,
6413
+ total_tokens: inputTokens,
6414
+ conversation_count: prevUsed === 0 || isReset ? 1 : 0,
6415
+ };
6416
+
6417
+ const bucket = getHourlyBucket(hourlyState, "workbuddy", model, bucketStart);
6418
+ addTotals(bucket.totals, delta);
6419
+ touchedBuckets.add(bucketKey("workbuddy", model, bucketStart));
6420
+ sqliteSessions[sessionId] = {
6421
+ used: usedNow,
6422
+ updatedAt: updatedAtRaw,
6423
+ model,
6424
+ };
6425
+ eventsAggregated++;
6426
+ }
6427
+ } catch (err) {
6428
+ // SQLite fallback is best effort; detailed JSONL remains authoritative.
6429
+ }
6430
+ }
6431
+
6056
6432
  // Cap dedup set to last 10k IDs to bound cursor state size — same convention
6057
6433
  // as CodeBuddy/Kimi/Copilot so cursors.json doesn't grow unbounded.
6058
6434
  const seenArr = Array.from(seenIds);
6059
6435
  const cappedSeen =
6060
6436
  seenArr.length > 10_000 ? seenArr.slice(seenArr.length - 10_000) : seenArr;
6437
+ const sqliteSessionEntries = Object.entries(sqliteSessions);
6438
+ const cappedSqliteSessions =
6439
+ sqliteSessionEntries.length > 10_000
6440
+ ? Object.fromEntries(
6441
+ sqliteSessionEntries
6442
+ .sort((a, b) => toNonNegativeInt(b[1]?.used) - toNonNegativeInt(a[1]?.used))
6443
+ .slice(0, 10_000),
6444
+ )
6445
+ : sqliteSessions;
6446
+ const detailedSessionEntries = Object.entries(detailedSessions);
6447
+ const cappedDetailedSessions =
6448
+ detailedSessionEntries.length > 10_000
6449
+ ? Object.fromEntries(detailedSessionEntries.slice(detailedSessionEntries.length - 10_000))
6450
+ : detailedSessions;
6061
6451
 
6062
6452
  const bucketsQueued = await enqueueTouchedBuckets({
6063
6453
  queuePath,
@@ -6071,6 +6461,8 @@ async function parseWorkbuddyIncremental({
6071
6461
  ...workbuddyState,
6072
6462
  seenIds: cappedSeen,
6073
6463
  fileOffsets,
6464
+ sqliteSessions: cappedSqliteSessions,
6465
+ detailedSessions: cappedDetailedSessions,
6074
6466
  updatedAt,
6075
6467
  };
6076
6468