tokentracker-cli 0.65.0 → 0.65.2
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 +476 -0
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dashboard/dist/assets/{ActivityHeatmap-Ccf0I3uP.js → ActivityHeatmap-CggJI8SR.js} +1 -1
- package/dashboard/dist/assets/{Card-BSF8dYEt.js → Card-5au9t9nC.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-Bjp6eg2f.js → ConfirmModal-DdczuiTm.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-BInQkoDe.js → DashboardPage-VAYxz_kf.js} +2 -2
- package/dashboard/dist/assets/{DevicePage-B0yxbDDR.js → DevicePage-DQJlqpU3.js} +1 -1
- package/dashboard/dist/assets/{DialogTitle-uw-Oa5Qv.js → DialogTitle-DR64sBQk.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-DPIBqXRl.js → FadeIn-CGzcyAaJ.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-DDXfJ1uC.js → HeaderGithubStar-DiJi3EYZ.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-KC93tPlt.js → IpCheckPage-CoGzED0h.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-Itx2uPWI.js → LandingPage-D_h4MI77.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardAvatar-Bv44b_Kl.js → LeaderboardAvatar-BuSHrfaq.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-DDq5gzyz.js → LeaderboardPage-aP0oQNBq.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-LQyYrCf1.js → LeaderboardProfileModal-zWSPrjXm.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfilePage-3you3mbA.js → LeaderboardProfilePage-BNR__NEp.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-CrLvR8vE.js → LimitsPage-CbwEV4tL.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-Di1gnfIi.js → LocalOnlyNotice-DFxhTdeP.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-f9QHz_uZ.js → LoginPage-6m5w2iay.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-7rjAxAqk.js → PopoverPopup-6bH8WcTu.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-DKA7HGpJ.js → ResetPasswordPage-ByOumS6-.js} +1 -1
- package/dashboard/dist/assets/{Select-2gikPo1a.js → Select-DrNJJE2o.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-DZx33XF1.js → SelectItemText-BTHh_2vw.js} +1 -1
- package/dashboard/dist/assets/SettingsPage-BIxrrxUF.js +1 -0
- package/dashboard/dist/assets/{SkillsPage-B6BJ9UZp.js → SkillsPage-Cg8iXYaS.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-D8p3Tx7E.js → WidgetsPage-Bi8d2YwV.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-cXEfhti9.js → WrappedPage-Cc2kLDpE.js} +1 -1
- package/dashboard/dist/assets/{agent-logos-D3iFlaWg.js → agent-logos-CzNQ3IaQ.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-D87Aw_ZN.js → arrow-up-right-FgMhJ3Pu.js} +1 -1
- package/dashboard/dist/assets/{download-BtcEs1OB.js → download-Bo9t1yfc.js} +1 -1
- package/dashboard/dist/assets/{main-DqnXui98.js → main-6K89gDOX.js} +21 -20
- package/dashboard/dist/assets/{main-CGx8BJ4O.css → main-BHwig9Um.css} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-DAxlRSu2.js → use-limits-display-prefs-DN1nv1HE.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-CgvB_B6W.js → use-native-settings-6ygLVyJu.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-DOfX6aSy.js → use-ordered-list-li19cWJh.js} +1 -1
- package/dashboard/dist/assets/{use-quality-per-dollar-pref-kEbwQwk2.js → use-quality-per-dollar-pref-rNMT2kff.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-la-KPfJj.js → use-usage-limits-kVHyIhp3.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-LDRg7ST0.js → useCurrency-g0G5pwGx.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-lP1Is9IY.js → useScrollLock-Dg7JyjFK.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +1 -1
- package/src/commands/init.js +3 -2
- package/src/commands/serve.js +59 -1
- package/src/commands/status.js +26 -17
- package/src/commands/sync.js +672 -98
- package/src/lib/openclaw-session-plugin.js +2 -2
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +602 -178
- package/src/lib/wsl-probe.js +181 -0
- package/dashboard/dist/assets/SettingsPage-CwHPmuZV.js +0 -1
package/src/commands/sync.js
CHANGED
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
listRolloutFiles,
|
|
11
11
|
listRolloutFilesDeep,
|
|
12
12
|
codexSessionIdFromPath,
|
|
13
|
+
filterColdCodexRolloutFiles,
|
|
13
14
|
listClaudeProjectFiles,
|
|
14
15
|
listGeminiSessionFiles,
|
|
15
16
|
listOpencodeMessageFiles,
|
|
@@ -152,11 +153,50 @@ const CLOUD_CONVERSATIONS_BACKFILL_KEY = "cloudConversationsBackfill_2026_06";
|
|
|
152
153
|
// would lose that history (ref the v6 ground-truth-repair data-loss incident).
|
|
153
154
|
const CODEX_RESCAN_DEDUP_REPAIR_KEY = "codexRescanDedupRepair_2026_06";
|
|
154
155
|
const DROID_DUP_SESSION_REPAIR_KEY = "droidDupSessionInflationRepair_2026_06";
|
|
156
|
+
const CODEX_COLD_SCAN_AUDIT_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
157
|
+
const CODEX_COLD_SCAN_AUDIT_MAX_SYNCS = 288;
|
|
155
158
|
// 0.57.0 mis-attributed mimocode's mirrored Claude/claude-mem history to
|
|
156
159
|
// source=mimo (read the whole DB instead of only providerID=mimo rows). This
|
|
157
160
|
// one-time repair purges all source=mimo data from the local queues + cursor
|
|
158
161
|
// state so the next sync (providerID-filtered reader) rebuilds it correctly.
|
|
159
162
|
const MIMO_PROVIDER_REPAIR_KEY = "mimoClaudeMislabelRepair_2026_06";
|
|
163
|
+
const AUTO_SYNC_SOURCE_ALIASES = new Map([
|
|
164
|
+
["code", "every-code"],
|
|
165
|
+
["everycode", "every-code"],
|
|
166
|
+
["kilo", "kilo-cli"],
|
|
167
|
+
["kilo-code", "kilocode"],
|
|
168
|
+
["kimi_code", "kimi-code"],
|
|
169
|
+
["roo-code", "roocode"],
|
|
170
|
+
]);
|
|
171
|
+
const AUTO_SYNC_SOURCES = new Set([
|
|
172
|
+
"antigravity",
|
|
173
|
+
"claude",
|
|
174
|
+
"codebuddy",
|
|
175
|
+
"codex",
|
|
176
|
+
"copilot",
|
|
177
|
+
"craft",
|
|
178
|
+
"cursor",
|
|
179
|
+
"droid",
|
|
180
|
+
"every-code",
|
|
181
|
+
"gemini",
|
|
182
|
+
"goose",
|
|
183
|
+
"grok",
|
|
184
|
+
"hermes",
|
|
185
|
+
"kilo-cli",
|
|
186
|
+
"kilocode",
|
|
187
|
+
"kiro",
|
|
188
|
+
"kimi",
|
|
189
|
+
"kimi-code",
|
|
190
|
+
"mimo",
|
|
191
|
+
"omp",
|
|
192
|
+
"opencode",
|
|
193
|
+
"openclaw",
|
|
194
|
+
"pi",
|
|
195
|
+
"roocode",
|
|
196
|
+
"workbuddy",
|
|
197
|
+
"zcode",
|
|
198
|
+
"zed",
|
|
199
|
+
]);
|
|
160
200
|
|
|
161
201
|
function warnProviderParseFailure(label, err, opts) {
|
|
162
202
|
if (opts?.auto) return;
|
|
@@ -218,48 +258,88 @@ async function cmdSync(argv) {
|
|
|
218
258
|
const mimoHome = process.env.MIMO_HOME || path.join(xdgDataHome, "mimocode");
|
|
219
259
|
const zcodeHome = process.env.ZCODE_HOME || path.join(home, ".zcode");
|
|
220
260
|
|
|
221
|
-
// OpenClaw session plugin integration:
|
|
222
|
-
//
|
|
261
|
+
// OpenClaw session plugin integration: lifecycle hooks request an
|
|
262
|
+
// OpenClaw-only auto sync so unrelated providers do not get walked.
|
|
223
263
|
const openclawSignal = opts.fromOpenclaw
|
|
224
264
|
? resolveOpenclawSignal({ home, env: process.env })
|
|
225
265
|
: null;
|
|
226
266
|
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
267
|
+
const autoSourceScope = resolveAutoSourceScope(opts);
|
|
268
|
+
const isFullSourceScan = !autoSourceScope;
|
|
269
|
+
const sourceAllowed = (...sources) =>
|
|
270
|
+
!autoSourceScope || sources.includes(autoSourceScope);
|
|
271
|
+
|
|
272
|
+
const sources = [];
|
|
273
|
+
if (sourceAllowed("codex")) {
|
|
274
|
+
sources.push(
|
|
275
|
+
{ source: "codex", sessionsDir: path.join(codexHome, "sessions"), codexInventoryCache: true },
|
|
276
|
+
// Codex-Manager archives sessions to ~/.codex/archived_sessions/ on every
|
|
277
|
+
// account/channel switch (issue #187). Scanning it too keeps that usage
|
|
278
|
+
// counted instead of orphaning it in the cloud (a re-upload's upsert can
|
|
279
|
+
// never delete cloud rows whose local source file has vanished). Safe
|
|
280
|
+
// against double-counting: the codex event dedup keys on sessionUUID (in
|
|
281
|
+
// the filename) + event timestamp, both stable across a sessions/ ->
|
|
282
|
+
// archived_sessions/ move, so re-reading an archived copy is a no-op.
|
|
283
|
+
{ source: "codex", sessionsDir: path.join(codexHome, "archived_sessions"), deep: true },
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
if (sourceAllowed("every-code")) {
|
|
287
|
+
sources.push({ source: "every-code", sessionsDir: path.join(codeHome, "sessions") });
|
|
288
|
+
}
|
|
239
289
|
|
|
240
290
|
const rolloutFiles = [];
|
|
241
291
|
const seenSessions = new Set();
|
|
292
|
+
const codexDayInventoryCache =
|
|
293
|
+
cursors.codexDayInventoryCache && typeof cursors.codexDayInventoryCache === "object"
|
|
294
|
+
? cursors.codexDayInventoryCache
|
|
295
|
+
: { version: 1, days: {} };
|
|
296
|
+
if (sourceAllowed("codex")) cursors.codexDayInventoryCache = codexDayInventoryCache;
|
|
242
297
|
for (const entry of sources) {
|
|
243
298
|
if (seenSessions.has(entry.sessionsDir)) continue;
|
|
244
299
|
seenSessions.add(entry.sessionsDir);
|
|
245
300
|
const files = entry.deep
|
|
246
301
|
? await listRolloutFilesDeep(entry.sessionsDir)
|
|
247
|
-
: await listRolloutFiles(entry.sessionsDir
|
|
302
|
+
: await listRolloutFiles(entry.sessionsDir, entry.codexInventoryCache
|
|
303
|
+
? { dayInventoryCache: codexDayInventoryCache }
|
|
304
|
+
: undefined);
|
|
248
305
|
for (const filePath of files) {
|
|
249
306
|
rolloutFiles.push({ path: filePath, source: entry.source });
|
|
250
307
|
}
|
|
251
308
|
}
|
|
252
309
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
310
|
+
if (isFullSourceScan) {
|
|
311
|
+
await migrateRolloutCumulativeDeltaBuckets({ cursors, queuePath, rolloutFiles });
|
|
312
|
+
await repairCodexRescanInflation({
|
|
313
|
+
cursors,
|
|
314
|
+
queuePath,
|
|
315
|
+
queueStatePath,
|
|
316
|
+
projectQueuePath,
|
|
317
|
+
projectQueueStatePath,
|
|
318
|
+
rolloutFiles,
|
|
319
|
+
});
|
|
320
|
+
await repairDroidDuplicateSessionInflation({ cursors, queuePath, queueStatePath });
|
|
321
|
+
await repairMimoClaudeMislabel({
|
|
322
|
+
cursors,
|
|
323
|
+
queuePath,
|
|
324
|
+
queueStatePath,
|
|
325
|
+
projectQueuePath,
|
|
326
|
+
projectQueueStatePath,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const codexColdSkipEnabled = opts.auto && isFullSourceScan && sourceAllowed("codex");
|
|
331
|
+
const codexColdAuditDue = codexColdSkipEnabled
|
|
332
|
+
? isCodexColdScanAuditDue(cursors)
|
|
333
|
+
: false;
|
|
334
|
+
const codexColdFilter = codexColdSkipEnabled
|
|
335
|
+
? await filterColdCodexRolloutFiles({
|
|
336
|
+
rolloutFiles,
|
|
337
|
+
cursors,
|
|
338
|
+
projectEnabled: true,
|
|
339
|
+
auditDue: codexColdAuditDue,
|
|
340
|
+
})
|
|
341
|
+
: { rolloutFiles, skipped: 0 };
|
|
342
|
+
const rolloutFilesForParse = codexColdFilter.rolloutFiles;
|
|
263
343
|
|
|
264
344
|
const openclawFiles = openclawSignal?.sessionFile
|
|
265
345
|
? [{ path: openclawSignal.sessionFile, source: "openclaw" }]
|
|
@@ -267,14 +347,15 @@ async function cmdSync(argv) {
|
|
|
267
347
|
|
|
268
348
|
if (progress?.enabled) {
|
|
269
349
|
progress.start(
|
|
270
|
-
`Parsing ${renderBar(0)} 0/${formatNumber(
|
|
350
|
+
`Parsing ${renderBar(0)} 0/${formatNumber(rolloutFilesForParse.length)} files | buckets 0`,
|
|
271
351
|
);
|
|
272
352
|
}
|
|
273
353
|
|
|
274
354
|
let parseResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
355
|
+
let codexParseSucceeded = false;
|
|
275
356
|
try {
|
|
276
357
|
parseResult = await parseRolloutIncremental({
|
|
277
|
-
rolloutFiles,
|
|
358
|
+
rolloutFiles: rolloutFilesForParse,
|
|
278
359
|
cursors,
|
|
279
360
|
queuePath,
|
|
280
361
|
projectQueuePath,
|
|
@@ -288,12 +369,19 @@ async function cmdSync(argv) {
|
|
|
288
369
|
);
|
|
289
370
|
},
|
|
290
371
|
});
|
|
372
|
+
codexParseSucceeded = true;
|
|
291
373
|
} catch (err) {
|
|
292
374
|
warnProviderParseFailure("Codex", err, opts);
|
|
293
375
|
}
|
|
376
|
+
if (codexColdSkipEnabled && codexParseSucceeded) {
|
|
377
|
+
recordCodexColdScanAudit(cursors, {
|
|
378
|
+
fullAudit: codexColdAuditDue,
|
|
379
|
+
skipped: codexColdFilter.skipped,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
294
382
|
|
|
295
383
|
let openclawResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
296
|
-
if (openclawFiles.length > 0) {
|
|
384
|
+
if (sourceAllowed("openclaw") && openclawFiles.length > 0) {
|
|
297
385
|
// Only runs when explicitly triggered by the OpenClaw session plugin.
|
|
298
386
|
try {
|
|
299
387
|
openclawResult = await parseOpenclawIncremental({
|
|
@@ -309,30 +397,34 @@ async function cmdSync(argv) {
|
|
|
309
397
|
}
|
|
310
398
|
|
|
311
399
|
let openclawFallback = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
400
|
+
if (sourceAllowed("openclaw")) {
|
|
401
|
+
try {
|
|
402
|
+
openclawFallback = await applyOpenclawTotalsFallback({
|
|
403
|
+
trackerDir,
|
|
404
|
+
signal: openclawSignal,
|
|
405
|
+
cursors,
|
|
406
|
+
queuePath,
|
|
407
|
+
projectQueuePath,
|
|
408
|
+
});
|
|
409
|
+
} catch (err) {
|
|
410
|
+
warnProviderParseFailure("OpenClaw", err, opts);
|
|
411
|
+
}
|
|
322
412
|
}
|
|
323
413
|
openclawResult.filesProcessed += openclawFallback.filesProcessed;
|
|
324
414
|
openclawResult.eventsAggregated += openclawFallback.eventsAggregated;
|
|
325
415
|
openclawResult.bucketsQueued += openclawFallback.bucketsQueued;
|
|
326
416
|
|
|
327
|
-
const claudeFiles = await listClaudeProjectFiles(claudeProjectsDir);
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
417
|
+
const claudeFiles = sourceAllowed("claude") ? await listClaudeProjectFiles(claudeProjectsDir) : [];
|
|
418
|
+
if (isFullSourceScan) {
|
|
419
|
+
await reincludeClaudeMemObserverFiles({ cursors, claudeFiles, queuePath, queueStatePath });
|
|
420
|
+
await repairClaudeQueueFromGroundTruth({
|
|
421
|
+
cursors,
|
|
422
|
+
queuePath,
|
|
423
|
+
queueStatePath,
|
|
424
|
+
projectQueuePath,
|
|
425
|
+
projectQueueStatePath,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
336
428
|
let claudeResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
337
429
|
if (claudeFiles.length > 0) {
|
|
338
430
|
if (progress?.enabled) {
|
|
@@ -362,7 +454,7 @@ async function cmdSync(argv) {
|
|
|
362
454
|
}
|
|
363
455
|
}
|
|
364
456
|
|
|
365
|
-
const geminiFiles = await listGeminiSessionFiles(geminiTmpDir);
|
|
457
|
+
const geminiFiles = sourceAllowed("gemini") ? await listGeminiSessionFiles(geminiTmpDir) : [];
|
|
366
458
|
let geminiResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
367
459
|
if (geminiFiles.length > 0) {
|
|
368
460
|
if (progress?.enabled) {
|
|
@@ -392,7 +484,7 @@ async function cmdSync(argv) {
|
|
|
392
484
|
}
|
|
393
485
|
}
|
|
394
486
|
|
|
395
|
-
const antigravityFiles = await listAntigravityTranscripts(geminiHome);
|
|
487
|
+
const antigravityFiles = sourceAllowed("antigravity") ? await listAntigravityTranscripts(geminiHome) : [];
|
|
396
488
|
let antigravityResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
397
489
|
if (antigravityFiles.length > 0) {
|
|
398
490
|
if (progress?.enabled) {
|
|
@@ -422,7 +514,7 @@ async function cmdSync(argv) {
|
|
|
422
514
|
}
|
|
423
515
|
}
|
|
424
516
|
|
|
425
|
-
const opencodeFiles = await listOpencodeMessageFiles(opencodeStorageDir);
|
|
517
|
+
const opencodeFiles = sourceAllowed("opencode") ? await listOpencodeMessageFiles(opencodeStorageDir) : [];
|
|
426
518
|
let opencodeResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
427
519
|
if (opencodeFiles.length > 0) {
|
|
428
520
|
if (progress?.enabled) {
|
|
@@ -455,7 +547,7 @@ async function cmdSync(argv) {
|
|
|
455
547
|
// OpenCode v1.2+ stores messages in SQLite (opencode.db) instead of JSON files.
|
|
456
548
|
const opencodeDbPath = path.join(opencodeHome, "opencode.db");
|
|
457
549
|
let opencodeDbResult = { messagesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
458
|
-
const dbMessages = readOpencodeDbMessages(opencodeDbPath);
|
|
550
|
+
const dbMessages = sourceAllowed("opencode") ? readOpencodeDbMessages(opencodeDbPath) : [];
|
|
459
551
|
if (dbMessages.length > 0) {
|
|
460
552
|
if (progress?.enabled) {
|
|
461
553
|
progress.start(
|
|
@@ -493,7 +585,7 @@ async function cmdSync(argv) {
|
|
|
493
585
|
// the message indexes don't collide.
|
|
494
586
|
const kiloDbPath = path.join(kiloHome, "kilo.db");
|
|
495
587
|
let kiloResult = { messagesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
496
|
-
const kiloDbMessages = readOpencodeDbMessages(kiloDbPath);
|
|
588
|
+
const kiloDbMessages = sourceAllowed("kilo-cli") ? readOpencodeDbMessages(kiloDbPath) : [];
|
|
497
589
|
if (kiloDbMessages.length > 0) {
|
|
498
590
|
if (progress?.enabled) {
|
|
499
591
|
progress.start(
|
|
@@ -533,7 +625,7 @@ async function cmdSync(argv) {
|
|
|
533
625
|
// double-count and mislabel Claude usage as mimo.
|
|
534
626
|
const mimoDbPath = path.join(mimoHome, "mimocode.db");
|
|
535
627
|
let mimoResult = { messagesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
536
|
-
const mimoDbMessages = readMimoDbMessages(mimoDbPath);
|
|
628
|
+
const mimoDbMessages = sourceAllowed("mimo") ? readMimoDbMessages(mimoDbPath) : [];
|
|
537
629
|
if (mimoDbMessages.length > 0) {
|
|
538
630
|
if (progress?.enabled) {
|
|
539
631
|
progress.start(
|
|
@@ -574,7 +666,7 @@ async function cmdSync(argv) {
|
|
|
574
666
|
// would double-count.
|
|
575
667
|
const zcodeDbPath = path.join(zcodeHome, "cli", "db", "db.sqlite");
|
|
576
668
|
let zcodeResult = { messagesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
577
|
-
const zcodeDbMessages = readZcodeDbMessages(zcodeDbPath);
|
|
669
|
+
const zcodeDbMessages = sourceAllowed("zcode") ? readZcodeDbMessages(zcodeDbPath) : [];
|
|
578
670
|
if (zcodeDbMessages.length > 0) {
|
|
579
671
|
if (progress?.enabled) {
|
|
580
672
|
progress.start(
|
|
@@ -605,7 +697,7 @@ async function cmdSync(argv) {
|
|
|
605
697
|
}
|
|
606
698
|
|
|
607
699
|
// ── Kilo Code VS Code extension (Cline-style ui_messages.json) ──
|
|
608
|
-
const kilocodeTaskFiles = resolveKilocodeTaskFiles(process.env);
|
|
700
|
+
const kilocodeTaskFiles = sourceAllowed("kilocode") ? resolveKilocodeTaskFiles(process.env) : [];
|
|
609
701
|
let kilocodeResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
610
702
|
if (kilocodeTaskFiles.length > 0) {
|
|
611
703
|
if (progress?.enabled) {
|
|
@@ -636,7 +728,7 @@ async function cmdSync(argv) {
|
|
|
636
728
|
// ── Goose (Block) — SQLite sessions with cumulative tokens per session ──
|
|
637
729
|
const gooseDbPath = resolveGooseDbPath(process.env);
|
|
638
730
|
let gooseResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
639
|
-
if (fssync.existsSync(gooseDbPath)) {
|
|
731
|
+
if (sourceAllowed("goose") && gooseDbPath && fssync.existsSync(gooseDbPath)) {
|
|
640
732
|
if (progress?.enabled) {
|
|
641
733
|
progress.start(`Parsing Goose ${renderBar(0)} 0 sessions | buckets 0`);
|
|
642
734
|
}
|
|
@@ -661,7 +753,7 @@ async function cmdSync(argv) {
|
|
|
661
753
|
}
|
|
662
754
|
|
|
663
755
|
// ── Droid (Factory CLI) — passive reader for ~/.factory/sessions/*.settings.json ──
|
|
664
|
-
const droidSettingsFiles = listDroidSettingsFiles(process.env);
|
|
756
|
+
const droidSettingsFiles = sourceAllowed("droid") ? listDroidSettingsFiles(process.env) : [];
|
|
665
757
|
let droidResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
666
758
|
if (droidSettingsFiles.length > 0) {
|
|
667
759
|
if (progress?.enabled) {
|
|
@@ -696,7 +788,7 @@ async function cmdSync(argv) {
|
|
|
696
788
|
// ── Zed Agent (all providers; cumulative-delta over SQLite threads) ──
|
|
697
789
|
const zedDbPath = resolveZedDbPath(process.env);
|
|
698
790
|
let zedResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
699
|
-
if (fssync.existsSync(zedDbPath)) {
|
|
791
|
+
if (sourceAllowed("zed") && zedDbPath && fssync.existsSync(zedDbPath)) {
|
|
700
792
|
if (progress?.enabled) {
|
|
701
793
|
progress.start(`Parsing Zed Agent ${renderBar(0)} 0 threads | buckets 0`);
|
|
702
794
|
}
|
|
@@ -721,7 +813,7 @@ async function cmdSync(argv) {
|
|
|
721
813
|
}
|
|
722
814
|
|
|
723
815
|
// ── Roo Code VS Code extension (Cline-derived; rooveterinaryinc.roo-cline) ──
|
|
724
|
-
const roocodeTaskFiles = resolveRoocodeTaskFiles(process.env);
|
|
816
|
+
const roocodeTaskFiles = sourceAllowed("roocode") ? resolveRoocodeTaskFiles(process.env) : [];
|
|
725
817
|
let roocodeResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
726
818
|
if (roocodeTaskFiles.length > 0) {
|
|
727
819
|
if (progress?.enabled) {
|
|
@@ -755,10 +847,12 @@ async function cmdSync(argv) {
|
|
|
755
847
|
// cursor records under model="unknown". Purge those local buckets, emit
|
|
756
848
|
// zero retractions so the cloud upserts overwrite them to zero, and reset
|
|
757
849
|
// the incremental cursor so the fixed parser re-fetches all affected rows.
|
|
758
|
-
|
|
850
|
+
if (isFullSourceScan) {
|
|
851
|
+
await migrateCursorUnknownBuckets({ cursors, queuePath });
|
|
852
|
+
}
|
|
759
853
|
|
|
760
854
|
let cursorResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
761
|
-
if (isCursorInstalled({ home })) {
|
|
855
|
+
if (sourceAllowed("cursor") && isCursorInstalled({ home })) {
|
|
762
856
|
const cursorAuth = extractCursorSessionToken({ home });
|
|
763
857
|
if (cursorAuth) {
|
|
764
858
|
try {
|
|
@@ -801,7 +895,7 @@ async function cmdSync(argv) {
|
|
|
801
895
|
let kiroResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
802
896
|
const kiroDbPath = resolveKiroDbPath();
|
|
803
897
|
const kiroJsonlPath = resolveKiroJsonlPath();
|
|
804
|
-
if (fssync.existsSync(kiroDbPath) || fssync.existsSync(kiroJsonlPath)) {
|
|
898
|
+
if (sourceAllowed("kiro") && (fssync.existsSync(kiroDbPath) || fssync.existsSync(kiroJsonlPath))) {
|
|
805
899
|
if (progress?.enabled) {
|
|
806
900
|
progress.start(`Parsing Kiro ${renderBar(0)} | buckets 0`);
|
|
807
901
|
}
|
|
@@ -827,7 +921,7 @@ async function cmdSync(argv) {
|
|
|
827
921
|
// ── Hermes Agent (SQLite-based) ──
|
|
828
922
|
let hermesResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
829
923
|
const hermesPath = resolveHermesPath();
|
|
830
|
-
if (fssync.existsSync(hermesPath)) {
|
|
924
|
+
if (sourceAllowed("hermes") && hermesPath && fssync.existsSync(hermesPath)) {
|
|
831
925
|
if (progress?.enabled) {
|
|
832
926
|
progress.start(`Parsing Hermes ${renderBar(0)} | buckets 0`);
|
|
833
927
|
}
|
|
@@ -858,8 +952,8 @@ async function cmdSync(argv) {
|
|
|
858
952
|
// 4 chars/token from user prompt chars and assistant response chars.
|
|
859
953
|
let kiroCliResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
860
954
|
const kiroCliDb = resolveKiroCliDbPath(process.env);
|
|
861
|
-
const kiroCliSessionFiles = resolveKiroCliSessionFiles(process.env);
|
|
862
|
-
if (fssync.existsSync(kiroCliDb) || kiroCliSessionFiles.length > 0) {
|
|
955
|
+
const kiroCliSessionFiles = sourceAllowed("kiro") ? resolveKiroCliSessionFiles(process.env) : [];
|
|
956
|
+
if (sourceAllowed("kiro") && (fssync.existsSync(kiroCliDb) || kiroCliSessionFiles.length > 0)) {
|
|
863
957
|
if (progress?.enabled) {
|
|
864
958
|
progress.start(`Parsing Kiro CLI ${renderBar(0)} | buckets 0`);
|
|
865
959
|
}
|
|
@@ -885,7 +979,7 @@ async function cmdSync(argv) {
|
|
|
885
979
|
|
|
886
980
|
// ── Kimi (passive wire.jsonl reader) ──
|
|
887
981
|
let kimiResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
888
|
-
const kimiWireFiles = resolveKimiWireFiles(process.env);
|
|
982
|
+
const kimiWireFiles = sourceAllowed("kimi") ? resolveKimiWireFiles(process.env) : [];
|
|
889
983
|
if (kimiWireFiles.length > 0) {
|
|
890
984
|
if (progress?.enabled) {
|
|
891
985
|
progress.start(`Parsing Kimi Code ${renderBar(0)} | buckets 0`);
|
|
@@ -911,7 +1005,7 @@ async function cmdSync(argv) {
|
|
|
911
1005
|
|
|
912
1006
|
// ── Kimi Code official (@moonshot-ai/kimi-code, ~/.kimi-code) ──
|
|
913
1007
|
let kimiCodeResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
914
|
-
const kimiCodeWireFiles = resolveKimiCodeWireFiles(process.env);
|
|
1008
|
+
const kimiCodeWireFiles = sourceAllowed("kimi-code") ? resolveKimiCodeWireFiles(process.env) : [];
|
|
915
1009
|
if (kimiCodeWireFiles.length > 0) {
|
|
916
1010
|
if (progress?.enabled) {
|
|
917
1011
|
progress.start(`Parsing Kimi Code (official) ${renderBar(0)} | buckets 0`);
|
|
@@ -939,7 +1033,7 @@ async function cmdSync(argv) {
|
|
|
939
1033
|
// Tencent's CodeBuddy CLI is a Claude Code clone; no hook system, so we
|
|
940
1034
|
// tail the per-session JSONL conversation logs incrementally on each sync.
|
|
941
1035
|
let codebuddyResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
942
|
-
const codebuddyFiles = resolveCodebuddyProjectFiles(process.env);
|
|
1036
|
+
const codebuddyFiles = sourceAllowed("codebuddy") ? resolveCodebuddyProjectFiles(process.env) : [];
|
|
943
1037
|
if (codebuddyFiles.length > 0) {
|
|
944
1038
|
if (progress?.enabled) {
|
|
945
1039
|
progress.start(`Parsing CodeBuddy ${renderBar(0)} | buckets 0`);
|
|
@@ -969,7 +1063,7 @@ async function cmdSync(argv) {
|
|
|
969
1063
|
// sub-agent logs nest one level deeper, so the resolver recurses. See the
|
|
970
1064
|
// parser comment in rollout.js for the cache-aware token math.
|
|
971
1065
|
let workbuddyResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
972
|
-
const workbuddyFiles = resolveWorkbuddyProjectFiles(process.env);
|
|
1066
|
+
const workbuddyFiles = sourceAllowed("workbuddy") ? resolveWorkbuddyProjectFiles(process.env) : [];
|
|
973
1067
|
if (workbuddyFiles.length > 0) {
|
|
974
1068
|
if (progress?.enabled) {
|
|
975
1069
|
progress.start(`Parsing WorkBuddy ${renderBar(0)} | buckets 0`);
|
|
@@ -995,7 +1089,7 @@ async function cmdSync(argv) {
|
|
|
995
1089
|
|
|
996
1090
|
// ── oh-my-pi (passive ~/.omp/agent/sessions/**/*.jsonl reader) ──
|
|
997
1091
|
let ompResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
998
|
-
const ompFiles = resolveOmpSessionFiles(process.env);
|
|
1092
|
+
const ompFiles = sourceAllowed("omp") ? resolveOmpSessionFiles(process.env) : [];
|
|
999
1093
|
if (ompFiles.length > 0) {
|
|
1000
1094
|
if (progress?.enabled) {
|
|
1001
1095
|
progress.start(`Parsing oh-my-pi ${renderBar(0)} | buckets 0`);
|
|
@@ -1024,7 +1118,7 @@ async function cmdSync(argv) {
|
|
|
1024
1118
|
// prevents double-counting when explicit overrides (TOKENTRACKER_OMP_AGENT_DIR /
|
|
1025
1119
|
// TOKENTRACKER_PI_AGENT_DIR) bypass the install-signal disambiguator.
|
|
1026
1120
|
let piResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
1027
|
-
const piFiles = piAgentDirCollidesWithOmp(process.env)
|
|
1121
|
+
const piFiles = !sourceAllowed("pi") || piAgentDirCollidesWithOmp(process.env)
|
|
1028
1122
|
? []
|
|
1029
1123
|
: resolvePiSessionFiles(process.env);
|
|
1030
1124
|
if (piFiles.length > 0) {
|
|
@@ -1052,7 +1146,7 @@ async function cmdSync(argv) {
|
|
|
1052
1146
|
|
|
1053
1147
|
// ── Craft Agents (passive ~/.craft-agent + workspaces session.jsonl reader) ──
|
|
1054
1148
|
let craftResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
1055
|
-
const craftFiles = resolveCraftSessionFiles(process.env);
|
|
1149
|
+
const craftFiles = sourceAllowed("craft") ? resolveCraftSessionFiles(process.env) : [];
|
|
1056
1150
|
if (craftFiles.length > 0) {
|
|
1057
1151
|
if (progress?.enabled) {
|
|
1058
1152
|
progress.start(`Parsing Craft ${renderBar(0)} | buckets 0`);
|
|
@@ -1079,9 +1173,9 @@ async function cmdSync(argv) {
|
|
|
1079
1173
|
// ── Grok Build (xAI) ──
|
|
1080
1174
|
let grokResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
1081
1175
|
// Full passive scan of all Grok sessions (historical + any not covered by hook)
|
|
1082
|
-
const grokSessions = resolveGrokBuildSessions(process.env);
|
|
1176
|
+
const grokSessions = sourceAllowed("grok") ? resolveGrokBuildSessions(process.env) : [];
|
|
1083
1177
|
const grokSessionInputs = [...grokSessions];
|
|
1084
|
-
if (grokHookSignal && typeof grokHookSignal === "object") {
|
|
1178
|
+
if (sourceAllowed("grok") && grokHookSignal && typeof grokHookSignal === "object") {
|
|
1085
1179
|
const hookSessionId =
|
|
1086
1180
|
typeof grokHookSignal.sessionId === "string" && grokHookSignal.sessionId.trim()
|
|
1087
1181
|
? grokHookSignal.sessionId.trim()
|
|
@@ -1146,13 +1240,13 @@ async function cmdSync(argv) {
|
|
|
1146
1240
|
bucketsQueued: grokResult.bucketsQueued + grokScanResult.bucketsQueued,
|
|
1147
1241
|
};
|
|
1148
1242
|
}
|
|
1149
|
-
if (opts.repairGrok) {
|
|
1243
|
+
if (isFullSourceScan && opts.repairGrok) {
|
|
1150
1244
|
await repairGrokQueueFromSessionSnapshots({ cursors, queuePath, queueStatePath });
|
|
1151
1245
|
}
|
|
1152
1246
|
|
|
1153
1247
|
// ── GitHub Copilot CLI (OTEL JSONL files) ──
|
|
1154
1248
|
let copilotResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
1155
|
-
const copilotPaths = resolveCopilotOtelPaths(process.env);
|
|
1249
|
+
const copilotPaths = sourceAllowed("copilot") ? resolveCopilotOtelPaths(process.env) : [];
|
|
1156
1250
|
if (copilotPaths.length > 0) {
|
|
1157
1251
|
if (progress?.enabled) {
|
|
1158
1252
|
progress.start(`Parsing Copilot ${renderBar(0)} | buckets 0`);
|
|
@@ -1176,7 +1270,7 @@ async function cmdSync(argv) {
|
|
|
1176
1270
|
}
|
|
1177
1271
|
}
|
|
1178
1272
|
|
|
1179
|
-
if (cursors?.projectHourly?.projects && projectQueuePath && projectQueueStatePath) {
|
|
1273
|
+
if (isFullSourceScan && cursors?.projectHourly?.projects && projectQueuePath && projectQueueStatePath) {
|
|
1180
1274
|
for (const [projectKey, meta] of Object.entries(cursors.projectHourly.projects)) {
|
|
1181
1275
|
if (!meta || typeof meta !== "object") continue;
|
|
1182
1276
|
if (meta.status !== "blocked" || !meta.purge_pending) continue;
|
|
@@ -1192,7 +1286,9 @@ async function cmdSync(argv) {
|
|
|
1192
1286
|
}
|
|
1193
1287
|
}
|
|
1194
1288
|
|
|
1195
|
-
|
|
1289
|
+
if (isFullSourceScan) {
|
|
1290
|
+
await applyCloudConversationsBackfill({ cursors, queueStatePath });
|
|
1291
|
+
}
|
|
1196
1292
|
|
|
1197
1293
|
cursors.updatedAt = new Date().toISOString();
|
|
1198
1294
|
await writeJson(cursorsPath, cursors);
|
|
@@ -1253,11 +1349,10 @@ async function cmdSync(argv) {
|
|
|
1253
1349
|
|
|
1254
1350
|
const afterState = (await readJson(queueStatePath)) || { offset: 0 };
|
|
1255
1351
|
const queueSize = await safeStatSize(queuePath);
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
Math.max(0, projectQueueSize - Number(projectAfterState.offset || 0));
|
|
1352
|
+
// Only the main queue is uploaded by drainQueueToCloud. project.queue.jsonl
|
|
1353
|
+
// is local project-usage state, so counting it here creates false backlog
|
|
1354
|
+
// and can keep auto retry alive even after cloud sync has drained.
|
|
1355
|
+
const pendingBytes = Math.max(0, queueSize - Number(afterState.offset || 0));
|
|
1261
1356
|
|
|
1262
1357
|
if (pendingBytes <= 0) {
|
|
1263
1358
|
await clearAutoRetry(trackerDir);
|
|
@@ -1269,7 +1364,8 @@ async function cmdSync(argv) {
|
|
|
1269
1364
|
retryAtMs,
|
|
1270
1365
|
reason: "backlog",
|
|
1271
1366
|
pendingBytes,
|
|
1272
|
-
source: "auto-backlog",
|
|
1367
|
+
source: autoSourceScope ? `${autoSourceScope}-backlog` : "auto-backlog",
|
|
1368
|
+
syncSource: autoSourceScope,
|
|
1273
1369
|
autoRetryNoSpawn: runtime.autoRetryNoSpawn,
|
|
1274
1370
|
});
|
|
1275
1371
|
}
|
|
@@ -1362,6 +1458,7 @@ function parseArgs(argv) {
|
|
|
1362
1458
|
fromNotify: false,
|
|
1363
1459
|
fromRetry: false,
|
|
1364
1460
|
fromOpenclaw: false,
|
|
1461
|
+
source: null,
|
|
1365
1462
|
drain: false,
|
|
1366
1463
|
repairGrok: false,
|
|
1367
1464
|
};
|
|
@@ -1371,6 +1468,11 @@ function parseArgs(argv) {
|
|
|
1371
1468
|
else if (a === "--from-notify") out.fromNotify = true;
|
|
1372
1469
|
else if (a === "--from-retry") out.fromRetry = true;
|
|
1373
1470
|
else if (a === "--from-openclaw") out.fromOpenclaw = true;
|
|
1471
|
+
else if (a === "--source") {
|
|
1472
|
+
out.source = normalizeSyncSource(argv[i + 1]);
|
|
1473
|
+
i += 1;
|
|
1474
|
+
}
|
|
1475
|
+
else if (a.startsWith("--source=")) out.source = normalizeSyncSource(a.slice("--source=".length));
|
|
1374
1476
|
else if (a === "--drain") out.drain = true;
|
|
1375
1477
|
else if (a === "--repair-grok") out.repairGrok = true;
|
|
1376
1478
|
else throw new Error(`Unknown option: ${a}`);
|
|
@@ -1378,6 +1480,70 @@ function parseArgs(argv) {
|
|
|
1378
1480
|
return out;
|
|
1379
1481
|
}
|
|
1380
1482
|
|
|
1483
|
+
function resolveAutoSourceScope(opts) {
|
|
1484
|
+
if (!opts?.auto) return null;
|
|
1485
|
+
if (opts.fromOpenclaw) return "openclaw";
|
|
1486
|
+
if (opts.fromRetry) return normalizeSyncSource(opts.source);
|
|
1487
|
+
if (!opts.fromNotify) return null;
|
|
1488
|
+
return normalizeSyncSource(opts.source);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
function normalizeSyncSource(value) {
|
|
1492
|
+
if (typeof value !== "string") return null;
|
|
1493
|
+
const normalized = value.trim().toLowerCase();
|
|
1494
|
+
if (!normalized) return null;
|
|
1495
|
+
const aliased = AUTO_SYNC_SOURCE_ALIASES.get(normalized) || normalized;
|
|
1496
|
+
return AUTO_SYNC_SOURCES.has(aliased) ? aliased : null;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
function isCodexColdScanAuditDue(cursors, nowMs = Date.now()) {
|
|
1500
|
+
const state = cursors?.codexColdScanAudit;
|
|
1501
|
+
if (!state || typeof state !== "object" || state.version !== 1) return true;
|
|
1502
|
+
const lastFullScanAtMs = Number(state.lastFullScanAtMs);
|
|
1503
|
+
if (!Number.isFinite(lastFullScanAtMs) || lastFullScanAtMs <= 0) return true;
|
|
1504
|
+
if (Number.isFinite(nowMs) && lastFullScanAtMs - nowMs > 5 * 60 * 1000) return true;
|
|
1505
|
+
if (Number.isFinite(nowMs) && nowMs - lastFullScanAtMs >= CODEX_COLD_SCAN_AUDIT_INTERVAL_MS) {
|
|
1506
|
+
return true;
|
|
1507
|
+
}
|
|
1508
|
+
const syncsSinceFullScan = Number(state.syncsSinceFullScan || 0);
|
|
1509
|
+
return (
|
|
1510
|
+
Number.isFinite(syncsSinceFullScan) &&
|
|
1511
|
+
syncsSinceFullScan >= CODEX_COLD_SCAN_AUDIT_MAX_SYNCS
|
|
1512
|
+
);
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
function recordCodexColdScanAudit(cursors, { fullAudit = false, skipped = 0 } = {}, nowMs = Date.now()) {
|
|
1516
|
+
if (!cursors || typeof cursors !== "object") return;
|
|
1517
|
+
const prev =
|
|
1518
|
+
cursors.codexColdScanAudit && typeof cursors.codexColdScanAudit === "object"
|
|
1519
|
+
? cursors.codexColdScanAudit
|
|
1520
|
+
: {};
|
|
1521
|
+
const previousSyncs = Number(prev.syncsSinceFullScan || 0);
|
|
1522
|
+
const lastFullScanAtMs = Number(prev.lastFullScanAtMs);
|
|
1523
|
+
const next = {
|
|
1524
|
+
version: 1,
|
|
1525
|
+
lastFullScanAtMs: Number.isFinite(lastFullScanAtMs) && lastFullScanAtMs > 0
|
|
1526
|
+
? lastFullScanAtMs
|
|
1527
|
+
: nowMs,
|
|
1528
|
+
syncsSinceFullScan: Number.isFinite(previousSyncs) && previousSyncs > 0
|
|
1529
|
+
? previousSyncs
|
|
1530
|
+
: 0,
|
|
1531
|
+
lastSkippedFiles: Math.max(0, Number(skipped) || 0),
|
|
1532
|
+
updatedAt: new Date(nowMs).toISOString(),
|
|
1533
|
+
};
|
|
1534
|
+
if (fullAudit) {
|
|
1535
|
+
next.lastFullScanAtMs = nowMs;
|
|
1536
|
+
next.lastFullScanAt = new Date(nowMs).toISOString();
|
|
1537
|
+
next.syncsSinceFullScan = 0;
|
|
1538
|
+
} else {
|
|
1539
|
+
next.lastFullScanAt = Number.isFinite(next.lastFullScanAtMs)
|
|
1540
|
+
? new Date(next.lastFullScanAtMs).toISOString()
|
|
1541
|
+
: null;
|
|
1542
|
+
next.syncsSinceFullScan += 1;
|
|
1543
|
+
}
|
|
1544
|
+
cursors.codexColdScanAudit = next;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1381
1547
|
module.exports = {
|
|
1382
1548
|
cmdSync,
|
|
1383
1549
|
migrateCursorUnknownBuckets,
|
|
@@ -1388,6 +1554,10 @@ module.exports = {
|
|
|
1388
1554
|
reincludeClaudeMemObserverFiles,
|
|
1389
1555
|
repairGrokQueueFromSessionSnapshots,
|
|
1390
1556
|
applyCloudConversationsBackfill,
|
|
1557
|
+
scheduleAutoRetry,
|
|
1558
|
+
buildAutoRetryScript,
|
|
1559
|
+
isCodexColdScanAuditDue,
|
|
1560
|
+
recordCodexColdScanAudit,
|
|
1391
1561
|
CURSOR_UNKNOWN_MIGRATION_KEY,
|
|
1392
1562
|
ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY,
|
|
1393
1563
|
CODEX_RESCAN_DEDUP_REPAIR_KEY,
|
|
@@ -1569,6 +1739,7 @@ async function scheduleAutoRetry({
|
|
|
1569
1739
|
reason,
|
|
1570
1740
|
pendingBytes,
|
|
1571
1741
|
source,
|
|
1742
|
+
syncSource,
|
|
1572
1743
|
autoRetryNoSpawn,
|
|
1573
1744
|
}) {
|
|
1574
1745
|
const retryMs = coerceRetryMs(retryAtMs);
|
|
@@ -1578,19 +1749,33 @@ async function scheduleAutoRetry({
|
|
|
1578
1749
|
const nowMs = Date.now();
|
|
1579
1750
|
const existing = await readJson(retryPath);
|
|
1580
1751
|
const existingMs = coerceRetryMs(existing?.retryAtMs);
|
|
1752
|
+
const normalizedSyncSource = normalizeSyncSource(syncSource);
|
|
1581
1753
|
if (existingMs && existingMs >= retryMs - 1000) {
|
|
1754
|
+
const existingSyncSource = normalizeSyncSource(existing?.syncSource);
|
|
1755
|
+
if (existingSyncSource !== normalizedSyncSource) {
|
|
1756
|
+
await writeJson(
|
|
1757
|
+
retryPath,
|
|
1758
|
+
buildAutoRetryPayload({
|
|
1759
|
+
retryMs: existingMs,
|
|
1760
|
+
nowMs,
|
|
1761
|
+
reason,
|
|
1762
|
+
pendingBytes,
|
|
1763
|
+
source,
|
|
1764
|
+
syncSource: normalizedSyncSource,
|
|
1765
|
+
}),
|
|
1766
|
+
);
|
|
1767
|
+
}
|
|
1582
1768
|
return { scheduled: false, retryAtMs: existingMs };
|
|
1583
1769
|
}
|
|
1584
1770
|
|
|
1585
|
-
const payload = {
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
};
|
|
1771
|
+
const payload = buildAutoRetryPayload({
|
|
1772
|
+
retryMs,
|
|
1773
|
+
nowMs,
|
|
1774
|
+
reason,
|
|
1775
|
+
pendingBytes,
|
|
1776
|
+
source,
|
|
1777
|
+
syncSource: normalizedSyncSource,
|
|
1778
|
+
});
|
|
1594
1779
|
|
|
1595
1780
|
await writeJson(retryPath, payload);
|
|
1596
1781
|
|
|
@@ -1609,6 +1794,20 @@ async function scheduleAutoRetry({
|
|
|
1609
1794
|
return { scheduled: true, retryAtMs: retryMs };
|
|
1610
1795
|
}
|
|
1611
1796
|
|
|
1797
|
+
function buildAutoRetryPayload({ retryMs, nowMs, reason, pendingBytes, source, syncSource }) {
|
|
1798
|
+
const payload = {
|
|
1799
|
+
version: 1,
|
|
1800
|
+
retryAtMs: retryMs,
|
|
1801
|
+
retryAt: new Date(retryMs).toISOString(),
|
|
1802
|
+
reason: typeof reason === "string" && reason.length > 0 ? reason : "throttled",
|
|
1803
|
+
pendingBytes: Math.max(0, Number(pendingBytes || 0)),
|
|
1804
|
+
scheduledAt: new Date(nowMs).toISOString(),
|
|
1805
|
+
source: typeof source === "string" ? source : "auto",
|
|
1806
|
+
};
|
|
1807
|
+
if (syncSource) payload.syncSource = syncSource;
|
|
1808
|
+
return payload;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1612
1811
|
async function clearAutoRetry(trackerDir) {
|
|
1613
1812
|
const retryPath = path.join(trackerDir, AUTO_RETRY_FILENAME);
|
|
1614
1813
|
await fs.unlink(retryPath).catch(() => {});
|
|
@@ -1636,13 +1835,18 @@ function buildAutoRetryScript({ retryPath, trackerBinPath, fallbackPkg, delayMs
|
|
|
1636
1835
|
`const fallbackPkg = ${JSON.stringify(fallbackPkg)};\n` +
|
|
1637
1836
|
`const delayMs = ${Math.max(0, Math.floor(delayMs || 0))};\n` +
|
|
1638
1837
|
`setTimeout(() => {\n` +
|
|
1838
|
+
` let payload = null;\n` +
|
|
1639
1839
|
` let retryAtMs = 0;\n` +
|
|
1640
1840
|
` try {\n` +
|
|
1641
1841
|
` const raw = fs.readFileSync(retryPath, 'utf8');\n` +
|
|
1642
|
-
`
|
|
1842
|
+
` payload = JSON.parse(raw);\n` +
|
|
1843
|
+
` retryAtMs = Number(payload.retryAtMs || 0);\n` +
|
|
1643
1844
|
` } catch (_) {}\n` +
|
|
1644
1845
|
` if (!retryAtMs || Date.now() + 1000 < retryAtMs) process.exit(0);\n` +
|
|
1645
1846
|
` const argv = ['sync', '--auto', '--from-retry'];\n` +
|
|
1847
|
+
` if (payload && typeof payload.syncSource === 'string' && payload.syncSource.trim()) {\n` +
|
|
1848
|
+
` argv.push('--source', payload.syncSource.trim());\n` +
|
|
1849
|
+
` }\n` +
|
|
1646
1850
|
` const cmd = fs.existsSync(trackerBinPath)\n` +
|
|
1647
1851
|
` ? [process.execPath, trackerBinPath, ...argv]\n` +
|
|
1648
1852
|
` : ['npx', '--yes', fallbackPkg, ...argv];\n` +
|
|
@@ -2398,7 +2602,14 @@ async function migrateRolloutCumulativeDeltaBuckets({ cursors, queuePath, rollou
|
|
|
2398
2602
|
// contributed is gone from disk (genuinely deleted — Codex-Manager log rotation
|
|
2399
2603
|
// or user cleanup) the migration is skipped entirely — the forward dedup fix
|
|
2400
2604
|
// still prevents new double-counting; only this historical correction is deferred.
|
|
2401
|
-
async function repairCodexRescanInflation({
|
|
2605
|
+
async function repairCodexRescanInflation({
|
|
2606
|
+
cursors,
|
|
2607
|
+
queuePath,
|
|
2608
|
+
queueStatePath,
|
|
2609
|
+
projectQueuePath,
|
|
2610
|
+
projectQueueStatePath,
|
|
2611
|
+
rolloutFiles,
|
|
2612
|
+
}) {
|
|
2402
2613
|
if (!cursors || typeof cursors !== "object") return false;
|
|
2403
2614
|
const migrations = (cursors.migrations ||= {});
|
|
2404
2615
|
// A COMPLETED run writes an ISO-string timestamp (final — never re-run). A
|
|
@@ -2419,6 +2630,7 @@ async function repairCodexRescanInflation({ cursors, queuePath, queueStatePath,
|
|
|
2419
2630
|
if (fp && src === "codex") codexFiles.push(fp);
|
|
2420
2631
|
}
|
|
2421
2632
|
const codexFileSet = new Set(codexFiles);
|
|
2633
|
+
const projectRepairEnabled = typeof projectQueuePath === "string" && projectQueuePath.length > 0;
|
|
2422
2634
|
|
|
2423
2635
|
// GUARD (data-loss prevention, ref the v6 ground-truth-repair incident): the
|
|
2424
2636
|
// rebuild can only reproduce buckets from the files this sync re-parses
|
|
@@ -2439,7 +2651,7 @@ async function repairCodexRescanInflation({ cursors, queuePath, queueStatePath,
|
|
|
2439
2651
|
}
|
|
2440
2652
|
if (cursors.files && typeof cursors.files === "object") {
|
|
2441
2653
|
for (const fp of Object.keys(cursors.files)) {
|
|
2442
|
-
if (
|
|
2654
|
+
if (!isCodexSessionCursorPath(fp)) continue;
|
|
2443
2655
|
if (codexFileSet.has(fp)) continue; // exact file re-scanned this run
|
|
2444
2656
|
const id = codexSessionIdFromPath(fp);
|
|
2445
2657
|
if (id && scannedSessionIds.has(id)) continue; // same session scanned elsewhere (moved)
|
|
@@ -2461,6 +2673,9 @@ async function repairCodexRescanInflation({ cursors, queuePath, queueStatePath,
|
|
|
2461
2673
|
// would permanently zero a user's codex history.
|
|
2462
2674
|
let rebuilt;
|
|
2463
2675
|
const tmpQueue = `${queuePath}.codexrebuild.${process.pid}.${Date.now()}`;
|
|
2676
|
+
const tmpProjectQueue = projectRepairEnabled
|
|
2677
|
+
? `${projectQueuePath}.codexrebuild.${process.pid}.${Date.now()}`
|
|
2678
|
+
: null;
|
|
2464
2679
|
try {
|
|
2465
2680
|
const tmpCursors = {
|
|
2466
2681
|
version: 1,
|
|
@@ -2472,6 +2687,7 @@ async function repairCodexRescanInflation({ cursors, queuePath, queueStatePath,
|
|
|
2472
2687
|
rolloutFiles: codexFiles.map((p) => ({ path: p, source: "codex" })),
|
|
2473
2688
|
cursors: tmpCursors,
|
|
2474
2689
|
queuePath: tmpQueue,
|
|
2690
|
+
projectQueuePath: tmpProjectQueue,
|
|
2475
2691
|
});
|
|
2476
2692
|
let tmpRaw = "";
|
|
2477
2693
|
try {
|
|
@@ -2479,12 +2695,22 @@ async function repairCodexRescanInflation({ cursors, queuePath, queueStatePath,
|
|
|
2479
2695
|
} catch (e) {
|
|
2480
2696
|
if (e?.code !== "ENOENT") throw e;
|
|
2481
2697
|
}
|
|
2698
|
+
let tmpProjectRaw = "";
|
|
2699
|
+
if (tmpProjectQueue) {
|
|
2700
|
+
try {
|
|
2701
|
+
tmpProjectRaw = await fs.readFile(tmpProjectQueue, "utf8");
|
|
2702
|
+
} catch (e) {
|
|
2703
|
+
if (e?.code !== "ENOENT") throw e;
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2482
2706
|
rebuilt = {
|
|
2483
2707
|
buckets: tmpCursors.hourly.buckets || {},
|
|
2484
2708
|
groupQueued: tmpCursors.hourly.groupQueued || {},
|
|
2485
2709
|
codexHashes: Array.isArray(tmpCursors.codexHashes) ? tmpCursors.codexHashes : [],
|
|
2486
2710
|
files: tmpCursors.files || {},
|
|
2487
2711
|
queueRows: tmpRaw.split("\n").filter((l) => l.trim()),
|
|
2712
|
+
projectHourly: tmpCursors.projectHourly || null,
|
|
2713
|
+
projectQueueRows: tmpProjectRaw.split("\n").filter((l) => l.trim()),
|
|
2488
2714
|
};
|
|
2489
2715
|
} catch (e) {
|
|
2490
2716
|
console.error(
|
|
@@ -2494,6 +2720,7 @@ async function repairCodexRescanInflation({ cursors, queuePath, queueStatePath,
|
|
|
2494
2720
|
return false;
|
|
2495
2721
|
} finally {
|
|
2496
2722
|
await fs.rm(tmpQueue, { force: true }).catch(() => {});
|
|
2723
|
+
if (tmpProjectQueue) await fs.rm(tmpProjectQueue, { force: true }).catch(() => {});
|
|
2497
2724
|
}
|
|
2498
2725
|
|
|
2499
2726
|
// SANITY: codex files exist on disk but the rebuild produced no codex buckets
|
|
@@ -2506,6 +2733,64 @@ async function repairCodexRescanInflation({ cursors, queuePath, queueStatePath,
|
|
|
2506
2733
|
);
|
|
2507
2734
|
return false;
|
|
2508
2735
|
}
|
|
2736
|
+
if (projectRepairEnabled) {
|
|
2737
|
+
const malformedProjectRows = await countMalformedCodexProjectQueueRows(projectQueuePath);
|
|
2738
|
+
if (malformedProjectRows > 0) {
|
|
2739
|
+
console.error(
|
|
2740
|
+
`[sync] codex rescan repair: found ${malformedProjectRows} malformed codex project queue row(s) — skipping to avoid data loss`,
|
|
2741
|
+
);
|
|
2742
|
+
return false;
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
const existingProjectKeys = new Set([
|
|
2746
|
+
...(await projectUsageKeysFromQueuePath(projectQueuePath, "codex")),
|
|
2747
|
+
...projectUsageKeysFromState(cursors.projectHourly, "codex"),
|
|
2748
|
+
]);
|
|
2749
|
+
const rebuiltProjectKeys = new Set([
|
|
2750
|
+
...projectUsageKeysFromQueueRows(rebuilt.projectQueueRows, "codex"),
|
|
2751
|
+
...projectUsageKeysFromState(rebuilt.projectHourly, "codex"),
|
|
2752
|
+
]);
|
|
2753
|
+
const missingProjectKeys = [...existingProjectKeys].filter((key) => !rebuiltProjectKeys.has(key));
|
|
2754
|
+
if (missingProjectKeys.length > 0) {
|
|
2755
|
+
console.error(
|
|
2756
|
+
`[sync] codex rescan repair: project rebuild missed ${missingProjectKeys.length} existing codex project bucket(s) — skipping to avoid data loss`,
|
|
2757
|
+
);
|
|
2758
|
+
return false;
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
const existingProjectTotals = mergeMaxTotals(
|
|
2762
|
+
await projectUsageTotalsFromQueuePath(projectQueuePath, "codex"),
|
|
2763
|
+
projectUsageTotalsFromState(cursors.projectHourly, "codex"),
|
|
2764
|
+
);
|
|
2765
|
+
const rebuiltProjectTotals = mergeMaxTotals(
|
|
2766
|
+
projectUsageTotalsFromQueueRows(rebuilt.projectQueueRows, "codex"),
|
|
2767
|
+
projectUsageTotalsFromState(rebuilt.projectHourly, "codex"),
|
|
2768
|
+
);
|
|
2769
|
+
const existingMainHourTotals = mergeMaxTotals(
|
|
2770
|
+
await mainUsageHourTotalsFromQueuePath(queuePath, "codex"),
|
|
2771
|
+
mainUsageHourTotalsFromState(cursors.hourly, "codex"),
|
|
2772
|
+
);
|
|
2773
|
+
const rebuiltMainHourTotals = mergeMaxTotals(
|
|
2774
|
+
mainUsageHourTotalsFromQueueRows(rebuilt.queueRows, "codex"),
|
|
2775
|
+
mainUsageHourTotalsFromState({ buckets: rebuilt.buckets }, "codex"),
|
|
2776
|
+
);
|
|
2777
|
+
const partialProjectKeys = [];
|
|
2778
|
+
for (const [key, existingTotal] of existingProjectTotals.entries()) {
|
|
2779
|
+
const rebuiltTotal = rebuiltProjectTotals.get(key);
|
|
2780
|
+
if (!Number.isFinite(rebuiltTotal) || rebuiltTotal >= existingTotal) continue;
|
|
2781
|
+
const [, source, hourStart] = key.split("|");
|
|
2782
|
+
const mainKey = `${source}|${hourStart}`;
|
|
2783
|
+
const existingMainTotal = existingMainHourTotals.get(mainKey) || 0;
|
|
2784
|
+
const rebuiltMainTotal = rebuiltMainHourTotals.get(mainKey) || 0;
|
|
2785
|
+
if (rebuiltMainTotal >= existingMainTotal) partialProjectKeys.push(key);
|
|
2786
|
+
}
|
|
2787
|
+
if (partialProjectKeys.length > 0) {
|
|
2788
|
+
console.error(
|
|
2789
|
+
`[sync] codex rescan repair: project rebuild lowered ${partialProjectKeys.length} existing codex project bucket(s) without a matching main-bucket repair — skipping to avoid data loss`,
|
|
2790
|
+
);
|
|
2791
|
+
return false;
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2509
2794
|
|
|
2510
2795
|
// COMMIT (only after a verified rebuild). A crash partway just leaves the
|
|
2511
2796
|
// migration to re-run next sync — re-rebuild + re-strip + re-commit converges.
|
|
@@ -2560,14 +2845,65 @@ async function repairCodexRescanInflation({ cursors, queuePath, queueStatePath,
|
|
|
2560
2845
|
}
|
|
2561
2846
|
cursors.files ||= {};
|
|
2562
2847
|
for (const fp of Object.keys(cursors.files)) {
|
|
2563
|
-
if (
|
|
2848
|
+
if (isCodexSessionCursorPath(fp)) delete cursors.files[fp];
|
|
2564
2849
|
}
|
|
2565
2850
|
for (const [fp, v] of Object.entries(rebuilt.files)) {
|
|
2566
2851
|
cursors.files[fp] = v;
|
|
2567
2852
|
}
|
|
2568
2853
|
cursors.codexHashes = rebuilt.codexHashes;
|
|
2569
2854
|
|
|
2570
|
-
// 3.
|
|
2855
|
+
// 3. Project usage mirrors the main Codex repair: drop inflated Codex project
|
|
2856
|
+
// rows, append the rebuilt rows, and swap only Codex project buckets. Project
|
|
2857
|
+
// metadata is merged so visibility/purge state from non-Codex sources stays.
|
|
2858
|
+
if (projectRepairEnabled) {
|
|
2859
|
+
let projectRaw = "";
|
|
2860
|
+
try {
|
|
2861
|
+
projectRaw = await fs.readFile(projectQueuePath, "utf8");
|
|
2862
|
+
} catch (e) {
|
|
2863
|
+
if (e?.code !== "ENOENT") throw e;
|
|
2864
|
+
}
|
|
2865
|
+
const keptProjectRows = [];
|
|
2866
|
+
for (const line of projectRaw.split("\n")) {
|
|
2867
|
+
if (!line.trim()) continue;
|
|
2868
|
+
let row;
|
|
2869
|
+
try {
|
|
2870
|
+
row = JSON.parse(line);
|
|
2871
|
+
} catch (_e) {
|
|
2872
|
+
keptProjectRows.push(line);
|
|
2873
|
+
continue;
|
|
2874
|
+
}
|
|
2875
|
+
if (row?.source === "codex") continue;
|
|
2876
|
+
keptProjectRows.push(line);
|
|
2877
|
+
}
|
|
2878
|
+
await ensureDir(path.dirname(projectQueuePath));
|
|
2879
|
+
const tmp = `${projectQueuePath}.tmp.${process.pid}.${Date.now()}`;
|
|
2880
|
+
await fs.writeFile(
|
|
2881
|
+
tmp,
|
|
2882
|
+
keptProjectRows.concat(rebuilt.projectQueueRows).join("\n") + "\n",
|
|
2883
|
+
"utf8",
|
|
2884
|
+
);
|
|
2885
|
+
await fs.rename(tmp, projectQueuePath);
|
|
2886
|
+
|
|
2887
|
+
const projectHourly = (cursors.projectHourly ||= { version: 2, buckets: {}, projects: {} });
|
|
2888
|
+
projectHourly.version = 2;
|
|
2889
|
+
projectHourly.buckets ||= {};
|
|
2890
|
+
projectHourly.projects ||= {};
|
|
2891
|
+
for (const [key, bucket] of Object.entries(projectHourly.buckets)) {
|
|
2892
|
+
const source = typeof bucket?.source === "string" ? bucket.source : key.split("|")[1];
|
|
2893
|
+
if (source === "codex") delete projectHourly.buckets[key];
|
|
2894
|
+
}
|
|
2895
|
+
const rebuiltProjectHourly = rebuilt.projectHourly || {};
|
|
2896
|
+
for (const [key, bucket] of Object.entries(rebuiltProjectHourly.buckets || {})) {
|
|
2897
|
+
const source = typeof bucket?.source === "string" ? bucket.source : key.split("|")[1];
|
|
2898
|
+
if (source === "codex") projectHourly.buckets[key] = bucket;
|
|
2899
|
+
}
|
|
2900
|
+
for (const [key, meta] of Object.entries(rebuiltProjectHourly.projects || {})) {
|
|
2901
|
+
if (meta && typeof meta === "object") projectHourly.projects[key] = meta;
|
|
2902
|
+
}
|
|
2903
|
+
projectHourly.updatedAt = new Date().toISOString();
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
// 4. Reset the cloud upload offset so the corrected queue re-uploads. Other
|
|
2571
2907
|
// sources re-upsert idempotently (last emission per key wins).
|
|
2572
2908
|
if (typeof queueStatePath === "string" && queueStatePath) {
|
|
2573
2909
|
let uploadState = {};
|
|
@@ -2581,11 +2917,249 @@ async function repairCodexRescanInflation({ cursors, queuePath, queueStatePath,
|
|
|
2581
2917
|
uploadState.note = "reset_after_codex_rescan_dedup_2026_06";
|
|
2582
2918
|
await fs.writeFile(queueStatePath, JSON.stringify(uploadState));
|
|
2583
2919
|
}
|
|
2920
|
+
if (projectRepairEnabled && typeof projectQueueStatePath === "string" && projectQueueStatePath) {
|
|
2921
|
+
let uploadState = {};
|
|
2922
|
+
try {
|
|
2923
|
+
uploadState = JSON.parse(await fs.readFile(projectQueueStatePath, "utf8"));
|
|
2924
|
+
} catch (_e) {
|
|
2925
|
+
uploadState = {};
|
|
2926
|
+
}
|
|
2927
|
+
uploadState.offset = 0;
|
|
2928
|
+
uploadState.updatedAt = new Date().toISOString();
|
|
2929
|
+
uploadState.note = "reset_after_codex_rescan_dedup_2026_06";
|
|
2930
|
+
await fs.writeFile(projectQueueStatePath, JSON.stringify(uploadState));
|
|
2931
|
+
}
|
|
2584
2932
|
|
|
2585
2933
|
migrations[CODEX_RESCAN_DEDUP_REPAIR_KEY] = new Date().toISOString();
|
|
2586
2934
|
return true;
|
|
2587
2935
|
}
|
|
2588
2936
|
|
|
2937
|
+
function isCodexSessionCursorPath(filePath) {
|
|
2938
|
+
if (typeof filePath !== "string") return false;
|
|
2939
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
2940
|
+
return /\/\.codex\/(?:archived_)?sessions\//.test(normalized);
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
async function projectUsageKeysFromQueuePath(queuePath, source) {
|
|
2944
|
+
if (typeof queuePath !== "string" || !queuePath) return [];
|
|
2945
|
+
let raw = "";
|
|
2946
|
+
try {
|
|
2947
|
+
raw = await fs.readFile(queuePath, "utf8");
|
|
2948
|
+
} catch (e) {
|
|
2949
|
+
if (e?.code !== "ENOENT") throw e;
|
|
2950
|
+
return [];
|
|
2951
|
+
}
|
|
2952
|
+
return projectUsageKeysFromQueueRows(raw.split("\n").filter((line) => line.trim()), source);
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
function projectUsageKeysFromQueueRows(rows, source) {
|
|
2956
|
+
const keys = [];
|
|
2957
|
+
for (const line of Array.isArray(rows) ? rows : []) {
|
|
2958
|
+
let row;
|
|
2959
|
+
try {
|
|
2960
|
+
row = JSON.parse(line);
|
|
2961
|
+
} catch (_e) {
|
|
2962
|
+
continue;
|
|
2963
|
+
}
|
|
2964
|
+
const key = projectUsageKeyFromFields({
|
|
2965
|
+
projectKey: row?.project_key,
|
|
2966
|
+
source: row?.source,
|
|
2967
|
+
hourStart: row?.hour_start,
|
|
2968
|
+
});
|
|
2969
|
+
if (key && row?.source === source) keys.push(key);
|
|
2970
|
+
}
|
|
2971
|
+
return keys;
|
|
2972
|
+
}
|
|
2973
|
+
|
|
2974
|
+
function projectUsageKeysFromState(projectState, source) {
|
|
2975
|
+
const buckets =
|
|
2976
|
+
projectState && typeof projectState === "object" && projectState.buckets
|
|
2977
|
+
? projectState.buckets
|
|
2978
|
+
: {};
|
|
2979
|
+
const keys = [];
|
|
2980
|
+
for (const [key, bucket] of Object.entries(buckets)) {
|
|
2981
|
+
const bucketSource = typeof bucket?.source === "string" ? bucket.source : key.split("|")[1];
|
|
2982
|
+
if (bucketSource !== source) continue;
|
|
2983
|
+
const usageKey =
|
|
2984
|
+
projectUsageKeyFromFields({
|
|
2985
|
+
projectKey: bucket?.project_key,
|
|
2986
|
+
source: bucketSource,
|
|
2987
|
+
hourStart: bucket?.hour_start,
|
|
2988
|
+
}) || key;
|
|
2989
|
+
keys.push(usageKey);
|
|
2990
|
+
}
|
|
2991
|
+
return keys;
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
function projectUsageKeyFromFields({ projectKey, source, hourStart }) {
|
|
2995
|
+
if (
|
|
2996
|
+
typeof projectKey !== "string" ||
|
|
2997
|
+
typeof source !== "string" ||
|
|
2998
|
+
typeof hourStart !== "string" ||
|
|
2999
|
+
!projectKey ||
|
|
3000
|
+
!source ||
|
|
3001
|
+
!hourStart
|
|
3002
|
+
) {
|
|
3003
|
+
return null;
|
|
3004
|
+
}
|
|
3005
|
+
return `${projectKey}|${source}|${hourStart}`;
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
async function countMalformedCodexProjectQueueRows(queuePath) {
|
|
3009
|
+
if (typeof queuePath !== "string" || !queuePath) return 0;
|
|
3010
|
+
let raw = "";
|
|
3011
|
+
try {
|
|
3012
|
+
raw = await fs.readFile(queuePath, "utf8");
|
|
3013
|
+
} catch (e) {
|
|
3014
|
+
if (e?.code !== "ENOENT") throw e;
|
|
3015
|
+
return 0;
|
|
3016
|
+
}
|
|
3017
|
+
let count = 0;
|
|
3018
|
+
for (const line of raw.split("\n")) {
|
|
3019
|
+
if (!line.trim()) continue;
|
|
3020
|
+
let row;
|
|
3021
|
+
try {
|
|
3022
|
+
row = JSON.parse(line);
|
|
3023
|
+
} catch (_e) {
|
|
3024
|
+
continue;
|
|
3025
|
+
}
|
|
3026
|
+
if (row?.source !== "codex") continue;
|
|
3027
|
+
const key = projectUsageKeyFromFields({
|
|
3028
|
+
projectKey: row?.project_key,
|
|
3029
|
+
source: row?.source,
|
|
3030
|
+
hourStart: row?.hour_start,
|
|
3031
|
+
});
|
|
3032
|
+
if (!key) count += 1;
|
|
3033
|
+
}
|
|
3034
|
+
return count;
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
async function projectUsageTotalsFromQueuePath(queuePath, source) {
|
|
3038
|
+
if (typeof queuePath !== "string" || !queuePath) return new Map();
|
|
3039
|
+
let raw = "";
|
|
3040
|
+
try {
|
|
3041
|
+
raw = await fs.readFile(queuePath, "utf8");
|
|
3042
|
+
} catch (e) {
|
|
3043
|
+
if (e?.code !== "ENOENT") throw e;
|
|
3044
|
+
return new Map();
|
|
3045
|
+
}
|
|
3046
|
+
return projectUsageTotalsFromQueueRows(raw.split("\n").filter((line) => line.trim()), source);
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
function projectUsageTotalsFromQueueRows(rows, source) {
|
|
3050
|
+
const totals = new Map();
|
|
3051
|
+
for (const line of Array.isArray(rows) ? rows : []) {
|
|
3052
|
+
let row;
|
|
3053
|
+
try {
|
|
3054
|
+
row = JSON.parse(line);
|
|
3055
|
+
} catch (_e) {
|
|
3056
|
+
continue;
|
|
3057
|
+
}
|
|
3058
|
+
const key = projectUsageKeyFromFields({
|
|
3059
|
+
projectKey: row?.project_key,
|
|
3060
|
+
source: row?.source,
|
|
3061
|
+
hourStart: row?.hour_start,
|
|
3062
|
+
});
|
|
3063
|
+
if (!key || row?.source !== source) continue;
|
|
3064
|
+
setMaxTotal(totals, key, Number(row.total_tokens || 0));
|
|
3065
|
+
}
|
|
3066
|
+
return totals;
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
function projectUsageTotalsFromState(projectState, source) {
|
|
3070
|
+
const buckets =
|
|
3071
|
+
projectState && typeof projectState === "object" && projectState.buckets
|
|
3072
|
+
? projectState.buckets
|
|
3073
|
+
: {};
|
|
3074
|
+
const totals = new Map();
|
|
3075
|
+
for (const [key, bucket] of Object.entries(buckets)) {
|
|
3076
|
+
const bucketSource = typeof bucket?.source === "string" ? bucket.source : key.split("|")[1];
|
|
3077
|
+
if (bucketSource !== source) continue;
|
|
3078
|
+
const usageKey =
|
|
3079
|
+
projectUsageKeyFromFields({
|
|
3080
|
+
projectKey: bucket?.project_key,
|
|
3081
|
+
source: bucketSource,
|
|
3082
|
+
hourStart: bucket?.hour_start,
|
|
3083
|
+
}) || key;
|
|
3084
|
+
setMaxTotal(totals, usageKey, Number(bucket?.totals?.total_tokens || 0));
|
|
3085
|
+
}
|
|
3086
|
+
return totals;
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
async function mainUsageHourTotalsFromQueuePath(queuePath, source) {
|
|
3090
|
+
if (typeof queuePath !== "string" || !queuePath) return new Map();
|
|
3091
|
+
let raw = "";
|
|
3092
|
+
try {
|
|
3093
|
+
raw = await fs.readFile(queuePath, "utf8");
|
|
3094
|
+
} catch (e) {
|
|
3095
|
+
if (e?.code !== "ENOENT") throw e;
|
|
3096
|
+
return new Map();
|
|
3097
|
+
}
|
|
3098
|
+
return mainUsageHourTotalsFromQueueRows(raw.split("\n").filter((line) => line.trim()), source);
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
function mainUsageHourTotalsFromQueueRows(rows, source) {
|
|
3102
|
+
const modelTotals = new Map();
|
|
3103
|
+
for (const line of Array.isArray(rows) ? rows : []) {
|
|
3104
|
+
let row;
|
|
3105
|
+
try {
|
|
3106
|
+
row = JSON.parse(line);
|
|
3107
|
+
} catch (_e) {
|
|
3108
|
+
continue;
|
|
3109
|
+
}
|
|
3110
|
+
if (row?.source !== source || typeof row?.hour_start !== "string") continue;
|
|
3111
|
+
const model = typeof row?.model === "string" && row.model ? row.model : "unknown";
|
|
3112
|
+
setMaxTotal(modelTotals, `${row.source}|${model}|${row.hour_start}`, Number(row.total_tokens || 0));
|
|
3113
|
+
}
|
|
3114
|
+
return collapseModelTotalsByHour(modelTotals);
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
function mainUsageHourTotalsFromState(hourlyState, source) {
|
|
3118
|
+
const buckets =
|
|
3119
|
+
hourlyState && typeof hourlyState === "object" && hourlyState.buckets
|
|
3120
|
+
? hourlyState.buckets
|
|
3121
|
+
: {};
|
|
3122
|
+
const modelTotals = new Map();
|
|
3123
|
+
for (const [key, bucket] of Object.entries(buckets)) {
|
|
3124
|
+
const parts = key.split("|");
|
|
3125
|
+
const bucketSource = typeof bucket?.source === "string" ? bucket.source : parts[0];
|
|
3126
|
+
if (bucketSource !== source) continue;
|
|
3127
|
+
const model = typeof bucket?.model === "string" && bucket.model ? bucket.model : parts[1] || "unknown";
|
|
3128
|
+
const hourStart =
|
|
3129
|
+
typeof bucket?.hour_start === "string" && bucket.hour_start ? bucket.hour_start : parts[2];
|
|
3130
|
+
if (typeof hourStart !== "string" || !hourStart) continue;
|
|
3131
|
+
setMaxTotal(modelTotals, `${bucketSource}|${model}|${hourStart}`, Number(bucket?.totals?.total_tokens || 0));
|
|
3132
|
+
}
|
|
3133
|
+
return collapseModelTotalsByHour(modelTotals);
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3136
|
+
function collapseModelTotalsByHour(modelTotals) {
|
|
3137
|
+
const totals = new Map();
|
|
3138
|
+
for (const [key, total] of modelTotals.entries()) {
|
|
3139
|
+
const [source, , hourStart] = key.split("|");
|
|
3140
|
+
const hourKey = `${source}|${hourStart}`;
|
|
3141
|
+
totals.set(hourKey, (totals.get(hourKey) || 0) + total);
|
|
3142
|
+
}
|
|
3143
|
+
return totals;
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
function mergeMaxTotals(...maps) {
|
|
3147
|
+
const merged = new Map();
|
|
3148
|
+
for (const map of maps) {
|
|
3149
|
+
if (!(map instanceof Map)) continue;
|
|
3150
|
+
for (const [key, total] of map.entries()) {
|
|
3151
|
+
setMaxTotal(merged, key, total);
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
return merged;
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3157
|
+
function setMaxTotal(map, key, total) {
|
|
3158
|
+
if (!key || !Number.isFinite(total)) return;
|
|
3159
|
+
const prev = map.get(key);
|
|
3160
|
+
if (!Number.isFinite(prev) || total > prev) map.set(key, total);
|
|
3161
|
+
}
|
|
3162
|
+
|
|
2589
3163
|
// One-time repair (#204): when the SAME Droid session id existed in two folders
|
|
2590
3164
|
// under ~/.factory/sessions, parseDroidIncremental's cumulative-delta loop made the
|
|
2591
3165
|
// lower-count file look like a reset and re-emitted each duplicate's full total on
|