tokentracker-cli 1.4.3 → 1.4.6
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.
- package/README.de.md +1 -0
- package/README.ja.md +1 -0
- package/README.ko.md +1 -0
- package/README.md +5 -0
- package/README.zh-CN.md +1 -0
- package/dashboard/dist/assets/{AchievementBadge-3IYidbrI.js → AchievementBadge-BmWRsZMx.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-DmZ_ojyM.js → AchievementsPage-CyIP2nck.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-DIYrrzhw.js → AchievementsSection-BeNyJ0LW.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-DMMExCe4.js → BadgeDetailModal-DIFnFzkv.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-y6vZNRvC.js → BotAnimated-CKVeJHit.js} +1 -1
- package/dashboard/dist/assets/{Card-LJX3G3zo.js → Card-lQmyKbh4.js} +1 -1
- package/dashboard/dist/assets/{CloudCheckoutPage-BGBgSXNE.js → CloudCheckoutPage-B_8y6dyX.js} +1 -1
- package/dashboard/dist/assets/{CloudPage-Dij2GTXi.js → CloudPage-BOQjUGAt.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-BIky6Fk0.js → CommandPalette-BV9LqokI.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-DbM99i0s.js → CommunityStatsModal-WELxKEFc.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-C_NH8vgO.js → ConfirmModal-B8H5o8c_.js} +1 -1
- package/dashboard/dist/assets/{Controls-BEAj_kvr.js → Controls-9ynPx9rF.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-DO7h2dfu.js → DashboardPage-B8z-U63p.js} +1 -1
- package/dashboard/dist/assets/{DateRangePopover-PQXB8v0k.js → DateRangePopover-DEqWsV_8.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-0JZ3OIWZ.js → DevicePage-CUOlUySM.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-BpWoD5h9.js → DialogClose-Dlg52bL6.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-vuQFBlM_.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-DoQgdh5w.js → DialogTitle-CduwXjqH.js} +1 -1
- package/dashboard/dist/assets/{ElectricLogo-0O87VjKv.js → ElectricLogo-DFt7_F2f.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-9ph1h_AG.js → FadeIn-DlyusE6n.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-DCZ92xbH.js → HeaderGithubStar-tUtJoFHh.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-CCKdmuOP.js → HoverTooltip-v-zkCqsX.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-CpyhpmdS.js → IpCheckPage-C5Y1uVEr.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-spvsPkYy.js → LandingPage-DzOJOAj7.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardPage-2nJ08ohw.js → LeaderboardPage-BBZ_6tCw.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProBadge-D6FSL3Qy.js → LeaderboardProBadge-DakoXIQn.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-DD1jX-rD.js → LeaderboardProfileModal-CINpES_x.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-DYqRrntL.js → LeaderboardProfilePage-B6ISf-1l.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-DXHikunk.js → LimitsPage-UVX730CQ.js} +8 -8
- package/dashboard/dist/assets/{LocalOnlyNotice-BURiQAtP.js → LocalOnlyNotice-BJSsmXF1.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-Cv2ijoJx.js → LoginCard-Dggv2aGv.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-DyOideDA.js → LoginModal-B8qDd2kY.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-BKNvqvYi.js → LoginPage-Dv66U8yJ.js} +1 -1
- package/dashboard/dist/assets/{PetPage-BBmozE0L.js → PetPage-CNwrQdKM.js} +2 -2
- package/dashboard/dist/assets/{PopoverPopup-DDnISLKM.js → PopoverPopup-CqmvpYOi.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-Da90GDcM.js → ProviderIcon-BF2Ch7wi.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-DsETr50j.js → ResetPasswordPage-B0R8VUU3.js} +1 -1
- package/dashboard/dist/assets/{Select-pZDawyxt.js → Select-idAiBnJG.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-D5wuuN7G.js → SelectItemText-DPmmPY_D.js} +1 -1
- package/dashboard/dist/assets/{SelfHostPage-sm-PARYj.js → SelfHostPage-oR9cPK02.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-D4f8h0sL.js → ServiceStatusPage-CdV9gfGF.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-xVLsTN4m.js → SessionsPage-BkD34VlL.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-Dt9iFheY.js → SettingsPage-DsthWCyM.js} +3 -3
- package/dashboard/dist/assets/{SkillsPage-BdWalPpd.js → SkillsPage-CzMk9s3x.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-d2_ml0tE.js → TokenGalaxy-DQ6o8YBp.js} +1 -1
- package/dashboard/dist/assets/{TooltipPopup-B4masxDW.js → TooltipPopup-CMQjA-HE.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-CceYpa2O.js → TrendMonitor-DrV1m8E7.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-CSN0829H.js → WidgetsPage-Cc16WM0Y.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-gP4WfO_m.js → WrappedPage-pMwbM4R7.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-BtJVmciH.js → __vite-browser-external-DCy_YLfe.js} +3 -3
- package/dashboard/dist/assets/{agent-logos-wxccxfWz.js → agent-logos-Zw7LNkMO.js} +1 -1
- package/dashboard/dist/assets/{arrow-left-Bd5C77R2.js → arrow-left-DycYjE-k.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-C1RqrK4A.js → arrow-right-uNxLHoJ0.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-B_b2i8hn.js → arrow-up-right-CmdJ7gd7.js} +1 -1
- package/dashboard/dist/assets/{calendar-days-C8eYygme.js → calendar-days-CJ33_t5O.js} +1 -1
- package/dashboard/dist/assets/{check-BlBHefQ7.js → check-Dbubk4Il.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-mjUspqZv.js → chevron-down-BwvBVAOs.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-CEkMnWXf.js → cloud-checkout-IG-IOpeP.js} +1 -1
- package/dashboard/dist/assets/{composite-CzXFY9IC.js → composite-IIgJPPIw.js} +1 -1
- package/dashboard/dist/assets/{download-CQvg59w1.js → download-DxtH7w6a.js} +1 -1
- package/dashboard/dist/assets/{external-link-CDYfIZ3Z.js → external-link-evKf3mnD.js} +1 -1
- package/dashboard/dist/assets/{flame-C9NXQRil.js → flame-D2qOE_W4.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-fKy_3qpW.js → format-tokens-CqDmm-sK.js} +1 -1
- package/dashboard/dist/assets/{gift-WmV4SBGR.js → gift-Ge11HK1x.js} +1 -1
- package/dashboard/dist/assets/{host-mode-C5MvZ7SV.js → host-mode-8_TNh7yE.js} +1 -1
- package/dashboard/dist/assets/{icons-B1EtsaZs.js → icons-OmfWvE5K.js} +1 -1
- package/dashboard/dist/assets/{index-9EZ3o2RV.js → index-BYuEzP9p.js} +1 -1
- package/dashboard/dist/assets/{index-BEecBW06.js → index-BsYtyo4U.js} +1 -1
- package/dashboard/dist/assets/{index-DXNwBFNi.js → index-tbe1aERA.js} +1 -1
- package/dashboard/dist/assets/{info-CWDhggIW.js → info-xhrso9aO.js} +1 -1
- package/dashboard/dist/assets/{link-2-DEWNkK7s.js → link-2-BgNpTaO4.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-DotT5gVp.js → loader-circle-DRooZ0e5.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-qZnGgJAz.js → modifiers.esm-D-QAEpOZ.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-DOP5eqBD.js → monitor-smartphone-Bwu7__2B.js} +1 -1
- package/dashboard/dist/assets/{play-BUsybi9m.js → play-CBPvTiWM.js} +1 -1
- package/dashboard/dist/assets/{plus-DAjojh_m.js → plus-B4lFIyP8.js} +1 -1
- package/dashboard/dist/assets/{provider-display-BCQQYPWD.js → provider-display-CQlWusXH.js} +1 -1
- package/dashboard/dist/assets/{react-BgpqhoCj.js → react-DDOtS4Bd.js} +1 -1
- package/dashboard/dist/assets/{safePolygon-C-PFpZSA.js → safePolygon-BGfBVKoy.js} +1 -1
- package/dashboard/dist/assets/{search-W1A-6ugo.js → search-DVyNq72J.js} +1 -1
- package/dashboard/dist/assets/{server-CE7pFjDr.js → server-EpUTEsOf.js} +1 -1
- package/dashboard/dist/assets/{shield-check-KL5xc2FR.js → shield-check-DRg8cPKV.js} +1 -1
- package/dashboard/dist/assets/{skills-api-DL_zLjKA.js → skills-api-y42dEF96.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-CehJQvFH.js → sortable.esm-D2S1gXCK.js} +1 -1
- package/dashboard/dist/assets/{store-BHAXkHPR.js → store-DfGPS2IR.js} +1 -1
- package/dashboard/dist/assets/{terminal-Br5vyD6Q.js → terminal-hXIrrL2b.js} +1 -1
- package/dashboard/dist/assets/{trash-CzaCJP6G.js → trash-cjaASRQl.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-DbJrecQw.js → use-community-stats-B-nipjWt.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-DGhwmlX8.js → use-latest-request-guard-DIEYrSzn.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-T7KWAWZ6.js → use-limits-display-prefs-BdHDwBBC.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-MZJhS328.js → use-native-settings-7w71_h44.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-BDN24weA.js → use-ordered-list-CSMpK4ar.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-HUGf223R.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-DUu16y8Z.js → use-session-efficiency-pref-BagBVWW6.js} +1 -1
- package/dashboard/dist/assets/{use-transform-BczE46xm.js → use-transform-sjGGXCNn.js} +1 -1
- package/dashboard/dist/assets/use-usage-limits-Nml-wUB-.js +1 -0
- package/dashboard/dist/assets/{useCurrency-BgZmWbkx.js → useCurrency-DAKIY1nR.js} +1 -1
- package/dashboard/dist/assets/{usePositioner-dWENyY8z.js → usePositioner-B7vVQ7th.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-D7s1-Zuk.js → useTokenFormat-gtgw8k2Z.js} +1 -1
- package/dashboard/dist/assets/{zap-Dr__bKlq.js → zap-DFWOiTJ1.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/ip-check.html +1 -1
- package/dashboard/dist/leaderboard.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +1 -1
- package/src/commands/sync.js +571 -34
- package/src/lib/async-queue-reader.js +37 -16
- package/src/lib/codex-context-breakdown.js +8 -1
- package/src/lib/codex-rollout-parser.js +65 -5
- package/src/lib/grok-limits.js +56 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +690 -60
- package/src/lib/session-analytics.js +1 -1
- package/src/lib/usage-limits.js +33 -1
- package/dashboard/dist/assets/DialogDescription-ne_rYSD7.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-CfCY-K-a.js +0 -1
- package/dashboard/dist/assets/use-usage-limits-CjyDfH-M.js +0 -1
package/src/lib/rollout.js
CHANGED
|
@@ -96,10 +96,13 @@ async function listRolloutFiles(sessionsDir, options = {}) {
|
|
|
96
96
|
: null;
|
|
97
97
|
const stats = options?.stats && typeof options.stats === "object" ? options.stats : null;
|
|
98
98
|
if (dayInventoryCache) {
|
|
99
|
-
|
|
99
|
+
// v2: isRolloutFileName also accepts rollout-*.jsonl.zst/.zstd. Old v1
|
|
100
|
+
// day lists omit archives that were already on disk when the directory
|
|
101
|
+
// metadata last matched, so bump to force a one-time rescan.
|
|
102
|
+
if (dayInventoryCache.version !== 2) {
|
|
100
103
|
dayInventoryCache.days = {};
|
|
101
104
|
}
|
|
102
|
-
dayInventoryCache.version =
|
|
105
|
+
dayInventoryCache.version = 2;
|
|
103
106
|
if (!dayInventoryCache.days || typeof dayInventoryCache.days !== "object") {
|
|
104
107
|
dayInventoryCache.days = {};
|
|
105
108
|
}
|
|
@@ -180,7 +183,75 @@ function isRolloutFileName(name) {
|
|
|
180
183
|
if (typeof name !== "string") return false;
|
|
181
184
|
if (name.includes("/") || name.includes("\\")) return false;
|
|
182
185
|
if (name !== path.basename(name)) return false;
|
|
183
|
-
|
|
186
|
+
if (!name.startsWith("rollout-")) return false;
|
|
187
|
+
return name.endsWith(".jsonl") || isCompressedRolloutFileName(name);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Compressed rollout archives cannot be tailed by byte offset: the cursor
|
|
191
|
+
// stores compressed bytes while the parser consumes decompressed text. Treat
|
|
192
|
+
// them as content-addressed snapshots — one parse per (inode, size) version —
|
|
193
|
+
// reusing the DSH session's zstd machinery (built-in zlib first,
|
|
194
|
+
// `@mongodb-js/zstd` fallback) including its multi-frame handling. Returns
|
|
195
|
+
// null for a missing/non-file path; an empty archive yields no lines (the
|
|
196
|
+
// same outcome as an empty uncompressed rollout).
|
|
197
|
+
const CODEX_ROLLOUT_COMPRESSED_PATTERN = /^rollout-.*\.jsonl\.zstd?$/;
|
|
198
|
+
const CODEX_ROLLOUT_TEXT_MAX_BYTES = 256 * 1024 * 1024;
|
|
199
|
+
|
|
200
|
+
function isCompressedRolloutFileName(name) {
|
|
201
|
+
return typeof name === "string" && CODEX_ROLLOUT_COMPRESSED_PATTERN.test(name);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function isCompressedRolloutPath(filePath) {
|
|
205
|
+
if (typeof filePath !== "string") return false;
|
|
206
|
+
return isCompressedRolloutFileName(path.basename(filePath));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function readCodexRolloutSnapshot(filePath, { maxOutputBytes = CODEX_ROLLOUT_TEXT_MAX_BYTES } = {}) {
|
|
210
|
+
const handle = await fs.open(filePath, "r").catch(() => null);
|
|
211
|
+
if (!handle) return null;
|
|
212
|
+
try {
|
|
213
|
+
const initialStat = await handle.stat().catch(() => null);
|
|
214
|
+
if (!initialStat || !initialStat.isFile()) return null;
|
|
215
|
+
const outputLimit = Number.isSafeInteger(maxOutputBytes) && maxOutputBytes >= 0
|
|
216
|
+
? maxOutputBytes
|
|
217
|
+
: CODEX_ROLLOUT_TEXT_MAX_BYTES;
|
|
218
|
+
let data;
|
|
219
|
+
try {
|
|
220
|
+
data = await handle.readFile();
|
|
221
|
+
} catch (_error) {
|
|
222
|
+
// Transient mount/IO failures (e.g. WSL ESTALE) must not abort the rest
|
|
223
|
+
// of sync; treat like a missing archive and keep the prior cursor.
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
const metadata = {
|
|
227
|
+
size: data.length,
|
|
228
|
+
mtimeMs: Number.isFinite(initialStat.mtimeMs) ? initialStat.mtimeMs : 0,
|
|
229
|
+
inode: initialStat.ino || 0,
|
|
230
|
+
};
|
|
231
|
+
if (data.length === 0) return { ...metadata, unchanged: false, lines: [] };
|
|
232
|
+
let decoded;
|
|
233
|
+
try {
|
|
234
|
+
const inspected = inspectDshZstdFrames(data, outputLimit);
|
|
235
|
+
decoded = await decodeDshZstd(data, { maxOutputBytes: outputLimit, inspected });
|
|
236
|
+
} catch (_error) {
|
|
237
|
+
// Malformed / truncated / oversized archives must not abort the rest of
|
|
238
|
+
// the sync: return null so the caller keeps the prior cursor and retries.
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
// Fatal UTF-8: uncompressed JSONL parsing rejects invalid bytes under
|
|
242
|
+
// invalidRecordPolicy=throw. toString("utf8") would silently replace them
|
|
243
|
+
// and claim utf8Valid, so a corrupted discriminator could drop usage
|
|
244
|
+
// during repairCodexRescanInflation. Fail the snapshot instead.
|
|
245
|
+
let text;
|
|
246
|
+
try {
|
|
247
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(decoded);
|
|
248
|
+
} catch (_error) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
return { ...metadata, unchanged: false, lines: text.length > 0 ? text.split("\n") : [] };
|
|
252
|
+
} finally {
|
|
253
|
+
await handle.close().catch(() => {});
|
|
254
|
+
}
|
|
184
255
|
}
|
|
185
256
|
|
|
186
257
|
function directoryInventoryStatKey(st) {
|
|
@@ -206,7 +277,7 @@ async function listRolloutFilesDeep(dir) {
|
|
|
206
277
|
const p = path.join(d, e.name);
|
|
207
278
|
if (e.isDirectory()) {
|
|
208
279
|
await walk(p);
|
|
209
|
-
} else if (e.isFile() &&
|
|
280
|
+
} else if (e.isFile() && isRolloutFileName(e.name)) {
|
|
210
281
|
out.push(p);
|
|
211
282
|
}
|
|
212
283
|
}
|
|
@@ -517,6 +588,16 @@ async function parseRolloutIncremental({
|
|
|
517
588
|
? prev?.codexUsageRecord || (prev?.lastTotal ? { sawTokenCount: true } : null)
|
|
518
589
|
: null;
|
|
519
590
|
|
|
591
|
+
// Compressed archives are whole-file snapshots. If the compressed bytes
|
|
592
|
+
// changed under the same inode, force historical dedup; parseRolloutFile
|
|
593
|
+
// ignores retained baselines for the parse itself. Do NOT clear cursor
|
|
594
|
+
// fields here — a soft decode failure must leave the prior cursor intact.
|
|
595
|
+
const compressedSnapshotRescan = isCompressedRolloutPath(filePath)
|
|
596
|
+
&& sameInode
|
|
597
|
+
&& !truncated
|
|
598
|
+
&& prevOffset > 0
|
|
599
|
+
&& prevOffset !== st.size;
|
|
600
|
+
|
|
520
601
|
const codexProjectFastPath =
|
|
521
602
|
projectEnabled && (fileSource === DEFAULT_SOURCE || fileSource === "acode");
|
|
522
603
|
const projectOffset = sameInode && !truncated ? Number(prev.projectOffset || 0) : 0;
|
|
@@ -575,7 +656,8 @@ async function parseRolloutIncremental({
|
|
|
575
656
|
? getAcodeEventDedup()
|
|
576
657
|
: null;
|
|
577
658
|
const codexEventTracker = rolloutEventDedup
|
|
578
|
-
? (
|
|
659
|
+
? (compressedSnapshotRescan
|
|
660
|
+
|| needsHistoricalRolloutDedup({
|
|
579
661
|
filePath,
|
|
580
662
|
prev,
|
|
581
663
|
sameInode,
|
|
@@ -629,6 +711,22 @@ async function parseRolloutIncremental({
|
|
|
629
711
|
invalidRecordPolicy,
|
|
630
712
|
});
|
|
631
713
|
|
|
714
|
+
if (result.snapshotUnavailable) {
|
|
715
|
+
// Leave cursors.files[key] untouched so the next readable snapshot is
|
|
716
|
+
// compared against the prior (inode, offset, baselines).
|
|
717
|
+
if (cb) {
|
|
718
|
+
cb({
|
|
719
|
+
index: idx + 1,
|
|
720
|
+
total: totalFiles,
|
|
721
|
+
filePath,
|
|
722
|
+
filesProcessed,
|
|
723
|
+
eventsAggregated,
|
|
724
|
+
bucketsQueued: touchedBuckets.size,
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
|
|
632
730
|
const nextCursor = {
|
|
633
731
|
inode,
|
|
634
732
|
offset: result.endOffset,
|
|
@@ -2256,7 +2354,7 @@ function openclawUsageEventIdentity(obj, msg, usage, model) {
|
|
|
2256
2354
|
function codexSessionIdFromPath(filePath) {
|
|
2257
2355
|
if (typeof filePath !== "string") return null;
|
|
2258
2356
|
const m = filePath.match(
|
|
2259
|
-
/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\.jsonl
|
|
2357
|
+
/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\.jsonl(?:\.zstd?)?$/,
|
|
2260
2358
|
);
|
|
2261
2359
|
return m ? m[1] : null;
|
|
2262
2360
|
}
|
|
@@ -2291,6 +2389,108 @@ async function parseRolloutFile({
|
|
|
2291
2389
|
const endOffset = st.size;
|
|
2292
2390
|
const projectFileContexts = [];
|
|
2293
2391
|
addProjectFileContext(projectFileContexts, projectContext);
|
|
2392
|
+
// Compressed archives are immutable snapshots, not append-only logs: a byte
|
|
2393
|
+
// range of the compressed file is not a prefix of the decompressed text, so
|
|
2394
|
+
// resume-in-the-middle is impossible. Parse the whole snapshot whenever its
|
|
2395
|
+
// (inode, size) identity is new; unchanged snapshots are skipped by the
|
|
2396
|
+
// caller before reaching this function.
|
|
2397
|
+
if (isCompressedRolloutPath(filePath)) {
|
|
2398
|
+
// Mirror the uncompressed exhausted-cursor guard. When a custom
|
|
2399
|
+
// publicRepoResolver disables the outer skip, an unchanged archive must
|
|
2400
|
+
// not be re-parsed with retained baselines (that inflates totals).
|
|
2401
|
+
if (startOffset >= endOffset) {
|
|
2402
|
+
return {
|
|
2403
|
+
endOffset,
|
|
2404
|
+
lastTotal,
|
|
2405
|
+
tokenUsageBaselines,
|
|
2406
|
+
lastModel,
|
|
2407
|
+
lastServiceTier: typeof lastServiceTier === "string" ? lastServiceTier : null,
|
|
2408
|
+
modelAttributionState: previousModelAttributionState,
|
|
2409
|
+
usageRecordState: previousUsageRecordState,
|
|
2410
|
+
eventsAggregated: 0,
|
|
2411
|
+
projectFileContexts,
|
|
2412
|
+
};
|
|
2413
|
+
}
|
|
2414
|
+
const snapshot = await readCodexRolloutSnapshot(filePath);
|
|
2415
|
+
if (!snapshot) {
|
|
2416
|
+
// Soft sync skips a bad archive and keeps the prior cursor so later
|
|
2417
|
+
// files still count. Strict rebuilds (invalidRecordPolicy=throw), used
|
|
2418
|
+
// by repairCodexRescanInflation, must fail closed: a silent skip here
|
|
2419
|
+
// would let another readable session satisfy the non-empty-bucket
|
|
2420
|
+
// safeguard and permanently delete this archive's prior usage.
|
|
2421
|
+
if (invalidRecordPolicy === "throw") {
|
|
2422
|
+
throw new Error(`unreadable Codex compressed archive: ${filePath}`);
|
|
2423
|
+
}
|
|
2424
|
+
return {
|
|
2425
|
+
endOffset: startOffset,
|
|
2426
|
+
lastTotal,
|
|
2427
|
+
tokenUsageBaselines,
|
|
2428
|
+
lastModel,
|
|
2429
|
+
lastServiceTier: typeof lastServiceTier === "string" ? lastServiceTier : null,
|
|
2430
|
+
modelAttributionState: previousModelAttributionState,
|
|
2431
|
+
usageRecordState: previousUsageRecordState,
|
|
2432
|
+
eventsAggregated: 0,
|
|
2433
|
+
projectFileContexts,
|
|
2434
|
+
// Tell the incremental caller not to rewrite cursors.files[key]. If the
|
|
2435
|
+
// archive inode changed under a transient decode miss, overwriting the
|
|
2436
|
+
// prior cursor would drop historical-dedup baselines and double-count.
|
|
2437
|
+
snapshotUnavailable: true,
|
|
2438
|
+
};
|
|
2439
|
+
}
|
|
2440
|
+
// Decompressed lines have no meaningful compressed-byte offsets; the
|
|
2441
|
+
// shared parser only needs their content plus termination flags. Byte
|
|
2442
|
+
// counters stay at the snapshot's compressed size so the cursor records
|
|
2443
|
+
// (inode, size) identity, not decompressed progress.
|
|
2444
|
+
const records = (async function* snapshotRecords() {
|
|
2445
|
+
for (let index = 0; index < snapshot.lines.length; index += 1) {
|
|
2446
|
+
const raw = snapshot.lines[index];
|
|
2447
|
+
const terminated = index + 1 < snapshot.lines.length || raw.endsWith("\n");
|
|
2448
|
+
yield {
|
|
2449
|
+
line: terminated ? raw : `${raw}`,
|
|
2450
|
+
physicalBytes: 0,
|
|
2451
|
+
terminated,
|
|
2452
|
+
utf8Valid: true,
|
|
2453
|
+
};
|
|
2454
|
+
}
|
|
2455
|
+
})();
|
|
2456
|
+
// Size changed under the same inode: drop retained baselines for THIS
|
|
2457
|
+
// parse only. Soft-fail above still returns the caller-supplied baselines
|
|
2458
|
+
// so the prior cursor is preserved when decode fails.
|
|
2459
|
+
const rescanBaselines = startOffset > 0 && startOffset < endOffset;
|
|
2460
|
+
const parsed = await parseRolloutRecords({
|
|
2461
|
+
records,
|
|
2462
|
+
startOffset: 0,
|
|
2463
|
+
endOffset: 0,
|
|
2464
|
+
lastTotal: rescanBaselines ? null : lastTotal,
|
|
2465
|
+
tokenUsageBaselines: rescanBaselines ? null : tokenUsageBaselines,
|
|
2466
|
+
lastModel: rescanBaselines ? null : lastModel,
|
|
2467
|
+
lastServiceTier: rescanBaselines ? null : lastServiceTier,
|
|
2468
|
+
modelAttributionState: rescanBaselines ? null : previousModelAttributionState,
|
|
2469
|
+
usageRecordState: rescanBaselines ? null : previousUsageRecordState,
|
|
2470
|
+
compactionResponseIds,
|
|
2471
|
+
hourlyState,
|
|
2472
|
+
touchedBuckets,
|
|
2473
|
+
source,
|
|
2474
|
+
projectState,
|
|
2475
|
+
projectTouchedBuckets,
|
|
2476
|
+
projectRef,
|
|
2477
|
+
projectKey,
|
|
2478
|
+
projectMetaCache,
|
|
2479
|
+
publicRepoCache,
|
|
2480
|
+
publicRepoResolver,
|
|
2481
|
+
projectContext,
|
|
2482
|
+
projectFileContexts,
|
|
2483
|
+
seenCodexEvents,
|
|
2484
|
+
sessionId,
|
|
2485
|
+
filePath,
|
|
2486
|
+
invalidRecordPolicy,
|
|
2487
|
+
});
|
|
2488
|
+
// parseRolloutRecords counts streamed bytes; snapshots have none, so pin
|
|
2489
|
+
// the cursor to the compressed size — the (inode, size) identity that
|
|
2490
|
+
// makes an unchanged archive skip on the next sync.
|
|
2491
|
+
parsed.endOffset = snapshot.size;
|
|
2492
|
+
return parsed;
|
|
2493
|
+
}
|
|
2294
2494
|
if (startOffset >= endOffset) {
|
|
2295
2495
|
return {
|
|
2296
2496
|
endOffset,
|
|
@@ -2309,7 +2509,78 @@ async function parseRolloutFile({
|
|
|
2309
2509
|
start: startOffset,
|
|
2310
2510
|
end: endOffset - 1,
|
|
2311
2511
|
});
|
|
2512
|
+
// Compressed snapshots never reach this branch (parseRolloutFile routes them
|
|
2513
|
+
// to parseRolloutRecords directly); the byte-counting stream below is only
|
|
2514
|
+
// meaningful for append-only uncompressed logs.
|
|
2515
|
+
const records = (async function* streamRecords() {
|
|
2516
|
+
const invalidUtf8 = invalidRecordPolicy === "throw" ? "throw" : "record";
|
|
2517
|
+
for await (const record of physicalJsonlRecords(stream, { invalidUtf8 })) {
|
|
2518
|
+
yield record;
|
|
2519
|
+
}
|
|
2520
|
+
})();
|
|
2521
|
+
|
|
2522
|
+
return parseRolloutRecords({
|
|
2523
|
+
records,
|
|
2524
|
+
startOffset,
|
|
2525
|
+
endOffset,
|
|
2526
|
+
lastTotal,
|
|
2527
|
+
tokenUsageBaselines,
|
|
2528
|
+
lastModel,
|
|
2529
|
+
lastServiceTier,
|
|
2530
|
+
modelAttributionState: previousModelAttributionState,
|
|
2531
|
+
usageRecordState: previousUsageRecordState,
|
|
2532
|
+
compactionResponseIds,
|
|
2533
|
+
hourlyState,
|
|
2534
|
+
touchedBuckets,
|
|
2535
|
+
source,
|
|
2536
|
+
projectState,
|
|
2537
|
+
projectTouchedBuckets,
|
|
2538
|
+
projectRef,
|
|
2539
|
+
projectKey,
|
|
2540
|
+
projectMetaCache,
|
|
2541
|
+
publicRepoCache,
|
|
2542
|
+
publicRepoResolver,
|
|
2543
|
+
projectContext,
|
|
2544
|
+
projectFileContexts,
|
|
2545
|
+
seenCodexEvents,
|
|
2546
|
+
sessionId,
|
|
2547
|
+
filePath,
|
|
2548
|
+
invalidRecordPolicy,
|
|
2549
|
+
});
|
|
2550
|
+
}
|
|
2312
2551
|
|
|
2552
|
+
async function parseRolloutRecords({
|
|
2553
|
+
records,
|
|
2554
|
+
startOffset,
|
|
2555
|
+
endOffset,
|
|
2556
|
+
lastTotal,
|
|
2557
|
+
tokenUsageBaselines,
|
|
2558
|
+
lastModel,
|
|
2559
|
+
lastServiceTier,
|
|
2560
|
+
modelAttributionState: previousModelAttributionState,
|
|
2561
|
+
usageRecordState: previousUsageRecordState = null,
|
|
2562
|
+
compactionResponseIds = null,
|
|
2563
|
+
hourlyState,
|
|
2564
|
+
touchedBuckets,
|
|
2565
|
+
source,
|
|
2566
|
+
projectState,
|
|
2567
|
+
projectTouchedBuckets,
|
|
2568
|
+
projectRef,
|
|
2569
|
+
projectKey,
|
|
2570
|
+
projectMetaCache,
|
|
2571
|
+
publicRepoCache,
|
|
2572
|
+
publicRepoResolver,
|
|
2573
|
+
projectContext,
|
|
2574
|
+
projectFileContexts: existingProjectFileContexts,
|
|
2575
|
+
seenCodexEvents,
|
|
2576
|
+
sessionId,
|
|
2577
|
+
filePath,
|
|
2578
|
+
invalidRecordPolicy,
|
|
2579
|
+
}) {
|
|
2580
|
+
const projectFileContexts = Array.isArray(existingProjectFileContexts)
|
|
2581
|
+
? existingProjectFileContexts
|
|
2582
|
+
: [];
|
|
2583
|
+
addProjectFileContext(projectFileContexts, projectContext);
|
|
2313
2584
|
let model = typeof lastModel === "string" ? lastModel : null;
|
|
2314
2585
|
// Codex writes thread_settings_applied before the turn_context of the turn it
|
|
2315
2586
|
// takes effect on, and token_count rows carry no turn id, so a row's tier is
|
|
@@ -2393,8 +2664,7 @@ async function parseRolloutFile({
|
|
|
2393
2664
|
eventsAggregated += 1;
|
|
2394
2665
|
}
|
|
2395
2666
|
|
|
2396
|
-
|
|
2397
|
-
for await (const record of physicalJsonlRecords(stream, { invalidUtf8 })) {
|
|
2667
|
+
for await (const record of records) {
|
|
2398
2668
|
scannedEndOffset += record.physicalBytes;
|
|
2399
2669
|
if (record.terminated) committedEndOffset = scannedEndOffset;
|
|
2400
2670
|
if (!record.utf8Valid) {
|
|
@@ -2695,6 +2965,80 @@ async function scanRolloutProjectFileContexts({
|
|
|
2695
2965
|
};
|
|
2696
2966
|
}
|
|
2697
2967
|
|
|
2968
|
+
// Compressed archives cannot be scanned as byte streams: JSONL newlines in
|
|
2969
|
+
// the compressed payload are not record boundaries. Decode the snapshot,
|
|
2970
|
+
// then walk decompressed lines for cwd changes. Pin the cursor to the
|
|
2971
|
+
// compressed size so the next sync's unchanged check keeps working.
|
|
2972
|
+
if (isCompressedRolloutPath(filePath)) {
|
|
2973
|
+
const snapshot = await readCodexRolloutSnapshot(filePath);
|
|
2974
|
+
if (!snapshot) {
|
|
2975
|
+
if (invalidRecordPolicy === "throw") {
|
|
2976
|
+
throw new Error(`unreadable Codex compressed archive: ${filePath}`);
|
|
2977
|
+
}
|
|
2978
|
+
return {
|
|
2979
|
+
endOffset,
|
|
2980
|
+
lastTotal,
|
|
2981
|
+
tokenUsageBaselines,
|
|
2982
|
+
lastModel,
|
|
2983
|
+
lastServiceTier: lastServiceTier || null,
|
|
2984
|
+
modelAttributionState,
|
|
2985
|
+
eventsAggregated: 0,
|
|
2986
|
+
projectFileContexts,
|
|
2987
|
+
snapshotUnavailable: true,
|
|
2988
|
+
};
|
|
2989
|
+
}
|
|
2990
|
+
let currentCwd = null;
|
|
2991
|
+
for (const raw of snapshot.lines) {
|
|
2992
|
+
const line = typeof raw === "string" ? raw : "";
|
|
2993
|
+
if (
|
|
2994
|
+
!line ||
|
|
2995
|
+
!(
|
|
2996
|
+
line.includes('"turn_context"') ||
|
|
2997
|
+
line.includes('"session_meta"')
|
|
2998
|
+
) ||
|
|
2999
|
+
!line.includes('"cwd"')
|
|
3000
|
+
) {
|
|
3001
|
+
continue;
|
|
3002
|
+
}
|
|
3003
|
+
let obj;
|
|
3004
|
+
try {
|
|
3005
|
+
obj = JSON.parse(line);
|
|
3006
|
+
} catch (error) {
|
|
3007
|
+
if (invalidRecordPolicy === "throw") throw error;
|
|
3008
|
+
continue;
|
|
3009
|
+
}
|
|
3010
|
+
if (
|
|
3011
|
+
(obj?.type !== "turn_context" && obj?.type !== "session_meta") ||
|
|
3012
|
+
!obj?.payload ||
|
|
3013
|
+
typeof obj.payload !== "object" ||
|
|
3014
|
+
typeof obj.payload.cwd !== "string"
|
|
3015
|
+
) {
|
|
3016
|
+
continue;
|
|
3017
|
+
}
|
|
3018
|
+
const nextCwd = obj.payload.cwd.trim();
|
|
3019
|
+
if (!nextCwd || nextCwd === currentCwd) continue;
|
|
3020
|
+
const context = await resolveProjectContextForPath({
|
|
3021
|
+
startDir: wsl.mapWslCwdToUnc(nextCwd, filePath),
|
|
3022
|
+
projectMetaCache,
|
|
3023
|
+
publicRepoCache,
|
|
3024
|
+
publicRepoResolver,
|
|
3025
|
+
projectState,
|
|
3026
|
+
});
|
|
3027
|
+
currentCwd = nextCwd;
|
|
3028
|
+
addProjectFileContext(projectFileContexts, context);
|
|
3029
|
+
}
|
|
3030
|
+
return {
|
|
3031
|
+
endOffset: snapshot.size,
|
|
3032
|
+
lastTotal,
|
|
3033
|
+
tokenUsageBaselines,
|
|
3034
|
+
lastModel,
|
|
3035
|
+
lastServiceTier: lastServiceTier || null,
|
|
3036
|
+
modelAttributionState,
|
|
3037
|
+
eventsAggregated: 0,
|
|
3038
|
+
projectFileContexts,
|
|
3039
|
+
};
|
|
3040
|
+
}
|
|
3041
|
+
|
|
2698
3042
|
const stream = fssync.createReadStream(filePath, { start: 0, end: endOffset - 1 });
|
|
2699
3043
|
let currentCwd = null;
|
|
2700
3044
|
let scannedEndOffset = 0;
|
|
@@ -10802,14 +11146,102 @@ function workbuddySqliteUsageSnapshot(row) {
|
|
|
10802
11146
|
};
|
|
10803
11147
|
}
|
|
10804
11148
|
|
|
11149
|
+
// WorkBuddy writes OpenAI/DeepSeek-shaped rawUsage on every LLM round-trip.
|
|
11150
|
+
// The totals pass and the project pass below need the same breakdown, so the
|
|
11151
|
+
// arithmetic lives here once. Callers treat total_tokens === 0 as "no billable
|
|
11152
|
+
// usage": every component is non-negative, so that is equivalent to testing
|
|
11153
|
+
// each field for zero.
|
|
11154
|
+
function workbuddyUsageDelta(rawUsage) {
|
|
11155
|
+
const promptTokens = toNonNegativeInt(rawUsage.prompt_tokens);
|
|
11156
|
+
const completionTokens = toNonNegativeInt(rawUsage.completion_tokens);
|
|
11157
|
+
const promptDetails =
|
|
11158
|
+
rawUsage.prompt_tokens_details && typeof rawUsage.prompt_tokens_details === "object"
|
|
11159
|
+
? rawUsage.prompt_tokens_details
|
|
11160
|
+
: {};
|
|
11161
|
+
const completionDetails =
|
|
11162
|
+
rawUsage.completion_tokens_details && typeof rawUsage.completion_tokens_details === "object"
|
|
11163
|
+
? rawUsage.completion_tokens_details
|
|
11164
|
+
: {};
|
|
11165
|
+
|
|
11166
|
+
// Cache reads are mirrored across up to three fields depending on which
|
|
11167
|
+
// upstream the auto-router used; take the largest non-zero mirror.
|
|
11168
|
+
const cacheRead = Math.max(
|
|
11169
|
+
toNonNegativeInt(rawUsage.cache_read_input_tokens),
|
|
11170
|
+
toNonNegativeInt(promptDetails.cached_tokens),
|
|
11171
|
+
toNonNegativeInt(rawUsage.prompt_cache_hit_tokens),
|
|
11172
|
+
);
|
|
11173
|
+
const cacheCreation = toNonNegativeInt(rawUsage.cache_creation_input_tokens);
|
|
11174
|
+
// prompt_tokens is the FULL prompt: subtract BOTH reads and writes so
|
|
11175
|
+
// input_tokens is pure non-cached input (no double-counting cache writes).
|
|
11176
|
+
const inputTokens = Math.max(0, promptTokens - cacheRead - cacheCreation);
|
|
11177
|
+
// completion_tokens INCLUDES reasoning (verified: total == prompt+completion).
|
|
11178
|
+
const reasoningTokens = Math.min(
|
|
11179
|
+
completionTokens,
|
|
11180
|
+
toNonNegativeInt(completionDetails.reasoning_tokens),
|
|
11181
|
+
);
|
|
11182
|
+
const outputTokens = Math.max(0, completionTokens - reasoningTokens);
|
|
11183
|
+
|
|
11184
|
+
return {
|
|
11185
|
+
input_tokens: inputTokens,
|
|
11186
|
+
cached_input_tokens: cacheRead,
|
|
11187
|
+
cache_creation_input_tokens: cacheCreation,
|
|
11188
|
+
output_tokens: outputTokens,
|
|
11189
|
+
reasoning_output_tokens: reasoningTokens,
|
|
11190
|
+
total_tokens: inputTokens + outputTokens + cacheRead + cacheCreation + reasoningTokens,
|
|
11191
|
+
conversation_count: 1,
|
|
11192
|
+
};
|
|
11193
|
+
}
|
|
11194
|
+
|
|
11195
|
+
// WorkBuddy stamps `cwd` on its records rather than in a session header, and a
|
|
11196
|
+
// session can open with a record that lacks it while later appends omit it
|
|
11197
|
+
// altogether. Resolve it from a bounded prefix of the file so every record in
|
|
11198
|
+
// the incremental window is attributed, not just the ones that happen to carry
|
|
11199
|
+
// their own cwd. A file with no recoverable cwd stays outside project
|
|
11200
|
+
// attribution — there is nothing to attribute it to.
|
|
11201
|
+
async function resolveWorkbuddyFileCwd(filePath, maxScanBytes = 8 * 1024 * 1024) {
|
|
11202
|
+
let stream;
|
|
11203
|
+
try {
|
|
11204
|
+
const stat = fssync.statSync(filePath);
|
|
11205
|
+
if (stat.size <= 0) return null;
|
|
11206
|
+
stream = fssync.createReadStream(filePath, {
|
|
11207
|
+
start: 0,
|
|
11208
|
+
end: Math.min(stat.size, maxScanBytes) - 1,
|
|
11209
|
+
});
|
|
11210
|
+
} catch {
|
|
11211
|
+
return null;
|
|
11212
|
+
}
|
|
11213
|
+
let cwd = null;
|
|
11214
|
+
try {
|
|
11215
|
+
for await (const record of physicalJsonlRecords(stream, {
|
|
11216
|
+
invalidUtf8: "record",
|
|
11217
|
+
maxRecordBytes: OMP_LIKE_MAX_RECORD_BYTES,
|
|
11218
|
+
})) {
|
|
11219
|
+
if (!record.line || !record.line.trim()) continue;
|
|
11220
|
+
let entry;
|
|
11221
|
+
try { entry = JSON.parse(record.line); } catch { continue; }
|
|
11222
|
+
if (entry && typeof entry === "object" && typeof entry.cwd === "string" && entry.cwd) {
|
|
11223
|
+
cwd = entry.cwd;
|
|
11224
|
+
break;
|
|
11225
|
+
}
|
|
11226
|
+
}
|
|
11227
|
+
} catch {
|
|
11228
|
+
// No recoverable cwd — the file stays un-attributed.
|
|
11229
|
+
} finally {
|
|
11230
|
+
try { stream.destroy(); } catch {}
|
|
11231
|
+
}
|
|
11232
|
+
return cwd;
|
|
11233
|
+
}
|
|
11234
|
+
|
|
10805
11235
|
async function parseWorkbuddyIncremental({
|
|
10806
11236
|
projectFiles,
|
|
10807
11237
|
cursors,
|
|
10808
11238
|
queuePath,
|
|
11239
|
+
projectQueuePath,
|
|
10809
11240
|
onProgress,
|
|
10810
11241
|
env,
|
|
10811
11242
|
defaultModel,
|
|
10812
11243
|
source = "workbuddy",
|
|
11244
|
+
publicRepoResolver,
|
|
10813
11245
|
} = {}) {
|
|
10814
11246
|
await ensureDir(path.dirname(queuePath));
|
|
10815
11247
|
// `source` doubles as the variant selector and the bucket source key, so the
|
|
@@ -10841,6 +11273,45 @@ async function parseWorkbuddyIncremental({
|
|
|
10841
11273
|
);
|
|
10842
11274
|
const detailedSessionsWithUsage = new Set();
|
|
10843
11275
|
|
|
11276
|
+
// The shared normalizers copy the bucket maps but alias each bucket/totals
|
|
11277
|
+
// object, and the enqueue helpers stamp queuedKey before appendFile runs.
|
|
11278
|
+
// Give the buckets this parser can mutate private copies so a failed append
|
|
11279
|
+
// leaves the caller's published state untouched — the same staging contract
|
|
11280
|
+
// the Devin reader uses. Callers retry from unchanged offsets, so a polluted
|
|
11281
|
+
// published state would add the same usage twice.
|
|
11282
|
+
const ownsWorkbuddyBucket = (key) =>
|
|
11283
|
+
(normalizeSourceInput(key.split(BUCKET_SEPARATOR)[0]) || DEFAULT_SOURCE) === variant;
|
|
11284
|
+
const ownsWorkbuddyProjectBucket = (key) => {
|
|
11285
|
+
// Project keys are `projectKey|source|hour`; a projectKey may itself
|
|
11286
|
+
// contain the separator, so read the source segment from the right.
|
|
11287
|
+
const last = key.lastIndexOf(BUCKET_SEPARATOR);
|
|
11288
|
+
const prev = last > 0 ? key.lastIndexOf(BUCKET_SEPARATOR, last - 1) : -1;
|
|
11289
|
+
const source = prev >= 0 ? key.slice(prev + 1, last) : "";
|
|
11290
|
+
return (normalizeSourceInput(source) || DEFAULT_SOURCE) === variant;
|
|
11291
|
+
};
|
|
11292
|
+
|
|
11293
|
+
// Project attribution runs as a second, independent pass over the same JSONL
|
|
11294
|
+
// files (see "Project pass" below). It keeps its own offsets and seen-set, so
|
|
11295
|
+
// an install that already has totals backfills project history on upgrade
|
|
11296
|
+
// without re-counting a single token in the totals queue.
|
|
11297
|
+
const projectEnabled = typeof projectQueuePath === "string" && projectQueuePath.length > 0;
|
|
11298
|
+
const projectState = projectEnabled ? normalizeProjectState(cursors?.projectHourly) : null;
|
|
11299
|
+
if (projectState) {
|
|
11300
|
+
projectState.buckets = stageProviderBuckets(projectState.buckets, ownsWorkbuddyProjectBucket);
|
|
11301
|
+
}
|
|
11302
|
+
const projectTouchedBuckets = projectEnabled ? new Set() : null;
|
|
11303
|
+
const projectMetaCache = projectEnabled ? new Map() : null;
|
|
11304
|
+
const publicRepoCache = projectEnabled ? new Map() : null;
|
|
11305
|
+
const projectSeenIds = new Set(
|
|
11306
|
+
Array.isArray(workbuddyState.projectSeenIds) ? workbuddyState.projectSeenIds : [],
|
|
11307
|
+
);
|
|
11308
|
+
const projectFileOffsets =
|
|
11309
|
+
workbuddyState.projectFileOffsets && typeof workbuddyState.projectFileOffsets === "object"
|
|
11310
|
+
? { ...workbuddyState.projectFileOffsets }
|
|
11311
|
+
: {};
|
|
11312
|
+
let projectBucketsQueued = 0;
|
|
11313
|
+
let projectCursorUpdated = false;
|
|
11314
|
+
|
|
10844
11315
|
const allFiles = Array.isArray(projectFiles)
|
|
10845
11316
|
? projectFiles
|
|
10846
11317
|
: resolveWorkbuddyProjectFiles(env || process.env, variant);
|
|
@@ -10866,10 +11337,15 @@ async function parseWorkbuddyIncremental({
|
|
|
10866
11337
|
detailedSessions,
|
|
10867
11338
|
updatedAt: new Date().toISOString(),
|
|
10868
11339
|
};
|
|
10869
|
-
return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
11340
|
+
return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0, projectBucketsQueued: 0, projectCursorUpdated: false };
|
|
10870
11341
|
}
|
|
10871
11342
|
|
|
10872
11343
|
const hourlyState = normalizeHourlyState(cursors?.hourly);
|
|
11344
|
+
hourlyState.buckets = stageProviderBuckets(hourlyState.buckets, ownsWorkbuddyBucket);
|
|
11345
|
+
hourlyState.groupQueued =
|
|
11346
|
+
hourlyState.groupQueued && typeof hourlyState.groupQueued === "object"
|
|
11347
|
+
? { ...hourlyState.groupQueued }
|
|
11348
|
+
: {};
|
|
10873
11349
|
const touchedBuckets = new Set();
|
|
10874
11350
|
const cb = typeof onProgress === "function" ? onProgress : null;
|
|
10875
11351
|
let recordsProcessed = 0;
|
|
@@ -10941,39 +11417,8 @@ async function parseWorkbuddyIncremental({
|
|
|
10941
11417
|
|
|
10942
11418
|
recordsProcessed++;
|
|
10943
11419
|
|
|
10944
|
-
const
|
|
10945
|
-
|
|
10946
|
-
const promptDetails =
|
|
10947
|
-
rawUsage.prompt_tokens_details && typeof rawUsage.prompt_tokens_details === "object"
|
|
10948
|
-
? rawUsage.prompt_tokens_details
|
|
10949
|
-
: {};
|
|
10950
|
-
const completionDetails =
|
|
10951
|
-
rawUsage.completion_tokens_details && typeof rawUsage.completion_tokens_details === "object"
|
|
10952
|
-
? rawUsage.completion_tokens_details
|
|
10953
|
-
: {};
|
|
10954
|
-
|
|
10955
|
-
// Cache reads are mirrored across up to three fields depending on which
|
|
10956
|
-
// upstream the auto-router used; take the largest non-zero mirror.
|
|
10957
|
-
const cacheRead = Math.max(
|
|
10958
|
-
toNonNegativeInt(rawUsage.cache_read_input_tokens),
|
|
10959
|
-
toNonNegativeInt(promptDetails.cached_tokens),
|
|
10960
|
-
toNonNegativeInt(rawUsage.prompt_cache_hit_tokens),
|
|
10961
|
-
);
|
|
10962
|
-
const cacheCreation = toNonNegativeInt(rawUsage.cache_creation_input_tokens);
|
|
10963
|
-
// prompt_tokens is the FULL prompt: subtract BOTH reads and writes so
|
|
10964
|
-
// input_tokens is pure non-cached input (no double-counting cache writes).
|
|
10965
|
-
const inputTokens = Math.max(0, promptTokens - cacheRead - cacheCreation);
|
|
10966
|
-
// completion_tokens INCLUDES reasoning (verified: total == prompt+completion).
|
|
10967
|
-
const reasoningTokens = Math.min(completionTokens, toNonNegativeInt(completionDetails.reasoning_tokens));
|
|
10968
|
-
const outputTokens = Math.max(0, completionTokens - reasoningTokens);
|
|
10969
|
-
|
|
10970
|
-
if (
|
|
10971
|
-
inputTokens === 0 &&
|
|
10972
|
-
outputTokens === 0 &&
|
|
10973
|
-
cacheRead === 0 &&
|
|
10974
|
-
cacheCreation === 0 &&
|
|
10975
|
-
reasoningTokens === 0
|
|
10976
|
-
) {
|
|
11420
|
+
const delta = workbuddyUsageDelta(rawUsage);
|
|
11421
|
+
if (delta.total_tokens === 0) {
|
|
10977
11422
|
seenIds.add(messageId);
|
|
10978
11423
|
continue;
|
|
10979
11424
|
}
|
|
@@ -10992,17 +11437,6 @@ async function parseWorkbuddyIncremental({
|
|
|
10992
11437
|
normalizeModelInput(entry.model) ||
|
|
10993
11438
|
fallbackModel;
|
|
10994
11439
|
|
|
10995
|
-
const delta = {
|
|
10996
|
-
input_tokens: inputTokens,
|
|
10997
|
-
cached_input_tokens: cacheRead,
|
|
10998
|
-
cache_creation_input_tokens: cacheCreation,
|
|
10999
|
-
output_tokens: outputTokens,
|
|
11000
|
-
reasoning_output_tokens: reasoningTokens,
|
|
11001
|
-
total_tokens:
|
|
11002
|
-
inputTokens + outputTokens + cacheRead + cacheCreation + reasoningTokens,
|
|
11003
|
-
conversation_count: 1,
|
|
11004
|
-
};
|
|
11005
|
-
|
|
11006
11440
|
const bucket = getHourlyBucket(hourlyState, variant, model, bucketStart);
|
|
11007
11441
|
addTotals(bucket.totals, delta);
|
|
11008
11442
|
touchedBuckets.add(bucketKey(variant, model, bucketStart));
|
|
@@ -11036,6 +11470,165 @@ async function parseWorkbuddyIncremental({
|
|
|
11036
11470
|
};
|
|
11037
11471
|
}
|
|
11038
11472
|
|
|
11473
|
+
// ── Project pass ───────────────────────────────────────────────────────
|
|
11474
|
+
// WorkBuddy stamps `cwd` on its records, so unlike the trace summaries below
|
|
11475
|
+
// there is nothing to infer: attribute each round-trip to the git repository
|
|
11476
|
+
// its cwd belongs to. This is a second loop over the same files with its own
|
|
11477
|
+
// cursor, so an install that already has totals backfills project history on
|
|
11478
|
+
// upgrade without touching (or double-counting) them.
|
|
11479
|
+
if (projectEnabled) {
|
|
11480
|
+
for (let fileIdx = 0; fileIdx < jsonlFiles.length; fileIdx++) {
|
|
11481
|
+
const filePath = jsonlFiles[fileIdx];
|
|
11482
|
+
let stat;
|
|
11483
|
+
try { stat = fssync.statSync(filePath); } catch { continue; }
|
|
11484
|
+
|
|
11485
|
+
const prevEntry = projectFileOffsets[filePath] || {};
|
|
11486
|
+
const prevSize = Number(prevEntry.size) || 0;
|
|
11487
|
+
const prevIno = prevEntry.ino;
|
|
11488
|
+
const inodeChanged = typeof prevIno === "number" && prevIno !== stat.ino;
|
|
11489
|
+
const truncated = stat.size < prevSize;
|
|
11490
|
+
const startOffset = truncated || inodeChanged ? 0 : prevSize;
|
|
11491
|
+
if (stat.size <= startOffset) continue;
|
|
11492
|
+
|
|
11493
|
+
// cwd is fixed for a session file's lifetime. Reuse the resolved value
|
|
11494
|
+
// while it is the same file, otherwise read it from the file head — not
|
|
11495
|
+
// from the incremental window, which may hold only appends that omit it.
|
|
11496
|
+
// Truncation (same inode, smaller size) must drop the cached cwd: the
|
|
11497
|
+
// replacement transcript may belong to a different checkout.
|
|
11498
|
+
let cwd =
|
|
11499
|
+
!inodeChanged && !truncated && typeof prevEntry.cwd === "string"
|
|
11500
|
+
? prevEntry.cwd
|
|
11501
|
+
: null;
|
|
11502
|
+
|
|
11503
|
+
// No recoverable cwd yet: do not advance past history. A later append
|
|
11504
|
+
// may stamp cwd, and early records must still be attributable then.
|
|
11505
|
+
// Check the unchanged-probe cache BEFORE the head scan so idle syncs do
|
|
11506
|
+
// not re-read up to 8 MiB of JSONL on every pass.
|
|
11507
|
+
if (
|
|
11508
|
+
!cwd &&
|
|
11509
|
+
Number(prevEntry.noCwdProbedSize) === stat.size &&
|
|
11510
|
+
prevEntry.ino === stat.ino &&
|
|
11511
|
+
Number(prevEntry.size) === startOffset
|
|
11512
|
+
) {
|
|
11513
|
+
continue;
|
|
11514
|
+
}
|
|
11515
|
+
if (!cwd) cwd = await resolveWorkbuddyFileCwd(filePath);
|
|
11516
|
+
if (!cwd) {
|
|
11517
|
+
projectCursorUpdated = true;
|
|
11518
|
+
projectFileOffsets[filePath] = {
|
|
11519
|
+
size: startOffset,
|
|
11520
|
+
mtimeMs: stat.mtimeMs,
|
|
11521
|
+
ino: stat.ino,
|
|
11522
|
+
noCwdProbedSize: stat.size,
|
|
11523
|
+
};
|
|
11524
|
+
continue;
|
|
11525
|
+
}
|
|
11526
|
+
|
|
11527
|
+
let projectRef = null;
|
|
11528
|
+
let projectKey = null;
|
|
11529
|
+
const projectContext = await resolveProjectContextForPath({
|
|
11530
|
+
startDir: wsl.mapWslCwdToUnc(cwd, filePath),
|
|
11531
|
+
projectMetaCache,
|
|
11532
|
+
publicRepoCache,
|
|
11533
|
+
publicRepoResolver,
|
|
11534
|
+
projectState,
|
|
11535
|
+
});
|
|
11536
|
+
projectRef = projectContext?.projectRef || null;
|
|
11537
|
+
projectKey = projectContext?.projectKey || null;
|
|
11538
|
+
|
|
11539
|
+
// cwd resolved but the repo is not yet public_verified (or has no git
|
|
11540
|
+
// checkout): keep the offset unadvanced so a later sync can retry, but
|
|
11541
|
+
// do not JSON-parse the transcript on every pass — there is nothing to
|
|
11542
|
+
// attribute until both identifiers exist.
|
|
11543
|
+
if (!projectKey || !projectRef) {
|
|
11544
|
+
projectCursorUpdated = true;
|
|
11545
|
+
projectFileOffsets[filePath] = {
|
|
11546
|
+
size: startOffset,
|
|
11547
|
+
mtimeMs: stat.mtimeMs,
|
|
11548
|
+
ino: stat.ino,
|
|
11549
|
+
cwd,
|
|
11550
|
+
};
|
|
11551
|
+
continue;
|
|
11552
|
+
}
|
|
11553
|
+
|
|
11554
|
+
const committedEndOffset = await scanOmpLikeTranscript(
|
|
11555
|
+
filePath,
|
|
11556
|
+
startOffset,
|
|
11557
|
+
OMP_LIKE_MAX_RECORD_BYTES,
|
|
11558
|
+
(entry) => {
|
|
11559
|
+
// Mirror the totals pass: a JSON null / non-object line must not
|
|
11560
|
+
// abort the whole WorkBuddy parse (and strand both cursors).
|
|
11561
|
+
if (!entry || typeof entry !== "object") return;
|
|
11562
|
+
|
|
11563
|
+
const provider = entry.providerData;
|
|
11564
|
+
const rawUsage = provider && typeof provider === "object" ? provider.rawUsage : null;
|
|
11565
|
+
if (!rawUsage || typeof rawUsage !== "object") return;
|
|
11566
|
+
|
|
11567
|
+
// Mirror the totals pass's suppression rules so the project queue
|
|
11568
|
+
// never reports a session the totals queue deliberately skipped.
|
|
11569
|
+
const sessionId =
|
|
11570
|
+
typeof entry.sessionId === "string" && entry.sessionId
|
|
11571
|
+
? entry.sessionId
|
|
11572
|
+
: path.basename(filePath, ".jsonl");
|
|
11573
|
+
const sqliteSession = sqliteSessions[sessionId];
|
|
11574
|
+
if (sqliteSession?.detailed || tracedSessionIds.has(sessionId)) return;
|
|
11575
|
+
|
|
11576
|
+
const tsMs =
|
|
11577
|
+
Number.isFinite(Number(entry.timestamp)) && Number(entry.timestamp) > 0
|
|
11578
|
+
? Number(entry.timestamp)
|
|
11579
|
+
: null;
|
|
11580
|
+
if (tsMs == null) return;
|
|
11581
|
+
|
|
11582
|
+
// Same response-level id preference as the totals pass, so both
|
|
11583
|
+
// queues dedup on identical keys.
|
|
11584
|
+
const messageId =
|
|
11585
|
+
typeof provider?.messageId === "string" && provider.messageId
|
|
11586
|
+
? provider.messageId
|
|
11587
|
+
: typeof entry.uuid === "string" && entry.uuid
|
|
11588
|
+
? entry.uuid
|
|
11589
|
+
: typeof entry.id === "string" && entry.id
|
|
11590
|
+
? entry.id
|
|
11591
|
+
: `${sessionId}:${tsMs}`;
|
|
11592
|
+
if (projectSeenIds.has(messageId)) return;
|
|
11593
|
+
projectSeenIds.add(messageId);
|
|
11594
|
+
// The totals pass also registers the raw entry id so an older or
|
|
11595
|
+
// rewritten copy of the same record cannot be counted twice; the
|
|
11596
|
+
// project pass has to keep the same alias.
|
|
11597
|
+
if (typeof entry.id === "string" && entry.id && entry.id !== messageId) {
|
|
11598
|
+
projectSeenIds.add(entry.id);
|
|
11599
|
+
}
|
|
11600
|
+
|
|
11601
|
+
const delta = workbuddyUsageDelta(rawUsage);
|
|
11602
|
+
if (delta.total_tokens === 0) return;
|
|
11603
|
+
|
|
11604
|
+
const bucketStart = toUtcHalfHourStart(new Date(tsMs).toISOString());
|
|
11605
|
+
if (!bucketStart) return;
|
|
11606
|
+
|
|
11607
|
+
const projectBucket = getProjectBucket(
|
|
11608
|
+
projectState,
|
|
11609
|
+
projectKey,
|
|
11610
|
+
source,
|
|
11611
|
+
bucketStart,
|
|
11612
|
+
projectRef,
|
|
11613
|
+
);
|
|
11614
|
+
addTotals(projectBucket.totals, delta);
|
|
11615
|
+
projectTouchedBuckets.add(projectBucketKey(projectKey, source, bucketStart));
|
|
11616
|
+
},
|
|
11617
|
+
);
|
|
11618
|
+
|
|
11619
|
+
// Commit only past records that were read in full: a record appended
|
|
11620
|
+
// while this pass was running, or an unfinished tail line, has to stay
|
|
11621
|
+
// visible to the next sync. Only advance after successful attribution.
|
|
11622
|
+
projectCursorUpdated = true;
|
|
11623
|
+
projectFileOffsets[filePath] = {
|
|
11624
|
+
size: committedEndOffset,
|
|
11625
|
+
mtimeMs: stat.mtimeMs,
|
|
11626
|
+
ino: stat.ino,
|
|
11627
|
+
cwd,
|
|
11628
|
+
};
|
|
11629
|
+
}
|
|
11630
|
+
}
|
|
11631
|
+
|
|
11039
11632
|
// Trace summaries are a fallback for sessions whose JSONL has no detailed
|
|
11040
11633
|
// providerData.rawUsage. A trace's modelInfo totals include the cache read
|
|
11041
11634
|
// portion of input, so split input into uncached input + cached input. Do
|
|
@@ -11294,6 +11887,23 @@ async function parseWorkbuddyIncremental({
|
|
|
11294
11887
|
? tracedSessionArr.slice(tracedSessionArr.length - 10_000)
|
|
11295
11888
|
: tracedSessionArr;
|
|
11296
11889
|
|
|
11890
|
+
const projectSeenArr = Array.from(projectSeenIds);
|
|
11891
|
+
const cappedProjectSeenIds =
|
|
11892
|
+
projectSeenArr.length > 10_000
|
|
11893
|
+
? projectSeenArr.slice(projectSeenArr.length - 10_000)
|
|
11894
|
+
: projectSeenArr;
|
|
11895
|
+
|
|
11896
|
+
// Publish only after both appends succeed: a throw here leaves `cursors`
|
|
11897
|
+
// untouched, so a retry re-derives the same contribution from the same
|
|
11898
|
+
// offsets instead of adding it on top of a half-applied state.
|
|
11899
|
+
projectBucketsQueued = projectEnabled
|
|
11900
|
+
? await enqueueTouchedProjectBuckets({
|
|
11901
|
+
projectQueuePath,
|
|
11902
|
+
projectState,
|
|
11903
|
+
projectTouchedBuckets,
|
|
11904
|
+
})
|
|
11905
|
+
: 0;
|
|
11906
|
+
|
|
11297
11907
|
const bucketsQueued = await enqueueTouchedBuckets({
|
|
11298
11908
|
queuePath,
|
|
11299
11909
|
hourlyState,
|
|
@@ -11302,6 +11912,10 @@ async function parseWorkbuddyIncremental({
|
|
|
11302
11912
|
const updatedAt = new Date().toISOString();
|
|
11303
11913
|
hourlyState.updatedAt = updatedAt;
|
|
11304
11914
|
cursors.hourly = hourlyState;
|
|
11915
|
+
if (projectState) {
|
|
11916
|
+
projectState.updatedAt = updatedAt;
|
|
11917
|
+
cursors.projectHourly = projectState;
|
|
11918
|
+
}
|
|
11305
11919
|
cursors[variant] = {
|
|
11306
11920
|
...workbuddyState,
|
|
11307
11921
|
seenIds: cappedSeen,
|
|
@@ -11310,10 +11924,22 @@ async function parseWorkbuddyIncremental({
|
|
|
11310
11924
|
fileOffsets,
|
|
11311
11925
|
sqliteSessions: cappedSqliteSessions,
|
|
11312
11926
|
detailedSessions: cappedDetailedSessions,
|
|
11927
|
+
...(projectEnabled
|
|
11928
|
+
? {
|
|
11929
|
+
projectSeenIds: cappedProjectSeenIds,
|
|
11930
|
+
projectFileOffsets,
|
|
11931
|
+
}
|
|
11932
|
+
: {}),
|
|
11313
11933
|
updatedAt,
|
|
11314
11934
|
};
|
|
11315
11935
|
|
|
11316
|
-
return {
|
|
11936
|
+
return {
|
|
11937
|
+
recordsProcessed,
|
|
11938
|
+
eventsAggregated,
|
|
11939
|
+
bucketsQueued,
|
|
11940
|
+
projectBucketsQueued,
|
|
11941
|
+
projectCursorUpdated: projectEnabled && projectCursorUpdated,
|
|
11942
|
+
};
|
|
11317
11943
|
}
|
|
11318
11944
|
|
|
11319
11945
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -14151,14 +14777,14 @@ function devinStringMap(value) {
|
|
|
14151
14777
|
// Stage only the bucket objects this parser can mutate. The shared
|
|
14152
14778
|
// normalizers copy the bucket maps but alias each bucket/totals object, and
|
|
14153
14779
|
// the enqueue helpers stamp queuedKey before appendFile runs — so a failed
|
|
14154
|
-
// append used to leave the caller's published state polluted. Only
|
|
14155
|
-
//
|
|
14780
|
+
// append used to leave the caller's published state polluted. Only entries
|
|
14781
|
+
// the calling parser owns get private copies; every other provider's buckets
|
|
14156
14782
|
// stay shared read-only references.
|
|
14157
|
-
function
|
|
14783
|
+
function stageProviderBuckets(buckets, isOwnedBucket) {
|
|
14158
14784
|
const staged = {};
|
|
14159
14785
|
for (const [key, bucket] of Object.entries(buckets || {})) {
|
|
14160
14786
|
staged[key] =
|
|
14161
|
-
bucket && typeof bucket === "object" &&
|
|
14787
|
+
bucket && typeof bucket === "object" && isOwnedBucket(key, bucket)
|
|
14162
14788
|
? {
|
|
14163
14789
|
...bucket,
|
|
14164
14790
|
totals:
|
|
@@ -14247,7 +14873,7 @@ async function parseDevinIncremental({
|
|
|
14247
14873
|
// rows recover either queue. Unrelated providers' buckets stay shared
|
|
14248
14874
|
// read-only references (this parser never writes their keys).
|
|
14249
14875
|
const hourlyState = normalizeHourlyState(cursors?.hourly);
|
|
14250
|
-
hourlyState.buckets =
|
|
14876
|
+
hourlyState.buckets = stageProviderBuckets(
|
|
14251
14877
|
hourlyState.buckets,
|
|
14252
14878
|
(key) =>
|
|
14253
14879
|
(normalizeSourceInput(parseBucketKey(key).source) || DEFAULT_SOURCE) ===
|
|
@@ -14266,7 +14892,7 @@ async function parseDevinIncremental({
|
|
|
14266
14892
|
if (projectState) {
|
|
14267
14893
|
// Project bucket keys are `projectKey|source|hourStart`; this parser only
|
|
14268
14894
|
// ever looks up keys whose middle segment is the devin source.
|
|
14269
|
-
projectState.buckets =
|
|
14895
|
+
projectState.buckets = stageProviderBuckets(projectState.buckets, (key) => {
|
|
14270
14896
|
const last = key.lastIndexOf(BUCKET_SEPARATOR);
|
|
14271
14897
|
const prev = last > 0 ? key.lastIndexOf(BUCKET_SEPARATOR, last - 1) : -1;
|
|
14272
14898
|
const source = prev >= 0 ? key.slice(prev + 1, last) : "";
|
|
@@ -25078,6 +25704,10 @@ module.exports = {
|
|
|
25078
25704
|
listRolloutFiles,
|
|
25079
25705
|
listRolloutFilesDeep,
|
|
25080
25706
|
codexSessionIdFromPath,
|
|
25707
|
+
isRolloutFileName,
|
|
25708
|
+
isCompressedRolloutFileName,
|
|
25709
|
+
isCompressedRolloutPath,
|
|
25710
|
+
readCodexRolloutSnapshot,
|
|
25081
25711
|
filterColdCodexRolloutFiles,
|
|
25082
25712
|
listClaudeProjectFiles,
|
|
25083
25713
|
listGeminiSessionFiles,
|