tokentracker-cli 0.97.2 → 0.98.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 (135) hide show
  1. package/dashboard/dist/assets/AchievementBadge-DlZklawB.js +1 -0
  2. package/dashboard/dist/assets/{AchievementsPage-CnkSNpJx.js → AchievementsPage-CprbOKnt.js} +2 -2
  3. package/dashboard/dist/assets/AchievementsSection-BKLJBRKw.js +1 -0
  4. package/dashboard/dist/assets/{BadgeDetailModal-BJBrgn24.js → BadgeDetailModal-CfVXLh_t.js} +1 -1
  5. package/dashboard/dist/assets/{BotAnimated-BJM20N9T.js → BotAnimated-wzqeGbEY.js} +1 -1
  6. package/dashboard/dist/assets/{Card-BRWOMoBk.js → Card-B-gcOAn7.js} +1 -1
  7. package/dashboard/dist/assets/{ClawdAnimated-CtX8cne4.js → ClawdAnimated-1c5ezBBN.js} +2 -2
  8. package/dashboard/dist/assets/CommandPalette-CD8ggEF6.js +1 -0
  9. package/dashboard/dist/assets/CommunityStatsModal-b2fPhi1E.js +1 -0
  10. package/dashboard/dist/assets/{ConfirmModal-ZZXg_OLV.js → ConfirmModal-ARBkT8AC.js} +1 -1
  11. package/dashboard/dist/assets/{Controls-VDvgw_ed.js → Controls-WFXDjIYG.js} +1 -1
  12. package/dashboard/dist/assets/DashboardPage-XtiZa2JN.js +1 -0
  13. package/dashboard/dist/assets/{DevicePage-CP5m8azV.js → DevicePage-BCwSpNXa.js} +1 -1
  14. package/dashboard/dist/assets/{DialogClose-8pTCallP.js → DialogClose-D2PE0TAc.js} +1 -1
  15. package/dashboard/dist/assets/DialogDescription-5LunWukT.js +1 -0
  16. package/dashboard/dist/assets/{DialogTitle-BWYdq4TT.js → DialogTitle-Bby0zSCw.js} +1 -1
  17. package/dashboard/dist/assets/{FadeIn-CEJfaKnL.js → FadeIn-Cx1hl4so.js} +1 -1
  18. package/dashboard/dist/assets/{HeaderGithubStar-Bg2vijF8.js → HeaderGithubStar-my4zR6Pp.js} +1 -1
  19. package/dashboard/dist/assets/{HoverTooltip-B03JQ3PO.js → HoverTooltip-DOiGAPLE.js} +1 -1
  20. package/dashboard/dist/assets/{IpCheckPage-DYZucBOf.js → IpCheckPage-4SdHlFuA.js} +1 -1
  21. package/dashboard/dist/assets/{LandingPage-m-byhnVx.js → LandingPage-bSWc9xiS.js} +2 -2
  22. package/dashboard/dist/assets/{LeaderboardAvatar-JtGhJC1s.js → LeaderboardAvatar-EN4117E5.js} +1 -1
  23. package/dashboard/dist/assets/LeaderboardPage-Bwjf-xZl.js +2 -0
  24. package/dashboard/dist/assets/{LeaderboardProfileModal-Cm4DDrcY.js → LeaderboardProfileModal-B1mgVtfP.js} +4 -4
  25. package/dashboard/dist/assets/{LeaderboardProfilePage-bKbMm856.js → LeaderboardProfilePage-DVt5S3lY.js} +1 -1
  26. package/dashboard/dist/assets/{LimitsPage-B271tZOj.js → LimitsPage-BCXjJE1B.js} +5 -5
  27. package/dashboard/dist/assets/{LocalOnlyNotice-DKIxuBP0.js → LocalOnlyNotice-CzeH4OoJ.js} +1 -1
  28. package/dashboard/dist/assets/LoginCard-aiqSQInP.js +1 -0
  29. package/dashboard/dist/assets/{LoginModal-PxTgIoLk.js → LoginModal-DdGKIi8h.js} +1 -1
  30. package/dashboard/dist/assets/{LoginPage-uEUnU7NP.js → LoginPage-D7xGSoMn.js} +1 -1
  31. package/dashboard/dist/assets/PetPage-Dlb6rNpF.js +1 -0
  32. package/dashboard/dist/assets/{PopoverPopup-DWIMb8D6.js → PopoverPopup-Xbruj7Zi.js} +1 -1
  33. package/dashboard/dist/assets/{ProviderIcon-DqqwXu3b.js → ProviderIcon-BPxacBzj.js} +1 -1
  34. package/dashboard/dist/assets/{ResetPasswordPage-BVuJTR9A.js → ResetPasswordPage-9wgPnLRj.js} +1 -1
  35. package/dashboard/dist/assets/{SegmentedControl-iHtWKWtc.js → SegmentedControl-CYFcZFNE.js} +1 -1
  36. package/dashboard/dist/assets/{Select-Cr9wq8mD.js → Select-zpFEMqpk.js} +1 -1
  37. package/dashboard/dist/assets/{SelectItemText-BWnP4yuq.js → SelectItemText-Bldm5Y0C.js} +1 -1
  38. package/dashboard/dist/assets/{ServiceStatusPage-DLuiUUYV.js → ServiceStatusPage-dgtwFazV.js} +1 -1
  39. package/dashboard/dist/assets/{SessionsPage-CBAH4ieT.js → SessionsPage-DHfpv53M.js} +2 -2
  40. package/dashboard/dist/assets/SettingsPage-DcGToOT7.js +2 -0
  41. package/dashboard/dist/assets/SkillsPage-DiQDLw2l.js +1 -0
  42. package/dashboard/dist/assets/TokenGalaxy-GDx7ftMU.js +4426 -0
  43. package/dashboard/dist/assets/ToolbarRootContext-PoCcP2AV.js +1 -0
  44. package/dashboard/dist/assets/{TrendMonitor-CCzwGDst.js → TrendMonitor-CIfq_yk8.js} +1 -1
  45. package/dashboard/dist/assets/{WidgetsPage-CcWCidAd.js → WidgetsPage-Dwy-E3Lf.js} +1 -1
  46. package/dashboard/dist/assets/{WrappedPage-B6dn8eyh.js → WrappedPage-Ce7NV6Ij.js} +1 -1
  47. package/dashboard/dist/assets/__vite-browser-external-D-WLjvZW.js +45 -0
  48. package/dashboard/dist/assets/{agent-logos-Dh2k8E_G.js → agent-logos-BY8n7b9g.js} +1 -1
  49. package/dashboard/dist/assets/arrow-up-right-BDiV6Wlk.js +1 -0
  50. package/dashboard/dist/assets/check-Chyo6ujc.js +1 -0
  51. package/dashboard/dist/assets/{composite-CSBcryzA.js → composite-DgBqMs_Z.js} +1 -1
  52. package/dashboard/dist/assets/copy-ENRv4pAy.js +1 -0
  53. package/dashboard/dist/assets/download-D3y3gd-z.js +1 -0
  54. package/dashboard/dist/assets/external-link-VxSAxWnH.js +1 -0
  55. package/dashboard/dist/assets/flame-CPdTWeRo.js +1 -0
  56. package/dashboard/dist/assets/{format-tokens-CqiCmYtk.js → format-tokens-BF2G89Q7.js} +1 -1
  57. package/dashboard/dist/assets/{icons-DD8OvYVG.js → icons-D6rfdVyV.js} +1 -1
  58. package/dashboard/dist/assets/{index-21lDN4rA.js → index-4bHcp2Ep.js} +1 -1
  59. package/dashboard/dist/assets/{index-Cm8jn8Va.js → index-CgmpxGym.js} +1 -1
  60. package/dashboard/dist/assets/{index-DLLivlEh.js → index-mzMCUT_4.js} +1 -1
  61. package/dashboard/dist/assets/info-BMso9PcY.js +1 -0
  62. package/dashboard/dist/assets/link-2-Bg34XOcd.js +1 -0
  63. package/dashboard/dist/assets/loader-circle-C4Vul1Yx.js +1 -0
  64. package/dashboard/dist/assets/maximize-2-BBqcwFFo.js +1 -0
  65. package/dashboard/dist/assets/{modifiers.esm-Cwj0l9Zb.js → modifiers.esm-B2F4Iiey.js} +1 -1
  66. package/dashboard/dist/assets/module-B5tW98jG.js +3 -0
  67. package/dashboard/dist/assets/monitor-smartphone-k1Ja3ngF.js +1 -0
  68. package/dashboard/dist/assets/plus-BaCIejxc.js +1 -0
  69. package/dashboard/dist/assets/provider-display-BYxmO9Bu.js +42 -0
  70. package/dashboard/dist/assets/{react-xH4nkyuj.js → react-xGx5RIqz.js} +1 -1
  71. package/dashboard/dist/assets/search-LZQC7j6a.js +1 -0
  72. package/dashboard/dist/assets/{skills-api-B7lb1cjO.js → skills-api-yLfQkApo.js} +1 -1
  73. package/dashboard/dist/assets/{sortable.esm-BUPRsWL6.js → sortable.esm-B8XRHrmA.js} +1 -1
  74. package/dashboard/dist/assets/{store-DneqOW0h.js → store-DfUIKvz3.js} +1 -1
  75. package/dashboard/dist/assets/terminal-CsFh6eZS.js +1 -0
  76. package/dashboard/dist/assets/trash-CVk10mDa.js +1 -0
  77. package/dashboard/dist/assets/{use-community-stats-DB98TGn9.js → use-community-stats-DP7lJyDV.js} +1 -1
  78. package/dashboard/dist/assets/{use-latest-request-guard-DK3KulKx.js → use-latest-request-guard-B7uLArxV.js} +1 -1
  79. package/dashboard/dist/assets/{use-limits-display-prefs-BACCRUMZ.js → use-limits-display-prefs-DG97cWcv.js} +1 -1
  80. package/dashboard/dist/assets/{use-ordered-list-DnKFdI9a.js → use-ordered-list-ChQc2v-Q.js} +1 -1
  81. package/dashboard/dist/assets/use-reduced-motion-BoFF1l9U.js +1 -0
  82. package/dashboard/dist/assets/use-session-efficiency-pref-CtF9JAlF.js +1 -0
  83. package/dashboard/dist/assets/{use-transform-DweC2iKa.js → use-transform-DVIBk7FO.js} +1 -1
  84. package/dashboard/dist/assets/{use-usage-limits-CN5xWIBs.js → use-usage-limits-BYQuJ9Ud.js} +1 -1
  85. package/dashboard/dist/assets/{useCurrency-CFjvWawL.js → useCurrency-CNkzJCP9.js} +1 -1
  86. package/dashboard/dist/assets/{useTokenFormat-Cf81HqWi.js → useTokenFormat-CU3MtXtS.js} +1 -1
  87. package/dashboard/dist/assets/zap-D5k595AR.js +1 -0
  88. package/dashboard/dist/index.html +1 -1
  89. package/dashboard/dist/ip-check.html +1 -1
  90. package/dashboard/dist/leaderboard.html +1 -1
  91. package/dashboard/dist/share.html +1 -1
  92. package/package.json +1 -1
  93. package/src/commands/sync.js +15 -4
  94. package/src/lib/cloud-account.js +108 -0
  95. package/src/lib/grok-limits.js +37 -0
  96. package/src/lib/opencode-config.js +20 -1
  97. package/src/lib/pricing/curated-overrides.json +22 -0
  98. package/src/lib/pricing/index.js +2 -0
  99. package/src/lib/pricing/seed-snapshot.json +1 -1
  100. package/src/lib/rollout.js +1044 -136
  101. package/src/lib/usage-limits.js +7 -1
  102. package/dashboard/dist/assets/AchievementBadge-Bf2T612R.js +0 -1
  103. package/dashboard/dist/assets/AchievementsSection-CPwsy04Y.js +0 -1
  104. package/dashboard/dist/assets/CommandPalette-C8BwOnSn.js +0 -1
  105. package/dashboard/dist/assets/CommunityStatsModal-BGMbgSjq.js +0 -1
  106. package/dashboard/dist/assets/DashboardPage-BcJGzQWK.js +0 -1
  107. package/dashboard/dist/assets/DialogDescription-DRQEfAWH.js +0 -1
  108. package/dashboard/dist/assets/LeaderboardPage-vVDKFLhl.js +0 -2
  109. package/dashboard/dist/assets/LoginCard-DA46RXDo.js +0 -1
  110. package/dashboard/dist/assets/PetPage-DDPZtxMw.js +0 -1
  111. package/dashboard/dist/assets/SettingsPage-Djku-i8K.js +0 -2
  112. package/dashboard/dist/assets/SkillsPage-DtBRoB8R.js +0 -1
  113. package/dashboard/dist/assets/TokenGalaxy-nvyPu5eV.js +0 -4314
  114. package/dashboard/dist/assets/ToolbarRootContext-DhpHv55w.js +0 -1
  115. package/dashboard/dist/assets/__vite-browser-external-sJsF0ioo.js +0 -45
  116. package/dashboard/dist/assets/arrow-up-right-BLDQog1A.js +0 -1
  117. package/dashboard/dist/assets/check-DNq_ySSn.js +0 -1
  118. package/dashboard/dist/assets/copy-DHRrNtRb.js +0 -1
  119. package/dashboard/dist/assets/download-CluyujgG.js +0 -1
  120. package/dashboard/dist/assets/external-link-Cf12tdLf.js +0 -1
  121. package/dashboard/dist/assets/flame-CJEYinxo.js +0 -1
  122. package/dashboard/dist/assets/info-dv3Y0z8Q.js +0 -1
  123. package/dashboard/dist/assets/link-2-CWHBxVuk.js +0 -1
  124. package/dashboard/dist/assets/loader-circle-9l5p9Dwa.js +0 -1
  125. package/dashboard/dist/assets/maximize-2-DQ2t3MJB.js +0 -1
  126. package/dashboard/dist/assets/module-DUSXqJtA.js +0 -3
  127. package/dashboard/dist/assets/monitor-smartphone-B-9at3iV.js +0 -1
  128. package/dashboard/dist/assets/plus-C_CTQjDD.js +0 -1
  129. package/dashboard/dist/assets/provider-display-XJQod7k9.js +0 -42
  130. package/dashboard/dist/assets/search-Ut41Fw1G.js +0 -1
  131. package/dashboard/dist/assets/terminal-DPQyZyHR.js +0 -1
  132. package/dashboard/dist/assets/trash-CVdV1bBR.js +0 -1
  133. package/dashboard/dist/assets/use-reduced-motion-BfdrKOsS.js +0 -1
  134. package/dashboard/dist/assets/use-session-efficiency-pref-BTm6RoSm.js +0 -1
  135. package/dashboard/dist/assets/zap-Vx_j5oJN.js +0 -1
@@ -4188,6 +4188,35 @@ function recordProjectMeta(projectState, meta) {
4188
4188
  projectState.projects[projectKey] = next;
4189
4189
  }
4190
4190
 
4191
+ function antigravityProjectAssignmentChanged(previous, projectContext) {
4192
+ const projectKey = projectContext?.projectKey || null;
4193
+ const projectRef = projectContext?.projectRef || null;
4194
+ const priorProjectKeys = new Set(
4195
+ Object.values(previous?.contributions || {})
4196
+ .map((contribution) => contribution?.projectKey)
4197
+ .filter((value) => typeof value === "string" && value.length > 0),
4198
+ );
4199
+ if (typeof previous?.projectKey === "string" && previous.projectKey) {
4200
+ priorProjectKeys.add(previous.projectKey);
4201
+ }
4202
+ const priorProjectRefs = new Set(
4203
+ Object.values(previous?.contributions || {})
4204
+ .map((contribution) => contribution?.projectRef)
4205
+ .filter((value) => typeof value === "string" && value.length > 0),
4206
+ );
4207
+ if (typeof previous?.projectRef === "string" && previous.projectRef) {
4208
+ priorProjectRefs.add(previous.projectRef);
4209
+ }
4210
+ return (
4211
+ priorProjectKeys.size !== (projectKey ? 1 : 0) ||
4212
+ (projectKey && !priorProjectKeys.has(projectKey)) ||
4213
+ priorProjectRefs.size !== (projectRef ? 1 : 0) ||
4214
+ (projectRef && !priorProjectRefs.has(projectRef)) ||
4215
+ (typeof previous?.projectStatus === "string" &&
4216
+ previous.projectStatus !== (projectContext?.status || null))
4217
+ );
4218
+ }
4219
+
4191
4220
  async function resolveProjectContextForFile({
4192
4221
  filePath,
4193
4222
  projectMetaCache,
@@ -15799,7 +15828,15 @@ function resolveReasonixHome(env = process.env) {
15799
15828
  if (env.REASONIX_STATE_HOME) {
15800
15829
  return expandHomePath(env.REASONIX_STATE_HOME, env);
15801
15830
  }
15802
- const home = env.HOME || require("node:os").homedir();
15831
+ // Windows installs of Git Bash / MSYS / conda export a HOME of their own
15832
+ // (often a POSIX-shaped path), so preferring it silently sends the scan to a
15833
+ // directory that does not exist and Reasonix drops out of `status` entirely
15834
+ // with no "skipped" line to explain it. Match resolveCopilotDbPaths and take
15835
+ // USERPROFILE first on win32.
15836
+ const home =
15837
+ process.platform === "win32"
15838
+ ? env.USERPROFILE || env.HOME || require("node:os").homedir()
15839
+ : env.HOME || require("node:os").homedir();
15803
15840
  return path.join(home, ".reasonix");
15804
15841
  }
15805
15842
 
@@ -19411,27 +19448,236 @@ function resolveAntigravityBrainDirs(geminiHome) {
19411
19448
  ];
19412
19449
  }
19413
19450
 
19414
- async function listAntigravitySessionFiles(brainDir) {
19451
+ function antigravityPathWithin(filePath, root) {
19452
+ if (typeof filePath !== "string" || typeof root !== "string") return false;
19453
+ const relative = path.relative(root, filePath);
19454
+ return Boolean(
19455
+ relative &&
19456
+ relative !== ".." &&
19457
+ !relative.startsWith(`..${path.sep}`) &&
19458
+ !path.isAbsolute(relative),
19459
+ );
19460
+ }
19461
+
19462
+ async function listAntigravitySessionFilesWithStatus(brainDir, knownFiles = null) {
19415
19463
  const out = [];
19416
- if (!brainDir || typeof brainDir !== "string") return out;
19417
- const entries = await safeReadDir(brainDir).catch(() => []);
19464
+ const known = knownFiles instanceof Set ? knownFiles : new Set(knownFiles || []);
19465
+ if (!brainDir || typeof brainDir !== "string") {
19466
+ return { files: out, complete: false };
19467
+ }
19468
+
19469
+ let entries;
19470
+ try {
19471
+ entries = await fs.readdir(brainDir, { withFileTypes: true });
19472
+ } catch (err) {
19473
+ // A missing optional install directory is an authoritative empty inventory
19474
+ // only when it has never contained a tracked session. If it has, the
19475
+ // directory may be temporarily unavailable and must not trigger retraction.
19476
+ return {
19477
+ files: out,
19478
+ complete:
19479
+ err?.code === "ENOENT" &&
19480
+ !Array.from(known).some((filePath) => antigravityPathWithin(filePath, brainDir)),
19481
+ };
19482
+ }
19483
+
19484
+ let complete = true;
19418
19485
  for (const entry of entries) {
19419
19486
  if (!entry.isDirectory()) continue;
19420
19487
  const logsDir = path.join(brainDir, entry.name, ".system_generated", "logs");
19421
19488
  const transcriptPath = path.join(logsDir, "transcript.jsonl");
19422
- const st = await fs.stat(transcriptPath).catch(() => null);
19423
- if (st && st.isFile()) {
19424
- out.push(transcriptPath);
19489
+ let st;
19490
+ try {
19491
+ st = await fs.stat(transcriptPath);
19492
+ } catch (err) {
19493
+ // ENOENT means this session directory currently has no transcript (or
19494
+ // the transcript was deleted). Other errors make the scan incomplete.
19495
+ if (err?.code !== "ENOENT") complete = false;
19496
+ continue;
19425
19497
  }
19498
+ if (st.isFile()) out.push(transcriptPath);
19426
19499
  }
19500
+
19427
19501
  out.sort((a, b) => a.localeCompare(b));
19428
- return out;
19502
+ return { files: out, complete };
19429
19503
  }
19430
19504
 
19431
- async function listAntigravityTranscripts(geminiHome) {
19505
+ async function listAntigravitySessionFiles(brainDir) {
19506
+ return (await listAntigravitySessionFilesWithStatus(brainDir)).files;
19507
+ }
19508
+
19509
+ async function listAntigravityTranscriptsWithStatus(geminiHome, knownFiles = null) {
19432
19510
  const dirs = resolveAntigravityBrainDirs(geminiHome);
19433
- const lists = await Promise.all(dirs.map((dir) => listAntigravitySessionFiles(dir)));
19434
- return lists.flat();
19511
+ const results = await Promise.all(
19512
+ dirs.map((dir) => listAntigravitySessionFilesWithStatus(dir, knownFiles)),
19513
+ );
19514
+ return {
19515
+ files: results.flatMap((result) => result.files),
19516
+ complete: results.every((result) => result.complete),
19517
+ };
19518
+ }
19519
+
19520
+ async function listAntigravityTranscripts(geminiHome) {
19521
+ return (await listAntigravityTranscriptsWithStatus(geminiHome)).files;
19522
+ }
19523
+
19524
+ const ANTIGRAVITY_CURSOR_VERSION = 2;
19525
+ // Bump when extractAntigravityGenInfo starts reading records it used to
19526
+ // discard. Kept apart from the cursor version: a cursor-version mismatch routes
19527
+ // to the legacy migration, which treats the cursor as having no per-file ledger.
19528
+ const ANTIGRAVITY_EXTRACTOR_REVISION = 1;
19529
+ const ANTIGRAVITY_MAX_CONTRIBUTIONS = 4096;
19530
+
19531
+ function hashAntigravityTranscript(buffer) {
19532
+ if (!Buffer.isBuffer(buffer)) return null;
19533
+ return crypto.createHash("sha256").update(buffer).digest("hex");
19534
+ }
19535
+
19536
+ function antigravityTranscriptPrefixMatches(buffer, previous) {
19537
+ if (!Buffer.isBuffer(buffer) || !previous || typeof previous.transcriptHash !== "string") {
19538
+ return false;
19539
+ }
19540
+ const previousSize = Number(previous.size);
19541
+ if (!Number.isSafeInteger(previousSize) || previousSize < 0 || buffer.length < previousSize) {
19542
+ return false;
19543
+ }
19544
+ return hashAntigravityTranscript(buffer.subarray(0, previousSize)) === previous.transcriptHash;
19545
+ }
19546
+
19547
+ async function readAntigravityTranscriptBytes(filePath) {
19548
+ try {
19549
+ return await fs.readFile(filePath);
19550
+ } catch (_error) {
19551
+ return null;
19552
+ }
19553
+ }
19554
+
19555
+ async function statAntigravityDatabase(dbPath) {
19556
+ const result = {
19557
+ dbExists: false,
19558
+ dbInode: 0,
19559
+ dbMtimeMs: 0,
19560
+ dbCtimeMs: 0,
19561
+ dbSize: 0,
19562
+ dbWalInode: 0,
19563
+ dbWalMtimeMs: 0,
19564
+ dbWalSize: 0,
19565
+ dbJournalInode: 0,
19566
+ dbJournalMtimeMs: 0,
19567
+ dbJournalSize: 0,
19568
+ };
19569
+ if (!dbPath) return result;
19570
+ for (const [suffix, prefix] of [
19571
+ ["", "db"],
19572
+ ["-wal", "dbWal"],
19573
+ ["-journal", "dbJournal"],
19574
+ ]) {
19575
+ const st = await fs.stat(`${dbPath}${suffix}`).catch(() => null);
19576
+ if (!st || !st.isFile()) continue;
19577
+ if (suffix === "") result.dbExists = true;
19578
+ result[`${prefix}MtimeMs`] = Number.isFinite(st.mtimeMs) ? st.mtimeMs : 0;
19579
+ if (suffix === "") {
19580
+ result.dbInode = Number.isFinite(st.ino) ? st.ino : 0;
19581
+ result.dbCtimeMs = Number.isFinite(st.ctimeMs) ? st.ctimeMs : 0;
19582
+ } else {
19583
+ result[`${prefix}Inode`] = Number.isFinite(st.ino) ? st.ino : 0;
19584
+ }
19585
+ result[`${prefix}Size`] = Number.isFinite(st.size) ? st.size : 0;
19586
+ }
19587
+ return result;
19588
+ }
19589
+
19590
+ // Neither the transcript nor the database changes when the extractor does, so
19591
+ // without this stamp a session read before an extractor fix keeps its totals.
19592
+ function antigravityExtractorStale(previous, dbPath) {
19593
+ return Boolean(
19594
+ dbPath &&
19595
+ previous &&
19596
+ previous.cursorVersion === ANTIGRAVITY_CURSOR_VERSION &&
19597
+ previous.extractorRevision !== ANTIGRAVITY_EXTRACTOR_REVISION,
19598
+ );
19599
+ }
19600
+
19601
+ function sameAntigravityDatabase(previous, current) {
19602
+ if (!previous || !current) return false;
19603
+ // SQLite updates the -shm read marks when a reader opens a WAL database.
19604
+ // Those mtime changes do not represent new usage metadata and must not force
19605
+ // a full transcript reconciliation on every sync.
19606
+ return [
19607
+ "dbExists",
19608
+ "dbInode",
19609
+ "dbMtimeMs",
19610
+ "dbCtimeMs",
19611
+ "dbSize",
19612
+ "dbWalInode",
19613
+ "dbWalMtimeMs",
19614
+ "dbWalSize",
19615
+ "dbJournalInode",
19616
+ "dbJournalMtimeMs",
19617
+ "dbJournalSize",
19618
+ ].every((field) => Number(previous[field] || 0) === Number(current[field] || 0));
19619
+ }
19620
+
19621
+ function capAntigravityContributions(contributions) {
19622
+ if (!contributions || typeof contributions !== "object") {
19623
+ return { contributions: {}, complete: true };
19624
+ }
19625
+ const entries = Object.entries(contributions);
19626
+ if (entries.length <= ANTIGRAVITY_MAX_CONTRIBUTIONS) {
19627
+ return { contributions, complete: true };
19628
+ }
19629
+
19630
+ entries.sort(([, left], [, right]) => {
19631
+ const leftTime = Date.parse(left?.bucketStart || "");
19632
+ const rightTime = Date.parse(right?.bucketStart || "");
19633
+ if (Number.isFinite(leftTime) && Number.isFinite(rightTime) && leftTime !== rightTime) {
19634
+ return rightTime - leftTime;
19635
+ }
19636
+ if (Number.isFinite(rightTime) !== Number.isFinite(leftTime)) {
19637
+ return Number.isFinite(rightTime) ? 1 : -1;
19638
+ }
19639
+ return 0;
19640
+ });
19641
+
19642
+ return {
19643
+ contributions: Object.fromEntries(entries.slice(0, ANTIGRAVITY_MAX_CONTRIBUTIONS)),
19644
+ complete: false,
19645
+ };
19646
+ }
19647
+
19648
+ function resetAntigravityBucketsForRebuild({
19649
+ hourlyState,
19650
+ projectState,
19651
+ sources,
19652
+ touchedBuckets,
19653
+ projectTouchedBuckets,
19654
+ }) {
19655
+ if (!sources || sources.size === 0) return;
19656
+
19657
+ for (const [key, bucket] of Object.entries(hourlyState?.buckets || {})) {
19658
+ if (!bucket || !bucket.totals) continue;
19659
+ const bucketSource = normalizeSourceInput(parseBucketKey(key).source);
19660
+ if (!sources.has(bucketSource)) continue;
19661
+ bucket.totals = initTotals();
19662
+ bucket.queuedKey = null;
19663
+ bucket.retractedUnknownKey = null;
19664
+ touchedBuckets.add(key);
19665
+ }
19666
+
19667
+ for (const key of Object.keys(hourlyState?.groupQueued || {})) {
19668
+ const groupSource = normalizeSourceInput(key.split(BUCKET_SEPARATOR)[0]);
19669
+ if (sources.has(groupSource)) delete hourlyState.groupQueued[key];
19670
+ }
19671
+
19672
+ if (!projectState || !projectTouchedBuckets) return;
19673
+ for (const [key, bucket] of Object.entries(projectState.buckets || {})) {
19674
+ const keyParts = key.split(BUCKET_SEPARATOR);
19675
+ const bucketSource = normalizeSourceInput(bucket?.source || keyParts.at(-2));
19676
+ if (!sources.has(bucketSource) || !bucket?.totals) continue;
19677
+ bucket.totals = initTotals();
19678
+ bucket.queuedKey = null;
19679
+ projectTouchedBuckets.add(key);
19680
+ }
19435
19681
  }
19436
19682
 
19437
19683
  async function parseAntigravityIncremental({
@@ -19442,6 +19688,10 @@ async function parseAntigravityIncremental({
19442
19688
  onProgress,
19443
19689
  source,
19444
19690
  publicRepoResolver,
19691
+ // Source-wide rebuilds may retract sessions absent from sessionFiles. Only
19692
+ // permit that when discovery explicitly completed; direct parser callers
19693
+ // retain the historical complete-inventory default for compatibility.
19694
+ inventoryComplete = true,
19445
19695
  }) {
19446
19696
  await ensureDir(path.dirname(queuePath));
19447
19697
  let filesProcessed = 0;
@@ -19450,17 +19700,147 @@ async function parseAntigravityIncremental({
19450
19700
  const cb = typeof onProgress === "function" ? onProgress : null;
19451
19701
  const files = Array.isArray(sessionFiles) ? sessionFiles : [];
19452
19702
  const totalFiles = files.length;
19453
- const hourlyState = normalizeHourlyState(cursors?.hourly);
19703
+ // Keep every mutation private until both queue appends succeed. The generic
19704
+ // normalizers intentionally preserve bucket object references for other
19705
+ // parsers, so Antigravity must deep-clone its working state here; otherwise a
19706
+ // queue failure can leave a committed cursor pointing at already-mutated
19707
+ // hourly totals.
19708
+ const hourlyState = structuredClone(normalizeHourlyState(cursors?.hourly));
19454
19709
  const projectEnabled = typeof projectQueuePath === "string" && projectQueuePath.length > 0;
19455
- const projectState = projectEnabled ? normalizeProjectState(cursors?.projectHourly) : null;
19710
+ const projectState = projectEnabled
19711
+ ? structuredClone(normalizeProjectState(cursors?.projectHourly))
19712
+ : null;
19456
19713
  const projectTouchedBuckets = projectEnabled ? new Set() : null;
19457
19714
  const projectMetaCache = projectEnabled ? new Map() : null;
19458
19715
  const publicRepoCache = projectEnabled ? new Map() : null;
19459
19716
  const touchedBuckets = new Set();
19460
19717
  const defaultSource = normalizeSourceInput(source) || "antigravity";
19718
+ const fileCursors =
19719
+ cursors.files && typeof cursors.files === "object" && !Array.isArray(cursors.files)
19720
+ ? structuredClone(cursors.files)
19721
+ : {};
19461
19722
 
19462
- if (!cursors.files || typeof cursors.files !== "object") {
19463
- cursors.files = {};
19723
+ const fileSources = new Set();
19724
+ const fileSourceByPath = new Map();
19725
+ for (const entry of files) {
19726
+ const filePath = typeof entry === "string" ? entry : entry?.path;
19727
+ const fileSource =
19728
+ typeof entry === "string"
19729
+ ? defaultSource
19730
+ : normalizeSourceInput(entry?.source) || defaultSource;
19731
+ if (filePath) fileSourceByPath.set(filePath, fileSource);
19732
+ fileSources.add(fileSource);
19733
+ }
19734
+ if (fileSources.size === 0) fileSources.add(defaultSource);
19735
+ for (const [filePath, prev] of Object.entries(fileCursors)) {
19736
+ if (!resolveAntigravityDbPath(filePath)) continue;
19737
+ const previousSource = normalizeSourceInput(prev?.source);
19738
+ if (previousSource) fileSources.add(previousSource);
19739
+ }
19740
+
19741
+ // Legacy cursors have no per-file contribution ledger. Rebuild when every
19742
+ // legacy file is still present; if discovery is incomplete, preserve the old
19743
+ // aggregate and migrate live files against transcript-only baselines below.
19744
+ const liveFilePaths = new Set(
19745
+ files
19746
+ .map((entry) => (typeof entry === "string" ? entry : entry?.path))
19747
+ .filter(Boolean),
19748
+ );
19749
+ const incompleteContributionPaths = new Set();
19750
+ const deferredIncompletePaths = new Set();
19751
+ let needsFullRebuild = false;
19752
+ for (const [filePath, prev] of Object.entries(fileCursors)) {
19753
+ if (!resolveAntigravityDbPath(filePath) || !prev || Number(prev.lastLine || 0) <= 0) {
19754
+ continue;
19755
+ }
19756
+ const isLegacy =
19757
+ prev.cursorVersion !== ANTIGRAVITY_CURSOR_VERSION ||
19758
+ !prev.contributions ||
19759
+ typeof prev.contributions !== "object";
19760
+ if (!isLegacy && prev.contributionsComplete === false) {
19761
+ incompleteContributionPaths.add(filePath);
19762
+ }
19763
+ }
19764
+
19765
+ // Project reassignment is another full-ledger change: a capped ledger cannot
19766
+ // retract contributions that fell outside its retained window. Resolve
19767
+ // incomplete files against a private project state before deciding whether a
19768
+ // source-wide rebuild is required.
19769
+ const preflightProjectState =
19770
+ projectEnabled && incompleteContributionPaths.size > 0
19771
+ ? structuredClone(projectState)
19772
+ : null;
19773
+ const preflightProjectMetaCache = preflightProjectState ? new Map() : null;
19774
+ const preflightPublicRepoCache = preflightProjectState ? new Map() : null;
19775
+
19776
+ // A capped contribution ledger is safe while its transcript/SQLite identity
19777
+ // is unchanged. If an incomplete ledger needs reconciliation, rebuild the
19778
+ // complete source baseline before applying any per-file deltas. When source
19779
+ // discovery is incomplete, defer the file instead: resetting the source
19780
+ // would erase contributions belonging to sessions omitted by discovery.
19781
+ if (!needsFullRebuild && incompleteContributionPaths.size > 0) {
19782
+ for (const filePath of incompleteContributionPaths) {
19783
+ if (!liveFilePaths.has(filePath)) {
19784
+ if (!inventoryComplete) {
19785
+ deferredIncompletePaths.add(filePath);
19786
+ } else {
19787
+ needsFullRebuild = true;
19788
+ }
19789
+ if (needsFullRebuild) break;
19790
+ continue;
19791
+ }
19792
+ const prev = cursors.files[filePath];
19793
+ const st = await fs.stat(filePath).catch(() => null);
19794
+ const dbPath = resolveAntigravityDbPath(filePath);
19795
+ const dbIdentity = await statAntigravityDatabase(dbPath);
19796
+ const sameInode = st && prev.inode === (st.ino || 0);
19797
+ const transcriptCanAppend =
19798
+ sameInode &&
19799
+ (Number.isFinite(st.size) ? st.size : 0) >= Number(prev.size || 0) &&
19800
+ antigravityTranscriptPrefixMatches(
19801
+ await readAntigravityTranscriptBytes(filePath),
19802
+ prev,
19803
+ );
19804
+ const sameSource =
19805
+ !fileSourceByPath.has(filePath) ||
19806
+ (prev.source == null ? defaultSource : prev.source) === fileSourceByPath.get(filePath);
19807
+ const sameDb = sameAntigravityDatabase(prev, dbIdentity);
19808
+ const staleExtractor = antigravityExtractorStale(prev, dbPath);
19809
+ let projectChanged = false;
19810
+ if (preflightProjectState) {
19811
+ const projectContext = await resolveProjectContextForFile({
19812
+ filePath,
19813
+ projectMetaCache: preflightProjectMetaCache,
19814
+ publicRepoCache: preflightPublicRepoCache,
19815
+ publicRepoResolver,
19816
+ projectState: preflightProjectState,
19817
+ });
19818
+ projectChanged = antigravityProjectAssignmentChanged(prev, projectContext);
19819
+ }
19820
+ if (!transcriptCanAppend || !sameDb || staleExtractor || !sameSource || projectChanged) {
19821
+ if (!inventoryComplete) {
19822
+ deferredIncompletePaths.add(filePath);
19823
+ } else {
19824
+ needsFullRebuild = true;
19825
+ }
19826
+ if (needsFullRebuild) break;
19827
+ }
19828
+ }
19829
+ }
19830
+
19831
+ if (needsFullRebuild) {
19832
+ for (const filePath of Object.keys(fileCursors)) {
19833
+ if (resolveAntigravityDbPath(filePath) && !liveFilePaths.has(filePath)) {
19834
+ delete fileCursors[filePath];
19835
+ }
19836
+ }
19837
+ resetAntigravityBucketsForRebuild({
19838
+ hourlyState,
19839
+ projectState,
19840
+ sources: fileSources,
19841
+ touchedBuckets,
19842
+ projectTouchedBuckets,
19843
+ });
19464
19844
  }
19465
19845
 
19466
19846
  for (let idx = 0; idx < files.length; idx++) {
@@ -19472,17 +19852,140 @@ async function parseAntigravityIncremental({
19472
19852
  ? defaultSource
19473
19853
  : normalizeSourceInput(entry?.source) || defaultSource;
19474
19854
  const st = await fs.stat(filePath).catch(() => null);
19475
- if (!st || !st.isFile()) continue;
19855
+ if (!st || !st.isFile()) {
19856
+ if (needsFullRebuild) {
19857
+ throw new Error(`Antigravity transcript disappeared during rebuild: ${filePath}`);
19858
+ }
19859
+ continue;
19860
+ }
19476
19861
 
19477
19862
  const key = filePath;
19478
- const prev = cursors.files[key] || null;
19863
+ if (deferredIncompletePaths.has(key)) continue;
19864
+ const prev = fileCursors[key] || null;
19865
+
19479
19866
  const inode = st.ino || 0;
19480
19867
  const size = Number.isFinite(st.size) ? st.size : 0;
19481
19868
  const mtimeMs = Number.isFinite(st.mtimeMs) ? st.mtimeMs : 0;
19869
+ const ctimeMs = Number.isFinite(st.ctimeMs) ? st.ctimeMs : 0;
19482
19870
 
19483
- const unchanged =
19484
- prev && prev.inode === inode && prev.size === size && prev.mtimeMs === mtimeMs;
19485
- if (unchanged) {
19871
+ const isLegacy = Boolean(
19872
+ prev &&
19873
+ (prev.cursorVersion !== ANTIGRAVITY_CURSOR_VERSION ||
19874
+ !prev.contributions ||
19875
+ typeof prev.contributions !== "object"),
19876
+ );
19877
+
19878
+ const dbPath = resolveAntigravityDbPath(filePath);
19879
+ const dbIdentity = await statAntigravityDatabase(dbPath);
19880
+
19881
+ const sameInode = prev && prev.inode === inode;
19882
+ const sameTranscriptStats =
19883
+ sameInode &&
19884
+ prev.size === size &&
19885
+ prev.mtimeMs === mtimeMs &&
19886
+ Number(prev.ctimeMs || 0) === ctimeMs;
19887
+ const sameDb = prev && sameAntigravityDatabase(prev, dbIdentity);
19888
+ const sameSource =
19889
+ !prev || (prev.source == null ? defaultSource : prev.source) === fileSource;
19890
+ const currentVersion = !isLegacy && prev?.cursorVersion === ANTIGRAVITY_CURSOR_VERSION;
19891
+ const staleExtractor = !isLegacy && antigravityExtractorStale(prev, dbPath);
19892
+ let transcriptBytes = null;
19893
+ let transcriptHash = typeof prev?.transcriptHash === "string" ? prev.transcriptHash : null;
19894
+ let sameTranscriptContent = sameTranscriptStats;
19895
+ let transcriptCanAppend = false;
19896
+
19897
+ // Stat metadata is only a fast hint. On any apparent file change, compare
19898
+ // the bytes so atomic replacement, truncation, and edits before the cursor
19899
+ // cannot be mistaken for an append. A cursor without this hash is treated
19900
+ // conservatively and gets one reconciliation pass.
19901
+ const transcriptReadRequired =
19902
+ !sameTranscriptStats || !currentVersion || !prev?.transcriptHash || needsFullRebuild;
19903
+ if (transcriptReadRequired) {
19904
+ transcriptBytes = await readAntigravityTranscriptBytes(filePath);
19905
+ const afterReadStat = await fs.stat(filePath).catch(() => null);
19906
+ if (
19907
+ !Buffer.isBuffer(transcriptBytes) ||
19908
+ !afterReadStat ||
19909
+ !afterReadStat.isFile() ||
19910
+ (afterReadStat.ino || 0) !== inode ||
19911
+ transcriptBytes.length !== size ||
19912
+ afterReadStat.size !== size ||
19913
+ afterReadStat.mtimeMs !== mtimeMs ||
19914
+ (Number.isFinite(afterReadStat.ctimeMs) ? afterReadStat.ctimeMs : 0) !== ctimeMs
19915
+ ) {
19916
+ throw new Error(`Antigravity transcript changed while being read: ${filePath}`);
19917
+ }
19918
+ transcriptHash = hashAntigravityTranscript(transcriptBytes);
19919
+ sameTranscriptContent =
19920
+ Boolean(
19921
+ (prev?.transcriptHash && transcriptHash && prev.transcriptHash === transcriptHash && prev.size === size) ||
19922
+ (!prev?.transcriptHash && sameTranscriptStats),
19923
+ );
19924
+ transcriptCanAppend =
19925
+ Boolean(
19926
+ prev &&
19927
+ transcriptBytes &&
19928
+ size >= Number(prev.size || 0) &&
19929
+ (prev.transcriptHash
19930
+ ? antigravityTranscriptPrefixMatches(transcriptBytes, prev)
19931
+ : sameInode),
19932
+ );
19933
+ }
19934
+
19935
+ const projectContext = projectEnabled
19936
+ ? await resolveProjectContextForFile({
19937
+ filePath,
19938
+ projectMetaCache,
19939
+ publicRepoCache,
19940
+ publicRepoResolver,
19941
+ projectState,
19942
+ })
19943
+ : null;
19944
+ const projectRef = projectContext?.projectRef || null;
19945
+ const projectKey = projectContext?.projectKey || null;
19946
+ const projectChanged =
19947
+ projectEnabled && antigravityProjectAssignmentChanged(prev, projectContext);
19948
+ const baselineLine = Number(prev?.baselineLine || (isLegacy ? prev?.lastLine || 0 : 0));
19949
+
19950
+ // Legacy migration: "no retroactive claim". The file's history stays in the
19951
+ // old aggregate untouched. If unchanged, stamp v2 cursor with contributions: {}
19952
+ // and queue no buckets.
19953
+ if (!needsFullRebuild && isLegacy && (sameTranscriptStats || sameTranscriptContent)) {
19954
+ const contributionState = capAntigravityContributions({});
19955
+ fileCursors[key] = {
19956
+ ...prev,
19957
+ inode,
19958
+ size,
19959
+ mtimeMs,
19960
+ ctimeMs,
19961
+ source: fileSource,
19962
+ transcriptHash:
19963
+ transcriptHash ||
19964
+ (transcriptBytes
19965
+ ? hashAntigravityTranscript(transcriptBytes)
19966
+ : prev?.transcriptHash || null),
19967
+ ...dbIdentity,
19968
+ projectKey: projectRef ? projectKey : null,
19969
+ projectRef: projectRef || null,
19970
+ projectStatus: projectContext?.status || null,
19971
+ projectConfigPath: projectContext?.configPath || null,
19972
+ projectConfigMtimeMs: projectContext?.configMtimeMs ?? null,
19973
+ projectConfigSize: projectContext?.configSize ?? null,
19974
+ projectReconciliationDeferred: false,
19975
+ baselineLine,
19976
+ cursorVersion: ANTIGRAVITY_CURSOR_VERSION,
19977
+ extractorRevision: ANTIGRAVITY_EXTRACTOR_REVISION,
19978
+ lastLine: Number(prev.lastLine || 0),
19979
+ contextTokens: Number(prev.contextTokens || 0),
19980
+ previousContextTokens: Number(prev.previousContextTokens || 0),
19981
+ currentModel: prev.currentModel,
19982
+ lastPlannerModel: prev.lastPlannerModel,
19983
+ usageSource: prev.usageSource || (dbIdentity ? "sqlite" : "estimated"),
19984
+ dbReadOk: prev.dbReadOk !== false,
19985
+ contributions: contributionState.contributions,
19986
+ contributionsComplete: contributionState.complete,
19987
+ updatedAt: new Date().toISOString(),
19988
+ };
19486
19989
  filesProcessed += 1;
19487
19990
  if (cb) {
19488
19991
  cb({
@@ -19497,54 +20000,250 @@ async function parseAntigravityIncremental({
19497
20000
  continue;
19498
20001
  }
19499
20002
 
19500
- const sameFile = prev && prev.inode === inode;
19501
- const lastLine = sameFile ? Number(prev.lastLine || 0) : 0;
19502
- const initialContextTokens = sameFile ? Number(prev.contextTokens || 0) : 0;
19503
- const initialPrevContext = sameFile ? Number(prev.previousContextTokens || 0) : 0;
19504
- const initialModel = sameFile && typeof prev.currentModel === "string" ? prev.currentModel : null;
19505
- const initialLastPlannerModel =
19506
- sameFile && typeof prev.lastPlannerModel === "string" ? prev.lastPlannerModel : null;
19507
- const initialUsageSource = sameFile && typeof prev.usageSource === "string" ? prev.usageSource : null;
19508
-
19509
- const projectContext = projectEnabled
19510
- ? await resolveProjectContextForFile({
20003
+ const metadataRetryNeeded = currentVersion && Boolean(dbPath) && prev?.dbReadOk !== true;
20004
+ if (
20005
+ sameTranscriptStats &&
20006
+ sameTranscriptContent &&
20007
+ sameDb &&
20008
+ sameSource &&
20009
+ currentVersion &&
20010
+ !staleExtractor &&
20011
+ !metadataRetryNeeded &&
20012
+ !projectChanged &&
20013
+ !needsFullRebuild
20014
+ ) {
20015
+ filesProcessed += 1;
20016
+ if (cb) {
20017
+ cb({
20018
+ index: idx + 1,
20019
+ total: totalFiles,
19511
20020
  filePath,
19512
- projectMetaCache,
19513
- publicRepoCache,
19514
- publicRepoResolver,
19515
- projectState,
19516
- })
19517
- : null;
19518
- const projectRef = projectContext?.projectRef || null;
19519
- const projectKey = projectContext?.projectKey || null;
20021
+ filesProcessed,
20022
+ eventsAggregated,
20023
+ bucketsQueued: touchedBuckets.size,
20024
+ });
20025
+ }
20026
+ continue;
20027
+ }
19520
20028
 
19521
- const result = await parseAntigravityFile({
19522
- filePath,
19523
- lastLine,
19524
- initialContextTokens,
19525
- initialPrevContext,
19526
- initialModel,
19527
- initialLastPlannerModel,
19528
- initialUsageSource,
19529
- hourlyState,
19530
- touchedBuckets,
19531
- source: fileSource,
19532
- projectState,
19533
- projectTouchedBuckets,
19534
- projectRef,
19535
- projectKey,
19536
- });
20029
+ // Reconcile historical contributions whenever the cursor needs migration or
20030
+ // SQLite changed, even when new transcript lines were appended in the same
20031
+ // sync. Otherwise the incremental branch stamps a migrated cursor without
20032
+ // repairing its old, incomplete totals. Legacy cursors have no retroactive
20033
+ // claim and are not routed through reconciliation.
20034
+ const needsReconcile =
20035
+ !needsFullRebuild &&
20036
+ !isLegacy &&
20037
+ prev &&
20038
+ (metadataRetryNeeded ||
20039
+ !currentVersion ||
20040
+ !sameDb ||
20041
+ staleExtractor ||
20042
+ !sameSource ||
20043
+ projectChanged ||
20044
+ (!sameTranscriptContent && !transcriptCanAppend));
20045
+
20046
+ // Reconciliation reparses the complete transcript. Even when stat metadata
20047
+ // looked unchanged, project/SQLite changes can select this path; obtain a
20048
+ // stable snapshot here rather than letting parseAntigravityFile silently
20049
+ // turn a transient read failure into an empty transcript.
20050
+ if (!transcriptBytes && (needsFullRebuild || needsReconcile)) {
20051
+ transcriptBytes = await readAntigravityTranscriptBytes(filePath);
20052
+ const afterReadStat = await fs.stat(filePath).catch(() => null);
20053
+ if (
20054
+ !Buffer.isBuffer(transcriptBytes) ||
20055
+ !afterReadStat ||
20056
+ !afterReadStat.isFile() ||
20057
+ (afterReadStat.ino || 0) !== inode ||
20058
+ transcriptBytes.length !== size ||
20059
+ afterReadStat.size !== size ||
20060
+ afterReadStat.mtimeMs !== mtimeMs ||
20061
+ (Number.isFinite(afterReadStat.ctimeMs) ? afterReadStat.ctimeMs : 0) !== ctimeMs
20062
+ ) {
20063
+ throw new Error(`Antigravity transcript changed while being read: ${filePath}`);
20064
+ }
20065
+ transcriptHash = hashAntigravityTranscript(transcriptBytes);
20066
+ sameTranscriptContent =
20067
+ Boolean(transcriptHash && prev?.transcriptHash === transcriptHash && prev.size === size);
20068
+ transcriptCanAppend = Boolean(
20069
+ prev &&
20070
+ size >= Number(prev.size || 0) &&
20071
+ antigravityTranscriptPrefixMatches(transcriptBytes, prev),
20072
+ );
20073
+ }
19537
20074
 
19538
- cursors.files[key] = {
20075
+ const protectExactState = needsReconcile && prev?.usageSource === "sqlite";
20076
+ const hourlyStateBeforeReconcile = protectExactState ? structuredClone(hourlyState) : null;
20077
+ const projectStateBeforeReconcile =
20078
+ protectExactState && projectState ? structuredClone(projectState) : null;
20079
+ const touchedBeforeReconcile = protectExactState ? new Set(touchedBuckets) : null;
20080
+ const projectTouchedBeforeReconcile =
20081
+ protectExactState && projectTouchedBuckets ? new Set(projectTouchedBuckets) : null;
20082
+ let result;
20083
+
20084
+ if (needsFullRebuild || needsReconcile) {
20085
+ if (
20086
+ !needsFullRebuild &&
20087
+ prev &&
20088
+ prev.contributions &&
20089
+ typeof prev.contributions === "object"
20090
+ ) {
20091
+ for (const c of Object.values(prev.contributions)) {
20092
+ if (!c || !c.totals) continue;
20093
+ const oldBucket = getHourlyBucket(hourlyState, c.source, c.model, c.bucketStart);
20094
+ subtractTotals(oldBucket.totals, c.totals);
20095
+ touchedBuckets.add(bucketKey(c.source, c.model, c.bucketStart));
20096
+ if (
20097
+ projectEnabled &&
20098
+ c.projectKey &&
20099
+ projectState &&
20100
+ projectTouchedBuckets
20101
+ ) {
20102
+ const oldProjectBucket = getProjectBucket(
20103
+ projectState,
20104
+ c.projectKey,
20105
+ c.source,
20106
+ c.bucketStart,
20107
+ c.projectRef || null,
20108
+ );
20109
+ subtractTotals(oldProjectBucket.totals, c.totals);
20110
+ projectTouchedBuckets.add(projectBucketKey(c.projectKey, c.source, c.bucketStart));
20111
+ }
20112
+ }
20113
+ }
20114
+ result = await parseAntigravityFile({
20115
+ filePath,
20116
+ ...(transcriptBytes ? { rawBuffer: transcriptBytes } : {}),
20117
+ lastLine: 0,
20118
+ baselineLine: needsFullRebuild ? 0 : baselineLine,
20119
+ watermarkLine: Number(prev?.lastLine || 0),
20120
+ initialUsageSource: prev?.usageSource,
20121
+ hourlyState,
20122
+ touchedBuckets,
20123
+ source: fileSource,
20124
+ projectState,
20125
+ projectTouchedBuckets,
20126
+ projectRef,
20127
+ projectKey,
20128
+ });
20129
+ } else {
20130
+ const canResume = sameTranscriptContent || transcriptCanAppend;
20131
+ const lastLine = canResume ? Number(prev?.lastLine || 0) : 0;
20132
+ const initialContextTokens = canResume ? Number(prev?.contextTokens || 0) : 0;
20133
+ const initialPrevContext = canResume ? Number(prev?.previousContextTokens || 0) : 0;
20134
+ const initialModel =
20135
+ canResume && typeof prev?.currentModel === "string" ? prev.currentModel : null;
20136
+ const initialLastPlannerModel =
20137
+ canResume && typeof prev?.lastPlannerModel === "string"
20138
+ ? prev.lastPlannerModel
20139
+ : null;
20140
+ const initialUsageSource =
20141
+ canResume && typeof prev?.usageSource === "string" ? prev.usageSource : null;
20142
+
20143
+ result = await parseAntigravityFile({
20144
+ filePath,
20145
+ ...(transcriptBytes ? { rawBuffer: transcriptBytes } : {}),
20146
+ lastLine,
20147
+ baselineLine,
20148
+ initialContextTokens,
20149
+ initialPrevContext,
20150
+ initialModel,
20151
+ initialLastPlannerModel,
20152
+ initialUsageSource,
20153
+ hourlyState,
20154
+ touchedBuckets,
20155
+ source: fileSource,
20156
+ projectState,
20157
+ projectTouchedBuckets,
20158
+ projectRef,
20159
+ projectKey,
20160
+ });
20161
+
20162
+ const mergedContributions = { ...(prev?.contributions || {}) };
20163
+ for (const [cKey, cVal] of Object.entries(result.contributions || {})) {
20164
+ if (!mergedContributions[cKey]) {
20165
+ mergedContributions[cKey] = { ...cVal, totals: { ...cVal.totals } };
20166
+ } else {
20167
+ addTotals(mergedContributions[cKey].totals, cVal.totals);
20168
+ }
20169
+ }
20170
+ result.contributions = mergedContributions;
20171
+ }
20172
+
20173
+ if (result.deferred && prev) {
20174
+ if (needsFullRebuild && !hourlyStateBeforeReconcile) {
20175
+ throw new Error(`Cannot rebuild Antigravity transcript while SQLite is unavailable: ${filePath}`);
20176
+ }
20177
+ if (hourlyStateBeforeReconcile) {
20178
+ for (const field of Object.keys(hourlyState)) delete hourlyState[field];
20179
+ Object.assign(hourlyState, hourlyStateBeforeReconcile);
20180
+ if (touchedBeforeReconcile) {
20181
+ touchedBuckets.clear();
20182
+ for (const touched of touchedBeforeReconcile) touchedBuckets.add(touched);
20183
+ }
20184
+ }
20185
+ if (projectState && projectStateBeforeReconcile) {
20186
+ for (const field of Object.keys(projectState)) delete projectState[field];
20187
+ Object.assign(projectState, projectStateBeforeReconcile);
20188
+ if (projectTouchedBeforeReconcile) {
20189
+ projectTouchedBuckets.clear();
20190
+ for (const touched of projectTouchedBeforeReconcile) projectTouchedBuckets.add(touched);
20191
+ }
20192
+ }
20193
+ fileCursors[key] = {
20194
+ ...prev,
20195
+ inode,
20196
+ size,
20197
+ mtimeMs,
20198
+ ctimeMs,
20199
+ source: fileSource,
20200
+ transcriptHash,
20201
+ ...dbIdentity,
20202
+ dbReadOk: false,
20203
+ updatedAt: new Date().toISOString(),
20204
+ };
20205
+ filesProcessed += 1;
20206
+ if (cb) {
20207
+ cb({
20208
+ index: idx + 1,
20209
+ total: totalFiles,
20210
+ filePath,
20211
+ filesProcessed,
20212
+ eventsAggregated,
20213
+ bucketsQueued: touchedBuckets.size,
20214
+ });
20215
+ }
20216
+ continue;
20217
+ }
20218
+
20219
+ const contributionState = capAntigravityContributions(result.contributions);
20220
+ fileCursors[key] = {
19539
20221
  inode,
19540
20222
  size,
19541
20223
  mtimeMs,
20224
+ ctimeMs,
20225
+ source: fileSource,
20226
+ transcriptHash,
20227
+ ...dbIdentity,
20228
+ projectKey: projectRef ? projectKey : null,
20229
+ projectRef: projectRef || null,
20230
+ projectStatus: projectContext?.status || null,
20231
+ projectConfigPath: projectContext?.configPath || null,
20232
+ projectConfigMtimeMs: projectContext?.configMtimeMs ?? null,
20233
+ projectConfigSize: projectContext?.configSize ?? null,
20234
+ projectReconciliationDeferred: false,
20235
+ baselineLine: needsFullRebuild ? 0 : baselineLine,
20236
+ cursorVersion: ANTIGRAVITY_CURSOR_VERSION,
20237
+ extractorRevision: ANTIGRAVITY_EXTRACTOR_REVISION,
19542
20238
  lastLine: result.lastLine,
19543
20239
  contextTokens: result.contextTokens,
19544
20240
  previousContextTokens: result.previousContextTokens,
19545
20241
  currentModel: result.currentModel,
19546
20242
  lastPlannerModel: result.lastPlannerModel,
19547
20243
  usageSource: result.usageSource,
20244
+ dbReadOk: result.dbReadOk !== false,
20245
+ contributions: contributionState.contributions,
20246
+ contributionsComplete: contributionState.complete,
19548
20247
  updatedAt: new Date().toISOString(),
19549
20248
  };
19550
20249
 
@@ -19568,6 +20267,7 @@ async function parseAntigravityIncremental({
19568
20267
  ? await enqueueTouchedProjectBuckets({ projectQueuePath, projectState, projectTouchedBuckets })
19569
20268
  : 0;
19570
20269
  hourlyState.updatedAt = new Date().toISOString();
20270
+ cursors.files = fileCursors;
19571
20271
  cursors.hourly = hourlyState;
19572
20272
  if (projectState) {
19573
20273
  projectState.updatedAt = new Date().toISOString();
@@ -19578,78 +20278,164 @@ async function parseAntigravityIncremental({
19578
20278
  }
19579
20279
 
19580
20280
  function decodeAntigravityVarint(buf, offset) {
20281
+ if (!Buffer.isBuffer(buf) || !Number.isSafeInteger(offset) || offset < 0) {
20282
+ throw new RangeError("invalid Antigravity protobuf offset");
20283
+ }
20284
+ const start = offset;
19581
20285
  let res = 0;
19582
- let shift = 0;
19583
- while (offset < buf.length) {
20286
+ for (let count = 0; count < 10; count++) {
20287
+ if (offset >= buf.length) throw new RangeError("truncated Antigravity protobuf varint");
19584
20288
  const b = buf[offset++];
19585
- res += (b & 0x7f) * 2 ** shift;
19586
- if (!(b & 0x80)) break;
19587
- shift += 7;
20289
+ // 64 bits is nine 7-bit groups plus one bit of a tenth byte.
20290
+ if (count === 9 && b > 1) throw new RangeError("Antigravity protobuf varint exceeds 64 bits");
20291
+ res += (b & 0x7f) * 2 ** (count * 7);
20292
+ if (!(b & 0x80)) {
20293
+ if (Number.isSafeInteger(res)) return [res, offset];
20294
+ // Antigravity writes 2^64 - 1 in fields the extractor never reads. The
20295
+ // float sum has already lost precision, so re-read the bytes exactly.
20296
+ let exact = 0n;
20297
+ for (let i = start; i < offset; i++) {
20298
+ exact |= BigInt(buf[i] & 0x7f) << BigInt((i - start) * 7);
20299
+ }
20300
+ return [exact, offset];
20301
+ }
19588
20302
  }
19589
- return [res, offset];
20303
+ throw new RangeError("overlong Antigravity protobuf varint");
19590
20304
  }
19591
20305
 
19592
20306
  function findAntigravityProtoFields(buf) {
20307
+ if (!Buffer.isBuffer(buf)) throw new RangeError("invalid Antigravity protobuf payload");
19593
20308
  const fields = [];
19594
20309
  let offset = 0;
19595
20310
  while (offset < buf.length) {
19596
20311
  const [tag, next] = decodeAntigravityVarint(buf, offset);
20312
+ if (typeof tag !== "number") throw new RangeError("unsafe Antigravity protobuf field tag");
19597
20313
  offset = next;
19598
- const fieldNum = tag >> 3;
20314
+ const fieldNum = Math.floor(tag / 8);
19599
20315
  const wireType = tag & 7;
20316
+ if (fieldNum <= 0) throw new RangeError("invalid Antigravity protobuf field number");
19600
20317
  if (wireType === 0) {
20318
+ // May be a BigInt; token reads use Number.isSafeInteger, so it is never billed.
19601
20319
  const [val, vNext] = decodeAntigravityVarint(buf, offset);
19602
20320
  offset = vNext;
19603
20321
  fields.push({ num: fieldNum, val });
19604
20322
  } else if (wireType === 2) {
19605
20323
  const [len, lNext] = decodeAntigravityVarint(buf, offset);
19606
20324
  offset = lNext;
20325
+ if (len > buf.length - offset) {
20326
+ throw new RangeError("truncated Antigravity protobuf field");
20327
+ }
19607
20328
  fields.push({ num: fieldNum, val: buf.subarray(offset, offset + len) });
19608
20329
  offset += len;
19609
20330
  } else if (wireType === 1) {
20331
+ if (buf.length - offset < 8) throw new RangeError("truncated Antigravity protobuf field");
19610
20332
  offset += 8;
19611
20333
  } else if (wireType === 5) {
20334
+ if (buf.length - offset < 4) throw new RangeError("truncated Antigravity protobuf field");
19612
20335
  offset += 4;
19613
20336
  } else {
19614
- break;
20337
+ throw new RangeError("unsupported Antigravity protobuf wire type");
19615
20338
  }
19616
20339
  }
19617
20340
  return fields;
19618
20341
  }
19619
20342
 
19620
20343
  function extractAntigravityGenInfo(buf) {
19621
- const root = findAntigravityProtoFields(buf);
19622
- const f1 = root.find((f) => f.num === 1)?.val;
19623
- if (!f1) return null;
19624
-
19625
- const inner = findAntigravityProtoFields(f1);
19626
- let model = null;
19627
- const f19 = inner.find((f) => f.num === 19)?.val;
19628
- if (f19) model = Buffer.from(f19).toString("utf8").trim();
20344
+ try {
20345
+ const root = findAntigravityProtoFields(buf);
20346
+ const f1 = root.find((f) => f.num === 1)?.val;
20347
+ if (!f1) return null;
20348
+
20349
+ const inner = findAntigravityProtoFields(f1);
20350
+ let model = null;
20351
+ const f19 = inner.find((f) => f.num === 19)?.val;
20352
+ if (f19) model = Buffer.from(f19).toString("utf8").trim();
20353
+
20354
+ let contextTokens = 0;
20355
+ const f9 = inner.find((f) => f.num === 9)?.val;
20356
+ if (f9) {
20357
+ const f10 = findAntigravityProtoFields(f9).find((f) => f.num === 10)?.val;
20358
+ if (f10) {
20359
+ const tok = findAntigravityProtoFields(f10).find((f) => f.num === 1)?.val;
20360
+ if (Number.isSafeInteger(tok)) contextTokens = tok;
20361
+ }
20362
+ }
19629
20363
 
19630
- let contextTokens = 0;
19631
- const f9 = inner.find((f) => f.num === 9)?.val;
19632
- if (f9) {
19633
- const f10 = findAntigravityProtoFields(f9).find((f) => f.num === 10)?.val;
19634
- if (f10) {
19635
- const tok = findAntigravityProtoFields(f10).find((f) => f.num === 1)?.val;
19636
- if (Number.isFinite(tok)) contextTokens = tok;
20364
+ let lastStepIndex = null;
20365
+ for (const f of inner) {
20366
+ if (f.num !== 20 || !f.val) continue;
20367
+ const kv = findAntigravityProtoFields(f.val);
20368
+ const k = kv.find((x) => x.num === 1)?.val;
20369
+ const v = kv.find((x) => x.num === 2)?.val;
20370
+ if (k && Buffer.from(k).toString("utf8") === "last_step_index" && v) {
20371
+ const rawStepIndex = Buffer.from(v).toString("utf8").trim();
20372
+ if (/^\d+$/.test(rawStepIndex)) {
20373
+ const parsed = Number(rawStepIndex);
20374
+ if (Number.isSafeInteger(parsed)) lastStepIndex = parsed;
20375
+ }
20376
+ }
19637
20377
  }
19638
- }
19639
20378
 
19640
- let lastStepIndex = null;
19641
- for (const f of inner) {
19642
- if (f.num !== 20 || !f.val) continue;
19643
- const kv = findAntigravityProtoFields(f.val);
19644
- const k = kv.find((x) => x.num === 1)?.val;
19645
- const v = kv.find((x) => x.num === 2)?.val;
19646
- if (k && Buffer.from(k).toString("utf8") === "last_step_index" && v) {
19647
- const parsed = parseInt(Buffer.from(v).toString("utf8"), 10);
19648
- if (Number.isFinite(parsed)) lastStepIndex = parsed;
20379
+ let uncachedInput;
20380
+ let cachedInput;
20381
+ let outputTokens;
20382
+ let textOutput;
20383
+ let reasoningOutput;
20384
+ let hasUsageMetadata = false;
20385
+
20386
+ const f4 = inner.find((f) => f.num === 4)?.val;
20387
+ if (f4) {
20388
+ // Antigravity internal usage message layout (Field 4):
20389
+ // - f4.1: system / tool instruction prefix tokens (per-model fixed prefix)
20390
+ // - f4.2: user / prompt tokens for this turn
20391
+ // - f4.3: total output tokens checksum (f4.9 text + f4.10 reasoning)
20392
+ // - f4.5: cached prompt tokens (cache read)
20393
+ // - f4.9: text output tokens
20394
+ // - f4.10: reasoning / thought tokens
20395
+ const f4fields = findAntigravityProtoFields(f4);
20396
+ const sTok = f4fields.find((f) => f.num === 1)?.val;
20397
+ const pTok = f4fields.find((f) => f.num === 2)?.val;
20398
+ const cTok = f4fields.find((f) => f.num === 5)?.val;
20399
+ const oTok = f4fields.find((f) => f.num === 3)?.val;
20400
+ const tTok = f4fields.find((f) => f.num === 9)?.val;
20401
+ const rTok = f4fields.find((f) => f.num === 10)?.val;
20402
+ if (
20403
+ Number.isSafeInteger(sTok) ||
20404
+ Number.isSafeInteger(pTok) ||
20405
+ Number.isSafeInteger(cTok) ||
20406
+ Number.isSafeInteger(oTok) ||
20407
+ Number.isSafeInteger(tTok) ||
20408
+ Number.isSafeInteger(rTok)
20409
+ ) {
20410
+ hasUsageMetadata = true;
20411
+ const sysTokens = Number.isSafeInteger(sTok) ? sTok : 0;
20412
+ const promptTokens = Number.isSafeInteger(pTok) ? pTok : 0;
20413
+ uncachedInput = sysTokens + promptTokens;
20414
+ cachedInput = Number.isSafeInteger(cTok) ? cTok : 0;
20415
+ outputTokens = Number.isSafeInteger(oTok) ? oTok : 0;
20416
+ textOutput = Number.isSafeInteger(tTok) ? tTok : 0;
20417
+ reasoningOutput = Number.isSafeInteger(rTok) ? rTok : 0;
20418
+ }
19649
20419
  }
19650
- }
19651
20420
 
19652
- return { model, contextTokens, lastStepIndex };
20421
+ return {
20422
+ model,
20423
+ contextTokens,
20424
+ lastStepIndex,
20425
+ ...(hasUsageMetadata
20426
+ ? {
20427
+ uncachedInput,
20428
+ cachedInput,
20429
+ outputTokens,
20430
+ textOutput,
20431
+ reasoningOutput,
20432
+ hasUsageMetadata: true,
20433
+ }
20434
+ : {}),
20435
+ };
20436
+ } catch (_error) {
20437
+ return null;
20438
+ }
19653
20439
  }
19654
20440
 
19655
20441
  function resolveAntigravityDbPath(transcriptPath) {
@@ -19661,32 +20447,67 @@ function resolveAntigravityDbPath(transcriptPath) {
19661
20447
 
19662
20448
  function readAntigravityConversationDb(dbPath) {
19663
20449
  if (!dbPath) return null;
20450
+ let snapshot = null;
20451
+ let effectiveDbPath = dbPath;
20452
+ // Antigravity keeps active generation metadata in SQLite WAL files. Reading
20453
+ // a UNC/WSL database directly is both lock-prone and can miss rows that have
20454
+ // not checkpointed into the main file, so copy the database and sidecars as
20455
+ // one local read view first.
20456
+ if (isUncPath(dbPath)) {
20457
+ try {
20458
+ snapshot = snapshotSqliteDb(dbPath);
20459
+ effectiveDbPath = snapshot.path;
20460
+ } catch (_error) {
20461
+ // Preserve the existing direct-read fallback for transient snapshot or
20462
+ // permission failures.
20463
+ }
20464
+ }
19664
20465
  try {
20466
+ if (!fssync.existsSync(effectiveDbPath)) return null;
19665
20467
  const rows = readSqliteJsonRows(
19666
- dbPath,
20468
+ effectiveDbPath,
19667
20469
  "SELECT idx, quote(data) as hex FROM gen_metadata ORDER BY idx",
19668
- { readOnly: true },
20470
+ {
20471
+ readOnly: true,
20472
+ throwOnReadFailure: true,
20473
+ maxBuffer: 64 * 1024 * 1024,
20474
+ timeout: 15_000,
20475
+ },
19669
20476
  );
19670
- if (!Array.isArray(rows) || rows.length === 0) return null;
20477
+ if (!Array.isArray(rows)) return null;
19671
20478
 
19672
20479
  const stepMap = new Map();
19673
20480
  for (const r of rows) {
19674
20481
  if (!r || typeof r.hex !== "string" || !r.hex.startsWith("X'")) continue;
19675
20482
  const buf = Buffer.from(r.hex.slice(2, -1), "hex");
19676
20483
  const info = extractAntigravityGenInfo(buf);
19677
- if (info && info.lastStepIndex != null && info.contextTokens > 0) {
20484
+ if (
20485
+ info &&
20486
+ info.lastStepIndex != null &&
20487
+ (info.hasUsageMetadata || info.contextTokens > 0)
20488
+ ) {
19678
20489
  stepMap.set(info.lastStepIndex + 1, info);
19679
20490
  }
19680
20491
  }
19681
- return stepMap.size > 0 ? stepMap : null;
20492
+ // An empty map means the database was read successfully but has no usable
20493
+ // metadata yet. Keep it distinct from null, which signals a transient read
20494
+ // failure and must be retried on the next sync.
20495
+ return stepMap;
19682
20496
  } catch (_) {
19683
20497
  return null;
20498
+ } finally {
20499
+ if (snapshot) snapshot.cleanup();
19684
20500
  }
19685
20501
  }
19686
20502
 
19687
20503
  async function parseAntigravityFile({
19688
20504
  filePath,
19689
- lastLine,
20505
+ rawBuffer = null,
20506
+ lastLine = 0,
20507
+ maxLine = null,
20508
+ baselineLine = 0,
20509
+ watermarkLine = 0,
20510
+ applyBuckets = true,
19690
20511
  initialContextTokens,
19691
20512
  initialPrevContext,
19692
20513
  initialModel,
@@ -19700,7 +20521,12 @@ async function parseAntigravityFile({
19700
20521
  projectRef,
19701
20522
  projectKey,
19702
20523
  }) {
19703
- const raw = await fs.readFile(filePath, "utf8").catch(() => "");
20524
+ const raw = Buffer.isBuffer(rawBuffer)
20525
+ ? rawBuffer.toString("utf8")
20526
+ : await fs.readFile(filePath, "utf8").catch(() => null);
20527
+ if (raw === null) {
20528
+ throw new Error(`Antigravity transcript could not be read: ${filePath}`);
20529
+ }
19704
20530
  if (!raw.trim()) {
19705
20531
  return {
19706
20532
  lastLine: 0,
@@ -19710,14 +20536,22 @@ async function parseAntigravityFile({
19710
20536
  currentModel: null,
19711
20537
  lastPlannerModel: null,
19712
20538
  usageSource: "estimated",
20539
+ dbReadOk: true,
20540
+ contributions: {},
19713
20541
  };
19714
20542
  }
19715
20543
 
19716
- const lines = raw
20544
+ const allLines = raw
19717
20545
  .split(/\r?\n/)
19718
20546
  .map((line) => line.trim())
19719
20547
  .filter(Boolean);
20548
+ const endLimit =
20549
+ Number.isFinite(maxLine) && maxLine >= 0
20550
+ ? Math.min(allLines.length, maxLine)
20551
+ : allLines.length;
20552
+ const lines = allLines.slice(0, endLimit);
19720
20553
  let eventsAggregated = 0;
20554
+ const contributions = {};
19721
20555
  const dbPath = resolveAntigravityDbPath(filePath);
19722
20556
  const stepMap = dbPath ? readAntigravityConversationDb(dbPath) : null;
19723
20557
  // Resume cached context-token total + model so historical lines (i < lastLine)
@@ -19728,11 +20562,30 @@ async function parseAntigravityFile({
19728
20562
  const cachedTokens = Number.isFinite(initialContextTokens) ? initialContextTokens : 0;
19729
20563
  const cachedPrev = Number.isFinite(initialPrevContext) ? initialPrevContext : 0;
19730
20564
  const cachedModel = typeof initialModel === "string" ? initialModel : null;
20565
+ const dbReadFailed = Boolean(dbPath) && stepMap === null;
20566
+ if (dbReadFailed && initialUsageSource === "sqlite") {
20567
+ return {
20568
+ lastLine: Math.min(Number.isFinite(lastLine) ? lastLine : 0, lines.length),
20569
+ eventsAggregated: 0,
20570
+ contextTokens: cachedTokens,
20571
+ previousContextTokens: cachedPrev,
20572
+ currentModel: cachedModel,
20573
+ lastPlannerModel:
20574
+ typeof initialLastPlannerModel === "string" ? initialLastPlannerModel : cachedModel,
20575
+ usageSource: "sqlite",
20576
+ dbReadOk: false,
20577
+ deferred: true,
20578
+ contributions: {},
20579
+ };
20580
+ }
19731
20581
  const sqliteCursor = initialUsageSource === "sqlite";
20582
+ const hasSqliteMetadata = Boolean(stepMap && stepMap.size > 0);
19732
20583
  const resumed =
19733
- canResume && (cachedTokens > 0 || cachedModel !== null) && (!stepMap || sqliteCursor);
20584
+ canResume &&
20585
+ (cachedTokens > 0 || cachedModel !== null) &&
20586
+ (!hasSqliteMetadata || sqliteCursor);
19734
20587
  const scanStart = resumed ? lastLine : 0;
19735
- let currentModel = resumed ? cachedModel : null;
20588
+ let currentModel = resumed ? cachedModel : typeof initialModel === "string" ? initialModel : null;
19736
20589
  if (!currentModel) {
19737
20590
  currentModel = await readAntigravityDefaultModel(filePath);
19738
20591
  }
@@ -19780,7 +20633,12 @@ async function parseAntigravityFile({
19780
20633
 
19781
20634
  if (!isNewEvent) {
19782
20635
  if (parsed.type === "PLANNER_RESPONSE") {
19783
- if (dbContextTokens > 0) {
20636
+ if (dbTurn?.hasUsageMetadata) {
20637
+ contextTokens =
20638
+ dbContextTokens > 0
20639
+ ? dbContextTokens
20640
+ : (dbTurn.uncachedInput || 0) + (dbTurn.cachedInput || 0);
20641
+ } else if (dbContextTokens > 0) {
19784
20642
  contextTokens = dbContextTokens;
19785
20643
  }
19786
20644
  previousContextTokens = contextTokens;
@@ -19812,52 +20670,97 @@ async function parseAntigravityFile({
19812
20670
  let billedPlanner = false;
19813
20671
 
19814
20672
  if (parsed.type === "PLANNER_RESPONSE") {
19815
- const content = typeof parsed.content === "string" ? parsed.content : "";
19816
- const thinking = typeof parsed.thinking === "string" ? parsed.thinking : "";
20673
+ if (dbTurn?.hasUsageMetadata) {
20674
+ const uncached = dbTurn.uncachedInput || 0;
20675
+ const cached = dbTurn.cachedInput || 0;
20676
+ const reasoning = dbTurn.reasoningOutput || 0;
20677
+ let output = 0;
20678
+ if (Number.isFinite(dbTurn.textOutput) && dbTurn.textOutput > 0) {
20679
+ output = dbTurn.textOutput;
20680
+ } else if (Number.isFinite(dbTurn.outputTokens)) {
20681
+ output = Math.max(0, dbTurn.outputTokens - reasoning);
20682
+ }
19817
20683
 
19818
- if (dbContextTokens > 0) {
19819
- contextTokens = dbContextTokens;
19820
- }
19821
- if (lastPlannerModel && model !== lastPlannerModel) {
19822
- previousContextTokens = 0;
19823
- }
19824
- const inputDelta = Math.max(0, contextTokens - previousContextTokens);
20684
+ delta.input_tokens = uncached;
20685
+ delta.cached_input_tokens = cached;
20686
+ delta.output_tokens = output;
20687
+ delta.reasoning_output_tokens = reasoning;
20688
+ delta.total_tokens = uncached + cached + output + reasoning;
20689
+ delta.billable_total_tokens = delta.total_tokens;
20690
+ delta.conversation_count = 1;
20691
+ billedPlanner = delta.total_tokens > 0;
20692
+
20693
+ contextTokens = dbContextTokens > 0 ? dbContextTokens : uncached + cached;
20694
+ } else {
20695
+ const content = typeof parsed.content === "string" ? parsed.content : "";
20696
+ const thinking = typeof parsed.thinking === "string" ? parsed.thinking : "";
19825
20697
 
19826
- const outputTokens =
19827
- antigravityValueTokens(content) + antigravityValueTokens(parsed.tool_calls);
19828
- const reasoningTokens = antigravityValueTokens(thinking);
20698
+ if (dbContextTokens > 0) {
20699
+ contextTokens = dbContextTokens;
20700
+ }
20701
+ if (lastPlannerModel && model !== lastPlannerModel) {
20702
+ previousContextTokens = 0;
20703
+ }
20704
+ const inputDelta = Math.max(0, contextTokens - previousContextTokens);
20705
+
20706
+ const outputTokens =
20707
+ antigravityValueTokens(content) + antigravityValueTokens(parsed.tool_calls);
20708
+ const reasoningTokens = antigravityValueTokens(thinking);
19829
20709
 
19830
- delta.input_tokens = inputDelta;
19831
- delta.output_tokens = outputTokens;
19832
- delta.reasoning_output_tokens = reasoningTokens;
19833
- delta.total_tokens = inputDelta + outputTokens + reasoningTokens;
19834
- delta.billable_total_tokens = delta.total_tokens;
19835
- delta.conversation_count = 1;
19836
- billedPlanner = delta.total_tokens > 0;
20710
+ delta.input_tokens = inputDelta;
20711
+ delta.output_tokens = outputTokens;
20712
+ delta.reasoning_output_tokens = reasoningTokens;
20713
+ delta.total_tokens = inputDelta + outputTokens + reasoningTokens;
20714
+ delta.billable_total_tokens = delta.total_tokens;
20715
+ delta.conversation_count = 1;
20716
+ billedPlanner = delta.total_tokens > 0;
20717
+ }
19837
20718
  }
19838
20719
 
19839
- if (!billedPlanner) {
20720
+ if (!billedPlanner || i < baselineLine) {
20721
+ if (billedPlanner) {
20722
+ previousContextTokens = contextTokens;
20723
+ lastPlannerModel = model;
20724
+ }
19840
20725
  contextTokens += eventContextTokens;
19841
20726
  lastCompletedLine = i + 1;
19842
20727
  continue;
19843
20728
  }
19844
20729
 
19845
- const bucket = getHourlyBucket(hourlyState, source, model, bucketStart);
19846
- addTotals(bucket.totals, delta);
19847
- touchedBuckets.add(bucketKey(source, model, bucketStart));
19848
-
19849
- if (projectKey && projectState && projectTouchedBuckets) {
19850
- const projectBucket = getProjectBucket(
19851
- projectState,
19852
- projectKey,
20730
+ const cKey = `${source}|${model}|${bucketStart}|${projectKey || ""}`;
20731
+ if (!contributions[cKey]) {
20732
+ contributions[cKey] = {
19853
20733
  source,
20734
+ model,
19854
20735
  bucketStart,
19855
- projectRef,
19856
- );
19857
- addTotals(projectBucket.totals, delta);
19858
- projectTouchedBuckets.add(projectBucketKey(projectKey, source, bucketStart));
20736
+ projectKey: projectKey || null,
20737
+ projectRef: projectRef || null,
20738
+ totals: initTotals(),
20739
+ };
20740
+ }
20741
+ addTotals(contributions[cKey].totals, delta);
20742
+
20743
+ if (applyBuckets !== false && hourlyState && touchedBuckets) {
20744
+ const bucket = getHourlyBucket(hourlyState, source, model, bucketStart);
20745
+ addTotals(bucket.totals, delta);
20746
+ touchedBuckets.add(bucketKey(source, model, bucketStart));
20747
+
20748
+ if (projectKey && projectState && projectTouchedBuckets) {
20749
+ const projectBucket = getProjectBucket(
20750
+ projectState,
20751
+ projectKey,
20752
+ source,
20753
+ bucketStart,
20754
+ projectRef,
20755
+ );
20756
+ addTotals(projectBucket.totals, delta);
20757
+ projectTouchedBuckets.add(projectBucketKey(projectKey, source, bucketStart));
20758
+ }
20759
+ }
20760
+ const isNewForCounter = watermarkLine > 0 ? i >= watermarkLine : true;
20761
+ if (isNewForCounter) {
20762
+ eventsAggregated += 1;
19859
20763
  }
19860
- eventsAggregated += 1;
19861
20764
  // Snapshot the pre-planner context first. The planner's own content+tool_calls
19862
20765
  // (eventContextTokens, added below) become part of the next turn's history,
19863
20766
  // so they MUST be billed as input on the next planner — don't fold them into
@@ -19875,7 +20778,9 @@ async function parseAntigravityFile({
19875
20778
  previousContextTokens,
19876
20779
  currentModel,
19877
20780
  lastPlannerModel,
19878
- usageSource: stepMap ? "sqlite" : "estimated",
20781
+ usageSource: hasSqliteMetadata ? "sqlite" : "estimated",
20782
+ dbReadOk: !dbPath || stepMap !== null,
20783
+ contributions,
19879
20784
  };
19880
20785
  }
19881
20786
 
@@ -22033,9 +22938,12 @@ module.exports = {
22033
22938
  parseGrokBuildIncremental,
22034
22939
 
22035
22940
  // Antigravity (Google Gemini) - Session logs parser
22941
+ ANTIGRAVITY_CURSOR_VERSION,
22942
+ ANTIGRAVITY_EXTRACTOR_REVISION,
22036
22943
  resolveAntigravityBrainDirs,
22037
22944
  listAntigravitySessionFiles,
22038
22945
  listAntigravityTranscripts,
22946
+ listAntigravityTranscriptsWithStatus,
22039
22947
  parseAntigravityIncremental,
22040
22948
  estimateAntigravityTokens,
22041
22949
  isCjkCodePoint,