tokentracker-cli 1.5.13 → 1.5.15

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 (115) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-Ct0vfdYb.js → AchievementBadge-CwjI28oT.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-dfoRxqOv.js → AchievementsPage-Dq544fjX.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-DHFyssU0.js → AchievementsSection-DufV6nod.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-ChJYZMSq.js → BadgeDetailModal-vw8xydKu.js} +1 -1
  5. package/dashboard/dist/assets/{BotAnimated-C_qaJy1-.js → BotAnimated-De0anwsh.js} +1 -1
  6. package/dashboard/dist/assets/{Card-BhK2nmOu.js → Card-CuSeWBDM.js} +1 -1
  7. package/dashboard/dist/assets/{CloudCheckoutPage-Ele1kkut.js → CloudCheckoutPage-CxFcOwAj.js} +1 -1
  8. package/dashboard/dist/assets/{CloudPage-C5DMqcoV.js → CloudPage-Beb7_JLW.js} +2 -2
  9. package/dashboard/dist/assets/{CommandPalette-C_hxoIwL.js → CommandPalette-PsUq2iRS.js} +1 -1
  10. package/dashboard/dist/assets/{CommunityStatsModal-BSTqBIEU.js → CommunityStatsModal-b54WaPbK.js} +1 -1
  11. package/dashboard/dist/assets/{ConfirmModal-DI4QK5EV.js → ConfirmModal-_NdunIh5.js} +1 -1
  12. package/dashboard/dist/assets/{Controls-zK1rzS_8.js → Controls-5Is6MWln.js} +1 -1
  13. package/dashboard/dist/assets/{DashboardPage-BdDnmc60.js → DashboardPage-C0n0qC7h.js} +1 -1
  14. package/dashboard/dist/assets/{DateRangePopover-DqwgxpNF.js → DateRangePopover-C3WfGy-0.js} +1 -1
  15. package/dashboard/dist/assets/{DevicePage-C0fQKKuI.js → DevicePage-Vn6k-mGd.js} +1 -1
  16. package/dashboard/dist/assets/{DialogClose-C_cbyHpb.js → DialogClose-CXttnna-.js} +1 -1
  17. package/dashboard/dist/assets/DialogDescription-C3KGos_G.js +1 -0
  18. package/dashboard/dist/assets/{DialogTitle-BcBpd--_.js → DialogTitle-B0cNIo0E.js} +1 -1
  19. package/dashboard/dist/assets/{ElectricLogo-Dw9gkExE.js → ElectricLogo-DC3XSfIu.js} +1 -1
  20. package/dashboard/dist/assets/{FadeIn-Ci69gBGl.js → FadeIn-H7S3TMHi.js} +1 -1
  21. package/dashboard/dist/assets/{HeaderGithubStar-pb03c0kb.js → HeaderGithubStar-xgbIo5OP.js} +1 -1
  22. package/dashboard/dist/assets/{HoverTooltip-Cqd0D789.js → HoverTooltip-CpzM5Dju.js} +1 -1
  23. package/dashboard/dist/assets/{IpCheckPage-qILbvOJn.js → IpCheckPage-BdV7S4dY.js} +1 -1
  24. package/dashboard/dist/assets/{LandingPage-CCIwaAeu.js → LandingPage-D9f28ryO.js} +2 -2
  25. package/dashboard/dist/assets/{LeaderboardPage-BwzABmKU.js → LeaderboardPage--WUFpkBI.js} +2 -2
  26. package/dashboard/dist/assets/{LeaderboardProBadge-7kdzfw8r.js → LeaderboardProBadge-CEvrQ75Y.js} +1 -1
  27. package/dashboard/dist/assets/{LeaderboardProfileModal-uMnN-IGe.js → LeaderboardProfileModal-Bn0HFr5h.js} +3 -3
  28. package/dashboard/dist/assets/{LeaderboardProfilePage-CQNc4MES.js → LeaderboardProfilePage-DJ7fQI68.js} +1 -1
  29. package/dashboard/dist/assets/{LimitsPage-CJ1LImRm.js → LimitsPage-7tPDheuX.js} +1 -1
  30. package/dashboard/dist/assets/{LocalOnlyNotice-Dm0l_F0I.js → LocalOnlyNotice-DlGbVQS8.js} +1 -1
  31. package/dashboard/dist/assets/{LoginCard--tUiBVtr.js → LoginCard-BZZmzUGh.js} +1 -1
  32. package/dashboard/dist/assets/{LoginModal-B1-5fFMY.js → LoginModal-BT5_bIxU.js} +1 -1
  33. package/dashboard/dist/assets/{LoginPage-BYl6NnZ8.js → LoginPage-DMjDUPPi.js} +1 -1
  34. package/dashboard/dist/assets/{PetPage-DJNGJZrR.js → PetPage-Dk3j6ITn.js} +2 -2
  35. package/dashboard/dist/assets/{PopoverPopup-aRAdIkTs.js → PopoverPopup-pugRu3RC.js} +1 -1
  36. package/dashboard/dist/assets/{ProviderIcon-uiA-KBe2.js → ProviderIcon-CtmL1CmY.js} +1 -1
  37. package/dashboard/dist/assets/{ResetPasswordPage-BZT353_T.js → ResetPasswordPage-BB6U7t8P.js} +1 -1
  38. package/dashboard/dist/assets/{Select-CPC04j0g.js → Select-7w1su8lw.js} +1 -1
  39. package/dashboard/dist/assets/{SelectItemText-D1vJVrFO.js → SelectItemText-CaEUmWkj.js} +1 -1
  40. package/dashboard/dist/assets/{SelfHostPage-FL3EDqmb.js → SelfHostPage-DctWoF-A.js} +1 -1
  41. package/dashboard/dist/assets/{ServiceStatusPage-BohSSNai.js → ServiceStatusPage-Dg3ljEXC.js} +1 -1
  42. package/dashboard/dist/assets/{SessionsPage-Cf_HASvt.js → SessionsPage-B1dvW-be.js} +1 -1
  43. package/dashboard/dist/assets/{SettingsPage-B4ztKHnj.js → SettingsPage-DuXSX1R2.js} +3 -3
  44. package/dashboard/dist/assets/{SkillsPage-BFiQkY7O.js → SkillsPage-CvXQFgc9.js} +1 -1
  45. package/dashboard/dist/assets/{TokenGalaxy-BHVdTJAP.js → TokenGalaxy-DKC8Rk_Y.js} +1 -1
  46. package/dashboard/dist/assets/{TooltipPopup-BMtQyFn6.js → TooltipPopup-Dp9EpHLG.js} +1 -1
  47. package/dashboard/dist/assets/{TrendMonitor-DN-g3CNm.js → TrendMonitor-DWnlBn3H.js} +1 -1
  48. package/dashboard/dist/assets/{WidgetsPage-Bv32yu8M.js → WidgetsPage-GECNmb1Q.js} +1 -1
  49. package/dashboard/dist/assets/{WrappedPage-HVTOehNb.js → WrappedPage-Dq_1lOnb.js} +1 -1
  50. package/dashboard/dist/assets/{__vite-browser-external-DnYJww7c.js → __vite-browser-external-CkF0pdTJ.js} +2 -2
  51. package/dashboard/dist/assets/{agent-logos-BOwAJQqL.js → agent-logos-Dqe3Vrah.js} +1 -1
  52. package/dashboard/dist/assets/{arrow-left-CeXuUuES.js → arrow-left-Cr_2QnDu.js} +1 -1
  53. package/dashboard/dist/assets/{arrow-right-DOdBG1FZ.js → arrow-right-BsK2sJgt.js} +1 -1
  54. package/dashboard/dist/assets/{arrow-up-right-Dponssa4.js → arrow-up-right-DkWMGqXo.js} +1 -1
  55. package/dashboard/dist/assets/{calendar-days-CsxE6ot0.js → calendar-days-BB9VV438.js} +1 -1
  56. package/dashboard/dist/assets/{check-BroNG_vu.js → check-bpSCJZWh.js} +1 -1
  57. package/dashboard/dist/assets/{chevron-down-C5ZpGeO1.js → chevron-down-CtECtDmx.js} +1 -1
  58. package/dashboard/dist/assets/{cloud-checkout-BtS-hZTc.js → cloud-checkout-C4PrzKhN.js} +1 -1
  59. package/dashboard/dist/assets/{composite-fgJbx88k.js → composite-BHisG82k.js} +1 -1
  60. package/dashboard/dist/assets/{copy-CHBuwNUs.js → copy-Dmxzd-Sw.js} +1 -1
  61. package/dashboard/dist/assets/{download-EKd2MHkv.js → download-CkdQzw2w.js} +1 -1
  62. package/dashboard/dist/assets/{external-link-GkSo9C78.js → external-link-C971FjvC.js} +1 -1
  63. package/dashboard/dist/assets/{flame-Cg96qJrE.js → flame-mKbp7kxw.js} +1 -1
  64. package/dashboard/dist/assets/{format-tokens-ChG2_n4h.js → format-tokens-C35FrJnH.js} +1 -1
  65. package/dashboard/dist/assets/{gift-B3QDbWPE.js → gift-BX2lSOmc.js} +1 -1
  66. package/dashboard/dist/assets/{icons-BEAyMjau.js → icons-ov92u52T.js} +1 -1
  67. package/dashboard/dist/assets/{index-B0kZThUB.js → index-9OCL-BL8.js} +1 -1
  68. package/dashboard/dist/assets/{index-DFcQyAnr.js → index-Bgm38oHe.js} +1 -1
  69. package/dashboard/dist/assets/{index-Cuc8ZXgl.js → index-Ca5xkNrK.js} +1 -1
  70. package/dashboard/dist/assets/{index-B9a1fK8I.js → index-DF03nIHb.js} +1 -1
  71. package/dashboard/dist/assets/{info-796bNttV.js → info-B-f9hWcw.js} +1 -1
  72. package/dashboard/dist/assets/{link-2-LYRIPLwz.js → link-2-l1wuezTo.js} +1 -1
  73. package/dashboard/dist/assets/{loader-circle-DpKC9Uel.js → loader-circle-Lmjrs8XX.js} +1 -1
  74. package/dashboard/dist/assets/{local-usage-transfer-Ctbuc6U4.js → local-usage-transfer-Ds6cF41d.js} +1 -1
  75. package/dashboard/dist/assets/{modifiers.esm-C5YTeNW1.js → modifiers.esm-tVlltJXO.js} +1 -1
  76. package/dashboard/dist/assets/{monitor-smartphone-DJUCfEvG.js → monitor-smartphone-XRiYiY8u.js} +1 -1
  77. package/dashboard/dist/assets/{play-LC-lnw6w.js → play-DaD9EMjk.js} +1 -1
  78. package/dashboard/dist/assets/{plus-CDF0S5xd.js → plus-D30fbsKQ.js} +1 -1
  79. package/dashboard/dist/assets/{provider-display-CkITUZgR.js → provider-display-CKdhBOwz.js} +1 -1
  80. package/dashboard/dist/assets/{react-BIpM-G7K.js → react-BY0y0K4x.js} +1 -1
  81. package/dashboard/dist/assets/{safePolygon-C3Ea423q.js → safePolygon-CSG_P7rG.js} +1 -1
  82. package/dashboard/dist/assets/{search-C-tN1mTM.js → search-Kiq8wVHA.js} +1 -1
  83. package/dashboard/dist/assets/{server-DJ4gq_fT.js → server-CjovGBGz.js} +1 -1
  84. package/dashboard/dist/assets/{shield-check-f75hry9N.js → shield-check-DF9MmMCq.js} +1 -1
  85. package/dashboard/dist/assets/{skills-api-CZ_vfnRL.js → skills-api-CEzr6p92.js} +1 -1
  86. package/dashboard/dist/assets/{sortable.esm-mLJSvq3M.js → sortable.esm-D7jd9mE_.js} +1 -1
  87. package/dashboard/dist/assets/{store-CZV1KXw2.js → store-D9_jo_do.js} +1 -1
  88. package/dashboard/dist/assets/{terminal-B9vOwCl9.js → terminal-CLOlvzR-.js} +1 -1
  89. package/dashboard/dist/assets/{trash-BN9GbynA.js → trash-DIe0fsDD.js} +1 -1
  90. package/dashboard/dist/assets/{use-community-stats-Bc2d8tqa.js → use-community-stats-gDkdiRBz.js} +1 -1
  91. package/dashboard/dist/assets/{use-latest-request-guard-_0H4Rk58.js → use-latest-request-guard-tXGuKvua.js} +1 -1
  92. package/dashboard/dist/assets/{use-limits-display-prefs-Dln-Rxps.js → use-limits-display-prefs-CCuLFy00.js} +1 -1
  93. package/dashboard/dist/assets/{use-native-settings-CpkcGzXp.js → use-native-settings-hgYqaiR7.js} +1 -1
  94. package/dashboard/dist/assets/{use-ordered-list-DpGA5U4S.js → use-ordered-list-D6-YzYJI.js} +1 -1
  95. package/dashboard/dist/assets/use-reduced-motion-DfbDxs5a.js +1 -0
  96. package/dashboard/dist/assets/{use-transform-JXJt1W74.js → use-transform-BEiXvAuE.js} +1 -1
  97. package/dashboard/dist/assets/{use-usage-limits-BK_H1-b7.js → use-usage-limits-BlwtyPtT.js} +1 -1
  98. package/dashboard/dist/assets/{useCurrency-ilbtKs16.js → useCurrency-CwCGJLHb.js} +1 -1
  99. package/dashboard/dist/assets/{usePositioner-M35LtCGR.js → usePositioner-BqHtRIrE.js} +1 -1
  100. package/dashboard/dist/assets/{useTokenFormat-BtTQjv7z.js → useTokenFormat-BUNBUX-x.js} +1 -1
  101. package/dashboard/dist/assets/{zap-5xemt9EH.js → zap-C0RFs2qQ.js} +1 -1
  102. package/dashboard/dist/index.html +1 -1
  103. package/dashboard/dist/ip-check.html +1 -1
  104. package/dashboard/dist/leaderboard.html +1 -1
  105. package/dashboard/dist/share.html +1 -1
  106. package/package.json +1 -1
  107. package/src/commands/sync.js +2 -2
  108. package/src/lib/limits-cache.js +475 -0
  109. package/src/lib/pricing/curated-overrides.json +24 -0
  110. package/src/lib/pricing/matcher.js +4 -1
  111. package/src/lib/pricing/seed-snapshot.json +1 -1
  112. package/src/lib/rollout.js +261 -102
  113. package/src/lib/usage-limits.js +370 -1063
  114. package/dashboard/dist/assets/DialogDescription--_wg9JdV.js +0 -1
  115. package/dashboard/dist/assets/use-reduced-motion-Cy21hg3t.js +0 -1
@@ -11641,26 +11641,14 @@ async function parseWorkbuddyIncremental({
11641
11641
  const startOffset = stat.size < prevSize || inodeChanged ? 0 : prevSize;
11642
11642
  if (stat.size <= startOffset) continue;
11643
11643
 
11644
- let stream;
11645
- try {
11646
- stream = fssync.createReadStream(filePath, {
11647
- encoding: "utf8",
11648
- start: startOffset,
11649
- });
11650
- } catch { continue; }
11651
- const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
11652
-
11653
- for await (const line of rl) {
11654
- if (!line || !line.trim()) continue;
11655
- let entry;
11656
- try { entry = JSON.parse(line); } catch { continue; }
11657
- if (!entry || typeof entry !== "object") continue;
11644
+ const committedEndOffset = await scanOmpLikeTranscript(filePath, startOffset, OMP_LIKE_MAX_RECORD_BYTES, (entry) => {
11645
+ if (!entry || typeof entry !== "object") return;
11658
11646
 
11659
11647
  // Usage is carried on ANY record with providerData.rawUsage — assistant
11660
11648
  // messages AND function_call records. Aggregate them all; dedup per id.
11661
11649
  const provider = entry.providerData;
11662
11650
  const rawUsage = provider && typeof provider === "object" ? provider.rawUsage : null;
11663
- if (!rawUsage || typeof rawUsage !== "object") continue;
11651
+ if (!rawUsage || typeof rawUsage !== "object") return;
11664
11652
 
11665
11653
  const sessionId =
11666
11654
  typeof entry.sessionId === "string" && entry.sessionId
@@ -11668,10 +11656,10 @@ async function parseWorkbuddyIncremental({
11668
11656
  : path.basename(filePath, ".jsonl");
11669
11657
  const sqliteSession = sqliteSessions[sessionId];
11670
11658
  if (sqliteSession?.detailed || tracedSessionIds.has(sessionId)) {
11671
- continue;
11659
+ return;
11672
11660
  }
11673
11661
  const tsMs =
11674
- Number.isFinite(Number(entry.timestamp)) && Number(entry.timestamp) > 0
11662
+ Number.isFinite(new Date(Number(entry.timestamp)).getTime()) && Number(entry.timestamp) > 0
11675
11663
  ? Number(entry.timestamp)
11676
11664
  : null;
11677
11665
  // One usage record per LLM round-trip; providerData.messageId is the
@@ -11688,24 +11676,24 @@ async function parseWorkbuddyIncremental({
11688
11676
  : tsMs != null
11689
11677
  ? `${sessionId}:${tsMs}`
11690
11678
  : null;
11691
- if (!messageId) continue;
11692
- if (seenIds.has(messageId)) continue;
11679
+ if (!messageId) return;
11680
+ if (seenIds.has(messageId)) return;
11693
11681
 
11694
11682
  recordsProcessed++;
11695
11683
 
11696
11684
  const delta = workbuddyUsageDelta(rawUsage);
11697
11685
  if (delta.total_tokens === 0) {
11698
11686
  seenIds.add(messageId);
11699
- continue;
11687
+ return;
11700
11688
  }
11701
11689
 
11702
11690
  if (tsMs == null) {
11703
11691
  seenIds.add(messageId);
11704
- continue;
11692
+ return;
11705
11693
  }
11706
11694
  const tsIso = new Date(tsMs).toISOString();
11707
11695
  const bucketStart = toUtcHalfHourStart(tsIso);
11708
- if (!bucketStart) continue;
11696
+ if (!bucketStart) return;
11709
11697
 
11710
11698
  const model =
11711
11699
  normalizeModelInput(provider.model) ||
@@ -11735,14 +11723,14 @@ async function parseWorkbuddyIncremental({
11735
11723
  bucketsQueued: touchedBuckets.size,
11736
11724
  });
11737
11725
  }
11738
- }
11726
+ }, { recoverReadErrors: true });
11739
11727
 
11740
- let postStat = stat;
11741
- try { postStat = fssync.statSync(filePath); } catch {}
11728
+ // As in the project pass, acknowledge only records consumed in full. An
11729
+ // unfinished tail or an append after EOF must remain visible next sync.
11742
11730
  fileOffsets[filePath] = {
11743
- size: postStat.size,
11744
- mtimeMs: postStat.mtimeMs,
11745
- ino: postStat.ino,
11731
+ size: committedEndOffset,
11732
+ mtimeMs: stat.mtimeMs,
11733
+ ino: stat.ino,
11746
11734
  };
11747
11735
  }
11748
11736
 
@@ -11850,7 +11838,7 @@ async function parseWorkbuddyIncremental({
11850
11838
  if (sqliteSession?.detailed || tracedSessionIds.has(sessionId)) return;
11851
11839
 
11852
11840
  const tsMs =
11853
- Number.isFinite(Number(entry.timestamp)) && Number(entry.timestamp) > 0
11841
+ Number.isFinite(new Date(Number(entry.timestamp)).getTime()) && Number(entry.timestamp) > 0
11854
11842
  ? Number(entry.timestamp)
11855
11843
  : null;
11856
11844
  if (tsMs == null) return;
@@ -11890,6 +11878,7 @@ async function parseWorkbuddyIncremental({
11890
11878
  addTotals(projectBucket.totals, delta);
11891
11879
  projectTouchedBuckets.add(projectBucketKey(projectKey, source, bucketStart));
11892
11880
  },
11881
+ { recoverReadErrors: true },
11893
11882
  );
11894
11883
 
11895
11884
  // Commit only past records that were read in full: a record appended
@@ -16891,7 +16880,7 @@ function ompEntryTimestampMs(msg, entry) {
16891
16880
  // records are orders of magnitude smaller.
16892
16881
  const OMP_LIKE_MAX_RECORD_BYTES = 64 * 1024 * 1024;
16893
16882
 
16894
- async function scanOmpLikeTranscript(filePath, startOffset, maxRecordBytes, onEntry) {
16883
+ async function scanOmpLikeTranscript(filePath, startOffset, maxRecordBytes, onEntry, { recoverReadErrors = false } = {}) {
16895
16884
  let stream;
16896
16885
  try {
16897
16886
  stream = fssync.createReadStream(filePath, { start: startOffset });
@@ -16900,26 +16889,33 @@ async function scanOmpLikeTranscript(filePath, startOffset, maxRecordBytes, onEn
16900
16889
  }
16901
16890
  let scannedEndOffset = startOffset;
16902
16891
  let committedEndOffset = startOffset;
16903
- for await (const record of physicalJsonlRecords(stream, {
16904
- invalidUtf8: "record",
16905
- maxRecordBytes,
16906
- })) {
16907
- scannedEndOffset += record.physicalBytes;
16908
- if (record.terminated) {
16909
- committedEndOffset = scannedEndOffset;
16910
- if (record.oversized || !record.utf8Valid || !record.line || !record.line.trim()) continue;
16892
+ try {
16893
+ for await (const record of physicalJsonlRecords(stream, {
16894
+ invalidUtf8: "record",
16895
+ maxRecordBytes,
16896
+ })) {
16897
+ scannedEndOffset += record.physicalBytes;
16898
+ if (record.terminated) {
16899
+ committedEndOffset = scannedEndOffset;
16900
+ if (record.oversized || !record.utf8Valid || !record.line || !record.line.trim()) continue;
16901
+ let entry;
16902
+ try { entry = JSON.parse(record.line); } catch { continue; }
16903
+ onEntry(entry);
16904
+ continue;
16905
+ }
16906
+ // A final incomplete record may still be in the process of being written.
16907
+ if (record.oversized || !record.utf8Valid || !record.line || !record.line.trim()) break;
16911
16908
  let entry;
16912
- try { entry = JSON.parse(record.line); } catch { continue; }
16909
+ try { entry = JSON.parse(record.line); } catch { break; }
16913
16910
  onEntry(entry);
16914
- continue;
16911
+ committedEndOffset = scannedEndOffset;
16912
+ break;
16915
16913
  }
16916
- // A final incomplete record may still be in the process of being written.
16917
- if (record.oversized || !record.utf8Valid || !record.line || !record.line.trim()) break;
16918
- let entry;
16919
- try { entry = JSON.parse(record.line); } catch { break; }
16920
- onEntry(entry);
16921
- committedEndOffset = scannedEndOffset;
16922
- break;
16914
+ } catch (error) {
16915
+ // WorkBuddy can publish complete records and continue with other files or
16916
+ // fallbacks after a read failure. Unfinished bytes are never acknowledged.
16917
+ // Do not swallow callback/parser errors, or change other readers' policy.
16918
+ if (!recoverReadErrors || stream.errored !== error) throw error;
16923
16919
  }
16924
16920
  return committedEndOffset;
16925
16921
  }
@@ -24394,6 +24390,9 @@ async function parseTraeCnApiIncremental({
24394
24390
  const DSH_SESSION_LOG_MAX_BYTES = 64 * 1024 * 1024;
24395
24391
  const DSH_SESSION_TEXT_MAX_BYTES = 128 * 1024 * 1024;
24396
24392
  const DSH_SOURCE = "dsh";
24393
+ // v2 excludes fork seeds and replayed assistant identities. Old ledgers need
24394
+ // a verified replacement, not a reset of the provider-wide hourly totals.
24395
+ const DSH_ACCOUNTING_VERSION = 2;
24397
24396
 
24398
24397
  // Precedence mirrors the harness's own resolveDshHome: an explicit
24399
24398
  // TokenTracker override, then the harness's $DSH_HOME, then `~/.dsh`. Tests
@@ -24760,20 +24759,48 @@ function legacyDshContributionsFromSnapshot(snapshot, previousState) {
24760
24759
  previousState.lastSeq < -1 ||
24761
24760
  previousState.inode !== snapshot.inode ||
24762
24761
  !Number.isFinite(previousState.size) ||
24763
- snapshot.size < previousState.size
24762
+ snapshot.size < previousState.size ||
24763
+ (snapshot.size === previousState.size && snapshot.mtimeMs !== previousState.mtimeMs)
24764
24764
  ) {
24765
24765
  return null;
24766
24766
  }
24767
24767
  const parsed = extractDshSessionUsage(snapshot.text, -1);
24768
- if (!parsed.complete || parsed.sessionId !== previousState.sessionId) return null;
24768
+ if (!parsed.complete || parsed.sessionId !== previousState.sessionId ||
24769
+ parsed.maxSeq < previousState.lastSeq) return null;
24769
24770
  // The pre-ledger parser replayed unknown-sequence usage on every pass, so
24770
24771
  // there is no safe prefix boundary for such a record. Defer instead.
24771
- if (parsed.deltas.some((delta) => !Number.isSafeInteger(delta.seq))) return null;
24772
+ const priorDeltas = previousState.accountingVersion === DSH_ACCOUNTING_VERSION
24773
+ ? parsed.deltas : parsed.rawDeltas;
24774
+ if (priorDeltas.some((delta) => !Number.isSafeInteger(delta.seq))) return null;
24772
24775
  return dshContributionsFromDeltas(
24773
- parsed.deltas.filter((delta) => delta.seq <= previousState.lastSeq),
24776
+ priorDeltas.filter((delta) => delta.seq <= previousState.lastSeq),
24774
24777
  );
24775
24778
  }
24776
24779
 
24780
+ // When the old and new artifacts differ, aggregate equality alone does not
24781
+ // establish that the replacement contains the historical events. Match the
24782
+ // count-only metadata as a multiset, allowing seq rewrites but no lost events.
24783
+ function dshRawUsageCoversPrefix(candidate, snapshot, previousState) {
24784
+ if (!snapshot?.text || !previousState) return false;
24785
+ const prefix = extractDshSessionUsage(snapshot.text).rawDeltas
24786
+ .filter((delta) => delta.seq != null && delta.seq <= previousState.lastSeq);
24787
+ const key = (delta) => JSON.stringify([
24788
+ delta.messageId, delta.model, delta.timeMs, delta.totals,
24789
+ ]);
24790
+ const available = new Map();
24791
+ for (const delta of candidate || []) {
24792
+ const identity = key(delta);
24793
+ available.set(identity, (available.get(identity) || 0) + 1);
24794
+ }
24795
+ for (const delta of prefix) {
24796
+ const identity = key(delta);
24797
+ const count = available.get(identity) || 0;
24798
+ if (!count) return false;
24799
+ available.set(identity, count - 1);
24800
+ }
24801
+ return true;
24802
+ }
24803
+
24777
24804
  function dshContributionsCoverPrior(prior, candidate) {
24778
24805
  for (const [key, previous] of Object.entries(prior || {})) {
24779
24806
  const current = candidate?.[key];
@@ -25167,17 +25194,20 @@ function isCompleteDshJsonLine(raw) {
25167
25194
  return index === text.length;
25168
25195
  }
25169
25196
 
25170
- // Parse one session log's plaintext into usage deltas, skipping events whose
25171
- // seq is at or below the watermark. Returns the deltas (each carrying the
25172
- // sequence, model and epoch-ms timestamp), the highest complete seq seen, the
25173
- // session id, and whether every non-empty JSONL line was complete.
25174
- function extractDshSessionUsage(text, lastSeq = -1) {
25175
- const deltas = [];
25197
+ // Read routing metadata and counts only; never materialize message content.
25198
+ // rawDeltas preserves the former seq-only interpretation for migration proof.
25199
+ // deltas excludes the inherited prefix and repeated assistant message ids.
25200
+ function extractDshSessionUsage(text, lastSeq = -1, lastLine = -1) {
25201
+ const rawDeltas = [];
25176
25202
  const watermark = Number.isFinite(lastSeq) ? lastSeq : -1;
25177
25203
  let maxSeq = watermark;
25204
+ let maxLine = -1;
25178
25205
  let sessionId = null;
25179
25206
  let headerModel = null;
25180
25207
  let complete = true;
25208
+ let isSeeded = false;
25209
+ let seedLength = null;
25210
+ let inheritedThrough = -1;
25181
25211
 
25182
25212
  const lines = String(text || "").split("\n");
25183
25213
  for (let i = 0; i < lines.length; i++) {
@@ -25185,29 +25215,37 @@ function extractDshSessionUsage(text, lastSeq = -1) {
25185
25215
  if (!line || !line.trim()) continue;
25186
25216
  if (!isCompleteDshJsonLine(line)) {
25187
25217
  complete = false;
25188
- // A later event cannot make this gap safe to acknowledge. Stop at the
25189
- // complete prefix so a repaired record is retried before seq advances.
25218
+ // Do not acknowledge records after a corrupt/torn interior line.
25190
25219
  break;
25191
25220
  }
25221
+ maxLine = i;
25192
25222
  const eventType = parseDshJsonString(findDshJsonProperty(line, "type"));
25193
25223
  if (!eventType) continue;
25194
-
25224
+ const data = findDshJsonProperty(line, "data");
25195
25225
  if (eventType === "session" || eventType === "session/start") {
25196
- const id =
25197
- parseDshJsonString(findDshJsonProperty(line, "id")) ||
25198
- parseDshJsonString(findDshJsonProperty(findDshJsonProperty(line, "data"), "id")) ||
25199
- parseDshJsonString(findDshJsonProperty(findDshJsonProperty(line, "data"), "sessionId"));
25226
+ const id = parseDshJsonString(findDshJsonProperty(line, "id")) ||
25227
+ parseDshJsonString(findDshJsonProperty(data, "id")) ||
25228
+ parseDshJsonString(findDshJsonProperty(data, "sessionId"));
25200
25229
  if (id) sessionId = id;
25230
+ isSeeded = findDshJsonProperty(line, "isSeeded") === "true" ||
25231
+ findDshJsonProperty(data, "isSeeded") === "true";
25232
+ const topLength = parseDshJsonNumber(findDshJsonProperty(line, "seedLength"));
25233
+ const length = Number.isFinite(topLength) ? topLength :
25234
+ parseDshJsonNumber(findDshJsonProperty(data, "seedLength"));
25235
+ if (Number.isSafeInteger(length) && length >= 0) seedLength = length;
25201
25236
  continue;
25202
25237
  }
25203
25238
 
25204
25239
  const seq = parseDshJsonNumber(findDshJsonProperty(line, "seq"));
25205
25240
  const seqKnown = Number.isFinite(seq) && seq >= 0;
25206
25241
  if (seqKnown && seq > maxSeq) maxSeq = seq;
25207
-
25208
- const data = findDshJsonProperty(line, "data");
25209
25242
  if (!data) continue;
25210
-
25243
+ if (eventType === "session/end-seed") {
25244
+ // Nested forks contain earlier markers. The LAST explicitly inherited
25245
+ // marker is the boundary; ordinary resume markers (data:{}) are not.
25246
+ if (findDshJsonProperty(data, "inherited") === "true") inheritedThrough = i;
25247
+ continue;
25248
+ }
25211
25249
  if (eventType === "request/header") {
25212
25250
  const header = findDshJsonProperty(data, "header");
25213
25251
  const config = findDshJsonProperty(header, "config");
@@ -25216,39 +25254,46 @@ function extractDshSessionUsage(text, lastSeq = -1) {
25216
25254
  if (normalized) headerModel = normalized;
25217
25255
  continue;
25218
25256
  }
25219
-
25220
25257
  if (eventType !== "assistant/message" && eventType !== "message/assistant") continue;
25221
- if (seqKnown && seq <= watermark) continue;
25222
-
25223
25258
  const message = findDshJsonProperty(data, "message");
25224
25259
  const source = message ? findDshJsonProperty(message, "source") : null;
25225
25260
  const model = normalizeDshModelName(
25226
25261
  (source && parseDshJsonString(findDshJsonProperty(source, "model"))) ||
25227
25262
  parseDshJsonString(findDshJsonProperty(data, "model")),
25228
25263
  ) || headerModel;
25229
- const totals = dshUsageToTotals(
25230
- parseDshUsageSlice(
25231
- findDshJsonProperty(data, "usage") || findDshJsonProperty(line, "usage"),
25232
- ),
25233
- );
25264
+ const totals = dshUsageToTotals(parseDshUsageSlice(
25265
+ findDshJsonProperty(data, "usage") || findDshJsonProperty(line, "usage"),
25266
+ ));
25234
25267
  if (!model || !totals) continue;
25235
-
25236
- const timeMs =
25237
- parseDshJsonNumber(findDshJsonProperty(line, "time")) ||
25268
+ const timeMs = parseDshJsonNumber(findDshJsonProperty(line, "time")) ||
25238
25269
  parseDshJsonNumber(findDshJsonProperty(line, "timestamp")) ||
25239
25270
  parseDshJsonNumber(findDshJsonProperty(data, "time")) ||
25240
25271
  parseDshJsonNumber(findDshJsonProperty(data, "timestamp"));
25241
25272
  if (!Number.isFinite(timeMs) || timeMs <= 0) continue;
25242
-
25243
- deltas.push({ seq: seqKnown ? seq : null, model, timeMs, totals });
25273
+ const id = parseDshJsonString(findDshJsonProperty(message, "id"));
25274
+ rawDeltas.push({ seq: seqKnown ? seq : null, line: i,
25275
+ messageId: id?.trim() || null, model, timeMs, totals });
25244
25276
  }
25245
25277
 
25246
- return { deltas, maxSeq, sessionId, complete };
25278
+ const seedComplete = !isSeeded || inheritedThrough >= 0 || seedLength != null;
25279
+ const seen = new Set();
25280
+ const deltas = rawDeltas.filter((delta) => {
25281
+ if (delta.messageId) {
25282
+ if (seen.has(delta.messageId)) return false;
25283
+ // Claim before applying the watermark: a replay at a newer seq is not new.
25284
+ seen.add(delta.messageId);
25285
+ }
25286
+ if (!seedComplete || delta.line <= inheritedThrough) return false;
25287
+ // Legacy seedLength is an exclusive seq boundary, not a line count.
25288
+ if (seedLength != null && (delta.seq == null || delta.seq < seedLength)) return false;
25289
+ return delta.seq != null ? delta.seq > watermark : delta.line > lastLine;
25290
+ });
25291
+ return { deltas, rawDeltas, maxSeq, maxLine, sessionId, complete, seedComplete };
25247
25292
  }
25248
25293
 
25249
25294
  // Incremental parser entrypoint. Mirrors the other passive JSONL readers:
25250
25295
  // identity-check each file (inode/size/mtime), re-read + re-parse only when it
25251
- // changed, dedup via the per-file seq watermark, accumulate into hourly
25296
+ // changed, dedup via message ownership plus seq/line watermarks, accumulate into hourly
25252
25297
  // buckets, then flush touched buckets to the queue. Session contribution ledgers
25253
25298
  // make artifact-path migrations replace one session instead of resetting DSH.
25254
25299
  async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgress }) {
@@ -25262,6 +25307,9 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25262
25307
  const storedSessionState =
25263
25308
  dshState.sessions && typeof dshState.sessions === "object" ? dshState.sessions : {};
25264
25309
  const sessionState = { ...storedSessionState };
25310
+ // Never prune ownership when an origin transcript disappears. Only opaque
25311
+ // message ids and session identities are persisted, not conversation text.
25312
+ const messageOwners = { ...(dshState.messageOwners || {}) };
25265
25313
 
25266
25314
  const hourlyState = normalizeHourlyState(cursors?.hourly);
25267
25315
  // normalizeHourlyState preserves bucket objects for the other incremental
@@ -25287,6 +25335,22 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25287
25335
  const presentFiles = new Set(files);
25288
25336
  const total = files.length;
25289
25337
  const deferredFilePaths = new Set();
25338
+ const legacyAliases = new Map();
25339
+ for (const [filePath, state] of Object.entries(fileState)) {
25340
+ if (!state?.sessionId || state.accountingVersion === DSH_ACCOUNTING_VERSION) continue;
25341
+ const aliases = legacyAliases.get(state.sessionId) || [];
25342
+ aliases.push(filePath);
25343
+ legacyAliases.set(state.sessionId, aliases);
25344
+ }
25345
+ for (const aliases of legacyAliases.values()) {
25346
+ if (aliases.length < 2) continue;
25347
+ // Older cursors cannot prove whether these alias paths were counted once
25348
+ // or independently. Keep both baselines instead of guessing a subtraction.
25349
+ for (const filePath of aliases) {
25350
+ deferredFilePaths.add(filePath);
25351
+ deferredMigrationPaths.set(filePath, "ambiguous-legacy-session-aliases");
25352
+ }
25353
+ }
25290
25354
  let recordsProcessed = 0;
25291
25355
  let eventsAggregated = 0;
25292
25356
 
@@ -25344,6 +25408,9 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25344
25408
  const needsLedgerBackfill = Boolean(
25345
25409
  previousSessionId && !previousSessionContributions && !previousFileContributions,
25346
25410
  );
25411
+ const needsAccountingUpgrade = Boolean(
25412
+ prev && prev.accountingVersion !== DSH_ACCOUNTING_VERSION,
25413
+ );
25347
25414
  let snapshot;
25348
25415
  let parsed;
25349
25416
  let fullParsed = null;
@@ -25352,7 +25419,7 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25352
25419
 
25353
25420
  try {
25354
25421
  snapshot = await readDshSessionSnapshot(filePath, {
25355
- previous: needsLedgerBackfill ? null : prev,
25422
+ previous: needsLedgerBackfill || needsAccountingUpgrade ? null : prev,
25356
25423
  });
25357
25424
  if (!snapshot) {
25358
25425
  // A resolver-selected replacement can disappear between discovery and
@@ -25360,10 +25427,11 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25360
25427
  // contribution. This is deliberately separate from a thrown read
25361
25428
  // error because a missing path returns null from the snapshot helper.
25362
25429
  deferStaleMigrationPaths(filePath, "replacement-missing");
25430
+ if (needsAccountingUpgrade) deferredMigrationPaths.set(filePath, "history-missing");
25363
25431
  reportProgress(idx, recordsProcessed, eventsAggregated, total);
25364
25432
  continue;
25365
25433
  }
25366
- if (snapshot.unchanged && !needsLedgerBackfill) {
25434
+ if (snapshot.unchanged && !needsLedgerBackfill && !needsAccountingUpgrade) {
25367
25435
  reportProgress(idx, recordsProcessed, eventsAggregated, total);
25368
25436
  continue;
25369
25437
  }
@@ -25385,18 +25453,18 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25385
25453
  ),
25386
25454
  );
25387
25455
  const lastSeq = fileReset || !Number.isFinite(prev?.lastSeq) ? -1 : prev.lastSeq;
25388
- parsed = extractDshSessionUsage(snapshot.text, lastSeq);
25456
+ parsed = extractDshSessionUsage(snapshot.text, lastSeq, fileReset ? -1 : prev?.lastLine);
25389
25457
  sessionChanged = Boolean(
25390
25458
  prev &&
25391
25459
  parsed.sessionId &&
25392
25460
  parsed.sessionId !== previousSessionId,
25393
25461
  );
25394
25462
  if (sessionChanged) parsed = extractDshSessionUsage(snapshot.text, -1);
25395
- if (!prev || needsLedgerBackfill || sessionChanged || fileReset) {
25463
+ if (!prev || needsLedgerBackfill || needsAccountingUpgrade || sessionChanged || fileReset) {
25396
25464
  fullParsed = extractDshSessionUsage(snapshot.text, -1);
25397
- if (!prev || sessionChanged || fileReset) parsed = fullParsed;
25465
+ if (!prev || needsAccountingUpgrade || sessionChanged || fileReset) parsed = fullParsed;
25398
25466
  }
25399
- if (!parsed.complete) snapshot.mtimeMs = -1;
25467
+ if (!parsed.complete || !parsed.seedComplete) snapshot.mtimeMs = -1;
25400
25468
  } catch (error) {
25401
25469
  if (process.env.TOKENTRACKER_DEBUG) {
25402
25470
  process.stderr.write(`[dsh] skipped ${filePath}: ${error?.message || error}\n`);
@@ -25405,11 +25473,22 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25405
25473
  // selected file list. Keep that cursor until the replacement is readable
25406
25474
  // so a failed migration cannot turn into an untracked double-count.
25407
25475
  deferStaleMigrationPaths(filePath, "replacement-read-failed");
25476
+ if (needsAccountingUpgrade) deferredMigrationPaths.set(filePath, "history-read-failed");
25408
25477
  reportProgress(idx, recordsProcessed, eventsAggregated, total);
25409
25478
  continue;
25410
25479
  }
25411
25480
 
25412
25481
  const sessionId = typeof parsed.sessionId === "string" ? parsed.sessionId : null;
25482
+ if ((legacyAliases.get(sessionId)?.length || 0) > 1) {
25483
+ reportProgress(idx, recordsProcessed, eventsAggregated, total);
25484
+ continue;
25485
+ }
25486
+ if (prev && !sessionId && (previousSessionId || needsAccountingUpgrade)) {
25487
+ deferredFilePaths.add(filePath);
25488
+ deferredMigrationPaths.set(filePath, "replacement-session-id-missing");
25489
+ reportProgress(idx, recordsProcessed, eventsAggregated, total);
25490
+ continue;
25491
+ }
25413
25492
  const stalePathWithoutId = sessionId
25414
25493
  ? null
25415
25494
  : Object.keys(fileState).find(
@@ -25427,8 +25506,15 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25427
25506
  const stalePath = staleFileForSession(sessionId, filePath);
25428
25507
  const previousPath =
25429
25508
  session?.lastPath && session.lastPath !== filePath ? session.lastPath : stalePath;
25509
+ if (previousPath && previousPath !== filePath) {
25510
+ // Another artifact currently owns this session ledger. Replacing that
25511
+ // ledger needs the full candidate, even if this alias has its own cursor.
25512
+ fullParsed = extractDshSessionUsage(snapshot.text);
25513
+ parsed = fullParsed;
25514
+ }
25430
25515
  const resetCurrentFile = Boolean(fileReset && !sessionChanged);
25431
- const replacementPath = previousPath || (resetCurrentFile ? filePath : null);
25516
+ const upgradeCurrentFile = needsAccountingUpgrade && !sessionChanged;
25517
+ const replacementPath = previousPath || (resetCurrentFile || upgradeCurrentFile ? filePath : null);
25432
25518
  const sessionContributions = storedDshContributions(session);
25433
25519
  const staleContributions = storedDshContributions(stalePath && fileState[stalePath]);
25434
25520
  const fileContributions = storedDshContributions(prev);
@@ -25451,7 +25537,9 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25451
25537
  // A pure rename preserves the same bytes. Reusing the parsed full
25452
25538
  // contribution is safe here because replacement below subtracts and
25453
25539
  // re-adds it; no sequence watermark is transferred.
25454
- oldContributions = dshContributionsFromDeltas(parsed.deltas);
25540
+ oldContributions = dshContributionsFromDeltas(
25541
+ previousState.accountingVersion === DSH_ACCOUNTING_VERSION ? parsed.deltas : parsed.rawDeltas,
25542
+ );
25455
25543
  }
25456
25544
 
25457
25545
  // Cursors written before the contribution ledger existed can still be
@@ -25464,10 +25552,13 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25464
25552
  const oldSnapshot = await readDshSessionSnapshot(previousPath).catch(() => null);
25465
25553
  oldContributions = legacyDshContributionsFromSnapshot(oldSnapshot, oldState);
25466
25554
  }
25555
+ if (sessionId && upgradeCurrentFile && !oldContributions) {
25556
+ oldContributions = legacyDshContributionsFromSnapshot(snapshot, prev);
25557
+ }
25467
25558
  const replaceSession = Boolean(
25468
25559
  sessionId &&
25469
25560
  oldContributions &&
25470
- (!prev || resetCurrentFile || (previousPath && previousPath !== filePath)),
25561
+ (!prev || resetCurrentFile || upgradeCurrentFile || (previousPath && previousPath !== filePath)),
25471
25562
  );
25472
25563
  const oldContributionCount = Object.keys(oldContributions || {}).length;
25473
25564
  if (sessionId && replacementPath && !oldContributions) {
@@ -25479,20 +25570,25 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25479
25570
  }
25480
25571
  if (
25481
25572
  replaceSession &&
25482
- (!parsed.complete || (oldContributionCount > 0 && parsed.deltas.length === 0))
25573
+ (!parsed.complete || !parsed.seedComplete || (oldContributionCount > 0 && parsed.rawDeltas.length === 0))
25483
25574
  ) {
25484
25575
  // A readable header-only or torn replacement is not authoritative. Keep
25485
25576
  // the old contribution until a complete replacement with usage arrives.
25486
25577
  deferredFilePaths.add(replacementPath);
25487
25578
  deferredMigrationPaths.set(
25488
25579
  replacementPath,
25489
- parsed.complete ? "replacement-has-no-usage" : "replacement-incomplete",
25580
+ !parsed.complete ? "replacement-incomplete" :
25581
+ !parsed.seedComplete ? "seed-boundary-unavailable" : "replacement-has-no-usage",
25490
25582
  );
25491
25583
  if (resetCurrentFile) fileState[filePath] = { ...prev, mtimeMs: -1 };
25492
25584
  reportProgress(idx, recordsProcessed, eventsAggregated, total);
25493
25585
  continue;
25494
25586
  }
25495
- const candidateContributions = dshContributionsFromDeltas(parsed.deltas);
25587
+ // A correction may legitimately be lower or zero. Verify that the FULL
25588
+ // raw log still accounts for the old ledger before subtracting anything.
25589
+ // Comparing corrected usage to the old totals would reject the very repair
25590
+ // this migration must make; accepting a cropped log would erase history.
25591
+ const candidateContributions = dshContributionsFromDeltas(parsed.rawDeltas);
25496
25592
  if (
25497
25593
  replaceSession &&
25498
25594
  oldContributionCount > 0 &&
@@ -25518,6 +25614,46 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25518
25614
  continue;
25519
25615
  }
25520
25616
 
25617
+ const owner = sessionId || dshSessionDirectoryKey(filePath);
25618
+ const acceptedDeltas = parsed.deltas.filter((delta) => {
25619
+ if (!delta.messageId) return true;
25620
+ const priorOwner = messageOwners[`a:${delta.messageId}`];
25621
+ return !priorOwner || (replaceSession && priorOwner === owner);
25622
+ });
25623
+ // Known ownership is stronger evidence than bucket totals. An equal or
25624
+ // larger divergent alias can still omit a previously counted message.
25625
+ const ownedIds = new Set(parsed.deltas.map((delta) => delta.messageId).filter(Boolean));
25626
+ const dropsOwnedId = replaceSession && Object.entries(messageOwners).some(([key, value]) =>
25627
+ value === owner && !ownedIds.has(key.slice(2)),
25628
+ );
25629
+ if (dropsOwnedId) {
25630
+ deferredFilePaths.add(replacementPath);
25631
+ deferredMigrationPaths.set(replacementPath, "replacement-drops-owned-message");
25632
+ reportProgress(idx, recordsProcessed, eventsAggregated, total);
25633
+ continue;
25634
+ }
25635
+ const correctedContributions = dshContributionsFromDeltas(acceptedDeltas);
25636
+ if (replaceSession && !dshContributionsCoverPrior(oldContributions, correctedContributions)) {
25637
+ // Aggregate >= is not proof: a cropped transcript can substitute larger
25638
+ // unrelated/replayed usage for a lost event. Reconstruct EXACTLY the
25639
+ // acknowledged prefix before allowing a downward/zero correction.
25640
+ const baselineState = previousState || prev;
25641
+ let baselineSnapshot = snapshot;
25642
+ if (previousPath && !sameDshSessionMetadata(baselineState, snapshot)) {
25643
+ baselineSnapshot = await readDshSessionSnapshot(previousPath).catch(() => null);
25644
+ }
25645
+ const proven = legacyDshContributionsFromSnapshot(baselineSnapshot, baselineState);
25646
+ if (baselineState?.accountingVersion === DSH_ACCOUNTING_VERSION ||
25647
+ !proven || !dshContributionsCoverPrior(oldContributions, proven) ||
25648
+ !dshContributionsCoverPrior(proven, oldContributions) ||
25649
+ !dshRawUsageCoversPrefix(parsed.rawDeltas, baselineSnapshot, baselineState)) {
25650
+ deferredFilePaths.add(replacementPath);
25651
+ deferredMigrationPaths.set(replacementPath, "historical-prefix-unverified");
25652
+ reportProgress(idx, recordsProcessed, eventsAggregated, total);
25653
+ continue;
25654
+ }
25655
+ }
25656
+
25521
25657
  if (replaceSession) {
25522
25658
  applyDshContributions({
25523
25659
  hourlyState,
@@ -25530,18 +25666,24 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25530
25666
 
25531
25667
  let nextContributions = replaceSession
25532
25668
  ? {}
25533
- : sessionContributions || fileContributions || {};
25534
- if (fullParsed) {
25535
- nextContributions = dshContributionsFromDeltas(fullParsed.deltas);
25536
- }
25537
-
25538
- for (const delta of parsed.deltas) {
25669
+ : sessionChanged ? {} : sessionContributions || fileContributions || {};
25670
+ // Backfill missing ledgers without re-adding the acknowledged prefix.
25671
+ if (fullParsed && !replaceSession && prev && !sessionChanged && !fileReset) {
25672
+ nextContributions = dshContributionsFromDeltas(fullParsed.deltas.filter((delta) =>
25673
+ (!delta.messageId || !messageOwners[`a:${delta.messageId}`] ||
25674
+ messageOwners[`a:${delta.messageId}`] === owner) &&
25675
+ (delta.seq != null ? delta.seq <= prev.lastSeq : delta.line <= prev.lastLine),
25676
+ ));
25677
+ }
25678
+
25679
+ for (const delta of acceptedDeltas) {
25680
+ if (delta.messageId) messageOwners[`a:${delta.messageId}`] = owner;
25539
25681
  const bucketStart = toUtcHalfHourStart(delta.timeMs);
25540
25682
  if (!bucketStart) continue;
25541
25683
  const bucket = getHourlyBucket(hourlyState, DSH_SOURCE, delta.model, bucketStart);
25542
25684
  addTotals(bucket.totals, delta.totals);
25543
25685
  touchedBuckets.add(bucketKey(DSH_SOURCE, delta.model, bucketStart));
25544
- if (!fullParsed) addDshContribution(nextContributions, delta.model, bucketStart, delta.totals);
25686
+ addDshContribution(nextContributions, delta.model, bucketStart, delta.totals);
25545
25687
  eventsAggregated += 1;
25546
25688
  }
25547
25689
 
@@ -25552,12 +25694,15 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25552
25694
  mtimeMs: snapshot.mtimeMs,
25553
25695
  sessionId,
25554
25696
  lastSeq: parsed.maxSeq,
25697
+ lastLine: parsed.maxLine,
25698
+ accountingVersion: DSH_ACCOUNTING_VERSION,
25555
25699
  contributions: nextContributions,
25556
25700
  updatedAt,
25557
25701
  };
25558
25702
  if (sessionId) {
25559
25703
  sessionState[sessionId] = {
25560
25704
  lastPath: filePath,
25705
+ accountingVersion: DSH_ACCOUNTING_VERSION,
25561
25706
  contributions: nextContributions,
25562
25707
  updatedAt,
25563
25708
  };
@@ -25575,7 +25720,11 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25575
25720
  // Retain pre-ledger identity until a replacement is verified. Its counted
25576
25721
  // usage survives file deletion, so elapsed time cannot authorize replay.
25577
25722
  // Current ledgers retain the same identity in sessionState instead.
25578
- if (legacySessionId && !storedDshContributions(state) && !sessionState[legacySessionId]) {
25723
+ if ((legacySessionId && !storedDshContributions(state) && !sessionState[legacySessionId]) ||
25724
+ (!legacySessionId && state.accountingVersion !== DSH_ACCOUNTING_VERSION)) {
25725
+ if (state.accountingVersion !== DSH_ACCOUNTING_VERSION) {
25726
+ deferredMigrationPaths.set(filePath, "history-unverified");
25727
+ }
25579
25728
  if (!Number.isFinite(Number(state?.missingSince))) {
25580
25729
  fileState[filePath] = { ...state, missingSince: nowMs };
25581
25730
  }
@@ -25584,10 +25733,20 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25584
25733
  delete fileState[filePath];
25585
25734
  }
25586
25735
 
25736
+ // Missing/corrupt historical transcripts retain their ledger and are made
25737
+ // visible as deferrals, never treated as a provider-wide reset opportunity.
25738
+ for (const [id, session] of Object.entries(sessionState)) {
25739
+ if (session?.accountingVersion !== DSH_ACCOUNTING_VERSION) {
25740
+ const key = session?.lastPath || `session:${id}`;
25741
+ if (!deferredMigrationPaths.has(key)) deferredMigrationPaths.set(key, "history-unverified");
25742
+ }
25743
+ }
25587
25744
  const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
25588
25745
  const updatedAt = new Date().toISOString();
25589
25746
  hourlyState.updatedAt = updatedAt;
25590
25747
  cursors.hourly = hourlyState;
25748
+ dshState.accountingVersion = DSH_ACCOUNTING_VERSION;
25749
+ dshState.messageOwners = messageOwners;
25591
25750
  dshState.files = fileState;
25592
25751
  dshState.sessions = sessionState;
25593
25752
  if (deferredMigrationPaths.size > 0) {