tokentracker-cli 0.48.0 → 0.49.0

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.
Files changed (53) hide show
  1. package/README.ja.md +3 -1
  2. package/README.ko.md +3 -1
  3. package/README.md +3 -1
  4. package/README.zh-CN.md +3 -1
  5. package/dashboard/dist/assets/{ActivityHeatmap-BW7r63JV.js → ActivityHeatmap-CItg7FNN.js} +1 -1
  6. package/dashboard/dist/assets/{Card-BWgji0yz.js → Card-B9jWqeQm.js} +1 -1
  7. package/dashboard/dist/assets/{DashboardPage-fwmRrxV3.js → DashboardPage-D-UdQdmb.js} +1 -1
  8. package/dashboard/dist/assets/{DevicePage-rxxuM-iC.js → DevicePage-BmhKFgDo.js} +1 -1
  9. package/dashboard/dist/assets/{DialogTitle-ltZ7viR5.js → DialogTitle-Dcuz0ACc.js} +1 -1
  10. package/dashboard/dist/assets/{FadeIn-DkGtOY3l.js → FadeIn-B-oMQCv_.js} +1 -1
  11. package/dashboard/dist/assets/{HeaderGithubStar-CUKVYH9j.js → HeaderGithubStar-C6x-l5U0.js} +1 -1
  12. package/dashboard/dist/assets/IpCheckPage-D1tltH7T.js +20 -0
  13. package/dashboard/dist/assets/{LandingPage-Y2ovBPVG.js → LandingPage-DnjQLNxt.js} +1 -1
  14. package/dashboard/dist/assets/{LeaderboardAvatar-DSGfEE93.js → LeaderboardAvatar-PNgWQxaR.js} +1 -1
  15. package/dashboard/dist/assets/{LeaderboardPage-BX4cvtg7.js → LeaderboardPage-DRCSwReV.js} +3 -3
  16. package/dashboard/dist/assets/{LeaderboardProfileModal-CJfefuSR.js → LeaderboardProfileModal-DvbGnWDm.js} +1 -1
  17. package/dashboard/dist/assets/{LeaderboardProfilePage-Xy0K-ppJ.js → LeaderboardProfilePage-Bmk16GFd.js} +1 -1
  18. package/dashboard/dist/assets/{LimitsPage-utdNxAg_.js → LimitsPage-DsdyKs5Z.js} +1 -1
  19. package/dashboard/dist/assets/{LocalOnlyNotice-CwGk0Fn3.js → LocalOnlyNotice-CIa2X9wJ.js} +1 -1
  20. package/dashboard/dist/assets/{LoginPage-BHlX2ZHJ.js → LoginPage-CnDLx50g.js} +1 -1
  21. package/dashboard/dist/assets/{PopoverPopup-BC4rHujH.js → PopoverPopup-ZsSFlvKU.js} +1 -1
  22. package/dashboard/dist/assets/{Select-BpDJKpfl.js → Select-CY2FgOFv.js} +1 -1
  23. package/dashboard/dist/assets/{SelectItemText-D1_H3hHS.js → SelectItemText-DI_GTNc2.js} +1 -1
  24. package/dashboard/dist/assets/{SettingsPage-DCVw-3Cv.js → SettingsPage-D1DZkCMo.js} +1 -1
  25. package/dashboard/dist/assets/{SkillsPage-B-9_Ufpw.js → SkillsPage-DorZCQ0W.js} +1 -1
  26. package/dashboard/dist/assets/{WidgetsPage-DJ4i97QU.js → WidgetsPage-BDLa_UaU.js} +1 -1
  27. package/dashboard/dist/assets/{WrappedPage-W8pPcYr2.js → WrappedPage-BKn5Q7iM.js} +1 -1
  28. package/dashboard/dist/assets/{agent-logos-yJi-7lhN.js → agent-logos-CM4Rt9bu.js} +1 -1
  29. package/dashboard/dist/assets/{arrow-up-right-BXhFthH9.js → arrow-up-right-jP0XdZdv.js} +1 -1
  30. package/dashboard/dist/assets/{download-D32KO7Ua.js → download-D8Hvx1kr.js} +1 -1
  31. package/dashboard/dist/assets/{info-C8QLdyUg.js → info-vv2jU1_C.js} +1 -1
  32. package/dashboard/dist/assets/{main-BmTIgbZL.js → main-9P4Ny5Xr.js} +15 -15
  33. package/dashboard/dist/assets/main-Br5SsufY.css +1 -0
  34. package/dashboard/dist/assets/{use-limits-display-prefs-DcmWcNeA.js → use-limits-display-prefs-BZVYlv9P.js} +1 -1
  35. package/dashboard/dist/assets/{use-native-settings-BLZi1heD.js → use-native-settings-CR_f7uLH.js} +1 -1
  36. package/dashboard/dist/assets/use-usage-limits-CvgpaBd_.js +1 -0
  37. package/dashboard/dist/assets/{useCurrency-ERbWumqM.js → useCurrency-Cq0FIlJZ.js} +1 -1
  38. package/dashboard/dist/assets/{useScrollLock-PJWfD6AS.js → useScrollLock-B5U1SJQV.js} +1 -1
  39. package/dashboard/dist/brand-logos/hermes.svg +1 -1
  40. package/dashboard/dist/index.html +2 -2
  41. package/dashboard/dist/share.html +2 -2
  42. package/package.json +1 -1
  43. package/src/commands/device-login.js +19 -3
  44. package/src/commands/sync.js +538 -348
  45. package/src/lib/local-api.js +11 -2
  46. package/src/lib/pricing/curated-overrides.json +2 -1
  47. package/src/lib/pricing/seed-snapshot.json +1 -1
  48. package/src/lib/rollout.js +98 -16
  49. package/src/lib/usage-limits.js +118 -25
  50. package/src/lib/wrapped-aggregator.js +12 -1
  51. package/dashboard/dist/assets/IpCheckPage-DqrupqCq.js +0 -15
  52. package/dashboard/dist/assets/main-BfK9LoKV.css +0 -1
  53. package/dashboard/dist/assets/use-usage-limits-BIJk3Nmb.js +0 -1
@@ -122,6 +122,20 @@ const CLAUDE_MEM_OBSERVER_PATH_SEGMENT = "--claude-mem-observer-sessions";
122
122
  // for whatever data is actually on disk. A targeted, log-gap-safe mimo
123
123
  // migration will ship later under its own key.
124
124
  const CLAUDE_GROUND_TRUTH_REPAIR_KEY = "claudeGroundTruthRepair_2026_05_v4";
125
+ // One-time full re-upload: the cloud ingest dropped `conversation_count` to 0
126
+ // from 2026-04-18 until the 2026-06-10 field-mapping fix (it read
127
+ // `b.conversations`; queue rows carry `conversation_count`). Historical cloud
128
+ // rows can only be repaired from each user's local queue.jsonl — resetting the
129
+ // upload offset replays the full queue and the ingest's whole-row upsert
130
+ // overwrites every historical bucket with the correct conversation counts
131
+ // (token columns replay to the same final values: last emission per key wins,
132
+ // exactly how the cloud rows were built the first time).
133
+ const CLOUD_CONVERSATIONS_BACKFILL_KEY = "cloudConversationsBackfill_2026_06";
134
+
135
+ function warnProviderParseFailure(label, err, opts) {
136
+ if (opts?.auto) return;
137
+ process.stderr.write(`${label} sync: ${err && err.message ? err.message : err}\n`);
138
+ }
125
139
 
126
140
  async function cmdSync(argv) {
127
141
  const opts = parseArgs(argv);
@@ -210,47 +224,61 @@ async function cmdSync(argv) {
210
224
  );
211
225
  }
212
226
 
213
- const parseResult = await parseRolloutIncremental({
214
- rolloutFiles,
215
- cursors,
216
- queuePath,
217
- projectQueuePath,
218
- onProgress: (p) => {
219
- if (!progress?.enabled) return;
220
- const pct = p.total > 0 ? p.index / p.total : 1;
221
- progress.update(
222
- `Parsing ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(
223
- p.bucketsQueued,
224
- )}`,
225
- );
226
- },
227
- });
227
+ let parseResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
228
+ try {
229
+ parseResult = await parseRolloutIncremental({
230
+ rolloutFiles,
231
+ cursors,
232
+ queuePath,
233
+ projectQueuePath,
234
+ onProgress: (p) => {
235
+ if (!progress?.enabled) return;
236
+ const pct = p.total > 0 ? p.index / p.total : 1;
237
+ progress.update(
238
+ `Parsing ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(
239
+ p.bucketsQueued,
240
+ )}`,
241
+ );
242
+ },
243
+ });
244
+ } catch (err) {
245
+ warnProviderParseFailure("Codex", err, opts);
246
+ }
228
247
 
229
248
  let openclawResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
230
249
  if (openclawFiles.length > 0) {
231
250
  // Only runs when explicitly triggered by OpenClaw hooks.
232
- openclawResult = await parseOpenclawIncremental({
233
- sessionFiles: openclawFiles,
251
+ try {
252
+ openclawResult = await parseOpenclawIncremental({
253
+ sessionFiles: openclawFiles,
254
+ cursors,
255
+ queuePath,
256
+ projectQueuePath,
257
+ source: "openclaw",
258
+ });
259
+ } catch (err) {
260
+ warnProviderParseFailure("OpenClaw", err, opts);
261
+ }
262
+ }
263
+
264
+ let openclawFallback = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
265
+ try {
266
+ openclawFallback = await applyOpenclawTotalsFallback({
267
+ trackerDir,
268
+ signal: openclawSignal,
234
269
  cursors,
235
270
  queuePath,
236
271
  projectQueuePath,
237
- source: "openclaw",
238
272
  });
273
+ } catch (err) {
274
+ warnProviderParseFailure("OpenClaw", err, opts);
239
275
  }
240
-
241
- const openclawFallback = await applyOpenclawTotalsFallback({
242
- trackerDir,
243
- signal: openclawSignal,
244
- cursors,
245
- queuePath,
246
- projectQueuePath,
247
- });
248
276
  openclawResult.filesProcessed += openclawFallback.filesProcessed;
249
277
  openclawResult.eventsAggregated += openclawFallback.eventsAggregated;
250
278
  openclawResult.bucketsQueued += openclawFallback.bucketsQueued;
251
279
 
252
280
  const claudeFiles = await listClaudeProjectFiles(claudeProjectsDir);
253
- await reincludeClaudeMemObserverFiles({ cursors, claudeFiles, queuePath });
281
+ await reincludeClaudeMemObserverFiles({ cursors, claudeFiles, queuePath, queueStatePath });
254
282
  await repairClaudeQueueFromGroundTruth({
255
283
  cursors,
256
284
  queuePath,
@@ -265,22 +293,26 @@ async function cmdSync(argv) {
265
293
  `Parsing Claude ${renderBar(0)} 0/${formatNumber(claudeFiles.length)} files | buckets 0`,
266
294
  );
267
295
  }
268
- claudeResult = await parseClaudeIncremental({
269
- projectFiles: claudeFiles,
270
- cursors,
271
- queuePath,
272
- projectQueuePath,
273
- onProgress: (p) => {
274
- if (!progress?.enabled) return;
275
- const pct = p.total > 0 ? p.index / p.total : 1;
276
- progress.update(
277
- `Parsing Claude ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(
278
- p.bucketsQueued,
279
- )}`,
280
- );
281
- },
282
- source: "claude",
283
- });
296
+ try {
297
+ claudeResult = await parseClaudeIncremental({
298
+ projectFiles: claudeFiles,
299
+ cursors,
300
+ queuePath,
301
+ projectQueuePath,
302
+ onProgress: (p) => {
303
+ if (!progress?.enabled) return;
304
+ const pct = p.total > 0 ? p.index / p.total : 1;
305
+ progress.update(
306
+ `Parsing Claude ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(
307
+ p.bucketsQueued,
308
+ )}`,
309
+ );
310
+ },
311
+ source: "claude",
312
+ });
313
+ } catch (err) {
314
+ warnProviderParseFailure("Claude", err, opts);
315
+ }
284
316
  }
285
317
 
286
318
  const geminiFiles = await listGeminiSessionFiles(geminiTmpDir);
@@ -291,22 +323,26 @@ async function cmdSync(argv) {
291
323
  `Parsing Gemini ${renderBar(0)} 0/${formatNumber(geminiFiles.length)} files | buckets 0`,
292
324
  );
293
325
  }
294
- geminiResult = await parseGeminiIncremental({
295
- sessionFiles: geminiFiles,
296
- cursors,
297
- queuePath,
298
- projectQueuePath,
299
- onProgress: (p) => {
300
- if (!progress?.enabled) return;
301
- const pct = p.total > 0 ? p.index / p.total : 1;
302
- progress.update(
303
- `Parsing Gemini ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(
304
- p.bucketsQueued,
305
- )}`,
306
- );
307
- },
308
- source: "gemini",
309
- });
326
+ try {
327
+ geminiResult = await parseGeminiIncremental({
328
+ sessionFiles: geminiFiles,
329
+ cursors,
330
+ queuePath,
331
+ projectQueuePath,
332
+ onProgress: (p) => {
333
+ if (!progress?.enabled) return;
334
+ const pct = p.total > 0 ? p.index / p.total : 1;
335
+ progress.update(
336
+ `Parsing Gemini ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(
337
+ p.bucketsQueued,
338
+ )}`,
339
+ );
340
+ },
341
+ source: "gemini",
342
+ });
343
+ } catch (err) {
344
+ warnProviderParseFailure("Gemini", err, opts);
345
+ }
310
346
  }
311
347
 
312
348
  const antigravityFiles = await listAntigravityTranscripts(geminiHome);
@@ -317,22 +353,26 @@ async function cmdSync(argv) {
317
353
  `Parsing Antigravity ${renderBar(0)} 0/${formatNumber(antigravityFiles.length)} files | buckets 0`,
318
354
  );
319
355
  }
320
- antigravityResult = await parseAntigravityIncremental({
321
- sessionFiles: antigravityFiles,
322
- cursors,
323
- queuePath,
324
- projectQueuePath,
325
- onProgress: (p) => {
326
- if (!progress?.enabled) return;
327
- const pct = p.total > 0 ? p.index / p.total : 1;
328
- progress.update(
329
- `Parsing Antigravity ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(
330
- p.bucketsQueued,
331
- )}`,
332
- );
333
- },
334
- source: "antigravity",
335
- });
356
+ try {
357
+ antigravityResult = await parseAntigravityIncremental({
358
+ sessionFiles: antigravityFiles,
359
+ cursors,
360
+ queuePath,
361
+ projectQueuePath,
362
+ onProgress: (p) => {
363
+ if (!progress?.enabled) return;
364
+ const pct = p.total > 0 ? p.index / p.total : 1;
365
+ progress.update(
366
+ `Parsing Antigravity ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(
367
+ p.bucketsQueued,
368
+ )}`,
369
+ );
370
+ },
371
+ source: "antigravity",
372
+ });
373
+ } catch (err) {
374
+ warnProviderParseFailure("Antigravity", err, opts);
375
+ }
336
376
  }
337
377
 
338
378
  const opencodeFiles = await listOpencodeMessageFiles(opencodeStorageDir);
@@ -343,22 +383,26 @@ async function cmdSync(argv) {
343
383
  `Parsing Opencode ${renderBar(0)} 0/${formatNumber(opencodeFiles.length)} files | buckets 0`,
344
384
  );
345
385
  }
346
- opencodeResult = await parseOpencodeIncremental({
347
- messageFiles: opencodeFiles,
348
- cursors,
349
- queuePath,
350
- projectQueuePath,
351
- onProgress: (p) => {
352
- if (!progress?.enabled) return;
353
- const pct = p.total > 0 ? p.index / p.total : 1;
354
- progress.update(
355
- `Parsing Opencode ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(
356
- p.total,
357
- )} files | buckets ${formatNumber(p.bucketsQueued)}`,
358
- );
359
- },
360
- source: "opencode",
361
- });
386
+ try {
387
+ opencodeResult = await parseOpencodeIncremental({
388
+ messageFiles: opencodeFiles,
389
+ cursors,
390
+ queuePath,
391
+ projectQueuePath,
392
+ onProgress: (p) => {
393
+ if (!progress?.enabled) return;
394
+ const pct = p.total > 0 ? p.index / p.total : 1;
395
+ progress.update(
396
+ `Parsing Opencode ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(
397
+ p.total,
398
+ )} files | buckets ${formatNumber(p.bucketsQueued)}`,
399
+ );
400
+ },
401
+ source: "opencode",
402
+ });
403
+ } catch (err) {
404
+ warnProviderParseFailure("Opencode", err, opts);
405
+ }
362
406
  }
363
407
 
364
408
  // OpenCode v1.2+ stores messages in SQLite (opencode.db) instead of JSON files.
@@ -371,22 +415,26 @@ async function cmdSync(argv) {
371
415
  `Parsing Opencode DB ${renderBar(0)} 0/${formatNumber(dbMessages.length)} msgs | buckets 0`,
372
416
  );
373
417
  }
374
- opencodeDbResult = await parseOpencodeDbIncremental({
375
- dbMessages,
376
- cursors,
377
- queuePath,
378
- projectQueuePath,
379
- onProgress: (p) => {
380
- if (!progress?.enabled) return;
381
- const pct = p.total > 0 ? p.index / p.total : 1;
382
- progress.update(
383
- `Parsing Opencode DB ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(
384
- p.total,
385
- )} msgs | buckets ${formatNumber(p.bucketsQueued)}`,
386
- );
387
- },
388
- source: "opencode",
389
- });
418
+ try {
419
+ opencodeDbResult = await parseOpencodeDbIncremental({
420
+ dbMessages,
421
+ cursors,
422
+ queuePath,
423
+ projectQueuePath,
424
+ onProgress: (p) => {
425
+ if (!progress?.enabled) return;
426
+ const pct = p.total > 0 ? p.index / p.total : 1;
427
+ progress.update(
428
+ `Parsing Opencode DB ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(
429
+ p.total,
430
+ )} msgs | buckets ${formatNumber(p.bucketsQueued)}`,
431
+ );
432
+ },
433
+ source: "opencode",
434
+ });
435
+ } catch (err) {
436
+ warnProviderParseFailure("Opencode DB", err, opts);
437
+ }
390
438
  opencodeResult.filesProcessed += opencodeDbResult.messagesProcessed;
391
439
  opencodeResult.eventsAggregated += opencodeDbResult.eventsAggregated;
392
440
  opencodeResult.bucketsQueued += opencodeDbResult.bucketsQueued;
@@ -405,23 +453,27 @@ async function cmdSync(argv) {
405
453
  `Parsing Kilo CLI ${renderBar(0)} 0/${formatNumber(kiloDbMessages.length)} msgs | buckets 0`,
406
454
  );
407
455
  }
408
- kiloResult = await parseOpencodeDbIncremental({
409
- dbMessages: kiloDbMessages,
410
- cursors,
411
- queuePath,
412
- projectQueuePath,
413
- onProgress: (p) => {
414
- if (!progress?.enabled) return;
415
- const pct = p.total > 0 ? p.index / p.total : 1;
416
- progress.update(
417
- `Parsing Kilo CLI ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(
418
- p.total,
419
- )} msgs | buckets ${formatNumber(p.bucketsQueued)}`,
420
- );
421
- },
422
- source: "kilo-cli",
423
- cursorKey: "kiloCli",
424
- });
456
+ try {
457
+ kiloResult = await parseOpencodeDbIncremental({
458
+ dbMessages: kiloDbMessages,
459
+ cursors,
460
+ queuePath,
461
+ projectQueuePath,
462
+ onProgress: (p) => {
463
+ if (!progress?.enabled) return;
464
+ const pct = p.total > 0 ? p.index / p.total : 1;
465
+ progress.update(
466
+ `Parsing Kilo CLI ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(
467
+ p.total,
468
+ )} msgs | buckets ${formatNumber(p.bucketsQueued)}`,
469
+ );
470
+ },
471
+ source: "kilo-cli",
472
+ cursorKey: "kiloCli",
473
+ });
474
+ } catch (err) {
475
+ warnProviderParseFailure("Kilo CLI", err, opts);
476
+ }
425
477
  }
426
478
 
427
479
  // ── Kilo Code VS Code extension (Cline-style ui_messages.json) ──
@@ -433,20 +485,24 @@ async function cmdSync(argv) {
433
485
  `Parsing Kilo Code ${renderBar(0)} 0/${formatNumber(kilocodeTaskFiles.length)} tasks | buckets 0`,
434
486
  );
435
487
  }
436
- kilocodeResult = await parseKilocodeIncremental({
437
- taskFiles: kilocodeTaskFiles,
438
- cursors,
439
- queuePath,
440
- onProgress: (p) => {
441
- if (!progress?.enabled) return;
442
- const pct = p.total > 0 ? p.index / p.total : 1;
443
- progress.update(
444
- `Parsing Kilo Code ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(
445
- p.total,
446
- )} tasks | buckets ${formatNumber(p.bucketsQueued)}`,
447
- );
448
- },
449
- });
488
+ try {
489
+ kilocodeResult = await parseKilocodeIncremental({
490
+ taskFiles: kilocodeTaskFiles,
491
+ cursors,
492
+ queuePath,
493
+ onProgress: (p) => {
494
+ if (!progress?.enabled) return;
495
+ const pct = p.total > 0 ? p.index / p.total : 1;
496
+ progress.update(
497
+ `Parsing Kilo Code ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(
498
+ p.total,
499
+ )} tasks | buckets ${formatNumber(p.bucketsQueued)}`,
500
+ );
501
+ },
502
+ });
503
+ } catch (err) {
504
+ warnProviderParseFailure("Kilo Code", err, opts);
505
+ }
450
506
  }
451
507
 
452
508
  // ── Goose (Block) — SQLite sessions with cumulative tokens per session ──
@@ -456,20 +512,24 @@ async function cmdSync(argv) {
456
512
  if (progress?.enabled) {
457
513
  progress.start(`Parsing Goose ${renderBar(0)} 0 sessions | buckets 0`);
458
514
  }
459
- gooseResult = await parseGooseIncremental({
460
- dbPath: gooseDbPath,
461
- cursors,
462
- queuePath,
463
- onProgress: (p) => {
464
- if (!progress?.enabled) return;
465
- const pct = p.total > 0 ? p.index / p.total : 1;
466
- progress.update(
467
- `Parsing Goose ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(
468
- p.total,
469
- )} sessions | buckets ${formatNumber(p.bucketsQueued)}`,
470
- );
471
- },
472
- });
515
+ try {
516
+ gooseResult = await parseGooseIncremental({
517
+ dbPath: gooseDbPath,
518
+ cursors,
519
+ queuePath,
520
+ onProgress: (p) => {
521
+ if (!progress?.enabled) return;
522
+ const pct = p.total > 0 ? p.index / p.total : 1;
523
+ progress.update(
524
+ `Parsing Goose ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(
525
+ p.total,
526
+ )} sessions | buckets ${formatNumber(p.bucketsQueued)}`,
527
+ );
528
+ },
529
+ });
530
+ } catch (err) {
531
+ warnProviderParseFailure("Goose", err, opts);
532
+ }
473
533
  }
474
534
 
475
535
  // ── Droid (Factory CLI) — passive reader for ~/.factory/sessions/*.settings.json ──
@@ -481,24 +541,28 @@ async function cmdSync(argv) {
481
541
  `Parsing Droid ${renderBar(0)} 0/${formatNumber(droidSettingsFiles.length)} sessions | buckets 0`,
482
542
  );
483
543
  }
484
- droidResult = await parseDroidIncremental({
485
- settingsFiles: droidSettingsFiles,
486
- cursors,
487
- queuePath,
488
- // Full-scan sync: drop cursor entries for any session whose
489
- // settings.json has disappeared off disk so cursors.droid stays
490
- // bounded by the actual on-disk session count.
491
- prune: true,
492
- onProgress: (p) => {
493
- if (!progress?.enabled) return;
494
- const pct = p.total > 0 ? p.index / p.total : 1;
495
- progress.update(
496
- `Parsing Droid ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(
497
- p.total,
498
- )} sessions | buckets ${formatNumber(p.bucketsQueued)}`,
499
- );
500
- },
501
- });
544
+ try {
545
+ droidResult = await parseDroidIncremental({
546
+ settingsFiles: droidSettingsFiles,
547
+ cursors,
548
+ queuePath,
549
+ // Full-scan sync: drop cursor entries for any session whose
550
+ // settings.json has disappeared off disk so cursors.droid stays
551
+ // bounded by the actual on-disk session count.
552
+ prune: true,
553
+ onProgress: (p) => {
554
+ if (!progress?.enabled) return;
555
+ const pct = p.total > 0 ? p.index / p.total : 1;
556
+ progress.update(
557
+ `Parsing Droid ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(
558
+ p.total,
559
+ )} sessions | buckets ${formatNumber(p.bucketsQueued)}`,
560
+ );
561
+ },
562
+ });
563
+ } catch (err) {
564
+ warnProviderParseFailure("Droid", err, opts);
565
+ }
502
566
  }
503
567
 
504
568
  // ── Zed Agent (all providers; cumulative-delta over SQLite threads) ──
@@ -508,20 +572,24 @@ async function cmdSync(argv) {
508
572
  if (progress?.enabled) {
509
573
  progress.start(`Parsing Zed Agent ${renderBar(0)} 0 threads | buckets 0`);
510
574
  }
511
- zedResult = await parseZedIncremental({
512
- dbPath: zedDbPath,
513
- cursors,
514
- queuePath,
515
- onProgress: (p) => {
516
- if (!progress?.enabled) return;
517
- const pct = p.total > 0 ? p.index / p.total : 1;
518
- progress.update(
519
- `Parsing Zed Agent ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(
520
- p.total,
521
- )} threads | buckets ${formatNumber(p.bucketsQueued)}`,
522
- );
523
- },
524
- });
575
+ try {
576
+ zedResult = await parseZedIncremental({
577
+ dbPath: zedDbPath,
578
+ cursors,
579
+ queuePath,
580
+ onProgress: (p) => {
581
+ if (!progress?.enabled) return;
582
+ const pct = p.total > 0 ? p.index / p.total : 1;
583
+ progress.update(
584
+ `Parsing Zed Agent ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(
585
+ p.total,
586
+ )} threads | buckets ${formatNumber(p.bucketsQueued)}`,
587
+ );
588
+ },
589
+ });
590
+ } catch (err) {
591
+ warnProviderParseFailure("Zed Agent", err, opts);
592
+ }
525
593
  }
526
594
 
527
595
  // ── Roo Code VS Code extension (Cline-derived; rooveterinaryinc.roo-cline) ──
@@ -533,20 +601,24 @@ async function cmdSync(argv) {
533
601
  `Parsing Roo Code ${renderBar(0)} 0/${formatNumber(roocodeTaskFiles.length)} tasks | buckets 0`,
534
602
  );
535
603
  }
536
- roocodeResult = await parseRoocodeIncremental({
537
- taskFiles: roocodeTaskFiles,
538
- cursors,
539
- queuePath,
540
- onProgress: (p) => {
541
- if (!progress?.enabled) return;
542
- const pct = p.total > 0 ? p.index / p.total : 1;
543
- progress.update(
544
- `Parsing Roo Code ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(
545
- p.total,
546
- )} tasks | buckets ${formatNumber(p.bucketsQueued)}`,
547
- );
548
- },
549
- });
604
+ try {
605
+ roocodeResult = await parseRoocodeIncremental({
606
+ taskFiles: roocodeTaskFiles,
607
+ cursors,
608
+ queuePath,
609
+ onProgress: (p) => {
610
+ if (!progress?.enabled) return;
611
+ const pct = p.total > 0 ? p.index / p.total : 1;
612
+ progress.update(
613
+ `Parsing Roo Code ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(
614
+ p.total,
615
+ )} tasks | buckets ${formatNumber(p.bucketsQueued)}`,
616
+ );
617
+ },
618
+ });
619
+ } catch (err) {
620
+ warnProviderParseFailure("Roo Code", err, opts);
621
+ }
550
622
  }
551
623
 
552
624
  // ── Cursor (API-based) ──
@@ -605,19 +677,23 @@ async function cmdSync(argv) {
605
677
  if (progress?.enabled) {
606
678
  progress.start(`Parsing Kiro ${renderBar(0)} | buckets 0`);
607
679
  }
608
- kiroResult = await parseKiroIncremental({
609
- dbPath: kiroDbPath,
610
- jsonlPath: kiroJsonlPath,
611
- cursors,
612
- queuePath,
613
- onProgress: (p) => {
614
- if (!progress?.enabled) return;
615
- const pct = p.total > 0 ? p.index / p.total : 1;
616
- progress.update(
617
- `Parsing Kiro ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} records | buckets ${formatNumber(p.bucketsQueued)}`,
618
- );
619
- },
620
- });
680
+ try {
681
+ kiroResult = await parseKiroIncremental({
682
+ dbPath: kiroDbPath,
683
+ jsonlPath: kiroJsonlPath,
684
+ cursors,
685
+ queuePath,
686
+ onProgress: (p) => {
687
+ if (!progress?.enabled) return;
688
+ const pct = p.total > 0 ? p.index / p.total : 1;
689
+ progress.update(
690
+ `Parsing Kiro ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} records | buckets ${formatNumber(p.bucketsQueued)}`,
691
+ );
692
+ },
693
+ });
694
+ } catch (err) {
695
+ warnProviderParseFailure("Kiro", err, opts);
696
+ }
621
697
  }
622
698
 
623
699
  // ── Hermes Agent (SQLite-based) ──
@@ -627,18 +703,22 @@ async function cmdSync(argv) {
627
703
  if (progress?.enabled) {
628
704
  progress.start(`Parsing Hermes ${renderBar(0)} | buckets 0`);
629
705
  }
630
- hermesResult = await parseHermesIncremental({
631
- hermesPath,
632
- cursors,
633
- queuePath,
634
- onProgress: (p) => {
635
- if (!progress?.enabled) return;
636
- const pct = p.total > 0 ? p.index / p.total : 1;
637
- progress.update(
638
- `Parsing Hermes ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} sessions | buckets ${formatNumber(p.bucketsQueued)}`,
639
- );
640
- },
641
- });
706
+ try {
707
+ hermesResult = await parseHermesIncremental({
708
+ hermesPath,
709
+ cursors,
710
+ queuePath,
711
+ onProgress: (p) => {
712
+ if (!progress?.enabled) return;
713
+ const pct = p.total > 0 ? p.index / p.total : 1;
714
+ progress.update(
715
+ `Parsing Hermes ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} sessions | buckets ${formatNumber(p.bucketsQueued)}`,
716
+ );
717
+ },
718
+ });
719
+ } catch (err) {
720
+ warnProviderParseFailure("Hermes", err, opts);
721
+ }
642
722
  }
643
723
 
644
724
  // ── Kiro CLI (reads ~/Library/Application Support/kiro-cli/data.sqlite3
@@ -682,19 +762,23 @@ async function cmdSync(argv) {
682
762
  if (progress?.enabled) {
683
763
  progress.start(`Parsing Kimi Code ${renderBar(0)} | buckets 0`);
684
764
  }
685
- kimiResult = await parseKimiIncremental({
686
- wireFiles: kimiWireFiles,
687
- cursors,
688
- queuePath,
689
- env: process.env,
690
- onProgress: (p) => {
691
- if (!progress?.enabled) return;
692
- const pct = p.total > 0 ? p.index / p.total : 1;
693
- progress.update(
694
- `Parsing Kimi Code ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(p.bucketsQueued)}`,
695
- );
696
- },
697
- });
765
+ try {
766
+ kimiResult = await parseKimiIncremental({
767
+ wireFiles: kimiWireFiles,
768
+ cursors,
769
+ queuePath,
770
+ env: process.env,
771
+ onProgress: (p) => {
772
+ if (!progress?.enabled) return;
773
+ const pct = p.total > 0 ? p.index / p.total : 1;
774
+ progress.update(
775
+ `Parsing Kimi Code ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(p.bucketsQueued)}`,
776
+ );
777
+ },
778
+ });
779
+ } catch (err) {
780
+ warnProviderParseFailure("Kimi Code", err, opts);
781
+ }
698
782
  }
699
783
 
700
784
  // ── Kimi Code official (@moonshot-ai/kimi-code, ~/.kimi-code) ──
@@ -704,19 +788,23 @@ async function cmdSync(argv) {
704
788
  if (progress?.enabled) {
705
789
  progress.start(`Parsing Kimi Code (official) ${renderBar(0)} | buckets 0`);
706
790
  }
707
- kimiCodeResult = await parseKimiCodeIncremental({
708
- wireFiles: kimiCodeWireFiles,
709
- cursors,
710
- queuePath,
711
- env: process.env,
712
- onProgress: (p) => {
713
- if (!progress?.enabled) return;
714
- const pct = p.total > 0 ? p.index / p.total : 1;
715
- progress.update(
716
- `Parsing Kimi Code (official) ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(p.bucketsQueued)}`,
717
- );
718
- },
719
- });
791
+ try {
792
+ kimiCodeResult = await parseKimiCodeIncremental({
793
+ wireFiles: kimiCodeWireFiles,
794
+ cursors,
795
+ queuePath,
796
+ env: process.env,
797
+ onProgress: (p) => {
798
+ if (!progress?.enabled) return;
799
+ const pct = p.total > 0 ? p.index / p.total : 1;
800
+ progress.update(
801
+ `Parsing Kimi Code (official) ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(p.bucketsQueued)}`,
802
+ );
803
+ },
804
+ });
805
+ } catch (err) {
806
+ warnProviderParseFailure("Kimi Code (official)", err, opts);
807
+ }
720
808
  }
721
809
 
722
810
  // ── CodeBuddy CLI (passive ~/.codebuddy/projects/**/*.jsonl reader) ──
@@ -728,19 +816,23 @@ async function cmdSync(argv) {
728
816
  if (progress?.enabled) {
729
817
  progress.start(`Parsing CodeBuddy ${renderBar(0)} | buckets 0`);
730
818
  }
731
- codebuddyResult = await parseCodebuddyIncremental({
732
- projectFiles: codebuddyFiles,
733
- cursors,
734
- queuePath,
735
- env: process.env,
736
- onProgress: (p) => {
737
- if (!progress?.enabled) return;
738
- const pct = p.total > 0 ? p.index / p.total : 1;
739
- progress.update(
740
- `Parsing CodeBuddy ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(p.bucketsQueued)}`,
741
- );
742
- },
743
- });
819
+ try {
820
+ codebuddyResult = await parseCodebuddyIncremental({
821
+ projectFiles: codebuddyFiles,
822
+ cursors,
823
+ queuePath,
824
+ env: process.env,
825
+ onProgress: (p) => {
826
+ if (!progress?.enabled) return;
827
+ const pct = p.total > 0 ? p.index / p.total : 1;
828
+ progress.update(
829
+ `Parsing CodeBuddy ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(p.bucketsQueued)}`,
830
+ );
831
+ },
832
+ });
833
+ } catch (err) {
834
+ warnProviderParseFailure("CodeBuddy", err, opts);
835
+ }
744
836
  }
745
837
 
746
838
  // ── oh-my-pi (passive ~/.omp/agent/sessions/**/*.jsonl reader) ──
@@ -750,19 +842,23 @@ async function cmdSync(argv) {
750
842
  if (progress?.enabled) {
751
843
  progress.start(`Parsing oh-my-pi ${renderBar(0)} | buckets 0`);
752
844
  }
753
- ompResult = await parseOmpIncremental({
754
- sessionFiles: ompFiles,
755
- cursors,
756
- queuePath,
757
- env: process.env,
758
- onProgress: (p) => {
759
- if (!progress?.enabled) return;
760
- const pct = p.total > 0 ? p.index / p.total : 1;
761
- progress.update(
762
- `Parsing oh-my-pi ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(p.bucketsQueued)}`,
763
- );
764
- },
765
- });
845
+ try {
846
+ ompResult = await parseOmpIncremental({
847
+ sessionFiles: ompFiles,
848
+ cursors,
849
+ queuePath,
850
+ env: process.env,
851
+ onProgress: (p) => {
852
+ if (!progress?.enabled) return;
853
+ const pct = p.total > 0 ? p.index / p.total : 1;
854
+ progress.update(
855
+ `Parsing oh-my-pi ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(p.bucketsQueued)}`,
856
+ );
857
+ },
858
+ });
859
+ } catch (err) {
860
+ warnProviderParseFailure("oh-my-pi", err, opts);
861
+ }
766
862
  }
767
863
 
768
864
  // ── pi (@mariozechner/pi-coding-agent) — passive ~/.pi/agent/sessions/**/*.jsonl reader ──
@@ -777,19 +873,23 @@ async function cmdSync(argv) {
777
873
  if (progress?.enabled) {
778
874
  progress.start(`Parsing pi ${renderBar(0)} | buckets 0`);
779
875
  }
780
- piResult = await parsePiIncremental({
781
- sessionFiles: piFiles,
782
- cursors,
783
- queuePath,
784
- env: process.env,
785
- onProgress: (p) => {
786
- if (!progress?.enabled) return;
787
- const pct = p.total > 0 ? p.index / p.total : 1;
788
- progress.update(
789
- `Parsing pi ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(p.bucketsQueued)}`,
790
- );
791
- },
792
- });
876
+ try {
877
+ piResult = await parsePiIncremental({
878
+ sessionFiles: piFiles,
879
+ cursors,
880
+ queuePath,
881
+ env: process.env,
882
+ onProgress: (p) => {
883
+ if (!progress?.enabled) return;
884
+ const pct = p.total > 0 ? p.index / p.total : 1;
885
+ progress.update(
886
+ `Parsing pi ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(p.bucketsQueued)}`,
887
+ );
888
+ },
889
+ });
890
+ } catch (err) {
891
+ warnProviderParseFailure("pi", err, opts);
892
+ }
793
893
  }
794
894
 
795
895
  // ── Craft Agents (passive ~/.craft-agent + workspaces session.jsonl reader) ──
@@ -799,19 +899,23 @@ async function cmdSync(argv) {
799
899
  if (progress?.enabled) {
800
900
  progress.start(`Parsing Craft ${renderBar(0)} | buckets 0`);
801
901
  }
802
- craftResult = await parseCraftIncremental({
803
- sessionFiles: craftFiles,
804
- cursors,
805
- queuePath,
806
- env: process.env,
807
- onProgress: (p) => {
808
- if (!progress?.enabled) return;
809
- const pct = p.total > 0 ? p.index / p.total : 1;
810
- progress.update(
811
- `Parsing Craft ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(p.bucketsQueued)}`,
812
- );
813
- },
814
- });
902
+ try {
903
+ craftResult = await parseCraftIncremental({
904
+ sessionFiles: craftFiles,
905
+ cursors,
906
+ queuePath,
907
+ env: process.env,
908
+ onProgress: (p) => {
909
+ if (!progress?.enabled) return;
910
+ const pct = p.total > 0 ? p.index / p.total : 1;
911
+ progress.update(
912
+ `Parsing Craft ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(p.bucketsQueued)}`,
913
+ );
914
+ },
915
+ });
916
+ } catch (err) {
917
+ warnProviderParseFailure("Craft", err, opts);
918
+ }
815
919
  }
816
920
 
817
921
  // ── Grok Build (xAI) ──
@@ -860,19 +964,24 @@ async function cmdSync(argv) {
860
964
  if (progress?.enabled) {
861
965
  progress.start(`Parsing Grok Build ${renderBar(0)} | buckets 0`);
862
966
  }
863
- const grokScanResult = await parseGrokBuildIncremental({
864
- sessions: grokSessionInputs,
865
- cursors,
866
- queuePath,
867
- env: process.env,
868
- onProgress: (p) => {
869
- if (!progress?.enabled) return;
870
- const pct = p.total > 0 ? p.index / p.total : 1;
871
- progress.update(
872
- `Parsing Grok Build ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} sessions | buckets ${formatNumber(p.bucketsQueued)}`,
873
- );
874
- },
875
- });
967
+ let grokScanResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
968
+ try {
969
+ grokScanResult = await parseGrokBuildIncremental({
970
+ sessions: grokSessionInputs,
971
+ cursors,
972
+ queuePath,
973
+ env: process.env,
974
+ onProgress: (p) => {
975
+ if (!progress?.enabled) return;
976
+ const pct = p.total > 0 ? p.index / p.total : 1;
977
+ progress.update(
978
+ `Parsing Grok Build ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} sessions | buckets ${formatNumber(p.bucketsQueued)}`,
979
+ );
980
+ },
981
+ });
982
+ } catch (err) {
983
+ warnProviderParseFailure("Grok Build", err, opts);
984
+ }
876
985
  grokResult = {
877
986
  recordsProcessed: grokResult.recordsProcessed + grokScanResult.recordsProcessed,
878
987
  eventsAggregated: grokResult.eventsAggregated + grokScanResult.eventsAggregated,
@@ -890,19 +999,23 @@ async function cmdSync(argv) {
890
999
  if (progress?.enabled) {
891
1000
  progress.start(`Parsing Copilot ${renderBar(0)} | buckets 0`);
892
1001
  }
893
- copilotResult = await parseCopilotIncremental({
894
- otelPaths: copilotPaths,
895
- cursors,
896
- queuePath,
897
- env: process.env,
898
- onProgress: (p) => {
899
- if (!progress?.enabled) return;
900
- const pct = p.total > 0 ? p.index / p.total : 1;
901
- progress.update(
902
- `Parsing Copilot ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(p.bucketsQueued)}`,
903
- );
904
- },
905
- });
1002
+ try {
1003
+ copilotResult = await parseCopilotIncremental({
1004
+ otelPaths: copilotPaths,
1005
+ cursors,
1006
+ queuePath,
1007
+ env: process.env,
1008
+ onProgress: (p) => {
1009
+ if (!progress?.enabled) return;
1010
+ const pct = p.total > 0 ? p.index / p.total : 1;
1011
+ progress.update(
1012
+ `Parsing Copilot ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(p.bucketsQueued)}`,
1013
+ );
1014
+ },
1015
+ });
1016
+ } catch (err) {
1017
+ warnProviderParseFailure("Copilot", err, opts);
1018
+ }
906
1019
  }
907
1020
 
908
1021
  if (cursors?.projectHourly?.projects && projectQueuePath && projectQueueStatePath) {
@@ -920,6 +1033,8 @@ async function cmdSync(argv) {
920
1033
  }
921
1034
  }
922
1035
 
1036
+ await applyCloudConversationsBackfill({ cursors, queueStatePath });
1037
+
923
1038
  cursors.updatedAt = new Date().toISOString();
924
1039
  await writeJson(cursorsPath, cursors);
925
1040
  if (grokHookSignalConsumed && grokHookSignalPath) {
@@ -1095,10 +1210,12 @@ module.exports = {
1095
1210
  migrateRolloutCumulativeDeltaBuckets,
1096
1211
  reincludeClaudeMemObserverFiles,
1097
1212
  repairGrokQueueFromSessionSnapshots,
1213
+ applyCloudConversationsBackfill,
1098
1214
  CURSOR_UNKNOWN_MIGRATION_KEY,
1099
1215
  ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY,
1100
1216
  CLAUDE_MEM_OBSERVER_REINCLUDE_KEY,
1101
1217
  GROK_APPEND_ONLY_REPAIR_MIGRATION_KEY,
1218
+ CLOUD_CONVERSATIONS_BACKFILL_KEY,
1102
1219
  };
1103
1220
 
1104
1221
  function normalizeString(value) {
@@ -1458,6 +1575,12 @@ async function readQueueBatch(queuePath, startOffset, maxBuckets) {
1458
1575
  const model = (typeof bucket?.model === "string" ? bucket.model.trim() : "") || "unknown";
1459
1576
  bucket.source = source;
1460
1577
  bucket.model = model;
1578
+ // Apply the same legacy-row corrections every local reader applies
1579
+ // (local-api readQueueData / project queue / wrapped aggregator). Without
1580
+ // this the cloud permanently kept the RAW legacy values — e.g. old Codex
1581
+ // rows whose input_tokens still include cached tokens (6-7x inflated) —
1582
+ // while the local dashboard showed corrected numbers.
1583
+ bucket = require("../lib/local-api").normalizeQueueRow(bucket);
1461
1584
  bucketMap.set(`${source}|${model}|${hourStart}`, bucket);
1462
1585
  linesRead += 1;
1463
1586
  if (linesRead >= maxBuckets) break;
@@ -1806,6 +1929,32 @@ async function repairGrokQueueFromSessionSnapshots({ cursors, queuePath, queueSt
1806
1929
  return true;
1807
1930
  }
1808
1931
 
1932
+ async function applyCloudConversationsBackfill({ cursors, queueStatePath }) {
1933
+ if (!cursors || typeof cursors !== "object") return false;
1934
+ cursors.migrations = cursors.migrations || {};
1935
+ if (cursors.migrations[CLOUD_CONVERSATIONS_BACKFILL_KEY]) return false;
1936
+
1937
+ // Reset ONLY the cloud upload offset. The queue file itself is untouched;
1938
+ // ingest upserts are idempotent per (user, device, hour, source, model),
1939
+ // so replaying the whole queue is safe — it costs upload batches, not
1940
+ // correctness. Project queue is never uploaded and is not touched.
1941
+ let prevOffset = 0;
1942
+ try {
1943
+ const st = (await readJson(queueStatePath)) || {};
1944
+ prevOffset = Number(st.offset || 0);
1945
+ } catch (_e) {
1946
+ /* missing state file — nothing to reset */
1947
+ }
1948
+ if (prevOffset > 0) {
1949
+ await writeJson(queueStatePath, { offset: 0, updatedAt: new Date().toISOString() });
1950
+ }
1951
+ cursors.migrations[CLOUD_CONVERSATIONS_BACKFILL_KEY] = {
1952
+ appliedAt: new Date().toISOString(),
1953
+ previousOffset: prevOffset,
1954
+ };
1955
+ return prevOffset > 0;
1956
+ }
1957
+
1809
1958
  async function migrateCursorUnknownBuckets({ cursors, queuePath }) {
1810
1959
  if (!cursors || typeof cursors !== "object") return;
1811
1960
  cursors.migrations = cursors.migrations || {};
@@ -2169,7 +2318,7 @@ async function repairClaudeQueueFromGroundTruth({
2169
2318
  return true;
2170
2319
  }
2171
2320
 
2172
- async function reincludeClaudeMemObserverFiles({ cursors, claudeFiles, queuePath }) {
2321
+ async function reincludeClaudeMemObserverFiles({ cursors, claudeFiles, queuePath, queueStatePath }) {
2173
2322
  if (!cursors || typeof cursors !== "object") return false;
2174
2323
  const migrations = (cursors.migrations ||= {});
2175
2324
  if (migrations[CLAUDE_MEM_OBSERVER_REINCLUDE_KEY]) return false;
@@ -2207,7 +2356,7 @@ async function reincludeClaudeMemObserverFiles({ cursors, claudeFiles, queuePath
2207
2356
  }
2208
2357
 
2209
2358
  const queueRowsRelabeled = typeof queuePath === "string" && queuePath
2210
- ? await relabelClaudeMemQueueRows(queuePath)
2359
+ ? await relabelClaudeMemQueueRows(queuePath, queueStatePath)
2211
2360
  : 0;
2212
2361
 
2213
2362
  migrations[CLAUDE_MEM_OBSERVER_REINCLUDE_KEY] = {
@@ -2219,7 +2368,7 @@ async function reincludeClaudeMemObserverFiles({ cursors, claudeFiles, queuePath
2219
2368
  return filesReset > 0 || hashesRemoved > 0 || queueRowsRelabeled > 0;
2220
2369
  }
2221
2370
 
2222
- async function relabelClaudeMemQueueRows(queuePath) {
2371
+ async function relabelClaudeMemQueueRows(queuePath, queueStatePath = null) {
2223
2372
  let raw;
2224
2373
  try {
2225
2374
  raw = await fs.readFile(queuePath, "utf8");
@@ -2228,32 +2377,73 @@ async function relabelClaudeMemQueueRows(queuePath) {
2228
2377
  }
2229
2378
  if (!raw || !raw.includes('"claude-mem"')) return 0;
2230
2379
 
2380
+ // The cloud-upload cursor (queue.state.json `offset`) is a byte position in
2381
+ // the pre-rewrite file. Relabeling shrinks rewritten lines ("claude-mem" →
2382
+ // "claude"), so the old offset would land mid-line in the new file and the
2383
+ // next drainQueueToCloud batch would skip part of a row (or a whole row).
2384
+ // Track the old→new byte mapping while rewriting and remap the offset to
2385
+ // the equivalent line boundary (same pattern as project-usage-purge.js).
2386
+ let previousOffset = 0;
2387
+ if (typeof queueStatePath === "string" && queueStatePath) {
2388
+ try {
2389
+ const st = JSON.parse(await fs.readFile(queueStatePath, "utf8"));
2390
+ const off = Number(st?.offset || 0);
2391
+ if (Number.isFinite(off) && off > 0) previousOffset = off;
2392
+ } catch (_e) {
2393
+ previousOffset = 0;
2394
+ }
2395
+ }
2396
+
2231
2397
  const lines = raw.split("\n");
2232
2398
  const out = [];
2233
2399
  let relabeled = 0;
2234
- for (const line of lines) {
2235
- if (!line) {
2236
- out.push(line);
2237
- continue;
2400
+ let inputOffset = 0;
2401
+ let outputOffset = 0;
2402
+ let nextOffset = 0;
2403
+ for (let i = 0; i < lines.length; i++) {
2404
+ const line = lines[i];
2405
+ const isLast = i === lines.length - 1;
2406
+ let outLine = line;
2407
+ if (line) {
2408
+ try {
2409
+ const obj = JSON.parse(line);
2410
+ if (obj && obj.source === "claude-mem") {
2411
+ obj.source = "claude";
2412
+ relabeled += 1;
2413
+ outLine = JSON.stringify(obj);
2414
+ }
2415
+ } catch (_e) {
2416
+ // keep malformed lines verbatim
2417
+ }
2238
2418
  }
2239
- let obj;
2419
+ out.push(outLine);
2420
+ inputOffset += Buffer.byteLength(line, "utf8") + (isLast ? 0 : 1);
2421
+ outputOffset += Buffer.byteLength(outLine, "utf8") + (isLast ? 0 : 1);
2422
+ // Upload offsets always sit at line boundaries; a mid-line offset
2423
+ // (corruption) rounds down to the previous boundary so no row is skipped
2424
+ // — worst case a row is re-uploaded, and cloud ingest upserts by key.
2425
+ if (inputOffset <= previousOffset) nextOffset = outputOffset;
2426
+ }
2427
+ if (relabeled === 0) return 0;
2428
+
2429
+ // Atomic rewrite: temp file in the same directory + rename, so a crash
2430
+ // mid-write can never leave queue.jsonl truncated.
2431
+ const tmpPath = `${queuePath}.tmp`;
2432
+ await fs.writeFile(tmpPath, out.join("\n"), "utf8");
2433
+ await fs.rename(tmpPath, queuePath);
2434
+
2435
+ if (typeof queueStatePath === "string" && queueStatePath && previousOffset > 0) {
2436
+ let state = {};
2240
2437
  try {
2241
- obj = JSON.parse(line);
2438
+ state = JSON.parse(await fs.readFile(queueStatePath, "utf8"));
2439
+ if (!state || typeof state !== "object") state = {};
2242
2440
  } catch (_e) {
2243
- out.push(line);
2244
- continue;
2245
- }
2246
- if (obj && obj.source === "claude-mem") {
2247
- obj.source = "claude";
2248
- relabeled += 1;
2249
- out.push(JSON.stringify(obj));
2250
- } else {
2251
- out.push(line);
2441
+ state = {};
2252
2442
  }
2443
+ state.offset = nextOffset;
2444
+ state.updatedAt = new Date().toISOString();
2445
+ await fs.writeFile(queueStatePath, JSON.stringify(state), "utf8");
2253
2446
  }
2254
- if (relabeled === 0) return 0;
2255
-
2256
- await fs.writeFile(queuePath, out.join("\n"), "utf8");
2257
2447
  return relabeled;
2258
2448
  }
2259
2449