tokentracker-cli 0.84.5 → 0.84.7
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 +10 -8
- package/README.ja.md +9 -7
- package/README.ko.md +9 -7
- package/README.md +12 -10
- package/README.zh-CN.md +9 -7
- package/dashboard/dist/assets/{AchievementBadge-DJvRfvu1.js → AchievementBadge-DAPsTMsx.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-Cr8WToxS.js → AchievementsPage-B9oXfXvm.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-BCPYB46e.js → AchievementsSection-DI_LTZb3.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-D-12Ek-H.js → BadgeDetailModal--WCrJWVa.js} +1 -1
- package/dashboard/dist/assets/Card-BzbSN3EC.js +1 -0
- package/dashboard/dist/assets/{ClawdAnimated-DoGgAppT.js → ClawdAnimated-Dgxae1At.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-DIKocEAD.js → CommandPalette-BiZBYV5A.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-BNuMbEWy.js → CommunityStatsModal-C6jZx10_.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-F2gPIIS9.js → ConfirmModal-lD-mHMvO.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-YvoCEGG9.js → DashboardPage-DCPNoiRG.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-CbC_J22s.js → DevicePage-DiF2IOjJ.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-VEynQCYg.js → DialogClose-Crx2EFnI.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-CHsHo924.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-BbAVHI_v.js → DialogTitle-amHXttsp.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-CZVqSnis.js → FadeIn-CIhF_9PP.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-Cs_h332Y.js → HeaderGithubStar-D6Ph2GuY.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-DvvHTphK.js → HoverTooltip-BS8eO6tj.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-DPVmqSS_.js → IpCheckPage-Cz1Je6WS.js} +1 -1
- package/dashboard/dist/assets/LandingPage-DqeXJfwn.js +2 -0
- package/dashboard/dist/assets/{LeaderboardAvatar-BAPZ2ROX.js → LeaderboardAvatar-D09yLoVA.js} +1 -1
- package/dashboard/dist/assets/LeaderboardPage-BetpFetS.js +2 -0
- package/dashboard/dist/assets/{LeaderboardProfileModal-r03akfe2.js → LeaderboardProfileModal-UW3KnANZ.js} +4 -4
- package/dashboard/dist/assets/{LeaderboardProfilePage-CU63YW2M.js → LeaderboardProfilePage-Chd_2AVY.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-uhpNteXJ.js +4 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-BHlcPCZk.js → LocalOnlyNotice-CpFFX8ro.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-C-dqUumR.js → LoginCard-DkZpGnVu.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-BFzRg4Iy.js → LoginModal-CALv6jkD.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-QtlnGF1S.js → LoginPage-BZuXHYAT.js} +1 -1
- package/dashboard/dist/assets/PetPage-C1LuDyYr.js +1 -0
- package/dashboard/dist/assets/{PopoverPopup-CltmqcrT.js → PopoverPopup-CluVhrMr.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-BqOI9EiD.js → ProviderIcon-D3YKjVKb.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-BsfCBUZD.js → ResetPasswordPage-CzN50nX5.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-D4biXw_Q.js → SegmentedControl-DFOAcEKU.js} +1 -1
- package/dashboard/dist/assets/{Select-DTw-14iE.js → Select-B3W_ihs_.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-3tUrnu3f.js → SelectItemText-FXRtXnFV.js} +1 -1
- package/dashboard/dist/assets/ServiceStatusPage-BItZqH7q.js +1 -0
- package/dashboard/dist/assets/{SessionsPage-DYYlNqHg.js → SessionsPage-DVQU-hTH.js} +2 -2
- package/dashboard/dist/assets/SettingsPage-ByvrwOi6.js +1 -0
- package/dashboard/dist/assets/SkillsPage-DrGr5Qij.js +1 -0
- package/dashboard/dist/assets/{TokenGalaxy-DU77e8sM.js → TokenGalaxy-ClF_n9od.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-DV3KGf-a.js → ToolbarRootContext-B82hGhAf.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-D6QYKmbl.js → TrendMonitor-DkWm7_1z.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-DSP4jCsH.js → WidgetsPage-DCY4pyR2.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-B0x6O_DU.js → WrappedPage-yGQ4DLqj.js} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-BdRrPLk6.js +1483 -0
- package/dashboard/dist/assets/__vite-browser-external-hXV_ZYTG.css +1 -0
- package/dashboard/dist/assets/{agent-logos-B2S_cdOL.js → agent-logos-D5SHGOxB.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-DNUVuhx2.js → arrow-up-right-CbMcAcss.js} +1 -1
- package/dashboard/dist/assets/{check-C3ARiiE_.js → check-Dyzh7lTz.js} +1 -1
- package/dashboard/dist/assets/{chevron-right-3bY7H8jO.js → chevron-right-DT06ZIKC.js} +1 -1
- package/dashboard/dist/assets/{copy-C-UIMKoO.js → copy-B3Kt3Wrf.js} +1 -1
- package/dashboard/dist/assets/{download-DtRzJfcr.js → download-B3rOytqI.js} +1 -1
- package/dashboard/dist/assets/external-link-BamTP0Ks.js +1 -0
- package/dashboard/dist/assets/{flame-Bas3ry-g.js → flame-msy2h1ms.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-DFtRUIH8.js → format-tokens-8b1OUM_8.js} +1 -1
- package/dashboard/dist/assets/{icons-CHBt4Tmo.js → icons-C2NR2_Tf.js} +1 -1
- package/dashboard/dist/assets/{index-CZRlrK7w.js → index-C8sDJzUY.js} +1 -1
- package/dashboard/dist/assets/{index-C1ADnvzm.js → index-CvTfpiCF.js} +1 -1
- package/dashboard/dist/assets/{index-4unZlm2w.js → index-x3vFJQFt.js} +1 -1
- package/dashboard/dist/assets/{info-BkrO-fg3.js → info-C9j23aGq.js} +1 -1
- package/dashboard/dist/assets/limits-providers-ySB9Ci5l.js +1 -0
- package/dashboard/dist/assets/{link-2-DTLFz8uG.js → link-2-rrcyTT6S.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-BqQW_Ipq.js → loader-circle-x9x9I8Hv.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-YdpeE4dq.js → maximize-2-7TWFb2k7.js} +1 -1
- package/dashboard/dist/assets/{provider-display-CgBOpjq0.js → provider-display-JbSwpvxU.js} +1 -1
- package/dashboard/dist/assets/{react-DD9jpv--.js → react-DK5clHtY.js} +1 -1
- package/dashboard/dist/assets/{search--TvX-fig.js → search-DukXRvEV.js} +1 -1
- package/dashboard/dist/assets/{skills-api-C5C8a1at.js → skills-api-BUFJuwiR.js} +1 -1
- package/dashboard/dist/assets/{store-CRA8gPnP.js → store-BTh7bv09.js} +1 -1
- package/dashboard/dist/assets/{terminal-BXZyl2ek.js → terminal-CcUqLtn1.js} +1 -1
- package/dashboard/dist/assets/{trash-2-IFdBQ65v.js → trash-2-Bq7hnvNq.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-LyJ5aMpX.js → use-community-stats-qTpx6ugG.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-CKvFS1Nn.js → use-limits-display-prefs-Di_J4eVv.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-Cg-4j_0z.js → use-native-settings-CIqpv9Z3.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-2pX5PXZO.js → use-ordered-list-OwogSXT7.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-5QGGPoom.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-DilJOODA.js → use-session-efficiency-pref-CjAvMh7u.js} +1 -1
- package/dashboard/dist/assets/{use-transform-DiyYTLpI.js → use-transform-KZLdt-NZ.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-DkUmp_nW.js → use-usage-limits-Tbifa037.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-lm7KjGoD.js → useCurrency-D2td-bkn.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-DVSyJ-GE.js → useScrollLock-Cvyfyezc.js} +1 -1
- package/dashboard/dist/assets/useTokenFormat-DYIUS6IH.js +1 -0
- package/dashboard/dist/assets/{zap-CKZt1_i5.js → zap-B19mEed5.js} +1 -1
- package/dashboard/dist/brand-logos/qoder.svg +5 -0
- package/dashboard/dist/index.html +18 -15
- package/dashboard/dist/ip-check.html +2 -2
- package/dashboard/dist/leaderboard.html +2 -2
- package/dashboard/dist/llms.txt +3 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +2 -2
- package/src/commands/init.js +1 -0
- package/src/commands/status.js +34 -7
- package/src/commands/sync.js +353 -5
- package/src/lib/codex-rollout-parser.js +17 -39
- package/src/lib/codex-token-usage.js +195 -0
- package/src/lib/local-api.js +4 -1
- package/src/lib/pet-packages.js +44 -2
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/provider-status.js +104 -0
- package/src/lib/qoder-limits.js +985 -0
- package/src/lib/rollout.js +744 -91
- package/src/lib/usage-limits.js +243 -43
- package/dashboard/dist/assets/Card-Ha3x9zIj.js +0 -1
- package/dashboard/dist/assets/DialogDescription-B9bb9Ar0.js +0 -1
- package/dashboard/dist/assets/LandingPage-BCvNh3zE.js +0 -2
- package/dashboard/dist/assets/LeaderboardPage-DZSxazHP.js +0 -2
- package/dashboard/dist/assets/LimitsPage-JWZDa5_Y.js +0 -4
- package/dashboard/dist/assets/PetPage-C6UMY2qD.js +0 -1
- package/dashboard/dist/assets/SettingsPage-CYlEboSl.js +0 -1
- package/dashboard/dist/assets/SkillsPage-Bzq8V5Pd.js +0 -1
- package/dashboard/dist/assets/__vite-browser-external-D3llGq6p.js +0 -1465
- package/dashboard/dist/assets/__vite-browser-external-WKilYZnq.css +0 -1
- package/dashboard/dist/assets/limits-providers-BtwYmYHA.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-BCR01kRg.js +0 -1
- package/dashboard/dist/assets/useTokenFormat-Li8BANKO.js +0 -1
|
@@ -75,8 +75,8 @@
|
|
|
75
75
|
"description": "Shareable Token Tracker dashboard snapshot."
|
|
76
76
|
}
|
|
77
77
|
</script>
|
|
78
|
-
<script type="module" crossorigin src="/assets/__vite-browser-external-
|
|
79
|
-
<link rel="stylesheet" crossorigin href="/assets/__vite-browser-external-
|
|
78
|
+
<script type="module" crossorigin src="/assets/__vite-browser-external-BdRrPLk6.js"></script>
|
|
79
|
+
<link rel="stylesheet" crossorigin href="/assets/__vite-browser-external-hXV_ZYTG.css">
|
|
80
80
|
</head>
|
|
81
81
|
<body>
|
|
82
82
|
<main class="aeo-seed-content" aria-label="Token Tracker share page summary">
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokentracker-cli",
|
|
3
|
-
"version": "0.84.
|
|
4
|
-
"description": "Local-first AI coding token usage and cost tracker for
|
|
3
|
+
"version": "0.84.7",
|
|
4
|
+
"description": "Local-first AI coding token usage and cost tracker for 28 tools, with a dashboard, desktop pet, widgets, achievements, and native macOS/Windows apps.",
|
|
5
5
|
"main": "src/cli.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"tokentracker-cli": "bin/tracker.js",
|
package/src/commands/init.js
CHANGED
package/src/commands/status.js
CHANGED
|
@@ -47,6 +47,7 @@ const {
|
|
|
47
47
|
resolveKimiWireFiles,
|
|
48
48
|
resolveKimiCodeWireFiles,
|
|
49
49
|
resolveKiroCliDbPath,
|
|
50
|
+
resolveKiroCliSessionFiles,
|
|
50
51
|
resolveCodebuddyHome,
|
|
51
52
|
resolveCodebuddyProjectFiles,
|
|
52
53
|
resolveWorkbuddyHome,
|
|
@@ -61,6 +62,7 @@ const {
|
|
|
61
62
|
resolveKilocodeTaskFiles,
|
|
62
63
|
resolveRoocodeTaskFiles,
|
|
63
64
|
resolveZedDbPath,
|
|
65
|
+
resolveQoderDbPaths,
|
|
64
66
|
resolveAnythingllmDbPath,
|
|
65
67
|
resolveGooseDbPath,
|
|
66
68
|
listDroidSettingsFiles,
|
|
@@ -229,12 +231,14 @@ async function cmdStatus(argv = []) {
|
|
|
229
231
|
const kimiCodeHome = process.env.KIMI_CODE_HOME || path.join(home, ".kimi-code");
|
|
230
232
|
const kimiCodeInstalled = fssync.existsSync(path.join(kimiCodeHome, "sessions"));
|
|
231
233
|
|
|
232
|
-
// Kiro CLI — reads
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
// surfaces the CLI sub-path separately for operators.
|
|
234
|
+
// Kiro CLI — reads the legacy SQLite/session files and Kiro CLI 2.13+
|
|
235
|
+
// event sessions. End-user dashboards show them merged under "Kiro"; this
|
|
236
|
+
// status line surfaces which passive sources are actually present.
|
|
236
237
|
const kiroCliDbPath = resolveKiroCliDbPath(process.env);
|
|
237
|
-
const
|
|
238
|
+
const kiroCliDbExists = fssync.existsSync(kiroCliDbPath);
|
|
239
|
+
const kiroCliSessionFiles = resolveKiroCliSessionFiles(process.env);
|
|
240
|
+
const kiroCliInstalled =
|
|
241
|
+
kiroCliDbExists || kiroCliSessionFiles.length > 0;
|
|
238
242
|
|
|
239
243
|
// AnythingLLM Desktop — per-message token metrics in workspace_chats.
|
|
240
244
|
const anythingllmDbPath = resolveAnythingllmDbPath(process.env);
|
|
@@ -318,6 +322,16 @@ async function cmdStatus(argv = []) {
|
|
|
318
322
|
const zcodeInstalled = zcodeActive.length > 0;
|
|
319
323
|
const zcodeDbPath = zcodeActive.join(" | ");
|
|
320
324
|
|
|
325
|
+
// Qoder Desktop 1.18+ — token usage lives in SharedClientCache/local.db.
|
|
326
|
+
const qoderPaths = resolveQoderDbPaths({
|
|
327
|
+
home,
|
|
328
|
+
env: process.env,
|
|
329
|
+
platform: process.platform,
|
|
330
|
+
});
|
|
331
|
+
const qoderActive = formatResolvedPaths(qoderPaths);
|
|
332
|
+
const qoderInstalled = qoderActive.length > 0;
|
|
333
|
+
const qoderDbPath = qoderActive.join(" | ");
|
|
334
|
+
|
|
321
335
|
// OpenCode (JSON files + SQLite DB) — passive scan of storage/message/ and opencode.db.
|
|
322
336
|
const opencodeStorageNativeValue = process.env.OPENCODE_HOME || path.join(xdgDataHome, "opencode");
|
|
323
337
|
const wslOpencodeStorageDir = process.platform === "win32" && shouldProbeWsl(process.env)
|
|
@@ -634,7 +648,14 @@ async function cmdStatus(argv = []) {
|
|
|
634
648
|
? { installed: true, files: kimiWireFiles.length + kimiCodeWireFiles.length }
|
|
635
649
|
: { installed: false },
|
|
636
650
|
kiro_cli: kiroCliInstalled
|
|
637
|
-
? {
|
|
651
|
+
? {
|
|
652
|
+
installed: true,
|
|
653
|
+
detail: kiroCliDbExists
|
|
654
|
+
? kiroCliDbPath
|
|
655
|
+
: path.dirname(kiroCliSessionFiles[0]),
|
|
656
|
+
database: kiroCliDbExists ? kiroCliDbPath : null,
|
|
657
|
+
files: kiroCliSessionFiles.length,
|
|
658
|
+
}
|
|
638
659
|
: { installed: false },
|
|
639
660
|
codebuddy: codebuddyInstalled
|
|
640
661
|
? { installed: true, files: codebuddyFiles.length }
|
|
@@ -663,6 +684,9 @@ async function cmdStatus(argv = []) {
|
|
|
663
684
|
zcode: zcodeInstalled
|
|
664
685
|
? { installed: true, detail: zcodeDbPath }
|
|
665
686
|
: { installed: false },
|
|
687
|
+
qoder: qoderInstalled
|
|
688
|
+
? { installed: true, detail: qoderDbPath }
|
|
689
|
+
: { installed: false },
|
|
666
690
|
kilocode: kilocodeInstalled
|
|
667
691
|
? { installed: true, files: kilocodeTaskFiles.length }
|
|
668
692
|
: { installed: false },
|
|
@@ -746,7 +770,7 @@ async function cmdStatus(argv = []) {
|
|
|
746
770
|
? `- Kimi Code: passive reader (${kimiWireFiles.length + kimiCodeWireFiles.length} wire.jsonl file${(kimiWireFiles.length + kimiCodeWireFiles.length) !== 1 ? "s" : ""} found, directories: ${kimiActive.join(" | ") || "none"})`
|
|
747
771
|
: null,
|
|
748
772
|
kiroCliInstalled
|
|
749
|
-
? `- Kiro CLI:
|
|
773
|
+
? `- Kiro CLI: passive reader (${kiroCliSessionFiles.length} session file${kiroCliSessionFiles.length !== 1 ? "s" : ""} found, SQLite ${kiroCliDbExists ? "found" : "not found"}; tokens approximated from char lengths and merged under 'kiro')`
|
|
750
774
|
: null,
|
|
751
775
|
codebuddyInstalled
|
|
752
776
|
? `- CodeBuddy hooks: ${codebuddyHookConfigured ? "set" : "unset"} (${codebuddyFiles.length} usage file${codebuddyFiles.length !== 1 ? "s" : ""} found)`
|
|
@@ -775,6 +799,9 @@ async function cmdStatus(argv = []) {
|
|
|
775
799
|
zcodeInstalled
|
|
776
800
|
? `- ZCode: passive reader (${zcodeDbPath})`
|
|
777
801
|
: null,
|
|
802
|
+
qoderInstalled
|
|
803
|
+
? `- Qoder: passive reader (${qoderDbPath})`
|
|
804
|
+
: null,
|
|
778
805
|
opencodeInstalled
|
|
779
806
|
? `- OpenCode: passive reader (storage: ${opencodeStorageActive.join(" | ") || "not found"}, DB: ${opencodeDbActive.join(" | ") || "not found"})`
|
|
780
807
|
: null,
|
package/src/commands/sync.js
CHANGED
|
@@ -20,6 +20,8 @@ const {
|
|
|
20
20
|
readOpencodeDbMessages,
|
|
21
21
|
readMimoDbMessages,
|
|
22
22
|
readZcodeDbMessages,
|
|
23
|
+
resolveQoderDbPaths,
|
|
24
|
+
readQoderDbMessages,
|
|
23
25
|
resolveKiroDbPath,
|
|
24
26
|
resolveKiroJsonlPath,
|
|
25
27
|
resolveHermesPath,
|
|
@@ -35,6 +37,7 @@ const {
|
|
|
35
37
|
parseGeminiIncremental,
|
|
36
38
|
parseOpencodeIncremental,
|
|
37
39
|
parseOpencodeDbIncremental,
|
|
40
|
+
parseQoderDbIncremental,
|
|
38
41
|
parseOpenclawIncremental,
|
|
39
42
|
parseCursorApiIncremental,
|
|
40
43
|
parseKiroIncremental,
|
|
@@ -113,6 +116,11 @@ const {
|
|
|
113
116
|
} = require("../lib/cursor-store");
|
|
114
117
|
const { resolveTrackerPaths } = require("../lib/tracker-paths");
|
|
115
118
|
const { resolveRuntimeConfig } = require("../lib/runtime-config");
|
|
119
|
+
const { extractTokenCount } = require("../lib/codex-rollout-parser");
|
|
120
|
+
const {
|
|
121
|
+
consumeUsageDelta,
|
|
122
|
+
createUsageDeltaState,
|
|
123
|
+
} = require("../lib/codex-token-usage");
|
|
116
124
|
|
|
117
125
|
const CURSOR_UNKNOWN_MIGRATION_KEY = "cursorUnknownPurge_2026_04";
|
|
118
126
|
const ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY = "rolloutCumulativeDeltaReparse_2026_05";
|
|
@@ -189,6 +197,17 @@ const CODEX_RESCAN_DEDUP_REPAIR_KEY = "codexRescanDedupRepair_2026_06";
|
|
|
189
197
|
// offset reset for the cloud overwrite). Pre-gated: installs with no forked
|
|
190
198
|
// rollout on disk carry no fork phantom and mark done without the rebuild.
|
|
191
199
|
const CODEX_FORK_REPLAY_REPAIR_KEY = "codexForkReplayRepair_2026_07";
|
|
200
|
+
// One-time repair for Codex rollouts that contain token counters from multiple
|
|
201
|
+
// interleaved SessionState instances. Older parsers subtracted each cumulative
|
|
202
|
+
// total from the most recently observed total, even when it belonged to another
|
|
203
|
+
// state, which could turn an ordinary turn into a multi-billion-token delta.
|
|
204
|
+
// v2 removes the unreliable multi_agent_version pre-gate and validates every
|
|
205
|
+
// contributing rollout before committing the rebuild. The new key is required
|
|
206
|
+
// so installs that finalized the original migration on a false negative retry.
|
|
207
|
+
const CODEX_USAGE_LINEAGE_REPAIR_KEY = "codexUsageLineageRepair_2026_07_v2";
|
|
208
|
+
// Keep the one escalated desktop refresh bounded; explicit full syncs can retry
|
|
209
|
+
// the same migration without this ceiling when the history needs a deeper scan.
|
|
210
|
+
const CODEX_BACKGROUND_LINEAGE_SCAN_MAX_BYTES = 1024 * 1024;
|
|
192
211
|
const DROID_DUP_SESSION_REPAIR_KEY = "droidDupSessionInflationRepair_2026_06";
|
|
193
212
|
const CODEX_COLD_SCAN_AUDIT_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
194
213
|
const NOTIFY_LOCK_WAIT_MS = 130_000;
|
|
@@ -236,6 +255,7 @@ const AUTO_SYNC_SOURCES = new Set([
|
|
|
236
255
|
"opencode",
|
|
237
256
|
"openclaw",
|
|
238
257
|
"pi",
|
|
258
|
+
"qoder",
|
|
239
259
|
"roocode",
|
|
240
260
|
"workbuddy",
|
|
241
261
|
"zcode",
|
|
@@ -457,6 +477,28 @@ async function cmdSync(argv, context = {}) {
|
|
|
457
477
|
if (autoSourceScope) return sources.includes(autoSourceScope);
|
|
458
478
|
return true;
|
|
459
479
|
};
|
|
480
|
+
// Desktop refreshes are deliberately lightweight, but that also means a
|
|
481
|
+
// migration wired only to full sync would never repair an existing native
|
|
482
|
+
// install. Escalate exactly the first eligible background run: fresh
|
|
483
|
+
// installs have no persisted Codex buckets to repair, while retryable
|
|
484
|
+
// failures remain reserved for an explicit full sync instead of turning
|
|
485
|
+
// every five-minute refresh into a deep historical scan.
|
|
486
|
+
const backgroundCodexUsageMigrationEligible = Boolean(
|
|
487
|
+
isBackgroundLightweightSync &&
|
|
488
|
+
sourceAllowed("codex") &&
|
|
489
|
+
!cursors.migrations?.[CODEX_USAGE_LINEAGE_REPAIR_KEY]
|
|
490
|
+
);
|
|
491
|
+
const hasPersistedCodexUsage = Object.keys(cursors.hourly?.buckets || {})
|
|
492
|
+
.some((key) => key.startsWith("codex|"));
|
|
493
|
+
const backgroundCodexUsageRepair =
|
|
494
|
+
backgroundCodexUsageMigrationEligible && hasPersistedCodexUsage;
|
|
495
|
+
if (backgroundCodexUsageMigrationEligible && !hasPersistedCodexUsage) {
|
|
496
|
+
// A fresh install has no old-parser buckets to repair. Mark it complete
|
|
497
|
+
// before the first bounded parse so the next refresh does not mistake
|
|
498
|
+
// newly parsed, already-correct data for legacy history.
|
|
499
|
+
(cursors.migrations ||= {})[CODEX_USAGE_LINEAGE_REPAIR_KEY] =
|
|
500
|
+
new Date().toISOString();
|
|
501
|
+
}
|
|
460
502
|
|
|
461
503
|
const sources = [];
|
|
462
504
|
if (sourceAllowed("codex")) {
|
|
@@ -467,13 +509,13 @@ async function cmdSync(argv, context = {}) {
|
|
|
467
509
|
const codexPaths = resolveInstallPaths({ nativeValue: codexNativeValue, wslValue: wslCodexDir });
|
|
468
510
|
if (codexPaths.native) {
|
|
469
511
|
sources.push({ source: "codex", sessionsDir: path.join(codexPaths.native, "sessions"), codexInventoryCache: true });
|
|
470
|
-
if (!isBackgroundLightweightSync) {
|
|
512
|
+
if (!isBackgroundLightweightSync || backgroundCodexUsageRepair) {
|
|
471
513
|
sources.push({ source: "codex", sessionsDir: path.join(codexPaths.native, "archived_sessions"), deep: true });
|
|
472
514
|
}
|
|
473
515
|
}
|
|
474
516
|
if (codexPaths.wsl) {
|
|
475
517
|
sources.push({ source: "codex", sessionsDir: path.join(codexPaths.wsl, "sessions"), codexInventoryCache: true });
|
|
476
|
-
if (!isBackgroundLightweightSync) {
|
|
518
|
+
if (!isBackgroundLightweightSync || backgroundCodexUsageRepair) {
|
|
477
519
|
sources.push({ source: "codex", sessionsDir: path.join(codexPaths.wsl, "archived_sessions"), deep: true });
|
|
478
520
|
}
|
|
479
521
|
}
|
|
@@ -523,7 +565,7 @@ async function cmdSync(argv, context = {}) {
|
|
|
523
565
|
projectQueueStatePath,
|
|
524
566
|
rolloutFiles,
|
|
525
567
|
});
|
|
526
|
-
await repairCodexForkReplayInflation({
|
|
568
|
+
const codexForkRepairRan = await repairCodexForkReplayInflation({
|
|
527
569
|
cursors,
|
|
528
570
|
queuePath,
|
|
529
571
|
queueStatePath,
|
|
@@ -532,6 +574,15 @@ async function cmdSync(argv, context = {}) {
|
|
|
532
574
|
rolloutFiles,
|
|
533
575
|
legacyRepairRan: codexRescanRepairRan,
|
|
534
576
|
});
|
|
577
|
+
await repairCodexInterleavedUsageInflation({
|
|
578
|
+
cursors,
|
|
579
|
+
queuePath,
|
|
580
|
+
queueStatePath,
|
|
581
|
+
projectQueuePath,
|
|
582
|
+
projectQueueStatePath,
|
|
583
|
+
rolloutFiles,
|
|
584
|
+
legacyRepairRan: codexRescanRepairRan || codexForkRepairRan,
|
|
585
|
+
});
|
|
535
586
|
await repairDroidDuplicateSessionInflation({ cursors, queuePath, queueStatePath });
|
|
536
587
|
await repairMimoClaudeMislabel({
|
|
537
588
|
cursors,
|
|
@@ -540,6 +591,33 @@ async function cmdSync(argv, context = {}) {
|
|
|
540
591
|
projectQueuePath,
|
|
541
592
|
projectQueueStatePath,
|
|
542
593
|
});
|
|
594
|
+
} else if (backgroundCodexUsageRepair) {
|
|
595
|
+
await cursorStore.materializeAllCodexState(cursors);
|
|
596
|
+
try {
|
|
597
|
+
await repairCodexInterleavedUsageInflation({
|
|
598
|
+
cursors,
|
|
599
|
+
queuePath,
|
|
600
|
+
queueStatePath,
|
|
601
|
+
projectQueuePath,
|
|
602
|
+
projectQueueStatePath,
|
|
603
|
+
rolloutFiles,
|
|
604
|
+
maxLineageScanBytes: CODEX_BACKGROUND_LINEAGE_SCAN_MAX_BYTES,
|
|
605
|
+
});
|
|
606
|
+
} catch (err) {
|
|
607
|
+
warnProviderParseFailure("Codex usage lineage repair", err, opts);
|
|
608
|
+
} finally {
|
|
609
|
+
// Rebuild failures intentionally leave the migration key unset so a
|
|
610
|
+
// future full sync can retry. Record a retryable sentinel here to keep
|
|
611
|
+
// routine native background refreshes bounded after that first attempt.
|
|
612
|
+
const migrations = (cursors.migrations ||= {});
|
|
613
|
+
if (!migrations[CODEX_USAGE_LINEAGE_REPAIR_KEY]) {
|
|
614
|
+
migrations[CODEX_USAGE_LINEAGE_REPAIR_KEY] = {
|
|
615
|
+
skipped: true,
|
|
616
|
+
reason: "background_repair_requires_full_sync",
|
|
617
|
+
at: new Date().toISOString(),
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
}
|
|
543
621
|
}
|
|
544
622
|
|
|
545
623
|
const codexColdSkipEnabled = opts.auto && sourceAllowed("codex");
|
|
@@ -903,6 +981,47 @@ async function cmdSync(argv, context = {}) {
|
|
|
903
981
|
};
|
|
904
982
|
}
|
|
905
983
|
|
|
984
|
+
let qoderResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
985
|
+
if (sourceAllowed("qoder")) {
|
|
986
|
+
const qoderPaths = resolveQoderDbPaths({
|
|
987
|
+
home,
|
|
988
|
+
env: process.env,
|
|
989
|
+
platform: process.platform,
|
|
990
|
+
});
|
|
991
|
+
if (qoderPaths.native || qoderPaths.wsl) {
|
|
992
|
+
if (progress?.enabled) {
|
|
993
|
+
progress.start(`Parsing Qoder ${renderBar(0)} | buckets 0`);
|
|
994
|
+
}
|
|
995
|
+
try {
|
|
996
|
+
const result = await multiInstallParse({
|
|
997
|
+
paths: qoderPaths,
|
|
998
|
+
parserFn: async ({ dbPath, ...rest }) => {
|
|
999
|
+
const dbMessages = await readQoderDbMessages(dbPath);
|
|
1000
|
+
const parsed = await parseQoderDbIncremental({ dbMessages, ...rest });
|
|
1001
|
+
return {
|
|
1002
|
+
recordsProcessed: parsed.messagesProcessed || 0,
|
|
1003
|
+
eventsAggregated: parsed.eventsAggregated || 0,
|
|
1004
|
+
bucketsQueued: parsed.bucketsQueued || 0,
|
|
1005
|
+
};
|
|
1006
|
+
},
|
|
1007
|
+
providerName: "qoder",
|
|
1008
|
+
cursors,
|
|
1009
|
+
queuePath,
|
|
1010
|
+
projectQueuePath,
|
|
1011
|
+
getParams: (dbPath) => ({ dbPath }),
|
|
1012
|
+
onProgress: makeProviderProgress("Qoder"),
|
|
1013
|
+
});
|
|
1014
|
+
qoderResult = {
|
|
1015
|
+
recordsProcessed: result.recordsProcessed || 0,
|
|
1016
|
+
eventsAggregated: result.eventsAggregated || 0,
|
|
1017
|
+
bucketsQueued: result.bucketsQueued || 0,
|
|
1018
|
+
};
|
|
1019
|
+
} catch (err) {
|
|
1020
|
+
warnProviderParseFailure("Qoder", err, opts);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
906
1025
|
async function parseOpencodeDbForInstall({ dbPath, readFn, source, cursorKey, ...rest }) {
|
|
907
1026
|
if (!dbPath || !fssync.existsSync(dbPath)) {
|
|
908
1027
|
return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
@@ -1326,8 +1445,9 @@ async function cmdSync(argv, context = {}) {
|
|
|
1326
1445
|
);
|
|
1327
1446
|
}
|
|
1328
1447
|
|
|
1329
|
-
// ── Kiro CLI (reads ~/Library/Application Support/kiro-cli/data.sqlite3
|
|
1330
|
-
//
|
|
1448
|
+
// ── Kiro CLI (reads ~/Library/Application Support/kiro-cli/data.sqlite3,
|
|
1449
|
+
// legacy ~/.kiro/sessions/cli/{uuid}.json, and Kiro CLI 2.13+
|
|
1450
|
+
// ~/.kiro/sessions/{workspace}/sess_{uuid}/messages.jsonl) ──
|
|
1331
1451
|
// Runs IN PARALLEL with the Kiro IDE branch above — NOT instead of it.
|
|
1332
1452
|
// Both emit source='kiro' so totals merge transparently; cursor state
|
|
1333
1453
|
// is isolated in cursors.kiroCli. Kiro CLI does not persist explicit
|
|
@@ -1996,6 +2116,7 @@ async function cmdSync(argv, context = {}) {
|
|
|
1996
2116
|
geminiResult.filesProcessed +
|
|
1997
2117
|
antigravityResult.filesProcessed +
|
|
1998
2118
|
opencodeResult.filesProcessed +
|
|
2119
|
+
qoderResult.recordsProcessed +
|
|
1999
2120
|
cursorResult.recordsProcessed +
|
|
2000
2121
|
kiroResult.recordsProcessed +
|
|
2001
2122
|
kiroCliResult.recordsProcessed +
|
|
@@ -2025,6 +2146,7 @@ async function cmdSync(argv, context = {}) {
|
|
|
2025
2146
|
geminiResult.bucketsQueued +
|
|
2026
2147
|
antigravityResult.bucketsQueued +
|
|
2027
2148
|
opencodeResult.bucketsQueued +
|
|
2149
|
+
qoderResult.bucketsQueued +
|
|
2028
2150
|
cursorResult.bucketsQueued +
|
|
2029
2151
|
kiroResult.bucketsQueued +
|
|
2030
2152
|
kiroCliResult.bucketsQueued +
|
|
@@ -2339,6 +2461,7 @@ module.exports = {
|
|
|
2339
2461
|
migrateRolloutCumulativeDeltaBuckets,
|
|
2340
2462
|
repairCodexRescanInflation,
|
|
2341
2463
|
repairCodexForkReplayInflation,
|
|
2464
|
+
repairCodexInterleavedUsageInflation,
|
|
2342
2465
|
repairDroidDuplicateSessionInflation,
|
|
2343
2466
|
repairMimoClaudeMislabel,
|
|
2344
2467
|
reincludeClaudeMemObserverFiles,
|
|
@@ -2352,6 +2475,7 @@ module.exports = {
|
|
|
2352
2475
|
ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY,
|
|
2353
2476
|
CODEX_RESCAN_DEDUP_REPAIR_KEY,
|
|
2354
2477
|
CODEX_FORK_REPLAY_REPAIR_KEY,
|
|
2478
|
+
CODEX_USAGE_LINEAGE_REPAIR_KEY,
|
|
2355
2479
|
DROID_DUP_SESSION_REPAIR_KEY,
|
|
2356
2480
|
CLAUDE_MEM_OBSERVER_REINCLUDE_KEY,
|
|
2357
2481
|
GROK_APPEND_ONLY_REPAIR_MIGRATION_KEY,
|
|
@@ -3405,6 +3529,7 @@ async function repairCodexRescanInflation({
|
|
|
3405
3529
|
projectQueuePath,
|
|
3406
3530
|
projectQueueStatePath,
|
|
3407
3531
|
rolloutFiles,
|
|
3532
|
+
expectedCodexFileSnapshots = null,
|
|
3408
3533
|
// The fork-replay repair (#169 follow-up) re-runs this exact rebuild under
|
|
3409
3534
|
// its own key: the rebuild always uses the CURRENT parser, so any parser fix
|
|
3410
3535
|
// shipped since the last run is applied to the rebuilt history.
|
|
@@ -3593,6 +3718,16 @@ async function repairCodexRescanInflation({
|
|
|
3593
3718
|
}
|
|
3594
3719
|
}
|
|
3595
3720
|
|
|
3721
|
+
const rebuildValidation = expectedCodexFileSnapshots instanceof Map
|
|
3722
|
+
? await validateCodexRebuildFileSnapshots(rebuilt.files, expectedCodexFileSnapshots)
|
|
3723
|
+
: { ok: true };
|
|
3724
|
+
if (!rebuildValidation.ok) {
|
|
3725
|
+
console.error(
|
|
3726
|
+
`[sync] codex rescan repair: ${rebuildValidation.reason} — skipping to avoid data loss`,
|
|
3727
|
+
);
|
|
3728
|
+
return false;
|
|
3729
|
+
}
|
|
3730
|
+
|
|
3596
3731
|
// COMMIT (only after a verified rebuild). A crash partway just leaves the
|
|
3597
3732
|
// migration to re-run next sync — re-rebuild + re-strip + re-commit converges.
|
|
3598
3733
|
//
|
|
@@ -3828,6 +3963,219 @@ async function repairCodexForkReplayInflation({
|
|
|
3828
3963
|
});
|
|
3829
3964
|
}
|
|
3830
3965
|
|
|
3966
|
+
// Rebuild historical Codex usage only when a rollout proves that the old
|
|
3967
|
+
// single-baseline parser crossed cumulative SessionState lineages. Detection is
|
|
3968
|
+
// based on the token counter stream itself, not multi_agent_version metadata:
|
|
3969
|
+
// affected legacy rollouts can predate that marker or carry it only in the
|
|
3970
|
+
// middle of a large file. Every contributing rollout must also be valid and
|
|
3971
|
+
// stable before the atomic rebuild can replace live history.
|
|
3972
|
+
async function repairCodexInterleavedUsageInflation({
|
|
3973
|
+
cursors,
|
|
3974
|
+
queuePath,
|
|
3975
|
+
queueStatePath,
|
|
3976
|
+
projectQueuePath,
|
|
3977
|
+
projectQueueStatePath,
|
|
3978
|
+
rolloutFiles,
|
|
3979
|
+
// Either older guarded repair rebuilt every Codex file with the current
|
|
3980
|
+
// parser earlier in this sync, so a second full rebuild is unnecessary.
|
|
3981
|
+
legacyRepairRan = false,
|
|
3982
|
+
maxLineageScanBytes = Infinity,
|
|
3983
|
+
}) {
|
|
3984
|
+
if (!cursors || typeof cursors !== "object") return false;
|
|
3985
|
+
const migrations = (cursors.migrations ||= {});
|
|
3986
|
+
const prior = migrations[CODEX_USAGE_LINEAGE_REPAIR_KEY];
|
|
3987
|
+
if (prior && !(typeof prior === "object" && prior.skipped)) return false;
|
|
3988
|
+
|
|
3989
|
+
if (legacyRepairRan) {
|
|
3990
|
+
migrations[CODEX_USAGE_LINEAGE_REPAIR_KEY] = new Date().toISOString();
|
|
3991
|
+
return false;
|
|
3992
|
+
}
|
|
3993
|
+
|
|
3994
|
+
const scan = await scanForInterleavedCodexUsage(rolloutFiles, {
|
|
3995
|
+
maxLineageScanBytes,
|
|
3996
|
+
});
|
|
3997
|
+
if (scan.indeterminate) {
|
|
3998
|
+
migrations[CODEX_USAGE_LINEAGE_REPAIR_KEY] = {
|
|
3999
|
+
skipped: true,
|
|
4000
|
+
reason: "usage_lineage_scan_indeterminate",
|
|
4001
|
+
at: new Date().toISOString(),
|
|
4002
|
+
};
|
|
4003
|
+
return false;
|
|
4004
|
+
}
|
|
4005
|
+
|
|
4006
|
+
if (!scan.affected) {
|
|
4007
|
+
if (!isCodexHistoryCovered(cursors, rolloutFiles)) {
|
|
4008
|
+
migrations[CODEX_USAGE_LINEAGE_REPAIR_KEY] = {
|
|
4009
|
+
skipped: true,
|
|
4010
|
+
reason: "codex_history_not_covered",
|
|
4011
|
+
at: new Date().toISOString(),
|
|
4012
|
+
};
|
|
4013
|
+
return false;
|
|
4014
|
+
}
|
|
4015
|
+
migrations[CODEX_USAGE_LINEAGE_REPAIR_KEY] = new Date().toISOString();
|
|
4016
|
+
return false;
|
|
4017
|
+
}
|
|
4018
|
+
|
|
4019
|
+
return repairCodexRescanInflation({
|
|
4020
|
+
cursors,
|
|
4021
|
+
queuePath,
|
|
4022
|
+
queueStatePath,
|
|
4023
|
+
projectQueuePath,
|
|
4024
|
+
projectQueueStatePath,
|
|
4025
|
+
rolloutFiles,
|
|
4026
|
+
expectedCodexFileSnapshots: scan.fileSnapshots,
|
|
4027
|
+
migrationKey: CODEX_USAGE_LINEAGE_REPAIR_KEY,
|
|
4028
|
+
uploadNote: "reset_after_codex_usage_lineage_2026_07_v2",
|
|
4029
|
+
});
|
|
4030
|
+
}
|
|
4031
|
+
|
|
4032
|
+
async function scanForInterleavedCodexUsage(
|
|
4033
|
+
rolloutFiles,
|
|
4034
|
+
{ maxLineageScanBytes = Infinity } = {},
|
|
4035
|
+
) {
|
|
4036
|
+
let affected = false;
|
|
4037
|
+
let remainingBytes = Number.isFinite(maxLineageScanBytes)
|
|
4038
|
+
? Math.max(0, maxLineageScanBytes)
|
|
4039
|
+
: Infinity;
|
|
4040
|
+
const fileSnapshots = new Map();
|
|
4041
|
+
const seenPaths = new Set();
|
|
4042
|
+
|
|
4043
|
+
for (const entry of Array.isArray(rolloutFiles) ? rolloutFiles : []) {
|
|
4044
|
+
const fp = typeof entry === "string" ? entry : entry?.path;
|
|
4045
|
+
const src = typeof entry === "string" ? "codex" : String(entry?.source || "codex");
|
|
4046
|
+
if (!fp || src !== "codex" || seenPaths.has(fp)) continue;
|
|
4047
|
+
seenPaths.add(fp);
|
|
4048
|
+
|
|
4049
|
+
const lineage = await scanCodexUsageLineages(fp, remainingBytes);
|
|
4050
|
+
if (lineage.indeterminate) {
|
|
4051
|
+
return { affected: false, indeterminate: true, fileSnapshots: new Map() };
|
|
4052
|
+
}
|
|
4053
|
+
affected ||= lineage.affected;
|
|
4054
|
+
fileSnapshots.set(fp, lineage.fileSnapshot);
|
|
4055
|
+
if (Number.isFinite(remainingBytes)) {
|
|
4056
|
+
remainingBytes = Math.max(0, remainingBytes - lineage.bytesRead);
|
|
4057
|
+
}
|
|
4058
|
+
}
|
|
4059
|
+
return { affected, indeterminate: false, fileSnapshots };
|
|
4060
|
+
}
|
|
4061
|
+
|
|
4062
|
+
async function scanCodexUsageLineages(filePath, maxBytes = Infinity) {
|
|
4063
|
+
let stream = null;
|
|
4064
|
+
let rl = null;
|
|
4065
|
+
try {
|
|
4066
|
+
const before = await readCodexFileSnapshot(filePath);
|
|
4067
|
+
const state = createUsageDeltaState();
|
|
4068
|
+
const byteLimit = Number.isFinite(maxBytes) ? Math.max(0, maxBytes) : Infinity;
|
|
4069
|
+
let bytesRead = 0;
|
|
4070
|
+
let affected = false;
|
|
4071
|
+
stream = fssync.createReadStream(filePath, {
|
|
4072
|
+
encoding: "utf8",
|
|
4073
|
+
highWaterMark: 32 * 1024,
|
|
4074
|
+
});
|
|
4075
|
+
rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
4076
|
+
for await (const line of rl) {
|
|
4077
|
+
bytesRead += Buffer.byteLength(line, "utf8") + 1;
|
|
4078
|
+
if (bytesRead > byteLimit) return { affected: false, indeterminate: true };
|
|
4079
|
+
if (!line.trim()) continue;
|
|
4080
|
+
let obj;
|
|
4081
|
+
try {
|
|
4082
|
+
obj = JSON.parse(line);
|
|
4083
|
+
} catch (_e) {
|
|
4084
|
+
return { affected: false, indeterminate: true };
|
|
4085
|
+
}
|
|
4086
|
+
const token = extractTokenCount(obj);
|
|
4087
|
+
const info = token?.info;
|
|
4088
|
+
if (!info || typeof info !== "object") continue;
|
|
4089
|
+
consumeUsageDelta(state, info.last_token_usage, info.total_token_usage);
|
|
4090
|
+
if (state.sawInterleaved || state.sawDivergentCumulative) {
|
|
4091
|
+
affected = true;
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
const after = await readCodexFileSnapshot(filePath);
|
|
4095
|
+
if (!sameCodexFileSnapshot(before, after)) {
|
|
4096
|
+
return { affected: false, indeterminate: true };
|
|
4097
|
+
}
|
|
4098
|
+
return {
|
|
4099
|
+
affected,
|
|
4100
|
+
indeterminate: false,
|
|
4101
|
+
bytesRead,
|
|
4102
|
+
fileSnapshot: after,
|
|
4103
|
+
};
|
|
4104
|
+
} catch (_e) {
|
|
4105
|
+
return { affected: false, indeterminate: true };
|
|
4106
|
+
} finally {
|
|
4107
|
+
if (rl) rl.close();
|
|
4108
|
+
if (stream) stream.destroy();
|
|
4109
|
+
}
|
|
4110
|
+
}
|
|
4111
|
+
|
|
4112
|
+
async function readCodexFileSnapshot(filePath) {
|
|
4113
|
+
const stat = await fs.stat(filePath);
|
|
4114
|
+
return {
|
|
4115
|
+
size: Number(stat.size),
|
|
4116
|
+
mtimeMs: Number(stat.mtimeMs),
|
|
4117
|
+
ctimeMs: Number(stat.ctimeMs),
|
|
4118
|
+
ino: Number(stat.ino),
|
|
4119
|
+
dev: Number(stat.dev),
|
|
4120
|
+
};
|
|
4121
|
+
}
|
|
4122
|
+
|
|
4123
|
+
function sameCodexFileSnapshot(left, right) {
|
|
4124
|
+
return Boolean(
|
|
4125
|
+
left &&
|
|
4126
|
+
right &&
|
|
4127
|
+
left.size === right.size &&
|
|
4128
|
+
left.mtimeMs === right.mtimeMs &&
|
|
4129
|
+
left.ctimeMs === right.ctimeMs &&
|
|
4130
|
+
left.ino === right.ino &&
|
|
4131
|
+
left.dev === right.dev
|
|
4132
|
+
);
|
|
4133
|
+
}
|
|
4134
|
+
|
|
4135
|
+
async function validateCodexRebuildFileSnapshots(rebuiltFiles, expectedSnapshots) {
|
|
4136
|
+
for (const [filePath, expected] of expectedSnapshots) {
|
|
4137
|
+
let actual;
|
|
4138
|
+
try {
|
|
4139
|
+
actual = await readCodexFileSnapshot(filePath);
|
|
4140
|
+
} catch (_e) {
|
|
4141
|
+
return { ok: false, reason: "a scanned rollout became unreadable during rebuild" };
|
|
4142
|
+
}
|
|
4143
|
+
if (!sameCodexFileSnapshot(expected, actual)) {
|
|
4144
|
+
return { ok: false, reason: "a scanned rollout changed during rebuild" };
|
|
4145
|
+
}
|
|
4146
|
+
const rebuiltCursor = rebuiltFiles?.[filePath];
|
|
4147
|
+
if (!rebuiltCursor) {
|
|
4148
|
+
return { ok: false, reason: "the rebuild omitted a scanned rollout cursor" };
|
|
4149
|
+
}
|
|
4150
|
+
if (Number(rebuiltCursor.offset) !== expected.size) {
|
|
4151
|
+
return { ok: false, reason: "the rebuild did not consume a scanned rollout through EOF" };
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
return { ok: true };
|
|
4155
|
+
}
|
|
4156
|
+
|
|
4157
|
+
function isCodexHistoryCovered(cursors, rolloutFiles) {
|
|
4158
|
+
const scannedPaths = new Set();
|
|
4159
|
+
const scannedSessionIds = new Set();
|
|
4160
|
+
for (const entry of Array.isArray(rolloutFiles) ? rolloutFiles : []) {
|
|
4161
|
+
const fp = typeof entry === "string" ? entry : entry?.path;
|
|
4162
|
+
const src = typeof entry === "string" ? "codex" : String(entry?.source || "codex");
|
|
4163
|
+
if (!fp || src !== "codex") continue;
|
|
4164
|
+
scannedPaths.add(fp);
|
|
4165
|
+
const id = codexSessionIdFromPath(fp);
|
|
4166
|
+
if (id) scannedSessionIds.add(id);
|
|
4167
|
+
}
|
|
4168
|
+
if (!cursors.files || typeof cursors.files !== "object") return true;
|
|
4169
|
+
for (const fp of Object.keys(cursors.files)) {
|
|
4170
|
+
if (!isCodexSessionCursorPath(fp)) continue;
|
|
4171
|
+
if (scannedPaths.has(fp)) continue;
|
|
4172
|
+
const id = codexSessionIdFromPath(fp);
|
|
4173
|
+
if (id && scannedSessionIds.has(id)) continue;
|
|
4174
|
+
return false;
|
|
4175
|
+
}
|
|
4176
|
+
return true;
|
|
4177
|
+
}
|
|
4178
|
+
|
|
3831
4179
|
// Scans codex rollout heads for the fork marker. The child session_meta (with
|
|
3832
4180
|
// forked_from_id) is the FIRST line of a forked rollout — observed at byte
|
|
3833
4181
|
// offset ≤169 across every local sample — so a bounded head read is
|