tokentracker-cli 0.81.2 → 0.81.3

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 (128) hide show
  1. package/dashboard/dist/assets/AchievementBadge-DiSoAalB.js +1 -0
  2. package/dashboard/dist/assets/AchievementsPage-DI_cvzqy.js +2 -0
  3. package/dashboard/dist/assets/AchievementsSection-ClWZZNvc.js +1 -0
  4. package/dashboard/dist/assets/BadgeDetailModal-DpQeG_6H.js +1 -0
  5. package/dashboard/dist/assets/Card-B0hyofoy.js +1 -0
  6. package/dashboard/dist/assets/ClawdAnimated-DZJGQ3-9.js +1 -0
  7. package/dashboard/dist/assets/CommunityStatsModal-Dcikt0kT.js +1 -0
  8. package/dashboard/dist/assets/ConfirmModal-DCfRP4oI.js +1 -0
  9. package/dashboard/dist/assets/DashboardPage-DBajsWEp.js +1 -0
  10. package/dashboard/dist/assets/DevicePage-De6KuFaJ.js +1 -0
  11. package/dashboard/dist/assets/DialogDescription-uQ4gVIv9.js +1 -0
  12. package/dashboard/dist/assets/DialogTitle-BvElfi0r.js +1 -0
  13. package/dashboard/dist/assets/FadeIn-DZoN7vbQ.js +1 -0
  14. package/dashboard/dist/assets/HeaderGithubStar-C9HQnEsI.js +1 -0
  15. package/dashboard/dist/assets/{IpCheckPage-CMf9CHFr.js → IpCheckPage-82zniXwO.js} +3 -3
  16. package/dashboard/dist/assets/{LandingPage-BT61_TqT.js → LandingPage-Df9fdR0n.js} +168 -168
  17. package/dashboard/dist/assets/LeaderboardAvatar-CjfEgoR8.js +1 -0
  18. package/dashboard/dist/assets/LeaderboardPage-BdbE6Nhj.js +2 -0
  19. package/dashboard/dist/assets/{LeaderboardProfileModal-C_kxqOMn.js → LeaderboardProfileModal-CDTK21dp.js} +4 -4
  20. package/dashboard/dist/assets/{LeaderboardProfilePage-zgNsE0EQ.js → LeaderboardProfilePage-B6QWnwB1.js} +3 -3
  21. package/dashboard/dist/assets/{LimitsPage-BiQpqxj8.js → LimitsPage-f0L0d15j.js} +4 -4
  22. package/dashboard/dist/assets/LocalOnlyNotice-DOcSDrOy.js +1 -0
  23. package/dashboard/dist/assets/LoginPage-WY_sRA7I.js +1 -0
  24. package/dashboard/dist/assets/PetPage-CNbgNE6f.js +1 -0
  25. package/dashboard/dist/assets/PopoverPopup-Cv525-dF.js +1 -0
  26. package/dashboard/dist/assets/ResetPasswordPage-vrcj6bBP.js +1 -0
  27. package/dashboard/dist/assets/Select-3HkgkO8y.js +1 -0
  28. package/dashboard/dist/assets/SelectItemText-Bz8FAKEv.js +1 -0
  29. package/dashboard/dist/assets/SettingsPage-CVldAt69.js +1 -0
  30. package/dashboard/dist/assets/SkillsPage-Dnz3eGcE.js +1 -0
  31. package/dashboard/dist/assets/TrendMonitor-OopQ06B8.js +19 -0
  32. package/dashboard/dist/assets/WidgetsPage-4dWjZkc9.js +1 -0
  33. package/dashboard/dist/assets/WrappedPage-Dbd-8EKD.js +1 -0
  34. package/dashboard/dist/assets/__vite-browser-external-CQxW1DuO.css +1 -0
  35. package/dashboard/dist/assets/__vite-browser-external-CxN2E6q5.js +1312 -0
  36. package/dashboard/dist/assets/agent-logos-CT9fumzx.js +1 -0
  37. package/dashboard/dist/assets/arrow-up-right-CGnVz9Km.js +1 -0
  38. package/dashboard/dist/assets/check-CkI3WNti.js +1 -0
  39. package/dashboard/dist/assets/copy-Bh3g2_O-.js +1 -0
  40. package/dashboard/dist/assets/download-BITw7nKX.js +1 -0
  41. package/dashboard/dist/assets/flame-ByAsnNar.js +1 -0
  42. package/dashboard/dist/assets/format-tokens-Cu8JtuN4.js +1 -0
  43. package/dashboard/dist/assets/icons-D9OB0N43.js +1 -0
  44. package/dashboard/dist/assets/index-lKnLA6Pd.js +1 -0
  45. package/dashboard/dist/assets/info-Mu2CLf-A.js +1 -0
  46. package/dashboard/dist/assets/link-2-CSlVaUw9.js +1 -0
  47. package/dashboard/dist/assets/loader-circle-CYck0YaS.js +1 -0
  48. package/dashboard/dist/assets/provider-display-BB925tPa.js +42 -0
  49. package/dashboard/dist/assets/terminal-4YysOnTe.js +1 -0
  50. package/dashboard/dist/assets/trash-2-C8-ZfrZG.js +1 -0
  51. package/dashboard/dist/assets/use-community-stats-Cxt-otko.js +1 -0
  52. package/dashboard/dist/assets/use-limits-display-prefs-BUFZh6fe.js +1 -0
  53. package/dashboard/dist/assets/use-native-settings-DbdPXxGf.js +1 -0
  54. package/dashboard/dist/assets/{use-ordered-list-DPbiKnPc.js → use-ordered-list-BZMM-iIj.js} +2 -2
  55. package/dashboard/dist/assets/use-quality-per-dollar-pref-DXoW5A0_.js +1 -0
  56. package/dashboard/dist/assets/use-transform-CuAoUb8H.js +1 -0
  57. package/dashboard/dist/assets/use-usage-limits-BEc3QMzR.js +1 -0
  58. package/dashboard/dist/assets/useCurrency-CpDnFC0N.js +1 -0
  59. package/dashboard/dist/assets/useScrollLock-Vyaif-7C.js +1 -0
  60. package/dashboard/dist/assets/useTokenFormat-B8FrUarf.js +1 -0
  61. package/dashboard/dist/assets/zap-CdeKJ6HL.js +1 -0
  62. package/dashboard/dist/index.html +2 -2
  63. package/dashboard/dist/share.html +2 -2
  64. package/package.json +1 -1
  65. package/src/commands/serve.js +8 -6
  66. package/src/commands/status.js +2 -1
  67. package/src/commands/sync.js +164 -36
  68. package/src/lib/codex-context-breakdown.js +330 -13
  69. package/src/lib/codex-rollout-parser.js +323 -42
  70. package/src/lib/cursor-store.js +1289 -0
  71. package/src/lib/diagnostics.js +8 -5
  72. package/src/lib/fs.js +254 -16
  73. package/src/lib/local-api.js +106 -17
  74. package/src/lib/pricing/index.js +8 -0
  75. package/src/lib/pricing/seed-snapshot.json +1 -1
  76. package/src/lib/rollout.js +101 -9
  77. package/dashboard/dist/assets/AchievementBadge-DXWHZrAL.js +0 -1
  78. package/dashboard/dist/assets/AchievementsPage-BhrQXniL.js +0 -2
  79. package/dashboard/dist/assets/AchievementsSection-gzP3m1Ml.js +0 -1
  80. package/dashboard/dist/assets/BadgeDetailModal-DGKPuvDU.js +0 -1
  81. package/dashboard/dist/assets/Card-BRtYQ-Ot.js +0 -1
  82. package/dashboard/dist/assets/ClawdAnimated-UsNG-qDv.js +0 -1
  83. package/dashboard/dist/assets/CommunityStatsModal-CJQw-P4t.js +0 -1
  84. package/dashboard/dist/assets/ConfirmModal-ByY4GDsy.js +0 -1
  85. package/dashboard/dist/assets/DashboardPage-CuLVnW98.js +0 -19
  86. package/dashboard/dist/assets/DevicePage-D67CxpmR.js +0 -1
  87. package/dashboard/dist/assets/DialogClose-Ci8hkTw5.js +0 -1
  88. package/dashboard/dist/assets/DialogDescription-C9vXUZU0.js +0 -1
  89. package/dashboard/dist/assets/DialogTitle-sLJxKWjv.js +0 -1
  90. package/dashboard/dist/assets/FadeIn-xVds37w6.js +0 -1
  91. package/dashboard/dist/assets/HeaderGithubStar-kK3omgmB.js +0 -1
  92. package/dashboard/dist/assets/LeaderboardAvatar-DyKz6NdT.js +0 -1
  93. package/dashboard/dist/assets/LeaderboardPage-DJ4zihJJ.js +0 -2
  94. package/dashboard/dist/assets/LocalOnlyNotice-DTrrUVbZ.js +0 -1
  95. package/dashboard/dist/assets/LoginPage-BOM9Q2sI.js +0 -1
  96. package/dashboard/dist/assets/PetPage-BURk7RG-.js +0 -1
  97. package/dashboard/dist/assets/PopoverPopup-BV_aoNBd.js +0 -1
  98. package/dashboard/dist/assets/ResetPasswordPage-SBL2LvXC.js +0 -1
  99. package/dashboard/dist/assets/Select-BsRdHde3.js +0 -1
  100. package/dashboard/dist/assets/SelectItemText-DgkrZxIq.js +0 -1
  101. package/dashboard/dist/assets/SettingsPage-DqxVsfnB.js +0 -1
  102. package/dashboard/dist/assets/SkillsPage-B0VfWkOh.js +0 -1
  103. package/dashboard/dist/assets/WidgetsPage-4QQ8wI_g.js +0 -1
  104. package/dashboard/dist/assets/WrappedPage-BK53khVY.js +0 -1
  105. package/dashboard/dist/assets/agent-logos-DXpW_Wmz.js +0 -1
  106. package/dashboard/dist/assets/arrow-up-right-DCc--V5G.js +0 -1
  107. package/dashboard/dist/assets/check-DqxuosV2.js +0 -1
  108. package/dashboard/dist/assets/copy-qChAT7MY.js +0 -1
  109. package/dashboard/dist/assets/download-W9R3sBph.js +0 -1
  110. package/dashboard/dist/assets/flame-0eyhMnfU.js +0 -1
  111. package/dashboard/dist/assets/format-tokens-CkfBUcoa.js +0 -1
  112. package/dashboard/dist/assets/info-DY1O85-U.js +0 -1
  113. package/dashboard/dist/assets/link-2-B_Z66hFT.js +0 -1
  114. package/dashboard/dist/assets/loader-circle-xH40lon2.js +0 -1
  115. package/dashboard/dist/assets/main-CeaB5N36.js +0 -1259
  116. package/dashboard/dist/assets/main-DAc77vwF.css +0 -1
  117. package/dashboard/dist/assets/provider-display-C_XKZQcF.js +0 -42
  118. package/dashboard/dist/assets/trash-2-DVhNJ74r.js +0 -1
  119. package/dashboard/dist/assets/use-community-stats-CO-nr2Vf.js +0 -1
  120. package/dashboard/dist/assets/use-limits-display-prefs-RRqmyoxt.js +0 -1
  121. package/dashboard/dist/assets/use-native-settings-79piPeET.js +0 -1
  122. package/dashboard/dist/assets/use-quality-per-dollar-pref-DyF49QRF.js +0 -1
  123. package/dashboard/dist/assets/use-transform-BvXyGAtb.js +0 -1
  124. package/dashboard/dist/assets/use-usage-limits-BiZ_I-KE.js +0 -1
  125. package/dashboard/dist/assets/useCurrency-D-C1e7eE.js +0 -1
  126. package/dashboard/dist/assets/useScrollLock-BfU2EMvG.js +0 -1
  127. package/dashboard/dist/assets/useTokenFormat-Cm4EvrQp.js +0 -1
  128. package/dashboard/dist/assets/zap-DlAOTwFs.js +0 -1
@@ -11,6 +11,7 @@
11
11
  "use strict";
12
12
 
13
13
  const crypto = require("node:crypto");
14
+ const fs = require("node:fs");
14
15
  const os = require("node:os");
15
16
  const path = require("node:path");
16
17
 
@@ -31,6 +32,7 @@ const {
31
32
  finalizeSkillRows,
32
33
  finalizeExecRows,
33
34
  buildSkillStatsEntry,
35
+ isCodexResumeInvalidated,
34
36
  } = require("./codex-rollout-parser");
35
37
 
36
38
  // ---------------------------------------------------------------------------
@@ -85,6 +87,50 @@ function mergeExecRows(map, rows) {
85
87
  }
86
88
  }
87
89
 
90
+ function mergeParsedSessionResults(previous, delta) {
91
+ if (!previous) return delta;
92
+ if (!delta) return previous;
93
+
94
+ const totals = emptyTotals();
95
+ mergeRollupTotals(totals, previous.totals || {});
96
+ mergeRollupTotals(totals, delta.totals || {});
97
+
98
+ const tools = new Map();
99
+ mergeRows(tools, previous.toolBreakdown?.tool_rows);
100
+ mergeRows(tools, delta.toolBreakdown?.tool_rows);
101
+ const skills = new Map();
102
+ mergeSkillRows(skills, previous.skillsBreakdown?.skill_rows);
103
+ mergeSkillRows(skills, delta.skillsBreakdown?.skill_rows);
104
+
105
+ const mergeExecGroup = (key) => {
106
+ const rows = new Map();
107
+ mergeExecRows(rows, previous.execCommandBreakdown?.[key]);
108
+ mergeExecRows(rows, delta.execCommandBreakdown?.[key]);
109
+ return finalizeExecRows(rows);
110
+ };
111
+
112
+ return {
113
+ sessionId: delta.sessionId || previous.sessionId,
114
+ cwd: delta.cwd || previous.cwd,
115
+ model: delta.model || previous.model,
116
+ provider: delta.provider || previous.provider,
117
+ version: delta.version || previous.version,
118
+ filePath: previous.filePath || delta.filePath,
119
+ turnCount: Number(previous.turnCount || 0) + Number(delta.turnCount || 0),
120
+ totals,
121
+ toolBreakdown: { tool_rows: finalizeToolRows(tools) },
122
+ skillsBreakdown: { skill_rows: finalizeSkillRows(skills) },
123
+ execCommandBreakdown: {
124
+ byType: mergeExecGroup("byType"),
125
+ byExit: mergeExecGroup("byExit"),
126
+ byExecutable: mergeExecGroup("byExecutable"),
127
+ byCommand: mergeExecGroup("byCommand"),
128
+ byDuration: mergeExecGroup("byDuration"),
129
+ byOutput: mergeExecGroup("byOutput"),
130
+ },
131
+ };
132
+ }
133
+
88
134
  // ---------------------------------------------------------------------------
89
135
  // Period helpers
90
136
  // ---------------------------------------------------------------------------
@@ -122,8 +168,9 @@ function buildDateRange({ period, date }) {
122
168
  const CACHE = new Map();
123
169
  const PARSED_GROUP_CACHE = new Map();
124
170
  const CACHE_TTL_MS = 60_000;
125
- const CACHE_SCHEMA_VERSION = "codex-context-v4";
171
+ const CACHE_SCHEMA_VERSION = "codex-context-v6";
126
172
  const MAX_PARSED_GROUP_CACHE_ENTRIES = 4096;
173
+ const PREFIX_HASH_READ_BYTES = 1024 * 1024;
127
174
 
128
175
  function cacheTimeZoneKey(timeZoneContext) {
129
176
  if (!timeZoneContext) return "";
@@ -131,7 +178,26 @@ function cacheTimeZoneKey(timeZoneContext) {
131
178
  }
132
179
 
133
180
  function statIdentity(filePath, stat) {
134
- return JSON.stringify([filePath, stat.size, stat.mtimeMs, stat.ctimeMs, stat.ino]);
181
+ return JSON.stringify([filePath, stat.dev, stat.ino, stat.size, stat.mtimeMs, stat.ctimeMs]);
182
+ }
183
+
184
+ function contentStatIdentity(filePath, stat) {
185
+ return JSON.stringify([
186
+ filePath,
187
+ stat.dev,
188
+ stat.ino,
189
+ stat.size,
190
+ stat.mtimeMs,
191
+ ]);
192
+ }
193
+
194
+ function isUnlinkedResumeSnapshot(filePath, before, after) {
195
+ return Boolean(
196
+ before &&
197
+ after &&
198
+ Number(after.nlink) === 0 &&
199
+ contentStatIdentity(filePath, before) === contentStatIdentity(filePath, after),
200
+ );
135
201
  }
136
202
 
137
203
  function inventorySignature(entries) {
@@ -152,6 +218,15 @@ function parsedGroupCacheKey(entries, { from, to, timeZoneContext }) {
152
218
  hash.update(to || "");
153
219
  hash.update("\0");
154
220
  hash.update(cacheTimeZoneKey(timeZoneContext));
221
+ for (const { filePath } of entries) {
222
+ hash.update(filePath);
223
+ hash.update("\0");
224
+ }
225
+ return hash.digest("hex");
226
+ }
227
+
228
+ function parsedGroupSignature(entries) {
229
+ const hash = crypto.createHash("sha256");
155
230
  for (const { filePath, stat } of entries) {
156
231
  hash.update(statIdentity(filePath, stat));
157
232
  hash.update("\0");
@@ -159,6 +234,123 @@ function parsedGroupCacheKey(entries, { from, to, timeZoneContext }) {
159
234
  return hash.digest("hex");
160
235
  }
161
236
 
237
+ function selectAppendHashPaths(candidateGroups) {
238
+ // Only the newest single-file group pays the cold hash cost. Any file that
239
+ // later grows becomes newest, takes one conservative full parse, and keeps
240
+ // its hash state for subsequent append-only refreshes.
241
+ let newest = null;
242
+ for (const entries of candidateGroups.values()) {
243
+ if (
244
+ entries.length !== 1 ||
245
+ !rolloutSessionIdFromPath(entries[0].filePath)
246
+ ) continue;
247
+ const entry = entries[0];
248
+ if (!newest) {
249
+ newest = entry;
250
+ continue;
251
+ }
252
+ const mtimeDelta = Number(entry.stat?.mtimeMs || 0) -
253
+ Number(newest.stat?.mtimeMs || 0);
254
+ const ctimeDelta = Number(entry.stat?.ctimeMs || 0) -
255
+ Number(newest.stat?.ctimeMs || 0);
256
+ if (
257
+ mtimeDelta > 0 ||
258
+ (mtimeDelta === 0 && ctimeDelta > 0) ||
259
+ (mtimeDelta === 0 && ctimeDelta === 0 &&
260
+ entry.filePath.localeCompare(newest.filePath) < 0)
261
+ ) {
262
+ newest = entry;
263
+ }
264
+ }
265
+ return newest ? new Set([newest.filePath]) : new Set();
266
+ }
267
+
268
+ async function digestFilePrefix(
269
+ sourceHandle,
270
+ endOffset,
271
+ diagnostics = null,
272
+ ) {
273
+ const end = Math.max(0, Number(endOffset) || 0);
274
+ const hash = crypto.createHash("sha256");
275
+ if (end === 0) return hash.digest("hex");
276
+ let bytesRead = 0;
277
+ const buffer = Buffer.allocUnsafe(PREFIX_HASH_READ_BYTES);
278
+ while (bytesRead < end) {
279
+ const length = Math.min(buffer.length, end - bytesRead);
280
+ const result = await sourceHandle.read(buffer, 0, length, bytesRead);
281
+ if (!result.bytesRead) break;
282
+ hash.update(buffer.subarray(0, result.bytesRead));
283
+ bytesRead += result.bytesRead;
284
+ }
285
+ if (diagnostics) {
286
+ diagnostics.prefix_validation_bytes =
287
+ Number(diagnostics.prefix_validation_bytes || 0) + bytesRead;
288
+ }
289
+ return bytesRead === end ? hash.digest("hex") : null;
290
+ }
291
+
292
+ async function openValidatedResumeFile(cached, entry, diagnostics = null) {
293
+ if (
294
+ !cached?.resumeState ||
295
+ !cached?.stat ||
296
+ !cached?.contentHashState ||
297
+ typeof cached.contentHashState.copy !== "function" ||
298
+ cached.appendable !== true ||
299
+ cached.filePath !== entry.filePath
300
+ ) return null;
301
+ const previous = cached.stat;
302
+ const current = entry.stat;
303
+ if (
304
+ Number(previous.dev) !== Number(current.dev) ||
305
+ Number(previous.ino) !== Number(current.ino) ||
306
+ Number(current.size) <= Number(previous.size) ||
307
+ Number(cached.endOffset) < 0 ||
308
+ Number(cached.endOffset) > Number(previous.size)
309
+ ) {
310
+ return null;
311
+ }
312
+ let handle = null;
313
+ let keepOpen = false;
314
+ try {
315
+ // Validation and suffix parsing share this handle so a path replacement
316
+ // cannot splice cached aggregates onto bytes from a different file.
317
+ handle = await fs.promises.open(entry.filePath, "r");
318
+ if (diagnostics) diagnostics.stat_calls += 1;
319
+ const opened = await handle.stat();
320
+ if (
321
+ statIdentity(entry.filePath, opened) !==
322
+ statIdentity(entry.filePath, current)
323
+ ) return null;
324
+
325
+ const expected = cached.contentHashState.copy().digest("hex");
326
+ const actual = await digestFilePrefix(
327
+ handle,
328
+ cached.endOffset,
329
+ diagnostics,
330
+ );
331
+ if (actual !== expected) return null;
332
+ if (diagnostics) diagnostics.stat_calls += 1;
333
+ const afterValidation = await handle.stat();
334
+ if (
335
+ statIdentity(entry.filePath, current) !==
336
+ statIdentity(entry.filePath, afterValidation) &&
337
+ !isUnlinkedResumeSnapshot(entry.filePath, current, afterValidation)
338
+ ) return null;
339
+ keepOpen = true;
340
+ return handle;
341
+ } catch {
342
+ return null;
343
+ } finally {
344
+ if (handle && !keepOpen) {
345
+ try {
346
+ await handle.close();
347
+ } catch {
348
+ // A failed validation already forces the conservative full parse.
349
+ }
350
+ }
351
+ }
352
+ }
353
+
162
354
  function rememberParsedGroup(key, value) {
163
355
  PARSED_GROUP_CACHE.delete(key);
164
356
  PARSED_GROUP_CACHE.set(key, value);
@@ -169,6 +361,17 @@ function rememberParsedGroup(key, value) {
169
361
  }
170
362
  }
171
363
 
364
+ function splitCapturedParse(parsed) {
365
+ const {
366
+ resumeState = null,
367
+ endOffset = 0,
368
+ appendable = false,
369
+ contentHashState = null,
370
+ ...result
371
+ } = parsed;
372
+ return { parsed: result, resumeState, endOffset, appendable, contentHashState };
373
+ }
374
+
172
375
  // ---------------------------------------------------------------------------
173
376
  // Main entry
174
377
  // ---------------------------------------------------------------------------
@@ -208,6 +411,10 @@ async function computeCodexContextBreakdown({
208
411
  opened_files: 0,
209
412
  parsed_files: 0,
210
413
  parse_cache_hits: 0,
414
+ incremental_parse_hits: 0,
415
+ incremental_parse_fallbacks: 0,
416
+ bytes_read: 0,
417
+ prefix_validation_bytes: 0,
211
418
  json_parse_calls: 0,
212
419
  };
213
420
  const discovered = discoverCodexSessionFiles(roots, {
@@ -245,6 +452,7 @@ async function computeCodexContextBreakdown({
245
452
  if (!candidateGroups.has(sessionKey)) candidateGroups.set(sessionKey, []);
246
453
  candidateGroups.get(sessionKey).push(entry);
247
454
  }
455
+ const appendHashPaths = selectAppendHashPaths(candidateGroups);
248
456
 
249
457
  for (const entries of candidateGroups.values()) {
250
458
  const filePaths = entries.map((entry) => entry.filePath);
@@ -252,22 +460,131 @@ async function computeCodexContextBreakdown({
252
460
  const parsedCacheKey = cacheable
253
461
  ? parsedGroupCacheKey(entries, { from: fromKey, to: toKey, timeZoneContext })
254
462
  : null;
255
- let parsed = parsedCacheKey ? PARSED_GROUP_CACHE.get(parsedCacheKey) : null;
256
- if (parsed) {
463
+ const signature = parsedGroupSignature(entries);
464
+ const cachedGroup = parsedCacheKey ? PARSED_GROUP_CACHE.get(parsedCacheKey) : null;
465
+ let parsed = null;
466
+ if (cachedGroup?.signature === signature) {
257
467
  // Cacheable groups are partitioned by rollout UUID, so their token event
258
468
  // keys cannot overlap and cache hits need not backfill seenTokenEvents.
259
469
  diagnostics.parse_cache_hits += 1;
470
+ parsed = cachedGroup.parsed;
260
471
  PARSED_GROUP_CACHE.delete(parsedCacheKey);
261
- PARSED_GROUP_CACHE.set(parsedCacheKey, parsed);
472
+ PARSED_GROUP_CACHE.set(parsedCacheKey, cachedGroup);
262
473
  } else {
263
- parsed = await parseCodexRolloutFile(filePaths.length === 1 ? filePaths[0] : filePaths, {
264
- from: fromKey,
265
- to: toKey,
266
- timeZoneContext,
267
- diagnostics,
268
- seenTokenEvents,
269
- });
270
- if (parsedCacheKey) rememberParsedGroup(parsedCacheKey, parsed);
474
+ let captured = null;
475
+ const singleEntry = entries.length === 1 ? entries[0] : null;
476
+ const appendHashEligible = Boolean(
477
+ singleEntry && appendHashPaths.has(singleEntry.filePath),
478
+ );
479
+ const resumeHandle = appendHashEligible
480
+ ? await openValidatedResumeFile(cachedGroup, singleEntry, diagnostics)
481
+ : null;
482
+ if (resumeHandle) {
483
+ try {
484
+ const resumed = await parseCodexRolloutFile(singleEntry.filePath, {
485
+ from: fromKey,
486
+ to: toKey,
487
+ timeZoneContext,
488
+ diagnostics,
489
+ seenTokenEvents: new Set(),
490
+ startOffset: cachedGroup.endOffset,
491
+ endOffset: singleEntry.stat.size,
492
+ resumeState: cachedGroup.resumeState,
493
+ captureResumeState: true,
494
+ captureContentHash: true,
495
+ contentHashState: cachedGroup.contentHashState,
496
+ sourceHandle: resumeHandle,
497
+ });
498
+ if (diagnostics) diagnostics.stat_calls += 1;
499
+ const afterParse = await resumeHandle.stat().catch(() => null);
500
+ let stableAfterParse = Boolean(
501
+ afterParse &&
502
+ statIdentity(singleEntry.filePath, afterParse) ===
503
+ statIdentity(singleEntry.filePath, singleEntry.stat),
504
+ );
505
+ if (
506
+ !stableAfterParse &&
507
+ isUnlinkedResumeSnapshot(
508
+ singleEntry.filePath,
509
+ singleEntry.stat,
510
+ afterParse,
511
+ )
512
+ ) {
513
+ try {
514
+ const expectedSnapshot = resumed.contentHashState.copy().digest("hex");
515
+ const actualSnapshot = await digestFilePrefix(
516
+ resumeHandle,
517
+ singleEntry.stat.size,
518
+ diagnostics,
519
+ );
520
+ stableAfterParse = actualSnapshot === expectedSnapshot;
521
+ } catch {
522
+ stableAfterParse = false;
523
+ }
524
+ }
525
+ if (stableAfterParse) {
526
+ const delta = splitCapturedParse(resumed);
527
+ captured = {
528
+ parsed: mergeParsedSessionResults(cachedGroup.parsed, delta.parsed),
529
+ resumeState: delta.resumeState,
530
+ endOffset: delta.endOffset,
531
+ appendable: delta.appendable,
532
+ contentHashState: delta.contentHashState,
533
+ };
534
+ diagnostics.incremental_parse_hits += 1;
535
+ } else {
536
+ diagnostics.incremental_parse_fallbacks += 1;
537
+ }
538
+ } catch (error) {
539
+ if (!isCodexResumeInvalidated(error)) throw error;
540
+ diagnostics.incremental_parse_fallbacks += 1;
541
+ } finally {
542
+ await resumeHandle.close();
543
+ }
544
+ }
545
+
546
+ if (!captured) {
547
+ let freshHandle = null;
548
+ let freshEndOffset = singleEntry?.stat?.size ?? null;
549
+ try {
550
+ if (resumeHandle && singleEntry) {
551
+ freshHandle = await fs.promises.open(singleEntry.filePath, "r");
552
+ if (diagnostics) diagnostics.stat_calls += 1;
553
+ freshEndOffset = (await freshHandle.stat()).size;
554
+ }
555
+ const fresh = await parseCodexRolloutFile(
556
+ filePaths.length === 1 ? filePaths[0] : filePaths,
557
+ {
558
+ from: fromKey,
559
+ to: toKey,
560
+ timeZoneContext,
561
+ diagnostics,
562
+ seenTokenEvents: filePaths.length === 1 ? new Set() : seenTokenEvents,
563
+ endOffset: freshEndOffset,
564
+ captureResumeState: Boolean(singleEntry),
565
+ captureContentHash: appendHashEligible,
566
+ sourceHandle: freshHandle,
567
+ },
568
+ );
569
+ captured = splitCapturedParse(fresh);
570
+ } finally {
571
+ await freshHandle?.close();
572
+ }
573
+ }
574
+
575
+ parsed = captured.parsed;
576
+ if (parsedCacheKey) {
577
+ rememberParsedGroup(parsedCacheKey, {
578
+ signature,
579
+ parsed,
580
+ resumeState: captured.resumeState,
581
+ endOffset: captured.endOffset,
582
+ appendable: captured.appendable === true,
583
+ contentHashState: captured.contentHashState,
584
+ filePath: singleEntry?.filePath || null,
585
+ stat: singleEntry?.stat || null,
586
+ });
587
+ }
271
588
  }
272
589
  if (!parsed || !parsed.totals || !parsed.totals.total_tokens) continue;
273
590
  sessions.push(parsed);