tokentracker-cli 0.84.6 → 0.84.8
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-CKIvD3lG.js → AchievementBadge-DAAenS6A.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-CZWCUiS5.js → AchievementsPage-DJ9GMKxL.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-BSk_fR8P.js → AchievementsSection-DtpLrh2Q.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-CjHkcmuq.js → BadgeDetailModal-CkE1DwdN.js} +1 -1
- package/dashboard/dist/assets/Card-Cs2vVMHG.js +1 -0
- package/dashboard/dist/assets/{ClawdAnimated-Cl6gmF7u.js → ClawdAnimated-BCSNeob2.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-B0rcodcF.js → CommandPalette-CFDTjp76.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-BtlTJbf4.js → CommunityStatsModal-D2oJpI9i.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-tJCWVZQH.js → ConfirmModal-D0a_xdwF.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-Dtxq8dzI.js → DashboardPage-CMCqvpkA.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-9USuEsRu.js → DevicePage-c9Fk13gJ.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-DxHdqMcl.js → DialogClose-jsfDvF2n.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-Dwxj5h5w.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-C1ZeQgf2.js → DialogTitle-Dt835L7T.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-Cx4jLl9G.js → FadeIn-DJssGrj8.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-BOyuGkoA.js → HeaderGithubStar-B9MwwA-P.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-BDhqMR3L.js → HoverTooltip-DPLMJkai.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-DTvA9Wmx.js → IpCheckPage-BXKjxEHe.js} +1 -1
- package/dashboard/dist/assets/LandingPage-BJNMQTec.js +2 -0
- package/dashboard/dist/assets/{LeaderboardAvatar-3ibNg6If.js → LeaderboardAvatar-BNwbnOa3.js} +1 -1
- package/dashboard/dist/assets/LeaderboardPage-BOxejyMy.js +2 -0
- package/dashboard/dist/assets/{LeaderboardProfileModal-DeGbJtvi.js → LeaderboardProfileModal-BEqUZCOD.js} +4 -4
- package/dashboard/dist/assets/{LeaderboardProfilePage-DEPzO6hW.js → LeaderboardProfilePage-CUDV5iyh.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-ejlJN1vi.js +4 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-C76LAT_3.js → LocalOnlyNotice-B3BSeCHd.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-DEyCJT67.js → LoginCard-2mAb696j.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-CueMQgAZ.js → LoginModal-cacKhc6U.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-CwqBb5Fd.js → LoginPage-rnBlie8p.js} +1 -1
- package/dashboard/dist/assets/PetPage-D3DhuS6O.js +1 -0
- package/dashboard/dist/assets/{PopoverPopup-Dt9VY2VQ.js → PopoverPopup-CxATX7M9.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-CHx4ohfx.js → ProviderIcon-B5b9LkmM.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-DB6R7fjf.js → ResetPasswordPage-BGKmQ_JW.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-BINOpXIY.js → SegmentedControl-BMeYCm2b.js} +1 -1
- package/dashboard/dist/assets/{Select-DcYXcsKv.js → Select-CYHXq8im.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-BDv2Kb3x.js → SelectItemText-DvI4i06h.js} +1 -1
- package/dashboard/dist/assets/ServiceStatusPage-dv26-VvI.js +1 -0
- package/dashboard/dist/assets/{SessionsPage-cL_w0ytB.js → SessionsPage-DBQruqkO.js} +2 -2
- package/dashboard/dist/assets/SettingsPage-BWRbQ3Iq.js +1 -0
- package/dashboard/dist/assets/SkillsPage-BonhKgAt.js +1 -0
- package/dashboard/dist/assets/{TokenGalaxy-BHq4YSv2.js → TokenGalaxy-BBUD1kpU.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-akXaaKb6.js → ToolbarRootContext-DPMm8uYz.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-DPhFXzQc.js → TrendMonitor-CZVE6gID.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-Nf0UEz7s.js → WidgetsPage-Bz9cieeZ.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-D4zOSsqD.js → WrappedPage-CruMolZd.js} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-C3VH6nS4.js +1483 -0
- package/dashboard/dist/assets/__vite-browser-external-hXV_ZYTG.css +1 -0
- package/dashboard/dist/assets/{agent-logos-Bja3VCAa.js → agent-logos-HIeYwIqN.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-CzcCNJJ2.js → arrow-up-right-n7Bi4Tvd.js} +1 -1
- package/dashboard/dist/assets/{check-DUHSZCYn.js → check-C5Q44G5k.js} +1 -1
- package/dashboard/dist/assets/{chevron-right-jLvhaNUy.js → chevron-right-BsnptUU0.js} +1 -1
- package/dashboard/dist/assets/{copy-ChyH-ynx.js → copy-BGuL9uFa.js} +1 -1
- package/dashboard/dist/assets/{download-CgwvXrUe.js → download-BljgPwR2.js} +1 -1
- package/dashboard/dist/assets/external-link-B4Oz8hUe.js +1 -0
- package/dashboard/dist/assets/{flame-ysf1F8uB.js → flame-ChLG_NZE.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-DAa2cubl.js → format-tokens-DjuNk13Z.js} +1 -1
- package/dashboard/dist/assets/{icons-DccoWQq9.js → icons-CkO42M-u.js} +1 -1
- package/dashboard/dist/assets/{index-CAycNPQf.js → index-CEsvYBWY.js} +1 -1
- package/dashboard/dist/assets/{index-CxExExXh.js → index-CGxawuWx.js} +1 -1
- package/dashboard/dist/assets/{index-kkQrDRat.js → index-D87oE_rV.js} +1 -1
- package/dashboard/dist/assets/{info-DWQQIq9M.js → info-et8xtJ4v.js} +1 -1
- package/dashboard/dist/assets/limits-providers-vO_86yRy.js +1 -0
- package/dashboard/dist/assets/{link-2-CKeVVq4P.js → link-2-Dge2rBkr.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-Dw0tu6IN.js → loader-circle-Cs3wpqJe.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-BMU0YzEF.js → maximize-2-zn71uZX3.js} +1 -1
- package/dashboard/dist/assets/{provider-display-Cf9I-VLS.js → provider-display-CTt6mzLe.js} +1 -1
- package/dashboard/dist/assets/{react-BhbcISKy.js → react-BbHDQe6Q.js} +1 -1
- package/dashboard/dist/assets/{search-BoY8EW1z.js → search-3CMhatWb.js} +1 -1
- package/dashboard/dist/assets/{skills-api-KVH21GJw.js → skills-api-Cm1aiqWL.js} +1 -1
- package/dashboard/dist/assets/{store-BL7rSajG.js → store-DclXV77t.js} +1 -1
- package/dashboard/dist/assets/{terminal-CeMuITdm.js → terminal-BC7uFcA7.js} +1 -1
- package/dashboard/dist/assets/{trash-2-jVUCKqrR.js → trash-2-BT8V531F.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-uWK0fE6l.js → use-community-stats-DJr59SGu.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-DWsF4ShM.js → use-limits-display-prefs-DU8OOFVb.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-SEbqWDvc.js → use-native-settings-BvSwI4BL.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-D4UhrPvw.js → use-ordered-list-Cq5AyV2x.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-D1_q3aRK.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-DbysyUTJ.js → use-session-efficiency-pref-WS8NhXuW.js} +1 -1
- package/dashboard/dist/assets/{use-transform-DaEssGaZ.js → use-transform-CLYsi8xc.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-CmmQ34Ov.js → use-usage-limits-CGoQQDG-.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-DvBEn4rx.js → useCurrency-D0GtEoJD.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-QozsdFng.js → useScrollLock-TIUBSeIM.js} +1 -1
- package/dashboard/dist/assets/useTokenFormat-B0TBL9La.js +1 -0
- package/dashboard/dist/assets/{zap-Dp_aJ1q6.js → zap-D89769CL.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 +17 -0
- package/src/commands/sync.js +146 -126
- 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 +782 -0
- package/src/lib/rollout.js +385 -0
- package/src/lib/usage-limits.js +31 -2
- package/dashboard/dist/assets/Card-D6hdzwvI.js +0 -1
- package/dashboard/dist/assets/DialogDescription-DgUSe6A1.js +0 -1
- package/dashboard/dist/assets/LandingPage-k1271lWV.js +0 -2
- package/dashboard/dist/assets/LeaderboardPage-Byww5UpT.js +0 -2
- package/dashboard/dist/assets/LimitsPage-DPtqL0PS.js +0 -4
- package/dashboard/dist/assets/PetPage--xQAjW4_.js +0 -1
- package/dashboard/dist/assets/SettingsPage-CHGA0bQS.js +0 -1
- package/dashboard/dist/assets/SkillsPage-I29SqkXO.js +0 -1
- package/dashboard/dist/assets/__vite-browser-external-UqlSqI-1.js +0 -1465
- package/dashboard/dist/assets/__vite-browser-external-WKilYZnq.css +0 -1
- package/dashboard/dist/assets/limits-providers-B_18X9Rp.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-C3UrOh55.js +0 -1
- package/dashboard/dist/assets/useTokenFormat-BTLrL7Mi.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-C3VH6nS4.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.8",
|
|
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
|
@@ -62,6 +62,7 @@ const {
|
|
|
62
62
|
resolveKilocodeTaskFiles,
|
|
63
63
|
resolveRoocodeTaskFiles,
|
|
64
64
|
resolveZedDbPath,
|
|
65
|
+
resolveQoderDbPaths,
|
|
65
66
|
resolveAnythingllmDbPath,
|
|
66
67
|
resolveGooseDbPath,
|
|
67
68
|
listDroidSettingsFiles,
|
|
@@ -321,6 +322,16 @@ async function cmdStatus(argv = []) {
|
|
|
321
322
|
const zcodeInstalled = zcodeActive.length > 0;
|
|
322
323
|
const zcodeDbPath = zcodeActive.join(" | ");
|
|
323
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
|
+
|
|
324
335
|
// OpenCode (JSON files + SQLite DB) — passive scan of storage/message/ and opencode.db.
|
|
325
336
|
const opencodeStorageNativeValue = process.env.OPENCODE_HOME || path.join(xdgDataHome, "opencode");
|
|
326
337
|
const wslOpencodeStorageDir = process.platform === "win32" && shouldProbeWsl(process.env)
|
|
@@ -673,6 +684,9 @@ async function cmdStatus(argv = []) {
|
|
|
673
684
|
zcode: zcodeInstalled
|
|
674
685
|
? { installed: true, detail: zcodeDbPath }
|
|
675
686
|
: { installed: false },
|
|
687
|
+
qoder: qoderInstalled
|
|
688
|
+
? { installed: true, detail: qoderDbPath }
|
|
689
|
+
: { installed: false },
|
|
676
690
|
kilocode: kilocodeInstalled
|
|
677
691
|
? { installed: true, files: kilocodeTaskFiles.length }
|
|
678
692
|
: { installed: false },
|
|
@@ -785,6 +799,9 @@ async function cmdStatus(argv = []) {
|
|
|
785
799
|
zcodeInstalled
|
|
786
800
|
? `- ZCode: passive reader (${zcodeDbPath})`
|
|
787
801
|
: null,
|
|
802
|
+
qoderInstalled
|
|
803
|
+
? `- Qoder: passive reader (${qoderDbPath})`
|
|
804
|
+
: null,
|
|
788
805
|
opencodeInstalled
|
|
789
806
|
? `- OpenCode: passive reader (storage: ${opencodeStorageActive.join(" | ") || "not found"}, DB: ${opencodeDbActive.join(" | ") || "not found"})`
|
|
790
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,
|
|
@@ -198,7 +201,10 @@ const CODEX_FORK_REPLAY_REPAIR_KEY = "codexForkReplayRepair_2026_07";
|
|
|
198
201
|
// interleaved SessionState instances. Older parsers subtracted each cumulative
|
|
199
202
|
// total from the most recently observed total, even when it belonged to another
|
|
200
203
|
// state, which could turn an ordinary turn into a multi-billion-token delta.
|
|
201
|
-
|
|
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";
|
|
202
208
|
// Keep the one escalated desktop refresh bounded; explicit full syncs can retry
|
|
203
209
|
// the same migration without this ceiling when the history needs a deeper scan.
|
|
204
210
|
const CODEX_BACKGROUND_LINEAGE_SCAN_MAX_BYTES = 1024 * 1024;
|
|
@@ -249,6 +255,7 @@ const AUTO_SYNC_SOURCES = new Set([
|
|
|
249
255
|
"opencode",
|
|
250
256
|
"openclaw",
|
|
251
257
|
"pi",
|
|
258
|
+
"qoder",
|
|
252
259
|
"roocode",
|
|
253
260
|
"workbuddy",
|
|
254
261
|
"zcode",
|
|
@@ -974,6 +981,47 @@ async function cmdSync(argv, context = {}) {
|
|
|
974
981
|
};
|
|
975
982
|
}
|
|
976
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
|
+
|
|
977
1025
|
async function parseOpencodeDbForInstall({ dbPath, readFn, source, cursorKey, ...rest }) {
|
|
978
1026
|
if (!dbPath || !fssync.existsSync(dbPath)) {
|
|
979
1027
|
return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
@@ -2068,6 +2116,7 @@ async function cmdSync(argv, context = {}) {
|
|
|
2068
2116
|
geminiResult.filesProcessed +
|
|
2069
2117
|
antigravityResult.filesProcessed +
|
|
2070
2118
|
opencodeResult.filesProcessed +
|
|
2119
|
+
qoderResult.recordsProcessed +
|
|
2071
2120
|
cursorResult.recordsProcessed +
|
|
2072
2121
|
kiroResult.recordsProcessed +
|
|
2073
2122
|
kiroCliResult.recordsProcessed +
|
|
@@ -2097,6 +2146,7 @@ async function cmdSync(argv, context = {}) {
|
|
|
2097
2146
|
geminiResult.bucketsQueued +
|
|
2098
2147
|
antigravityResult.bucketsQueued +
|
|
2099
2148
|
opencodeResult.bucketsQueued +
|
|
2149
|
+
qoderResult.bucketsQueued +
|
|
2100
2150
|
cursorResult.bucketsQueued +
|
|
2101
2151
|
kiroResult.bucketsQueued +
|
|
2102
2152
|
kiroCliResult.bucketsQueued +
|
|
@@ -3479,6 +3529,7 @@ async function repairCodexRescanInflation({
|
|
|
3479
3529
|
projectQueuePath,
|
|
3480
3530
|
projectQueueStatePath,
|
|
3481
3531
|
rolloutFiles,
|
|
3532
|
+
expectedCodexFileSnapshots = null,
|
|
3482
3533
|
// The fork-replay repair (#169 follow-up) re-runs this exact rebuild under
|
|
3483
3534
|
// its own key: the rebuild always uses the CURRENT parser, so any parser fix
|
|
3484
3535
|
// shipped since the last run is applied to the rebuilt history.
|
|
@@ -3667,6 +3718,16 @@ async function repairCodexRescanInflation({
|
|
|
3667
3718
|
}
|
|
3668
3719
|
}
|
|
3669
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
|
+
|
|
3670
3731
|
// COMMIT (only after a verified rebuild). A crash partway just leaves the
|
|
3671
3732
|
// migration to re-run next sync — re-rebuild + re-strip + re-commit converges.
|
|
3672
3733
|
//
|
|
@@ -3903,10 +3964,11 @@ async function repairCodexForkReplayInflation({
|
|
|
3903
3964
|
}
|
|
3904
3965
|
|
|
3905
3966
|
// Rebuild historical Codex usage only when a rollout proves that the old
|
|
3906
|
-
// single-baseline parser crossed cumulative SessionState lineages.
|
|
3907
|
-
//
|
|
3908
|
-
// predate
|
|
3909
|
-
//
|
|
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.
|
|
3910
3972
|
async function repairCodexInterleavedUsageInflation({
|
|
3911
3973
|
cursors,
|
|
3912
3974
|
queuePath,
|
|
@@ -3961,8 +4023,9 @@ async function repairCodexInterleavedUsageInflation({
|
|
|
3961
4023
|
projectQueuePath,
|
|
3962
4024
|
projectQueueStatePath,
|
|
3963
4025
|
rolloutFiles,
|
|
4026
|
+
expectedCodexFileSnapshots: scan.fileSnapshots,
|
|
3964
4027
|
migrationKey: CODEX_USAGE_LINEAGE_REPAIR_KEY,
|
|
3965
|
-
uploadNote: "
|
|
4028
|
+
uploadNote: "reset_after_codex_usage_lineage_2026_07_v2",
|
|
3966
4029
|
});
|
|
3967
4030
|
}
|
|
3968
4031
|
|
|
@@ -3970,138 +4033,41 @@ async function scanForInterleavedCodexUsage(
|
|
|
3970
4033
|
rolloutFiles,
|
|
3971
4034
|
{ maxLineageScanBytes = Infinity } = {},
|
|
3972
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
|
+
|
|
3973
4043
|
for (const entry of Array.isArray(rolloutFiles) ? rolloutFiles : []) {
|
|
3974
4044
|
const fp = typeof entry === "string" ? entry : entry?.path;
|
|
3975
4045
|
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;
|
|
4046
|
+
if (!fp || src !== "codex" || seenPaths.has(fp)) continue;
|
|
4047
|
+
seenPaths.add(fp);
|
|
3981
4048
|
|
|
3982
|
-
const lineage = await scanCodexUsageLineages(fp,
|
|
3983
|
-
if (lineage.indeterminate)
|
|
3984
|
-
|
|
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 };
|
|
4049
|
+
const lineage = await scanCodexUsageLineages(fp, remainingBytes);
|
|
4050
|
+
if (lineage.indeterminate) {
|
|
4051
|
+
return { affected: false, indeterminate: true, fileSnapshots: new Map() };
|
|
4028
4052
|
}
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
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
|
-
};
|
|
4053
|
+
affected ||= lineage.affected;
|
|
4054
|
+
fileSnapshots.set(fp, lineage.fileSnapshot);
|
|
4055
|
+
if (Number.isFinite(remainingBytes)) {
|
|
4056
|
+
remainingBytes = Math.max(0, remainingBytes - lineage.bytesRead);
|
|
4077
4057
|
}
|
|
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
4058
|
}
|
|
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
|
-
);
|
|
4059
|
+
return { affected, indeterminate: false, fileSnapshots };
|
|
4096
4060
|
}
|
|
4097
4061
|
|
|
4098
4062
|
async function scanCodexUsageLineages(filePath, maxBytes = Infinity) {
|
|
4099
4063
|
let stream = null;
|
|
4100
4064
|
let rl = null;
|
|
4101
4065
|
try {
|
|
4066
|
+
const before = await readCodexFileSnapshot(filePath);
|
|
4102
4067
|
const state = createUsageDeltaState();
|
|
4103
4068
|
const byteLimit = Number.isFinite(maxBytes) ? Math.max(0, maxBytes) : Infinity;
|
|
4104
4069
|
let bytesRead = 0;
|
|
4070
|
+
let affected = false;
|
|
4105
4071
|
stream = fssync.createReadStream(filePath, {
|
|
4106
4072
|
encoding: "utf8",
|
|
4107
4073
|
highWaterMark: 32 * 1024,
|
|
@@ -4110,22 +4076,31 @@ async function scanCodexUsageLineages(filePath, maxBytes = Infinity) {
|
|
|
4110
4076
|
for await (const line of rl) {
|
|
4111
4077
|
bytesRead += Buffer.byteLength(line, "utf8") + 1;
|
|
4112
4078
|
if (bytesRead > byteLimit) return { affected: false, indeterminate: true };
|
|
4113
|
-
if (!line.
|
|
4079
|
+
if (!line.trim()) continue;
|
|
4114
4080
|
let obj;
|
|
4115
4081
|
try {
|
|
4116
4082
|
obj = JSON.parse(line);
|
|
4117
4083
|
} catch (_e) {
|
|
4118
|
-
|
|
4084
|
+
return { affected: false, indeterminate: true };
|
|
4119
4085
|
}
|
|
4120
4086
|
const token = extractTokenCount(obj);
|
|
4121
4087
|
const info = token?.info;
|
|
4122
4088
|
if (!info || typeof info !== "object") continue;
|
|
4123
4089
|
consumeUsageDelta(state, info.last_token_usage, info.total_token_usage);
|
|
4124
4090
|
if (state.sawInterleaved || state.sawDivergentCumulative) {
|
|
4125
|
-
|
|
4091
|
+
affected = true;
|
|
4126
4092
|
}
|
|
4127
4093
|
}
|
|
4128
|
-
|
|
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
|
+
};
|
|
4129
4104
|
} catch (_e) {
|
|
4130
4105
|
return { affected: false, indeterminate: true };
|
|
4131
4106
|
} finally {
|
|
@@ -4134,6 +4109,51 @@ async function scanCodexUsageLineages(filePath, maxBytes = Infinity) {
|
|
|
4134
4109
|
}
|
|
4135
4110
|
}
|
|
4136
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
|
+
|
|
4137
4157
|
function isCodexHistoryCovered(cursors, rolloutFiles) {
|
|
4138
4158
|
const scannedPaths = new Set();
|
|
4139
4159
|
const scannedSessionIds = new Set();
|
package/src/lib/local-api.js
CHANGED
|
@@ -2684,7 +2684,10 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
2684
2684
|
json(res, { importable, codexDetected: importable.length > 0 });
|
|
2685
2685
|
return true;
|
|
2686
2686
|
}
|
|
2687
|
-
json(res, {
|
|
2687
|
+
json(res, {
|
|
2688
|
+
pets: pets.listInstalledPets(),
|
|
2689
|
+
hiddenBuiltinIds: pets.readHiddenBuiltinIds(),
|
|
2690
|
+
});
|
|
2688
2691
|
return true;
|
|
2689
2692
|
}
|
|
2690
2693
|
if (method === "POST") {
|
package/src/lib/pet-packages.js
CHANGED
|
@@ -10,6 +10,8 @@ const MAX_PACKAGE_BYTES = 12 * 1024 * 1024;
|
|
|
10
10
|
const MAX_MANIFEST_BYTES = 16 * 1024;
|
|
11
11
|
const MAX_SPRITESHEET_BYTES = 10 * 1024 * 1024;
|
|
12
12
|
const BUILTIN_IDS = new Set(["clawd", "sprout", "byte", "ember"]);
|
|
13
|
+
const REMOVABLE_BUILTIN_IDS = new Set(["sprout", "byte", "ember"]);
|
|
14
|
+
const HIDDEN_BUILTINS_FILE = ".hidden-builtins.json";
|
|
13
15
|
// codex-pets.net added `kind` to pet.json long after launch, and its enum may keep
|
|
14
16
|
// growing — packages published before that (and future kinds) must stay importable,
|
|
15
17
|
// so kind is optional metadata validated only as a short slug.
|
|
@@ -35,6 +37,44 @@ function resolveCodexPetsDir() {
|
|
|
35
37
|
);
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
function readHiddenBuiltinIds() {
|
|
41
|
+
const file = path.join(resolvePetsDir(), HIDDEN_BUILTINS_FILE);
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
44
|
+
if (!Array.isArray(parsed)) return [];
|
|
45
|
+
return [...new Set(parsed
|
|
46
|
+
.map((id) => String(id || "").trim().toLowerCase())
|
|
47
|
+
.filter((id) => REMOVABLE_BUILTIN_IDS.has(id)))].sort();
|
|
48
|
+
} catch {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function hideBuiltinPet(id) {
|
|
54
|
+
const normalized = String(id || "").trim().toLowerCase();
|
|
55
|
+
if (normalized === "clawd") throw new Error("Clawd cannot be removed");
|
|
56
|
+
if (!REMOVABLE_BUILTIN_IDS.has(normalized)) throw new Error("Invalid built-in pet id");
|
|
57
|
+
|
|
58
|
+
const root = resolvePetsDir();
|
|
59
|
+
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
60
|
+
const hidden = new Set(readHiddenBuiltinIds());
|
|
61
|
+
hidden.add(normalized);
|
|
62
|
+
const destination = path.join(root, HIDDEN_BUILTINS_FILE);
|
|
63
|
+
const stage = path.join(root, `${HIDDEN_BUILTINS_FILE}.${process.pid}-${crypto.randomBytes(6).toString("hex")}`);
|
|
64
|
+
const backup = `${stage}-backup`;
|
|
65
|
+
try {
|
|
66
|
+
fs.writeFileSync(stage, `${JSON.stringify([...hidden].sort(), null, 2)}\n`, { mode: 0o600 });
|
|
67
|
+
if (fs.existsSync(destination)) fs.renameSync(destination, backup);
|
|
68
|
+
fs.renameSync(stage, destination);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
try { fs.rmSync(stage, { force: true }); } catch {}
|
|
71
|
+
if (!fs.existsSync(destination) && fs.existsSync(backup)) fs.renameSync(backup, destination);
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
try { fs.rmSync(backup, { force: true }); } catch {}
|
|
75
|
+
return { id: normalized, hidden: true };
|
|
76
|
+
}
|
|
77
|
+
|
|
38
78
|
function readUInt24LE(buffer, offset) {
|
|
39
79
|
return buffer[offset] | (buffer[offset + 1] << 8) | (buffer[offset + 2] << 16);
|
|
40
80
|
}
|
|
@@ -368,10 +408,11 @@ async function installFromCodexPets(input, fetchImpl = fetch) {
|
|
|
368
408
|
|
|
369
409
|
function removeInstalledPet(id) {
|
|
370
410
|
const normalized = String(id || "").trim().toLowerCase();
|
|
371
|
-
if (!PET_ID_RE.test(normalized)
|
|
411
|
+
if (!PET_ID_RE.test(normalized)) throw new Error("Invalid pet id");
|
|
412
|
+
if (BUILTIN_IDS.has(normalized)) return hideBuiltinPet(normalized);
|
|
372
413
|
const root = resolvePetsDir();
|
|
373
414
|
const destination = path.join(root, normalized);
|
|
374
|
-
if (!destination.startsWith(`${root}${path.sep}`)) throw new Error("Invalid
|
|
415
|
+
if (!destination.startsWith(`${root}${path.sep}`)) throw new Error("Invalid pet path");
|
|
375
416
|
fs.rmSync(destination, { recursive: true, force: true });
|
|
376
417
|
petDirectoryCache.delete(destination);
|
|
377
418
|
return { id: normalized };
|
|
@@ -627,6 +668,7 @@ module.exports = {
|
|
|
627
668
|
petIdFromCodexPetsUrl,
|
|
628
669
|
readCodexBuiltinPets,
|
|
629
670
|
readCodexImportableAsset,
|
|
671
|
+
readHiddenBuiltinIds,
|
|
630
672
|
readWebpDimensions,
|
|
631
673
|
removeInstalledPet,
|
|
632
674
|
resolveCodexPetsDir,
|