tokentracker-cli 0.84.4 → 0.84.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/dashboard/dist/assets/{AchievementBadge-C29PwsK_.js → AchievementBadge-CKIvD3lG.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-BlZODLp8.js → AchievementsPage-CZWCUiS5.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-Bc6NMnIa.js → AchievementsSection-BSk_fR8P.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-BUy4636q.js → BadgeDetailModal-CjHkcmuq.js} +1 -1
- package/dashboard/dist/assets/{Card-CqxuFMAN.js → Card-D6hdzwvI.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-BG3m8BgW.js → ClawdAnimated-Cl6gmF7u.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-Dcwtom2b.js → CommandPalette-B0rcodcF.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-dBjHrgjE.js → CommunityStatsModal-BtlTJbf4.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-DncQkptq.js → ConfirmModal-tJCWVZQH.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-CdF5WNHN.js → DashboardPage-Dtxq8dzI.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-B-PO4s-r.js → DevicePage-9USuEsRu.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-DGtj17VY.js → DialogClose-DxHdqMcl.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-DgUSe6A1.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-DLkuu1Iw.js → DialogTitle-C1ZeQgf2.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-Dd5RBmXd.js → FadeIn-Cx4jLl9G.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-BBCOpHqO.js → HeaderGithubStar-BOyuGkoA.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-BbigYxdC.js → HoverTooltip-BDhqMR3L.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-BBpZYjSC.js → IpCheckPage-DTvA9Wmx.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-ttXnt_s9.js → LandingPage-k1271lWV.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-DOl_cOO9.js → LeaderboardAvatar-3ibNg6If.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-BYIyoLkB.js → LeaderboardPage-Byww5UpT.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-BO2n4LSQ.js → LeaderboardProfileModal-DeGbJtvi.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-CqwW5yMw.js → LeaderboardProfilePage-DEPzO6hW.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-B-w771Tl.js → LimitsPage-DPtqL0PS.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-Cqc_diwF.js → LocalOnlyNotice-C76LAT_3.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-D6zc1JKY.js → LoginCard-DEyCJT67.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-udN3TqoF.js → LoginModal-CueMQgAZ.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-BucPr3_X.js → LoginPage-CwqBb5Fd.js} +1 -1
- package/dashboard/dist/assets/{PetPage-6_nmMXWp.js → PetPage--xQAjW4_.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-DntczBmH.js → PopoverPopup-Dt9VY2VQ.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-Z6qlwSj1.js → ProviderIcon-CHx4ohfx.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-WqQwsQC1.js → ResetPasswordPage-DB6R7fjf.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-D3NM3dOv.js → SegmentedControl-BINOpXIY.js} +1 -1
- package/dashboard/dist/assets/{Select-DCpwSFK7.js → Select-DcYXcsKv.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-DICaclqa.js → SelectItemText-BDv2Kb3x.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-COHoSjPP.js → SessionsPage-cL_w0ytB.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-nzbrBo_U.js → SettingsPage-CHGA0bQS.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-DE8mK1lS.js → SkillsPage-I29SqkXO.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-BSWGbByr.js → TokenGalaxy-BHq4YSv2.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-CxxXTuTT.js → ToolbarRootContext-akXaaKb6.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-DillSTMV.js → TrendMonitor-DPhFXzQc.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-LqyxKYAt.js → WidgetsPage-Nf0UEz7s.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-DNDuRKjf.js → WrappedPage-D4zOSsqD.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-DEhUaWQY.js → __vite-browser-external-UqlSqI-1.js} +2 -2
- package/dashboard/dist/assets/{agent-logos-D34B25op.js → agent-logos-Bja3VCAa.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-BYWbL4WV.js → arrow-up-right-CzcCNJJ2.js} +1 -1
- package/dashboard/dist/assets/{check-DkfaR6JX.js → check-DUHSZCYn.js} +1 -1
- package/dashboard/dist/assets/{chevron-right-D2cpt_J3.js → chevron-right-jLvhaNUy.js} +1 -1
- package/dashboard/dist/assets/{copy-Cv5Ix0Pn.js → copy-ChyH-ynx.js} +1 -1
- package/dashboard/dist/assets/{download-5nr9Tl80.js → download-CgwvXrUe.js} +1 -1
- package/dashboard/dist/assets/{flame-B6M-msYH.js → flame-ysf1F8uB.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-DiRRwgY8.js → format-tokens-DAa2cubl.js} +1 -1
- package/dashboard/dist/assets/{icons-DQHoG48j.js → icons-DccoWQq9.js} +1 -1
- package/dashboard/dist/assets/{index-hSxFduER.js → index-CAycNPQf.js} +1 -1
- package/dashboard/dist/assets/{index-BoH_HaYH.js → index-CxExExXh.js} +1 -1
- package/dashboard/dist/assets/{index-vFee2-sp.js → index-kkQrDRat.js} +1 -1
- package/dashboard/dist/assets/{info-CM5MR8lk.js → info-DWQQIq9M.js} +1 -1
- package/dashboard/dist/assets/{limits-providers-BV4Y9nI4.js → limits-providers-B_18X9Rp.js} +1 -1
- package/dashboard/dist/assets/{link-2-Bd5jF09Z.js → link-2-CKeVVq4P.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-B5oZJSYV.js → loader-circle-Dw0tu6IN.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-CiYy9Kri.js → maximize-2-BMU0YzEF.js} +1 -1
- package/dashboard/dist/assets/{provider-display-lzLynfde.js → provider-display-Cf9I-VLS.js} +1 -1
- package/dashboard/dist/assets/{react-CllryaXi.js → react-BhbcISKy.js} +1 -1
- package/dashboard/dist/assets/{search-DDdsotHp.js → search-BoY8EW1z.js} +1 -1
- package/dashboard/dist/assets/{skills-api-487vZmwI.js → skills-api-KVH21GJw.js} +1 -1
- package/dashboard/dist/assets/{store-CYaTRd_k.js → store-BL7rSajG.js} +1 -1
- package/dashboard/dist/assets/{terminal-OyeOY0tb.js → terminal-CeMuITdm.js} +1 -1
- package/dashboard/dist/assets/{trash-2-COI9stXJ.js → trash-2-jVUCKqrR.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-DkzinngC.js → use-community-stats-uWK0fE6l.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-CiMETU5Y.js → use-limits-display-prefs-DWsF4ShM.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-qzpxm9Bh.js → use-native-settings-SEbqWDvc.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-BI-Qh9xR.js → use-ordered-list-D4UhrPvw.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-C3UrOh55.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-1WyhIjeG.js → use-session-efficiency-pref-DbysyUTJ.js} +1 -1
- package/dashboard/dist/assets/{use-transform-D5i_ArzG.js → use-transform-DaEssGaZ.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-Drcnt7__.js → use-usage-limits-CmmQ34Ov.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-KWJo1GIr.js → useCurrency-DvBEn4rx.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-K0FPky1Y.js → useScrollLock-QozsdFng.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-CwG4ck8v.js → useTokenFormat-BTLrL7Mi.js} +1 -1
- package/dashboard/dist/assets/{zap-wZ6IQAyU.js → zap-Dp_aJ1q6.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/status.js +17 -7
- package/src/commands/sync.js +333 -5
- package/src/lib/codex-rollout-parser.js +17 -39
- package/src/lib/codex-token-usage.js +195 -0
- package/src/lib/pricing/curated-overrides.json +536 -101
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +732 -163
- package/src/lib/usage-limits.js +211 -41
- package/dashboard/dist/assets/DialogDescription-wStLk8Br.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-DrsPn1ux.js +0 -1
package/src/commands/sync.js
CHANGED
|
@@ -113,6 +113,11 @@ const {
|
|
|
113
113
|
} = require("../lib/cursor-store");
|
|
114
114
|
const { resolveTrackerPaths } = require("../lib/tracker-paths");
|
|
115
115
|
const { resolveRuntimeConfig } = require("../lib/runtime-config");
|
|
116
|
+
const { extractTokenCount } = require("../lib/codex-rollout-parser");
|
|
117
|
+
const {
|
|
118
|
+
consumeUsageDelta,
|
|
119
|
+
createUsageDeltaState,
|
|
120
|
+
} = require("../lib/codex-token-usage");
|
|
116
121
|
|
|
117
122
|
const CURSOR_UNKNOWN_MIGRATION_KEY = "cursorUnknownPurge_2026_04";
|
|
118
123
|
const ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY = "rolloutCumulativeDeltaReparse_2026_05";
|
|
@@ -189,6 +194,14 @@ const CODEX_RESCAN_DEDUP_REPAIR_KEY = "codexRescanDedupRepair_2026_06";
|
|
|
189
194
|
// offset reset for the cloud overwrite). Pre-gated: installs with no forked
|
|
190
195
|
// rollout on disk carry no fork phantom and mark done without the rebuild.
|
|
191
196
|
const CODEX_FORK_REPLAY_REPAIR_KEY = "codexForkReplayRepair_2026_07";
|
|
197
|
+
// One-time repair for Codex rollouts that contain token counters from multiple
|
|
198
|
+
// interleaved SessionState instances. Older parsers subtracted each cumulative
|
|
199
|
+
// total from the most recently observed total, even when it belonged to another
|
|
200
|
+
// state, which could turn an ordinary turn into a multi-billion-token delta.
|
|
201
|
+
const CODEX_USAGE_LINEAGE_REPAIR_KEY = "codexUsageLineageRepair_2026_07";
|
|
202
|
+
// Keep the one escalated desktop refresh bounded; explicit full syncs can retry
|
|
203
|
+
// the same migration without this ceiling when the history needs a deeper scan.
|
|
204
|
+
const CODEX_BACKGROUND_LINEAGE_SCAN_MAX_BYTES = 1024 * 1024;
|
|
192
205
|
const DROID_DUP_SESSION_REPAIR_KEY = "droidDupSessionInflationRepair_2026_06";
|
|
193
206
|
const CODEX_COLD_SCAN_AUDIT_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
194
207
|
const NOTIFY_LOCK_WAIT_MS = 130_000;
|
|
@@ -457,6 +470,28 @@ async function cmdSync(argv, context = {}) {
|
|
|
457
470
|
if (autoSourceScope) return sources.includes(autoSourceScope);
|
|
458
471
|
return true;
|
|
459
472
|
};
|
|
473
|
+
// Desktop refreshes are deliberately lightweight, but that also means a
|
|
474
|
+
// migration wired only to full sync would never repair an existing native
|
|
475
|
+
// install. Escalate exactly the first eligible background run: fresh
|
|
476
|
+
// installs have no persisted Codex buckets to repair, while retryable
|
|
477
|
+
// failures remain reserved for an explicit full sync instead of turning
|
|
478
|
+
// every five-minute refresh into a deep historical scan.
|
|
479
|
+
const backgroundCodexUsageMigrationEligible = Boolean(
|
|
480
|
+
isBackgroundLightweightSync &&
|
|
481
|
+
sourceAllowed("codex") &&
|
|
482
|
+
!cursors.migrations?.[CODEX_USAGE_LINEAGE_REPAIR_KEY]
|
|
483
|
+
);
|
|
484
|
+
const hasPersistedCodexUsage = Object.keys(cursors.hourly?.buckets || {})
|
|
485
|
+
.some((key) => key.startsWith("codex|"));
|
|
486
|
+
const backgroundCodexUsageRepair =
|
|
487
|
+
backgroundCodexUsageMigrationEligible && hasPersistedCodexUsage;
|
|
488
|
+
if (backgroundCodexUsageMigrationEligible && !hasPersistedCodexUsage) {
|
|
489
|
+
// A fresh install has no old-parser buckets to repair. Mark it complete
|
|
490
|
+
// before the first bounded parse so the next refresh does not mistake
|
|
491
|
+
// newly parsed, already-correct data for legacy history.
|
|
492
|
+
(cursors.migrations ||= {})[CODEX_USAGE_LINEAGE_REPAIR_KEY] =
|
|
493
|
+
new Date().toISOString();
|
|
494
|
+
}
|
|
460
495
|
|
|
461
496
|
const sources = [];
|
|
462
497
|
if (sourceAllowed("codex")) {
|
|
@@ -467,13 +502,13 @@ async function cmdSync(argv, context = {}) {
|
|
|
467
502
|
const codexPaths = resolveInstallPaths({ nativeValue: codexNativeValue, wslValue: wslCodexDir });
|
|
468
503
|
if (codexPaths.native) {
|
|
469
504
|
sources.push({ source: "codex", sessionsDir: path.join(codexPaths.native, "sessions"), codexInventoryCache: true });
|
|
470
|
-
if (!isBackgroundLightweightSync) {
|
|
505
|
+
if (!isBackgroundLightweightSync || backgroundCodexUsageRepair) {
|
|
471
506
|
sources.push({ source: "codex", sessionsDir: path.join(codexPaths.native, "archived_sessions"), deep: true });
|
|
472
507
|
}
|
|
473
508
|
}
|
|
474
509
|
if (codexPaths.wsl) {
|
|
475
510
|
sources.push({ source: "codex", sessionsDir: path.join(codexPaths.wsl, "sessions"), codexInventoryCache: true });
|
|
476
|
-
if (!isBackgroundLightweightSync) {
|
|
511
|
+
if (!isBackgroundLightweightSync || backgroundCodexUsageRepair) {
|
|
477
512
|
sources.push({ source: "codex", sessionsDir: path.join(codexPaths.wsl, "archived_sessions"), deep: true });
|
|
478
513
|
}
|
|
479
514
|
}
|
|
@@ -523,7 +558,7 @@ async function cmdSync(argv, context = {}) {
|
|
|
523
558
|
projectQueueStatePath,
|
|
524
559
|
rolloutFiles,
|
|
525
560
|
});
|
|
526
|
-
await repairCodexForkReplayInflation({
|
|
561
|
+
const codexForkRepairRan = await repairCodexForkReplayInflation({
|
|
527
562
|
cursors,
|
|
528
563
|
queuePath,
|
|
529
564
|
queueStatePath,
|
|
@@ -532,6 +567,15 @@ async function cmdSync(argv, context = {}) {
|
|
|
532
567
|
rolloutFiles,
|
|
533
568
|
legacyRepairRan: codexRescanRepairRan,
|
|
534
569
|
});
|
|
570
|
+
await repairCodexInterleavedUsageInflation({
|
|
571
|
+
cursors,
|
|
572
|
+
queuePath,
|
|
573
|
+
queueStatePath,
|
|
574
|
+
projectQueuePath,
|
|
575
|
+
projectQueueStatePath,
|
|
576
|
+
rolloutFiles,
|
|
577
|
+
legacyRepairRan: codexRescanRepairRan || codexForkRepairRan,
|
|
578
|
+
});
|
|
535
579
|
await repairDroidDuplicateSessionInflation({ cursors, queuePath, queueStatePath });
|
|
536
580
|
await repairMimoClaudeMislabel({
|
|
537
581
|
cursors,
|
|
@@ -540,6 +584,33 @@ async function cmdSync(argv, context = {}) {
|
|
|
540
584
|
projectQueuePath,
|
|
541
585
|
projectQueueStatePath,
|
|
542
586
|
});
|
|
587
|
+
} else if (backgroundCodexUsageRepair) {
|
|
588
|
+
await cursorStore.materializeAllCodexState(cursors);
|
|
589
|
+
try {
|
|
590
|
+
await repairCodexInterleavedUsageInflation({
|
|
591
|
+
cursors,
|
|
592
|
+
queuePath,
|
|
593
|
+
queueStatePath,
|
|
594
|
+
projectQueuePath,
|
|
595
|
+
projectQueueStatePath,
|
|
596
|
+
rolloutFiles,
|
|
597
|
+
maxLineageScanBytes: CODEX_BACKGROUND_LINEAGE_SCAN_MAX_BYTES,
|
|
598
|
+
});
|
|
599
|
+
} catch (err) {
|
|
600
|
+
warnProviderParseFailure("Codex usage lineage repair", err, opts);
|
|
601
|
+
} finally {
|
|
602
|
+
// Rebuild failures intentionally leave the migration key unset so a
|
|
603
|
+
// future full sync can retry. Record a retryable sentinel here to keep
|
|
604
|
+
// routine native background refreshes bounded after that first attempt.
|
|
605
|
+
const migrations = (cursors.migrations ||= {});
|
|
606
|
+
if (!migrations[CODEX_USAGE_LINEAGE_REPAIR_KEY]) {
|
|
607
|
+
migrations[CODEX_USAGE_LINEAGE_REPAIR_KEY] = {
|
|
608
|
+
skipped: true,
|
|
609
|
+
reason: "background_repair_requires_full_sync",
|
|
610
|
+
at: new Date().toISOString(),
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
}
|
|
543
614
|
}
|
|
544
615
|
|
|
545
616
|
const codexColdSkipEnabled = opts.auto && sourceAllowed("codex");
|
|
@@ -1326,8 +1397,9 @@ async function cmdSync(argv, context = {}) {
|
|
|
1326
1397
|
);
|
|
1327
1398
|
}
|
|
1328
1399
|
|
|
1329
|
-
// ── Kiro CLI (reads ~/Library/Application Support/kiro-cli/data.sqlite3
|
|
1330
|
-
//
|
|
1400
|
+
// ── Kiro CLI (reads ~/Library/Application Support/kiro-cli/data.sqlite3,
|
|
1401
|
+
// legacy ~/.kiro/sessions/cli/{uuid}.json, and Kiro CLI 2.13+
|
|
1402
|
+
// ~/.kiro/sessions/{workspace}/sess_{uuid}/messages.jsonl) ──
|
|
1331
1403
|
// Runs IN PARALLEL with the Kiro IDE branch above — NOT instead of it.
|
|
1332
1404
|
// Both emit source='kiro' so totals merge transparently; cursor state
|
|
1333
1405
|
// is isolated in cursors.kiroCli. Kiro CLI does not persist explicit
|
|
@@ -2339,6 +2411,7 @@ module.exports = {
|
|
|
2339
2411
|
migrateRolloutCumulativeDeltaBuckets,
|
|
2340
2412
|
repairCodexRescanInflation,
|
|
2341
2413
|
repairCodexForkReplayInflation,
|
|
2414
|
+
repairCodexInterleavedUsageInflation,
|
|
2342
2415
|
repairDroidDuplicateSessionInflation,
|
|
2343
2416
|
repairMimoClaudeMislabel,
|
|
2344
2417
|
reincludeClaudeMemObserverFiles,
|
|
@@ -2352,6 +2425,7 @@ module.exports = {
|
|
|
2352
2425
|
ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY,
|
|
2353
2426
|
CODEX_RESCAN_DEDUP_REPAIR_KEY,
|
|
2354
2427
|
CODEX_FORK_REPLAY_REPAIR_KEY,
|
|
2428
|
+
CODEX_USAGE_LINEAGE_REPAIR_KEY,
|
|
2355
2429
|
DROID_DUP_SESSION_REPAIR_KEY,
|
|
2356
2430
|
CLAUDE_MEM_OBSERVER_REINCLUDE_KEY,
|
|
2357
2431
|
GROK_APPEND_ONLY_REPAIR_MIGRATION_KEY,
|
|
@@ -3828,6 +3902,260 @@ async function repairCodexForkReplayInflation({
|
|
|
3828
3902
|
});
|
|
3829
3903
|
}
|
|
3830
3904
|
|
|
3905
|
+
// Rebuild historical Codex usage only when a rollout proves that the old
|
|
3906
|
+
// single-baseline parser crossed cumulative SessionState lineages. The cheap
|
|
3907
|
+
// head gate keeps the one-time migration from fully reading installations that
|
|
3908
|
+
// predate Codex multi-agent support; the rebuild itself retains the existing
|
|
3909
|
+
// missing-history, project-queue, atomic-swap, and upload-reset guards.
|
|
3910
|
+
async function repairCodexInterleavedUsageInflation({
|
|
3911
|
+
cursors,
|
|
3912
|
+
queuePath,
|
|
3913
|
+
queueStatePath,
|
|
3914
|
+
projectQueuePath,
|
|
3915
|
+
projectQueueStatePath,
|
|
3916
|
+
rolloutFiles,
|
|
3917
|
+
// Either older guarded repair rebuilt every Codex file with the current
|
|
3918
|
+
// parser earlier in this sync, so a second full rebuild is unnecessary.
|
|
3919
|
+
legacyRepairRan = false,
|
|
3920
|
+
maxLineageScanBytes = Infinity,
|
|
3921
|
+
}) {
|
|
3922
|
+
if (!cursors || typeof cursors !== "object") return false;
|
|
3923
|
+
const migrations = (cursors.migrations ||= {});
|
|
3924
|
+
const prior = migrations[CODEX_USAGE_LINEAGE_REPAIR_KEY];
|
|
3925
|
+
if (prior && !(typeof prior === "object" && prior.skipped)) return false;
|
|
3926
|
+
|
|
3927
|
+
if (legacyRepairRan) {
|
|
3928
|
+
migrations[CODEX_USAGE_LINEAGE_REPAIR_KEY] = new Date().toISOString();
|
|
3929
|
+
return false;
|
|
3930
|
+
}
|
|
3931
|
+
|
|
3932
|
+
const scan = await scanForInterleavedCodexUsage(rolloutFiles, {
|
|
3933
|
+
maxLineageScanBytes,
|
|
3934
|
+
});
|
|
3935
|
+
if (scan.indeterminate) {
|
|
3936
|
+
migrations[CODEX_USAGE_LINEAGE_REPAIR_KEY] = {
|
|
3937
|
+
skipped: true,
|
|
3938
|
+
reason: "usage_lineage_scan_indeterminate",
|
|
3939
|
+
at: new Date().toISOString(),
|
|
3940
|
+
};
|
|
3941
|
+
return false;
|
|
3942
|
+
}
|
|
3943
|
+
|
|
3944
|
+
if (!scan.affected) {
|
|
3945
|
+
if (!isCodexHistoryCovered(cursors, rolloutFiles)) {
|
|
3946
|
+
migrations[CODEX_USAGE_LINEAGE_REPAIR_KEY] = {
|
|
3947
|
+
skipped: true,
|
|
3948
|
+
reason: "codex_history_not_covered",
|
|
3949
|
+
at: new Date().toISOString(),
|
|
3950
|
+
};
|
|
3951
|
+
return false;
|
|
3952
|
+
}
|
|
3953
|
+
migrations[CODEX_USAGE_LINEAGE_REPAIR_KEY] = new Date().toISOString();
|
|
3954
|
+
return false;
|
|
3955
|
+
}
|
|
3956
|
+
|
|
3957
|
+
return repairCodexRescanInflation({
|
|
3958
|
+
cursors,
|
|
3959
|
+
queuePath,
|
|
3960
|
+
queueStatePath,
|
|
3961
|
+
projectQueuePath,
|
|
3962
|
+
projectQueueStatePath,
|
|
3963
|
+
rolloutFiles,
|
|
3964
|
+
migrationKey: CODEX_USAGE_LINEAGE_REPAIR_KEY,
|
|
3965
|
+
uploadNote: "reset_after_codex_usage_lineage_2026_07",
|
|
3966
|
+
});
|
|
3967
|
+
}
|
|
3968
|
+
|
|
3969
|
+
async function scanForInterleavedCodexUsage(
|
|
3970
|
+
rolloutFiles,
|
|
3971
|
+
{ maxLineageScanBytes = Infinity } = {},
|
|
3972
|
+
) {
|
|
3973
|
+
for (const entry of Array.isArray(rolloutFiles) ? rolloutFiles : []) {
|
|
3974
|
+
const fp = typeof entry === "string" ? entry : entry?.path;
|
|
3975
|
+
const src = typeof entry === "string" ? "codex" : String(entry?.source || "codex");
|
|
3976
|
+
if (!fp || src !== "codex") continue;
|
|
3977
|
+
|
|
3978
|
+
const head = await scanCodexMultiAgentHead(fp);
|
|
3979
|
+
if (head.indeterminate) return { affected: false, indeterminate: true };
|
|
3980
|
+
if (!head.candidate) continue;
|
|
3981
|
+
|
|
3982
|
+
const lineage = await scanCodexUsageLineages(fp, maxLineageScanBytes);
|
|
3983
|
+
if (lineage.indeterminate) return { affected: false, indeterminate: true };
|
|
3984
|
+
if (lineage.affected) return { affected: true, indeterminate: false };
|
|
3985
|
+
}
|
|
3986
|
+
return { affected: false, indeterminate: false };
|
|
3987
|
+
}
|
|
3988
|
+
|
|
3989
|
+
// New rollouts emit multi_agent_version in the initial turn_context. A session
|
|
3990
|
+
// created by an older Codex build can later be resumed under a multi-agent
|
|
3991
|
+
// build, though, so an inactive initial context also checks the final context
|
|
3992
|
+
// from a bounded tail read. The 1 MiB ceilings protect this one-time migration
|
|
3993
|
+
// from malformed metadata; an inconclusive bound is retryable, never a final
|
|
3994
|
+
// "not affected" verdict.
|
|
3995
|
+
async function scanCodexMultiAgentHead(filePath) {
|
|
3996
|
+
const MAX_BYTES = 1024 * 1024;
|
|
3997
|
+
let stream = null;
|
|
3998
|
+
let rl = null;
|
|
3999
|
+
let needsTail = false;
|
|
4000
|
+
try {
|
|
4001
|
+
stream = fssync.createReadStream(filePath, {
|
|
4002
|
+
encoding: "utf8",
|
|
4003
|
+
highWaterMark: 32 * 1024,
|
|
4004
|
+
});
|
|
4005
|
+
rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
4006
|
+
let bytesRead = 0;
|
|
4007
|
+
for await (const line of rl) {
|
|
4008
|
+
bytesRead += Buffer.byteLength(line, "utf8") + 1;
|
|
4009
|
+
if (line.includes('"turn_context"')) {
|
|
4010
|
+
let obj;
|
|
4011
|
+
try {
|
|
4012
|
+
obj = JSON.parse(line);
|
|
4013
|
+
} catch (_e) {
|
|
4014
|
+
return { candidate: false, indeterminate: true };
|
|
4015
|
+
}
|
|
4016
|
+
if (obj?.type === "turn_context") {
|
|
4017
|
+
if (isActiveMultiAgentVersion(obj?.payload?.multi_agent_version)) {
|
|
4018
|
+
return { candidate: true, indeterminate: false };
|
|
4019
|
+
}
|
|
4020
|
+
needsTail = true;
|
|
4021
|
+
}
|
|
4022
|
+
}
|
|
4023
|
+
if (line.includes('"token_count"')) {
|
|
4024
|
+
needsTail = true;
|
|
4025
|
+
break;
|
|
4026
|
+
}
|
|
4027
|
+
if (bytesRead >= MAX_BYTES) return { candidate: false, indeterminate: true };
|
|
4028
|
+
}
|
|
4029
|
+
} catch (_e) {
|
|
4030
|
+
return { candidate: false, indeterminate: true };
|
|
4031
|
+
} finally {
|
|
4032
|
+
if (rl) rl.close();
|
|
4033
|
+
if (stream) stream.destroy();
|
|
4034
|
+
}
|
|
4035
|
+
if (needsTail) return scanCodexMultiAgentTail(filePath);
|
|
4036
|
+
return { candidate: false, indeterminate: false };
|
|
4037
|
+
}
|
|
4038
|
+
|
|
4039
|
+
async function scanCodexMultiAgentTail(filePath) {
|
|
4040
|
+
const MAX_BYTES = 1024 * 1024;
|
|
4041
|
+
let handle = null;
|
|
4042
|
+
try {
|
|
4043
|
+
handle = await fs.open(filePath, "r");
|
|
4044
|
+
const stat = await handle.stat();
|
|
4045
|
+
const size = Number(stat.size || 0);
|
|
4046
|
+
const start = Math.max(0, size - MAX_BYTES);
|
|
4047
|
+
const length = Math.max(0, size - start);
|
|
4048
|
+
const buffer = Buffer.alloc(length);
|
|
4049
|
+
let bytesRead = 0;
|
|
4050
|
+
while (bytesRead < length) {
|
|
4051
|
+
const result = await handle.read(
|
|
4052
|
+
buffer,
|
|
4053
|
+
bytesRead,
|
|
4054
|
+
length - bytesRead,
|
|
4055
|
+
start + bytesRead,
|
|
4056
|
+
);
|
|
4057
|
+
if (result.bytesRead <= 0) break;
|
|
4058
|
+
bytesRead += result.bytesRead;
|
|
4059
|
+
}
|
|
4060
|
+
if (bytesRead !== length) return { candidate: false, indeterminate: true };
|
|
4061
|
+
const lines = buffer.toString("utf8").split("\n");
|
|
4062
|
+
if (start > 0) lines.shift(); // discard a line truncated by the tail boundary
|
|
4063
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
4064
|
+
const line = lines[index];
|
|
4065
|
+
if (!line.includes('"turn_context"')) continue;
|
|
4066
|
+
let obj;
|
|
4067
|
+
try {
|
|
4068
|
+
obj = JSON.parse(line);
|
|
4069
|
+
} catch (_e) {
|
|
4070
|
+
return { candidate: false, indeterminate: true };
|
|
4071
|
+
}
|
|
4072
|
+
if (obj?.type !== "turn_context") continue;
|
|
4073
|
+
return {
|
|
4074
|
+
candidate: isActiveMultiAgentVersion(obj?.payload?.multi_agent_version),
|
|
4075
|
+
indeterminate: false,
|
|
4076
|
+
};
|
|
4077
|
+
}
|
|
4078
|
+
return { candidate: false, indeterminate: start > 0 };
|
|
4079
|
+
} catch (_e) {
|
|
4080
|
+
return { candidate: false, indeterminate: true };
|
|
4081
|
+
} finally {
|
|
4082
|
+
if (handle) await handle.close().catch(() => {});
|
|
4083
|
+
}
|
|
4084
|
+
}
|
|
4085
|
+
|
|
4086
|
+
function isActiveMultiAgentVersion(value) {
|
|
4087
|
+
if (value === true) return true;
|
|
4088
|
+
if (typeof value !== "string") return false;
|
|
4089
|
+
const normalized = value.trim().toLowerCase();
|
|
4090
|
+
return Boolean(
|
|
4091
|
+
normalized &&
|
|
4092
|
+
normalized !== "off" &&
|
|
4093
|
+
normalized !== "none" &&
|
|
4094
|
+
normalized !== "disabled",
|
|
4095
|
+
);
|
|
4096
|
+
}
|
|
4097
|
+
|
|
4098
|
+
async function scanCodexUsageLineages(filePath, maxBytes = Infinity) {
|
|
4099
|
+
let stream = null;
|
|
4100
|
+
let rl = null;
|
|
4101
|
+
try {
|
|
4102
|
+
const state = createUsageDeltaState();
|
|
4103
|
+
const byteLimit = Number.isFinite(maxBytes) ? Math.max(0, maxBytes) : Infinity;
|
|
4104
|
+
let bytesRead = 0;
|
|
4105
|
+
stream = fssync.createReadStream(filePath, {
|
|
4106
|
+
encoding: "utf8",
|
|
4107
|
+
highWaterMark: 32 * 1024,
|
|
4108
|
+
});
|
|
4109
|
+
rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
4110
|
+
for await (const line of rl) {
|
|
4111
|
+
bytesRead += Buffer.byteLength(line, "utf8") + 1;
|
|
4112
|
+
if (bytesRead > byteLimit) return { affected: false, indeterminate: true };
|
|
4113
|
+
if (!line.includes('"token_count"')) continue;
|
|
4114
|
+
let obj;
|
|
4115
|
+
try {
|
|
4116
|
+
obj = JSON.parse(line);
|
|
4117
|
+
} catch (_e) {
|
|
4118
|
+
continue;
|
|
4119
|
+
}
|
|
4120
|
+
const token = extractTokenCount(obj);
|
|
4121
|
+
const info = token?.info;
|
|
4122
|
+
if (!info || typeof info !== "object") continue;
|
|
4123
|
+
consumeUsageDelta(state, info.last_token_usage, info.total_token_usage);
|
|
4124
|
+
if (state.sawInterleaved || state.sawDivergentCumulative) {
|
|
4125
|
+
return { affected: true, indeterminate: false };
|
|
4126
|
+
}
|
|
4127
|
+
}
|
|
4128
|
+
return { affected: false, indeterminate: false };
|
|
4129
|
+
} catch (_e) {
|
|
4130
|
+
return { affected: false, indeterminate: true };
|
|
4131
|
+
} finally {
|
|
4132
|
+
if (rl) rl.close();
|
|
4133
|
+
if (stream) stream.destroy();
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
4136
|
+
|
|
4137
|
+
function isCodexHistoryCovered(cursors, rolloutFiles) {
|
|
4138
|
+
const scannedPaths = new Set();
|
|
4139
|
+
const scannedSessionIds = new Set();
|
|
4140
|
+
for (const entry of Array.isArray(rolloutFiles) ? rolloutFiles : []) {
|
|
4141
|
+
const fp = typeof entry === "string" ? entry : entry?.path;
|
|
4142
|
+
const src = typeof entry === "string" ? "codex" : String(entry?.source || "codex");
|
|
4143
|
+
if (!fp || src !== "codex") continue;
|
|
4144
|
+
scannedPaths.add(fp);
|
|
4145
|
+
const id = codexSessionIdFromPath(fp);
|
|
4146
|
+
if (id) scannedSessionIds.add(id);
|
|
4147
|
+
}
|
|
4148
|
+
if (!cursors.files || typeof cursors.files !== "object") return true;
|
|
4149
|
+
for (const fp of Object.keys(cursors.files)) {
|
|
4150
|
+
if (!isCodexSessionCursorPath(fp)) continue;
|
|
4151
|
+
if (scannedPaths.has(fp)) continue;
|
|
4152
|
+
const id = codexSessionIdFromPath(fp);
|
|
4153
|
+
if (id && scannedSessionIds.has(id)) continue;
|
|
4154
|
+
return false;
|
|
4155
|
+
}
|
|
4156
|
+
return true;
|
|
4157
|
+
}
|
|
4158
|
+
|
|
3831
4159
|
// Scans codex rollout heads for the fork marker. The child session_meta (with
|
|
3832
4160
|
// forked_from_id) is the FIRST line of a forked rollout — observed at byte
|
|
3833
4161
|
// offset ≤169 across every local sample — so a bounded head read is
|
|
@@ -11,6 +11,12 @@ const path = require("node:path");
|
|
|
11
11
|
const readline = require("node:readline");
|
|
12
12
|
|
|
13
13
|
const { listRolloutFiles } = require("./rollout");
|
|
14
|
+
const {
|
|
15
|
+
consumeUsageDelta,
|
|
16
|
+
createUsageDeltaState,
|
|
17
|
+
snapshotUsageBaselines,
|
|
18
|
+
totalsReset,
|
|
19
|
+
} = require("./codex-token-usage");
|
|
14
20
|
const {
|
|
15
21
|
emptyTotals,
|
|
16
22
|
addInto,
|
|
@@ -434,42 +440,10 @@ function normalizeUsage(u) {
|
|
|
434
440
|
return out;
|
|
435
441
|
}
|
|
436
442
|
|
|
437
|
-
function totalsReset(curr, prev) {
|
|
438
|
-
const a = Number(curr?.total_tokens);
|
|
439
|
-
const b = Number(prev?.total_tokens);
|
|
440
|
-
if (!Number.isFinite(a) || !Number.isFinite(b)) return false;
|
|
441
|
-
return a < b;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
443
|
function pickDelta(lastUsage, totalUsage, prevTotals) {
|
|
445
|
-
const
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
if (hasTotal && hasPrev) {
|
|
450
|
-
if (totalsReset(totalUsage, prevTotals)) {
|
|
451
|
-
const resetUsage = hasLast ? lastUsage : totalUsage;
|
|
452
|
-
return normalizeUsage(resetUsage);
|
|
453
|
-
}
|
|
454
|
-
const delta = {};
|
|
455
|
-
for (const k of [
|
|
456
|
-
"input_tokens",
|
|
457
|
-
"cached_input_tokens",
|
|
458
|
-
"cache_creation_input_tokens",
|
|
459
|
-
"output_tokens",
|
|
460
|
-
"reasoning_output_tokens",
|
|
461
|
-
"total_tokens",
|
|
462
|
-
]) {
|
|
463
|
-
const a = Number(totalUsage[k]);
|
|
464
|
-
const b = Number(prevTotals[k]);
|
|
465
|
-
if (Number.isFinite(a) && Number.isFinite(b)) delta[k] = Math.max(0, a - b);
|
|
466
|
-
}
|
|
467
|
-
return normalizeUsage(delta);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
if (hasLast) return normalizeUsage(lastUsage);
|
|
471
|
-
if (hasTotal) return normalizeUsage(totalUsage);
|
|
472
|
-
return null;
|
|
444
|
+
const state = createUsageDeltaState({ lastTotal: prevTotals });
|
|
445
|
+
const delta = consumeUsageDelta(state, lastUsage, totalUsage);
|
|
446
|
+
return delta ? normalizeUsage(delta) : null;
|
|
473
447
|
}
|
|
474
448
|
|
|
475
449
|
// ---------------------------------------------------------------------------
|
|
@@ -596,7 +570,10 @@ async function parseCodexRolloutFile(filePath, {
|
|
|
596
570
|
let provider = isResuming ? resumeState.provider || null : null;
|
|
597
571
|
let cliVersion = isResuming ? resumeState.cliVersion || null : null;
|
|
598
572
|
|
|
599
|
-
|
|
573
|
+
const usageDeltaState = createUsageDeltaState({
|
|
574
|
+
lastTotal: isResuming ? resumeState.prevTotals || null : null,
|
|
575
|
+
baselines: isResuming ? resumeState.tokenUsageBaselines || null : null,
|
|
576
|
+
});
|
|
600
577
|
let pendingToolNames = isResuming
|
|
601
578
|
? Array.from(resumeState.pendingToolNames || [])
|
|
602
579
|
: [];
|
|
@@ -875,8 +852,8 @@ async function parseCodexRolloutFile(filePath, {
|
|
|
875
852
|
const info = tokenCount.info;
|
|
876
853
|
const lastUsage = info?.last_token_usage;
|
|
877
854
|
const totalUsage = info?.total_token_usage;
|
|
878
|
-
const
|
|
879
|
-
|
|
855
|
+
const rawDelta = consumeUsageDelta(usageDeltaState, lastUsage, totalUsage);
|
|
856
|
+
const delta = rawDelta ? normalizeUsage(rawDelta) : null;
|
|
880
857
|
const isNewResumeEvent = noteTokenEvent(ts, lastUsage, totalUsage);
|
|
881
858
|
if (inRequestedRange) {
|
|
882
859
|
const eventSessionId = sessionId || rolloutSessionIdFromPath(primaryFilePath) || primaryFilePath;
|
|
@@ -927,7 +904,8 @@ async function parseCodexRolloutFile(filePath, {
|
|
|
927
904
|
model,
|
|
928
905
|
provider,
|
|
929
906
|
cliVersion,
|
|
930
|
-
prevTotals,
|
|
907
|
+
prevTotals: usageDeltaState.lastTotal,
|
|
908
|
+
tokenUsageBaselines: snapshotUsageBaselines(usageDeltaState),
|
|
931
909
|
pendingToolNames,
|
|
932
910
|
pendingSkills,
|
|
933
911
|
pendingExecDetails,
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// A rollout can contain TokenCount events from more than one Codex SessionState
|
|
4
|
+
// (for example, a parent agent and an embedded reviewer). Each SessionState has
|
|
5
|
+
// its own cumulative total, but the event does not carry a stable stream id.
|
|
6
|
+
// Recover the stream from Codex's invariant: total - last = that stream's prior
|
|
7
|
+
// total. Keeping a small LRU of stream heads also makes repeated rate-limit
|
|
8
|
+
// snapshots idempotent when the streams are interleaved.
|
|
9
|
+
|
|
10
|
+
const USAGE_FIELDS = [
|
|
11
|
+
"input_tokens",
|
|
12
|
+
"cached_input_tokens",
|
|
13
|
+
"cache_creation_input_tokens",
|
|
14
|
+
"output_tokens",
|
|
15
|
+
"reasoning_output_tokens",
|
|
16
|
+
"total_tokens",
|
|
17
|
+
];
|
|
18
|
+
const MAX_USAGE_BASELINES = 32;
|
|
19
|
+
|
|
20
|
+
function canonicalUsage(value) {
|
|
21
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
22
|
+
const out = {};
|
|
23
|
+
let sawNumber = false;
|
|
24
|
+
for (const field of USAGE_FIELDS) {
|
|
25
|
+
const raw = field === "cache_creation_input_tokens"
|
|
26
|
+
? value.cache_creation_input_tokens ?? value.cache_write_input_tokens
|
|
27
|
+
: value[field];
|
|
28
|
+
const number = Number(raw);
|
|
29
|
+
if (Number.isFinite(number) && number >= 0) {
|
|
30
|
+
out[field] = Math.floor(number);
|
|
31
|
+
sawNumber = true;
|
|
32
|
+
} else {
|
|
33
|
+
out[field] = 0;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return sawNumber ? out : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function usageSignature(value) {
|
|
40
|
+
const usage = canonicalUsage(value);
|
|
41
|
+
return usage ? USAGE_FIELDS.map((field) => usage[field]).join(":") : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function sameUsage(left, right) {
|
|
45
|
+
const leftSignature = usageSignature(left);
|
|
46
|
+
return leftSignature !== null && leftSignature === usageSignature(right);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function subtractUsage(totalUsage, lastUsage) {
|
|
50
|
+
const total = canonicalUsage(totalUsage);
|
|
51
|
+
const last = canonicalUsage(lastUsage);
|
|
52
|
+
if (!total || !last) return null;
|
|
53
|
+
const previous = {};
|
|
54
|
+
for (const field of USAGE_FIELDS) {
|
|
55
|
+
if (total[field] < last[field]) return null;
|
|
56
|
+
previous[field] = total[field] - last[field];
|
|
57
|
+
}
|
|
58
|
+
return previous;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function diffUsage(totalUsage, previousUsage) {
|
|
62
|
+
const total = canonicalUsage(totalUsage);
|
|
63
|
+
const previous = canonicalUsage(previousUsage);
|
|
64
|
+
if (!total || !previous) return null;
|
|
65
|
+
const delta = {};
|
|
66
|
+
for (const field of USAGE_FIELDS) {
|
|
67
|
+
delta[field] = Math.max(0, total[field] - previous[field]);
|
|
68
|
+
}
|
|
69
|
+
return delta;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function totalsReset(totalUsage, previousUsage) {
|
|
73
|
+
const total = canonicalUsage(totalUsage);
|
|
74
|
+
const previous = canonicalUsage(previousUsage);
|
|
75
|
+
return Boolean(total && previous && total.total_tokens < previous.total_tokens);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function createUsageDeltaState({ lastTotal = null, baselines = null } = {}) {
|
|
79
|
+
const state = {
|
|
80
|
+
lastTotal: null,
|
|
81
|
+
baselines: [],
|
|
82
|
+
sawDivergentCumulative: false,
|
|
83
|
+
sawInterleaved: false,
|
|
84
|
+
};
|
|
85
|
+
for (const baseline of Array.isArray(baselines) ? baselines : []) {
|
|
86
|
+
touchBaseline(state, baseline);
|
|
87
|
+
}
|
|
88
|
+
if (canonicalUsage(lastTotal)) touchBaseline(state, lastTotal);
|
|
89
|
+
return state;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function snapshotUsageBaselines(state) {
|
|
93
|
+
return Array.isArray(state?.baselines)
|
|
94
|
+
? state.baselines.map((usage) => ({ ...usage }))
|
|
95
|
+
: [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function consumeUsageDelta(state, lastUsage, totalUsage) {
|
|
99
|
+
if (!state || typeof state !== "object") {
|
|
100
|
+
throw new TypeError("usage delta state is required");
|
|
101
|
+
}
|
|
102
|
+
if (!Array.isArray(state.baselines)) state.baselines = [];
|
|
103
|
+
|
|
104
|
+
const last = canonicalUsage(lastUsage);
|
|
105
|
+
const total = canonicalUsage(totalUsage);
|
|
106
|
+
if (!total) return last;
|
|
107
|
+
|
|
108
|
+
const activeSignature = usageSignature(state.lastTotal);
|
|
109
|
+
const duplicateIndex = findBaselineIndex(state, total);
|
|
110
|
+
if (duplicateIndex >= 0) {
|
|
111
|
+
if (
|
|
112
|
+
activeSignature !== null &&
|
|
113
|
+
usageSignature(state.baselines[duplicateIndex]) !== activeSignature
|
|
114
|
+
) {
|
|
115
|
+
state.sawInterleaved = true;
|
|
116
|
+
}
|
|
117
|
+
touchBaselineAt(state, duplicateIndex, total);
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (last) {
|
|
122
|
+
const expectedPrevious = subtractUsage(total, last);
|
|
123
|
+
const lineageIndex = findBaselineIndex(state, expectedPrevious);
|
|
124
|
+
if (lineageIndex >= 0) {
|
|
125
|
+
if (
|
|
126
|
+
activeSignature !== null &&
|
|
127
|
+
usageSignature(state.baselines[lineageIndex]) !== activeSignature
|
|
128
|
+
) {
|
|
129
|
+
state.sawInterleaved = true;
|
|
130
|
+
}
|
|
131
|
+
touchBaselineAt(state, lineageIndex, total);
|
|
132
|
+
return last;
|
|
133
|
+
}
|
|
134
|
+
if (expectedPrevious) {
|
|
135
|
+
if (canonicalUsage(state.lastTotal)) state.sawDivergentCumulative = true;
|
|
136
|
+
touchBaseline(state, total);
|
|
137
|
+
return last;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const active = canonicalUsage(state.lastTotal);
|
|
142
|
+
if (active && !totalsReset(total, active)) {
|
|
143
|
+
const delta = diffUsage(total, active);
|
|
144
|
+
if (!last || Number(delta?.total_tokens || 0) <= Number(last.total_tokens || 0)) {
|
|
145
|
+
const activeIndex = findBaselineIndex(state, active);
|
|
146
|
+
touchBaselineAt(state, activeIndex, total);
|
|
147
|
+
return delta;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// A cumulative jump larger than last usage cannot come from the normal
|
|
151
|
+
// append_last_usage path. Treat it as a new stream instead of charging the
|
|
152
|
+
// unrelated cumulative gap; a later event will establish its lineage.
|
|
153
|
+
state.sawDivergentCumulative = true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
touchBaseline(state, total);
|
|
157
|
+
return last || total;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function findBaselineIndex(state, usage) {
|
|
161
|
+
const signature = usageSignature(usage);
|
|
162
|
+
if (signature === null) return -1;
|
|
163
|
+
return state.baselines.findIndex((baseline) => usageSignature(baseline) === signature);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function touchBaselineAt(state, index, usage) {
|
|
167
|
+
const canonical = canonicalUsage(usage);
|
|
168
|
+
if (!canonical) return;
|
|
169
|
+
if (Number.isInteger(index) && index >= 0 && index < state.baselines.length) {
|
|
170
|
+
state.baselines.splice(index, 1);
|
|
171
|
+
} else {
|
|
172
|
+
const duplicateIndex = findBaselineIndex(state, canonical);
|
|
173
|
+
if (duplicateIndex >= 0) state.baselines.splice(duplicateIndex, 1);
|
|
174
|
+
}
|
|
175
|
+
state.baselines.push(canonical);
|
|
176
|
+
while (state.baselines.length > MAX_USAGE_BASELINES) state.baselines.shift();
|
|
177
|
+
state.lastTotal = canonical;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function touchBaseline(state, usage) {
|
|
181
|
+
touchBaselineAt(state, findBaselineIndex(state, usage), usage);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = {
|
|
185
|
+
MAX_USAGE_BASELINES,
|
|
186
|
+
canonicalUsage,
|
|
187
|
+
consumeUsageDelta,
|
|
188
|
+
createUsageDeltaState,
|
|
189
|
+
diffUsage,
|
|
190
|
+
sameUsage,
|
|
191
|
+
snapshotUsageBaselines,
|
|
192
|
+
subtractUsage,
|
|
193
|
+
totalsReset,
|
|
194
|
+
usageSignature,
|
|
195
|
+
};
|