tokentracker-cli 0.79.9 → 0.80.1
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-B1WOPlu0.js → AchievementBadge-D617bvS9.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-9kHEQicS.js → AchievementsPage-mzE76Ah9.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-Dw05PYPr.js → AchievementsSection-8CYGBoPJ.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-DaWgiijJ.js → BadgeDetailModal-DM3veVmW.js} +1 -1
- package/dashboard/dist/assets/{Card-DYhrLLM4.js → Card-DWONuizy.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-BLy2LUKz.js → ClawdAnimated-CGQXyWuH.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-CWYqfht3.js → CommunityStatsModal-B5yDmJ0o.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-DcE2C24Y.js → ConfirmModal-d4wpsMPW.js} +1 -1
- package/dashboard/dist/assets/{Controls-CI7qw0-b.js → Controls-CMSqknaB.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-CAlgaBa7.js → DashboardPage-Cw6lmVSg.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-DinOyaeN.js → DevicePage-CsPSjSLH.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-CjfSDrLo.js → DialogClose-Cbs-z783.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-B0fmBJxQ.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-f7gNmCca.js → DialogTitle-C4mJiIQW.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-D_5w0Qh0.js → FadeIn-xOJwCmZ8.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-DTZyNKyL.js → HeaderGithubStar-oRz4rfC8.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-D8CViF98.js → IpCheckPage-CTwL-Zmz.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-CqN5KEjV.js → LandingPage-CqYJ3sZh.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardAvatar-CwUVO4wn.js → LeaderboardAvatar-CuF6Kzkj.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-B12HJ9LA.js → LeaderboardPage-BvOPIj-b.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-DFzPpsnV.js → LeaderboardProfileModal-DVXuKkRN.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-Z0ZYpNFd.js → LeaderboardProfilePage-RE1CPqzM.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-BpwioIa-.js → LimitsPage-BovkRgac.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-Covi6zdt.js → LocalOnlyNotice-CTFFU_ip.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-CRqbhwCx.js → LoginPage-mguBCTBZ.js} +1 -1
- package/dashboard/dist/assets/{PetPage-BY_yObVj.js → PetPage-C5KLH6Fm.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-B2dE3Ary.js → PopoverPopup-CLDh5yIk.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-C8IEJbVc.js → ResetPasswordPage-DnhgHQrV.js} +1 -1
- package/dashboard/dist/assets/{Select-BmRLYSqA.js → Select-DPSaPIlz.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-FC46bk8b.js → SelectItemText-jIBbXpqi.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-DipzOQVg.js → SettingsPage-BK0IqSjg.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-CziypUTf.js → SkillsPage-BQNoMpL2.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-CJegJhVR.js → WidgetsPage-308d8icu.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-DcMiKvzf.js → WrappedPage-T720sQ9S.js} +1 -1
- package/dashboard/dist/assets/{agent-logos-S7bF1LwW.js → agent-logos-Blo5C96d.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-D0yeL56y.js → arrow-up-right-DKz2Ua6U.js} +1 -1
- package/dashboard/dist/assets/check-fF6lQGN-.js +1 -0
- package/dashboard/dist/assets/{copy-D-z9ZtIu.js → copy-DcUFG0PD.js} +1 -1
- package/dashboard/dist/assets/{download-Dwxnd3QG.js → download-CNK1wtxV.js} +1 -1
- package/dashboard/dist/assets/{flame-Bdqk9A75.js → flame-BEhJZRaZ.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-DNVcCRwm.js → format-tokens-Bsfbd8nj.js} +1 -1
- package/dashboard/dist/assets/{info-CMLT6oUI.js → info-DRJLv-aV.js} +1 -1
- package/dashboard/dist/assets/{link-2-BmiHu726.js → link-2-C0GdaANY.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-CXVUQtzm.js → loader-circle-F9ymYeor.js} +1 -1
- package/dashboard/dist/assets/{main-B9m79io-.js → main-CThKxvKx.js} +3 -3
- package/dashboard/dist/assets/{provider-display-BoAesdke.js → provider-display-D4NdFjHN.js} +1 -1
- package/dashboard/dist/assets/{trash-2-C9Rh2DAL.js → trash-2-D9vulGMW.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-aZwYxrXR.js → use-community-stats-BU4dXEOM.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-tJKR83RT.js → use-limits-display-prefs-Bx2VJhwl.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-PjfJPK5v.js → use-native-settings-BSXaxaTC.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-D1QFmZUY.js → use-ordered-list-OA0HwwEL.js} +1 -1
- package/dashboard/dist/assets/{use-quality-per-dollar-pref-4N3Su4wC.js → use-quality-per-dollar-pref-Bqooc4FA.js} +1 -1
- package/dashboard/dist/assets/{use-transform-D9nurInL.js → use-transform-CZ9FdRGw.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-DapTpi_6.js → use-usage-limits-Ct_A4wTr.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-B0DH9ZZA.js → useCurrency-D4bvdaFB.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-DijV6DYS.js → useScrollLock-ZsWO-GoL.js} +1 -1
- package/dashboard/dist/assets/useTokenFormat-B3huMEws.js +1 -0
- package/dashboard/dist/assets/{zap-BVfhR3Ck.js → zap-C9Mp-GNU.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +1 -1
- package/src/commands/init.js +607 -6
- package/src/commands/sync.js +176 -7
- package/src/lib/codex-config.js +135 -40
- package/src/lib/codex-context-breakdown.js +123 -43
- package/src/lib/codex-rollout-parser.js +329 -32
- package/src/lib/local-api.js +11 -5
- package/src/lib/openclaw-session-plugin.js +13 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +230 -10
- package/src/lib/usage-limits.js +9 -5
- package/dashboard/dist/assets/DialogDescription-DWs-KIUA.js +0 -1
- package/dashboard/dist/assets/check-Dn9BLYEf.js +0 -1
- package/dashboard/dist/assets/useTokenFormat-nxdG6gi9.js +0 -1
package/src/commands/sync.js
CHANGED
|
@@ -172,6 +172,18 @@ const CLOUD_CONVERSATIONS_BACKFILL_KEY = "cloudConversationsBackfill_2026_06";
|
|
|
172
172
|
// ~/.codex/archived_sessions/ which sync does not scan) — clearing its bucket
|
|
173
173
|
// would lose that history (ref the v6 ground-truth-repair data-loss incident).
|
|
174
174
|
const CODEX_RESCAN_DEDUP_REPAIR_KEY = "codexRescanDedupRepair_2026_06";
|
|
175
|
+
// One-time repair (#169 follow-up): forked Codex rollouts replay the parent
|
|
176
|
+
// session's token history into the child file, and until the same-day burst
|
|
177
|
+
// detector landed in rollout.js the parser counted those replayed rows as live
|
|
178
|
+
// usage (PR #169's date guard only caught cross-day forks). Cursors sit at EOF
|
|
179
|
+
// on every already-parsed file, so the forward fix alone never corrects the
|
|
180
|
+
// inflated history. This re-runs the exact #187 guarded rebuild under a new key
|
|
181
|
+
// — the rebuild re-parses every codex file with the CURRENT (fork-aware)
|
|
182
|
+
// parser, so replay rows are excluded — and inherits all its safety properties
|
|
183
|
+
// (unreproducible-session skip, atomic throwaway rebuild, queue strip, upload
|
|
184
|
+
// offset reset for the cloud overwrite). Pre-gated: installs with no forked
|
|
185
|
+
// rollout on disk carry no fork phantom and mark done without the rebuild.
|
|
186
|
+
const CODEX_FORK_REPLAY_REPAIR_KEY = "codexForkReplayRepair_2026_07";
|
|
175
187
|
const DROID_DUP_SESSION_REPAIR_KEY = "droidDupSessionInflationRepair_2026_06";
|
|
176
188
|
const CODEX_COLD_SCAN_AUDIT_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
177
189
|
const CODEX_COLD_SCAN_AUDIT_MAX_SYNCS = 288;
|
|
@@ -273,8 +285,10 @@ function mergeCopilotAppDbStates(primary = {}, alias = {}) {
|
|
|
273
285
|
return { ...primary, ...newer, sessionTotals };
|
|
274
286
|
}
|
|
275
287
|
|
|
276
|
-
async function cmdSync(argv) {
|
|
288
|
+
async function cmdSync(argv, context = {}) {
|
|
277
289
|
const opts = parseArgs(argv);
|
|
290
|
+
const diagnostics = context && typeof context === "object" ? context.diagnostics : null;
|
|
291
|
+
const syncDiagnostics = diagnostics && typeof diagnostics === "object" ? diagnostics : null;
|
|
278
292
|
const home = os.homedir();
|
|
279
293
|
const { trackerDir } = await resolveTrackerPaths({ home });
|
|
280
294
|
|
|
@@ -405,7 +419,7 @@ async function cmdSync(argv) {
|
|
|
405
419
|
|
|
406
420
|
if (isFullSourceScan) {
|
|
407
421
|
await migrateRolloutCumulativeDeltaBuckets({ cursors, queuePath, rolloutFiles });
|
|
408
|
-
await repairCodexRescanInflation({
|
|
422
|
+
const codexRescanRepairRan = await repairCodexRescanInflation({
|
|
409
423
|
cursors,
|
|
410
424
|
queuePath,
|
|
411
425
|
queueStatePath,
|
|
@@ -413,6 +427,15 @@ async function cmdSync(argv) {
|
|
|
413
427
|
projectQueueStatePath,
|
|
414
428
|
rolloutFiles,
|
|
415
429
|
});
|
|
430
|
+
await repairCodexForkReplayInflation({
|
|
431
|
+
cursors,
|
|
432
|
+
queuePath,
|
|
433
|
+
queueStatePath,
|
|
434
|
+
projectQueuePath,
|
|
435
|
+
projectQueueStatePath,
|
|
436
|
+
rolloutFiles,
|
|
437
|
+
legacyRepairRan: codexRescanRepairRan,
|
|
438
|
+
});
|
|
416
439
|
await repairDroidDuplicateSessionInflation({ cursors, queuePath, queueStatePath });
|
|
417
440
|
await repairMimoClaudeMislabel({
|
|
418
441
|
cursors,
|
|
@@ -433,6 +456,7 @@ async function cmdSync(argv) {
|
|
|
433
456
|
cursors,
|
|
434
457
|
projectEnabled: true,
|
|
435
458
|
auditDue: codexColdAuditDue,
|
|
459
|
+
diagnostics: syncDiagnostics,
|
|
436
460
|
})
|
|
437
461
|
: { rolloutFiles, skipped: 0 };
|
|
438
462
|
const rolloutFilesForParse = codexColdFilter.rolloutFiles;
|
|
@@ -455,6 +479,7 @@ async function cmdSync(argv) {
|
|
|
455
479
|
cursors,
|
|
456
480
|
queuePath,
|
|
457
481
|
projectQueuePath,
|
|
482
|
+
diagnostics: syncDiagnostics,
|
|
458
483
|
onProgress: (p) => {
|
|
459
484
|
if (!progress?.enabled) return;
|
|
460
485
|
const pct = p.total > 0 ? p.index / p.total : 1;
|
|
@@ -1827,6 +1852,12 @@ async function cmdSync(argv) {
|
|
|
1827
1852
|
|
|
1828
1853
|
cursors.updatedAt = new Date().toISOString();
|
|
1829
1854
|
await writeJson(cursorsPath, cursors);
|
|
1855
|
+
if (syncDiagnostics) {
|
|
1856
|
+
const cursorStat = await fs.stat(cursorsPath);
|
|
1857
|
+
syncDiagnostics.cursor_commits = Number(syncDiagnostics.cursor_commits || 0) + 1;
|
|
1858
|
+
syncDiagnostics.cursor_bytes = Number(syncDiagnostics.cursor_bytes || 0) + cursorStat.size;
|
|
1859
|
+
syncDiagnostics.cursor_path = cursorsPath;
|
|
1860
|
+
}
|
|
1830
1861
|
if (grokHookSignalConsumed && grokHookSignalPath) {
|
|
1831
1862
|
await fs.unlink(grokHookSignalPath).catch(() => {});
|
|
1832
1863
|
}
|
|
@@ -2097,6 +2128,7 @@ module.exports = {
|
|
|
2097
2128
|
migrateCursorUnknownBuckets,
|
|
2098
2129
|
migrateRolloutCumulativeDeltaBuckets,
|
|
2099
2130
|
repairCodexRescanInflation,
|
|
2131
|
+
repairCodexForkReplayInflation,
|
|
2100
2132
|
repairDroidDuplicateSessionInflation,
|
|
2101
2133
|
repairMimoClaudeMislabel,
|
|
2102
2134
|
reincludeClaudeMemObserverFiles,
|
|
@@ -2109,6 +2141,7 @@ module.exports = {
|
|
|
2109
2141
|
CURSOR_UNKNOWN_MIGRATION_KEY,
|
|
2110
2142
|
ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY,
|
|
2111
2143
|
CODEX_RESCAN_DEDUP_REPAIR_KEY,
|
|
2144
|
+
CODEX_FORK_REPLAY_REPAIR_KEY,
|
|
2112
2145
|
DROID_DUP_SESSION_REPAIR_KEY,
|
|
2113
2146
|
CLAUDE_MEM_OBSERVER_REINCLUDE_KEY,
|
|
2114
2147
|
GROK_APPEND_ONLY_REPAIR_MIGRATION_KEY,
|
|
@@ -3091,6 +3124,11 @@ async function migrateRolloutCumulativeDeltaBuckets({ cursors, queuePath, rollou
|
|
|
3091
3124
|
delete cursors.files[filePath];
|
|
3092
3125
|
}
|
|
3093
3126
|
}
|
|
3127
|
+
// The migration clears Codex buckets and reparses the discovered corpus from
|
|
3128
|
+
// byte zero. Persisted event keys belong to the cleared buckets, so retaining
|
|
3129
|
+
// them can suppress the rebuild when a moved session still has an old path
|
|
3130
|
+
// cursor. Rebuild the hash inventory together with the buckets.
|
|
3131
|
+
cursors.codexHashes = [];
|
|
3094
3132
|
|
|
3095
3133
|
const buckets = cursors.hourly?.buckets;
|
|
3096
3134
|
const retractions = [];
|
|
@@ -3157,6 +3195,11 @@ async function repairCodexRescanInflation({
|
|
|
3157
3195
|
projectQueuePath,
|
|
3158
3196
|
projectQueueStatePath,
|
|
3159
3197
|
rolloutFiles,
|
|
3198
|
+
// The fork-replay repair (#169 follow-up) re-runs this exact rebuild under
|
|
3199
|
+
// its own key: the rebuild always uses the CURRENT parser, so any parser fix
|
|
3200
|
+
// shipped since the last run is applied to the rebuilt history.
|
|
3201
|
+
migrationKey = CODEX_RESCAN_DEDUP_REPAIR_KEY,
|
|
3202
|
+
uploadNote = "reset_after_codex_rescan_dedup_2026_06",
|
|
3160
3203
|
}) {
|
|
3161
3204
|
if (!cursors || typeof cursors !== "object") return false;
|
|
3162
3205
|
const migrations = (cursors.migrations ||= {});
|
|
@@ -3167,7 +3210,7 @@ async function repairCodexRescanInflation({
|
|
|
3167
3210
|
// found). Treating the skip sentinel as "done" is what left users like #187
|
|
3168
3211
|
// permanently stuck on the inflated value after upgrading (the key was truthy
|
|
3169
3212
|
// so the guard never got a second chance).
|
|
3170
|
-
const priorRepair = migrations[
|
|
3213
|
+
const priorRepair = migrations[migrationKey];
|
|
3171
3214
|
if (priorRepair && !(typeof priorRepair === "object" && priorRepair.skipped)) return false;
|
|
3172
3215
|
|
|
3173
3216
|
// Codex session files THIS sync discovered (source === "codex").
|
|
@@ -3203,7 +3246,7 @@ async function repairCodexRescanInflation({
|
|
|
3203
3246
|
if (codexFileSet.has(fp)) continue; // exact file re-scanned this run
|
|
3204
3247
|
const id = codexSessionIdFromPath(fp);
|
|
3205
3248
|
if (id && scannedSessionIds.has(id)) continue; // same session scanned elsewhere (moved)
|
|
3206
|
-
migrations[
|
|
3249
|
+
migrations[migrationKey] = {
|
|
3207
3250
|
skipped: true,
|
|
3208
3251
|
reason: "codex_session_unreproducible",
|
|
3209
3252
|
at: new Date().toISOString(),
|
|
@@ -3462,7 +3505,7 @@ async function repairCodexRescanInflation({
|
|
|
3462
3505
|
}
|
|
3463
3506
|
uploadState.offset = 0;
|
|
3464
3507
|
uploadState.updatedAt = new Date().toISOString();
|
|
3465
|
-
uploadState.note =
|
|
3508
|
+
uploadState.note = uploadNote;
|
|
3466
3509
|
await fs.writeFile(queueStatePath, JSON.stringify(uploadState));
|
|
3467
3510
|
}
|
|
3468
3511
|
if (projectRepairEnabled && typeof projectQueueStatePath === "string" && projectQueueStatePath) {
|
|
@@ -3474,14 +3517,140 @@ async function repairCodexRescanInflation({
|
|
|
3474
3517
|
}
|
|
3475
3518
|
uploadState.offset = 0;
|
|
3476
3519
|
uploadState.updatedAt = new Date().toISOString();
|
|
3477
|
-
uploadState.note =
|
|
3520
|
+
uploadState.note = uploadNote;
|
|
3478
3521
|
await fs.writeFile(projectQueueStatePath, JSON.stringify(uploadState));
|
|
3479
3522
|
}
|
|
3480
3523
|
|
|
3481
|
-
migrations[
|
|
3524
|
+
migrations[migrationKey] = new Date().toISOString();
|
|
3482
3525
|
return true;
|
|
3483
3526
|
}
|
|
3484
3527
|
|
|
3528
|
+
// #169 follow-up: repair the historical inflation left by same-day forked
|
|
3529
|
+
// Codex rollout replays (see CODEX_FORK_REPLAY_REPAIR_KEY). Delegates to the
|
|
3530
|
+
// #187 guarded rebuild with the fork-repair key.
|
|
3531
|
+
async function repairCodexForkReplayInflation({
|
|
3532
|
+
cursors,
|
|
3533
|
+
queuePath,
|
|
3534
|
+
queueStatePath,
|
|
3535
|
+
projectQueuePath,
|
|
3536
|
+
projectQueueStatePath,
|
|
3537
|
+
rolloutFiles,
|
|
3538
|
+
// True when the #187 repair ran its rebuild earlier in THIS sync: that
|
|
3539
|
+
// rebuild already used the fork-aware parser, so the history is clean and a
|
|
3540
|
+
// second rebuild would be pure waste.
|
|
3541
|
+
legacyRepairRan = false,
|
|
3542
|
+
}) {
|
|
3543
|
+
if (!cursors || typeof cursors !== "object") return false;
|
|
3544
|
+
const migrations = (cursors.migrations ||= {});
|
|
3545
|
+
// Same completed-vs-skipped semantics as the #187 repair: an ISO string is
|
|
3546
|
+
// final, a {skipped:true} sentinel retries (the skip condition can clear).
|
|
3547
|
+
const prior = migrations[CODEX_FORK_REPLAY_REPAIR_KEY];
|
|
3548
|
+
if (prior && !(typeof prior === "object" && prior.skipped)) return false;
|
|
3549
|
+
|
|
3550
|
+
if (legacyRepairRan) {
|
|
3551
|
+
migrations[CODEX_FORK_REPLAY_REPAIR_KEY] = new Date().toISOString();
|
|
3552
|
+
return false;
|
|
3553
|
+
}
|
|
3554
|
+
|
|
3555
|
+
// Pre-gate: fork phantom can only exist on installs that have (or had) a
|
|
3556
|
+
// forked rollout. Scanning file heads is cheap; the rebuild re-parses every
|
|
3557
|
+
// codex file. Files forked AFTER the parser fix shipped never accrue
|
|
3558
|
+
// phantom, so a definitive "no forks" verdict may finalize — but ONLY when
|
|
3559
|
+
// this scan can actually stand for the install's whole codex history:
|
|
3560
|
+
// 1. every candidate head was readable (an unreadable candidate could be
|
|
3561
|
+
// the one forked rollout), AND
|
|
3562
|
+
// 2. every codex session recorded in cursors is covered by THIS scan's
|
|
3563
|
+
// file list (same UUID-keyed reproducibility rule as the #187 guard).
|
|
3564
|
+
// A forked rollout can be real yet absent from one run's rolloutFiles —
|
|
3565
|
+
// WSL/native split probing, a temporarily missing ~/.codex, a deleted
|
|
3566
|
+
// file whose phantom is already in history — and finalizing off such a
|
|
3567
|
+
// partial view would permanently foreclose the repair this migration
|
|
3568
|
+
// exists to perform, while the guarded repair itself would only have
|
|
3569
|
+
// deferred retryably.
|
|
3570
|
+
// Either failure defers with the retryable {skipped:true} sentinel.
|
|
3571
|
+
const forkScan = await scanForForkedCodexRollout(rolloutFiles);
|
|
3572
|
+
if (!forkScan.forked) {
|
|
3573
|
+
if (forkScan.indeterminate) {
|
|
3574
|
+
migrations[CODEX_FORK_REPLAY_REPAIR_KEY] = {
|
|
3575
|
+
skipped: true,
|
|
3576
|
+
reason: "fork_scan_indeterminate",
|
|
3577
|
+
at: new Date().toISOString(),
|
|
3578
|
+
};
|
|
3579
|
+
return false;
|
|
3580
|
+
}
|
|
3581
|
+
const scannedSessionIds = new Set();
|
|
3582
|
+
const scannedPaths = new Set();
|
|
3583
|
+
for (const entry of Array.isArray(rolloutFiles) ? rolloutFiles : []) {
|
|
3584
|
+
const fp = typeof entry === "string" ? entry : entry?.path;
|
|
3585
|
+
const src = typeof entry === "string" ? "codex" : String(entry?.source || "codex");
|
|
3586
|
+
if (!fp || src !== "codex") continue;
|
|
3587
|
+
scannedPaths.add(fp);
|
|
3588
|
+
const id = codexSessionIdFromPath(fp);
|
|
3589
|
+
if (id) scannedSessionIds.add(id);
|
|
3590
|
+
}
|
|
3591
|
+
if (cursors.files && typeof cursors.files === "object") {
|
|
3592
|
+
for (const fp of Object.keys(cursors.files)) {
|
|
3593
|
+
if (!isCodexSessionCursorPath(fp)) continue;
|
|
3594
|
+
if (scannedPaths.has(fp)) continue;
|
|
3595
|
+
const id = codexSessionIdFromPath(fp);
|
|
3596
|
+
if (id && scannedSessionIds.has(id)) continue;
|
|
3597
|
+
migrations[CODEX_FORK_REPLAY_REPAIR_KEY] = {
|
|
3598
|
+
skipped: true,
|
|
3599
|
+
reason: "codex_history_not_covered",
|
|
3600
|
+
at: new Date().toISOString(),
|
|
3601
|
+
};
|
|
3602
|
+
return false;
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
migrations[CODEX_FORK_REPLAY_REPAIR_KEY] = new Date().toISOString();
|
|
3606
|
+
return false;
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
return repairCodexRescanInflation({
|
|
3610
|
+
cursors,
|
|
3611
|
+
queuePath,
|
|
3612
|
+
queueStatePath,
|
|
3613
|
+
projectQueuePath,
|
|
3614
|
+
projectQueueStatePath,
|
|
3615
|
+
rolloutFiles,
|
|
3616
|
+
migrationKey: CODEX_FORK_REPLAY_REPAIR_KEY,
|
|
3617
|
+
uploadNote: "reset_after_codex_fork_replay_2026_07",
|
|
3618
|
+
});
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3621
|
+
// Scans codex rollout heads for the fork marker. The child session_meta (with
|
|
3622
|
+
// forked_from_id) is the FIRST line of a forked rollout — observed at byte
|
|
3623
|
+
// offset ≤169 across every local sample — so a bounded head read is
|
|
3624
|
+
// sufficient. 64KB leaves ample margin for long base_instructions preceding
|
|
3625
|
+
// it; a substring false positive (the marker quoted in unrelated head content)
|
|
3626
|
+
// only costs one redundant guarded rebuild, never data. Tri-state result:
|
|
3627
|
+
// {forked:true} on a hit, {forked:false, indeterminate:false} only when every
|
|
3628
|
+
// candidate was inspected, {forked:false, indeterminate:true} when any head
|
|
3629
|
+
// read failed — the caller must not finalize "no forks" off a failed read.
|
|
3630
|
+
async function scanForForkedCodexRollout(rolloutFiles) {
|
|
3631
|
+
const HEAD_BYTES = 65536;
|
|
3632
|
+
let indeterminate = false;
|
|
3633
|
+
for (const entry of Array.isArray(rolloutFiles) ? rolloutFiles : []) {
|
|
3634
|
+
const fp = typeof entry === "string" ? entry : entry?.path;
|
|
3635
|
+
const src = typeof entry === "string" ? "codex" : String(entry?.source || "codex");
|
|
3636
|
+
if (!fp || src !== "codex") continue;
|
|
3637
|
+
let fh = null;
|
|
3638
|
+
try {
|
|
3639
|
+
fh = await fs.open(fp, "r");
|
|
3640
|
+
const buf = Buffer.alloc(HEAD_BYTES);
|
|
3641
|
+
const { bytesRead } = await fh.read(buf, 0, HEAD_BYTES, 0);
|
|
3642
|
+
if (buf.toString("utf8", 0, bytesRead).includes('"forked_from_id"')) {
|
|
3643
|
+
return { forked: true, indeterminate: false };
|
|
3644
|
+
}
|
|
3645
|
+
} catch (_e) {
|
|
3646
|
+
indeterminate = true;
|
|
3647
|
+
} finally {
|
|
3648
|
+
if (fh) await fh.close().catch(() => {});
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3651
|
+
return { forked: false, indeterminate };
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3485
3654
|
function isCodexSessionCursorPath(filePath) {
|
|
3486
3655
|
if (typeof filePath !== "string") return false;
|
|
3487
3656
|
const normalized = filePath.replace(/\\/g, "/");
|
package/src/lib/codex-config.js
CHANGED
|
@@ -55,12 +55,12 @@ async function restoreNotify({ configPath, notifyOriginalPath, expectedNotify })
|
|
|
55
55
|
const originalNotify = Array.isArray(original?.notify) ? original.notify : null;
|
|
56
56
|
const currentNotify = extractNotify(text);
|
|
57
57
|
|
|
58
|
-
if (expectedNotify &&
|
|
59
|
-
return { restored: false, skippedReason: "
|
|
58
|
+
if (!originalNotify && expectedNotify && currentNotify == null) {
|
|
59
|
+
return { restored: false, skippedReason: "no-backup-not-installed" };
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
if (
|
|
63
|
-
return { restored: false, skippedReason: "
|
|
62
|
+
if (expectedNotify && !arraysEqual(currentNotify, expectedNotify)) {
|
|
63
|
+
return { restored: false, skippedReason: "current-not-managed" };
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
const updated = originalNotify ? setNotify(text, originalNotify) : removeNotify(text);
|
|
@@ -161,15 +161,16 @@ function extractNotify(text) {
|
|
|
161
161
|
const lines = text.split(/\r?\n/);
|
|
162
162
|
for (let i = 0; i < lines.length; i++) {
|
|
163
163
|
const line = lines[i];
|
|
164
|
+
if (isTomlTableHeader(line)) break;
|
|
164
165
|
const m = line.match(/^\s*notify\s*=\s*(.*)\s*$/);
|
|
165
166
|
if (!m) continue;
|
|
166
167
|
|
|
167
168
|
const rhs = (m[1] || "").trim();
|
|
168
169
|
const literal = readTomlArrayLiteral(lines, i, rhs);
|
|
169
|
-
if (!literal)
|
|
170
|
+
if (!literal) return null;
|
|
170
171
|
|
|
171
172
|
const parsed = parseTomlStringArray(literal);
|
|
172
|
-
|
|
173
|
+
return parsed;
|
|
173
174
|
}
|
|
174
175
|
return null;
|
|
175
176
|
}
|
|
@@ -182,6 +183,14 @@ function setNotify(text, notifyCmd) {
|
|
|
182
183
|
let replaced = false;
|
|
183
184
|
for (let i = 0; i < lines.length; i++) {
|
|
184
185
|
const line = lines[i];
|
|
186
|
+
if (isTomlTableHeader(line)) {
|
|
187
|
+
if (!replaced) {
|
|
188
|
+
out.push(notifyLine);
|
|
189
|
+
replaced = true;
|
|
190
|
+
}
|
|
191
|
+
out.push(...lines.slice(i));
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
185
194
|
const m = line.match(/^\s*notify\s*=\s*(.*)\s*$/);
|
|
186
195
|
if (m) {
|
|
187
196
|
if (!replaced) {
|
|
@@ -190,7 +199,7 @@ function setNotify(text, notifyCmd) {
|
|
|
190
199
|
}
|
|
191
200
|
|
|
192
201
|
const rhs = (m[1] || "").trim();
|
|
193
|
-
i =
|
|
202
|
+
i = findNotifyBlockEnd(lines, i, rhs);
|
|
194
203
|
continue;
|
|
195
204
|
}
|
|
196
205
|
out.push(line);
|
|
@@ -211,10 +220,14 @@ function removeNotify(text) {
|
|
|
211
220
|
const out = [];
|
|
212
221
|
for (let i = 0; i < lines.length; i++) {
|
|
213
222
|
const line = lines[i];
|
|
223
|
+
if (isTomlTableHeader(line)) {
|
|
224
|
+
out.push(...lines.slice(i));
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
214
227
|
const m = line.match(/^\s*notify\s*=\s*(.*)\s*$/);
|
|
215
228
|
if (m) {
|
|
216
229
|
const rhs = (m[1] || "").trim();
|
|
217
|
-
i =
|
|
230
|
+
i = findNotifyBlockEnd(lines, i, rhs);
|
|
218
231
|
continue;
|
|
219
232
|
}
|
|
220
233
|
out.push(line);
|
|
@@ -225,45 +238,78 @@ function removeNotify(text) {
|
|
|
225
238
|
function parseTomlStringArray(rhs) {
|
|
226
239
|
// Minimal parser for TOML basic/literal string arrays.
|
|
227
240
|
if (!rhs.startsWith("[") || !rhs.endsWith("]")) return null;
|
|
228
|
-
const inner = rhs.slice(1, -1).trim();
|
|
229
|
-
if (!inner) return [];
|
|
230
241
|
|
|
231
242
|
const parts = [];
|
|
232
|
-
let
|
|
233
|
-
let
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (ch
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
current = "";
|
|
243
|
+
let i = 1;
|
|
244
|
+
let expectValue = true;
|
|
245
|
+
|
|
246
|
+
function skipSpaceAndComments() {
|
|
247
|
+
while (i < rhs.length) {
|
|
248
|
+
const ch = rhs[i];
|
|
249
|
+
if (/\s/.test(ch)) {
|
|
250
|
+
i += 1;
|
|
251
|
+
continue;
|
|
242
252
|
}
|
|
243
|
-
|
|
253
|
+
if (ch === "#") {
|
|
254
|
+
while (i < rhs.length && rhs[i] !== "\n") i += 1;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
break;
|
|
244
258
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function parseString() {
|
|
262
|
+
const quote = rhs[i];
|
|
263
|
+
if (quote !== '"' && quote !== "'") return null;
|
|
264
|
+
i += 1;
|
|
265
|
+
let value = "";
|
|
266
|
+
while (i < rhs.length) {
|
|
267
|
+
const ch = rhs[i];
|
|
268
|
+
if (ch === quote) {
|
|
269
|
+
i += 1;
|
|
270
|
+
return value;
|
|
271
|
+
}
|
|
272
|
+
if (ch === "\n" || ch === "\r") return null;
|
|
273
|
+
if (quote === '"' && ch === "\\") {
|
|
274
|
+
if (i + 1 >= rhs.length) return null;
|
|
275
|
+
const next = rhs[i + 1];
|
|
276
|
+
if (next === "b") value += "\b";
|
|
277
|
+
else if (next === "t") value += "\t";
|
|
278
|
+
else if (next === "n") value += "\n";
|
|
279
|
+
else if (next === "f") value += "\f";
|
|
280
|
+
else if (next === "r") value += "\r";
|
|
281
|
+
else if (next === '"' || next === "\\" || next === "/") value += next;
|
|
282
|
+
else return null;
|
|
283
|
+
i += 2;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
value += ch;
|
|
287
|
+
i += 1;
|
|
288
|
+
}
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
while (i < rhs.length) {
|
|
293
|
+
skipSpaceAndComments();
|
|
294
|
+
if (rhs[i] === "]") {
|
|
295
|
+
i += 1;
|
|
296
|
+
skipSpaceAndComments();
|
|
297
|
+
return i === rhs.length ? parts : null;
|
|
250
298
|
}
|
|
251
|
-
if (
|
|
252
|
-
if (i
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
else if (next === "t") current += "\t";
|
|
256
|
-
else if (next === "n") current += "\n";
|
|
257
|
-
else if (next === "f") current += "\f";
|
|
258
|
-
else if (next === "r") current += "\r";
|
|
259
|
-
else if (next === '"' || next === "\\" || next === "/") current += next;
|
|
260
|
-
else return null;
|
|
299
|
+
if (!expectValue) {
|
|
300
|
+
if (rhs[i] !== ",") return null;
|
|
301
|
+
i += 1;
|
|
302
|
+
expectValue = true;
|
|
261
303
|
continue;
|
|
262
304
|
}
|
|
263
|
-
|
|
305
|
+
|
|
306
|
+
const value = parseString();
|
|
307
|
+
if (value == null) return null;
|
|
308
|
+
parts.push(value);
|
|
309
|
+
expectValue = false;
|
|
264
310
|
}
|
|
265
311
|
|
|
266
|
-
return
|
|
312
|
+
return null;
|
|
267
313
|
}
|
|
268
314
|
|
|
269
315
|
function formatTomlStringArray(arr) {
|
|
@@ -278,6 +324,7 @@ function readTomlArrayLiteral(lines, startIndex, rhs) {
|
|
|
278
324
|
let quote = null;
|
|
279
325
|
let depth = 0;
|
|
280
326
|
let sawOpen = false;
|
|
327
|
+
let invalid = false;
|
|
281
328
|
|
|
282
329
|
function scanChunk(chunk) {
|
|
283
330
|
for (let i = 0; i < chunk.length; i++) {
|
|
@@ -295,7 +342,10 @@ function readTomlArrayLiteral(lines, startIndex, rhs) {
|
|
|
295
342
|
}
|
|
296
343
|
if (ch === "]") {
|
|
297
344
|
depth -= 1;
|
|
298
|
-
if (sawOpen && depth === 0)
|
|
345
|
+
if (sawOpen && depth === 0) {
|
|
346
|
+
if (!isTomlArrayTrailer(chunk.slice(i + 1))) invalid = true;
|
|
347
|
+
return i;
|
|
348
|
+
}
|
|
299
349
|
}
|
|
300
350
|
continue;
|
|
301
351
|
}
|
|
@@ -305,6 +355,10 @@ function readTomlArrayLiteral(lines, startIndex, rhs) {
|
|
|
305
355
|
continue;
|
|
306
356
|
}
|
|
307
357
|
if (quote === '"' && ch === "\\") {
|
|
358
|
+
if (i + 1 >= chunk.length) {
|
|
359
|
+
invalid = true;
|
|
360
|
+
return -1;
|
|
361
|
+
}
|
|
308
362
|
i += 1;
|
|
309
363
|
}
|
|
310
364
|
}
|
|
@@ -313,11 +367,13 @@ function readTomlArrayLiteral(lines, startIndex, rhs) {
|
|
|
313
367
|
|
|
314
368
|
const parts = [first];
|
|
315
369
|
let endPos = scanChunk(first);
|
|
370
|
+
if (invalid) return null;
|
|
316
371
|
if (endPos !== -1) return first.slice(0, endPos + 1).trim();
|
|
317
372
|
|
|
318
373
|
for (let j = startIndex + 1; j < lines.length; j++) {
|
|
319
374
|
const line = lines[j];
|
|
320
375
|
endPos = scanChunk(line);
|
|
376
|
+
if (invalid) return null;
|
|
321
377
|
if (endPos !== -1) {
|
|
322
378
|
parts.push(line.slice(0, endPos + 1));
|
|
323
379
|
return parts.join("\n").trim();
|
|
@@ -336,6 +392,7 @@ function findTomlArrayBlockEnd(lines, startIndex, rhs) {
|
|
|
336
392
|
let quote = null;
|
|
337
393
|
let depth = 0;
|
|
338
394
|
let sawOpen = false;
|
|
395
|
+
let invalid = false;
|
|
339
396
|
|
|
340
397
|
function scanChunk(chunk) {
|
|
341
398
|
for (let i = 0; i < chunk.length; i++) {
|
|
@@ -353,7 +410,10 @@ function findTomlArrayBlockEnd(lines, startIndex, rhs) {
|
|
|
353
410
|
}
|
|
354
411
|
if (ch === "]") {
|
|
355
412
|
depth -= 1;
|
|
356
|
-
if (sawOpen && depth === 0)
|
|
413
|
+
if (sawOpen && depth === 0) {
|
|
414
|
+
if (!isTomlArrayTrailer(chunk.slice(i + 1))) invalid = true;
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
357
417
|
}
|
|
358
418
|
continue;
|
|
359
419
|
}
|
|
@@ -363,6 +423,10 @@ function findTomlArrayBlockEnd(lines, startIndex, rhs) {
|
|
|
363
423
|
continue;
|
|
364
424
|
}
|
|
365
425
|
if (quote === '"' && ch === "\\") {
|
|
426
|
+
if (i + 1 >= chunk.length) {
|
|
427
|
+
invalid = true;
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
366
430
|
i += 1;
|
|
367
431
|
}
|
|
368
432
|
}
|
|
@@ -370,12 +434,43 @@ function findTomlArrayBlockEnd(lines, startIndex, rhs) {
|
|
|
370
434
|
}
|
|
371
435
|
|
|
372
436
|
if (scanChunk(first)) return startIndex;
|
|
437
|
+
if (invalid) return startIndex;
|
|
373
438
|
for (let j = startIndex + 1; j < lines.length; j++) {
|
|
374
439
|
if (scanChunk(lines[j])) return j;
|
|
440
|
+
if (invalid) return startIndex;
|
|
375
441
|
}
|
|
376
442
|
return startIndex;
|
|
377
443
|
}
|
|
378
444
|
|
|
445
|
+
function findNotifyBlockEnd(lines, startIndex, rhs) {
|
|
446
|
+
const endIndex = findTomlArrayBlockEnd(lines, startIndex, rhs);
|
|
447
|
+
const literal = readTomlArrayLiteral(lines, startIndex, rhs);
|
|
448
|
+
if (
|
|
449
|
+
endIndex !== startIndex ||
|
|
450
|
+
!rhs.trim().startsWith("[") ||
|
|
451
|
+
(literal && parseTomlStringArray(literal))
|
|
452
|
+
) {
|
|
453
|
+
return endIndex;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
for (let j = startIndex + 1; j < lines.length; j++) {
|
|
457
|
+
if (isTopLevelTomlBoundary(lines[j])) return j - 1;
|
|
458
|
+
}
|
|
459
|
+
return lines.length - 1;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function isTomlTableHeader(line) {
|
|
463
|
+
return /^\s*\[\[?[^\]]+\]\]?\s*(?:#.*)?$/.test(line);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function isTopLevelTomlBoundary(line) {
|
|
467
|
+
return /^\s*(?:\[\[?[^\]]+\]\]?|(?:[A-Za-z0-9_-]+|"[^"]+"|'[^']+')(?:\s*\.\s*(?:[A-Za-z0-9_-]+|"[^"]+"|'[^']+'))*\s*=)/.test(line);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function isTomlArrayTrailer(text) {
|
|
471
|
+
return /^\s*(?:#.*)?$/.test(text);
|
|
472
|
+
}
|
|
473
|
+
|
|
379
474
|
function arraysEqual(a, b) {
|
|
380
475
|
if (!Array.isArray(a) || !Array.isArray(b)) return false;
|
|
381
476
|
if (a.length !== b.length) return false;
|