tokentracker-cli 1.5.13 → 1.5.14

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 (111) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-Ct0vfdYb.js → AchievementBadge-DqJWve1h.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-dfoRxqOv.js → AchievementsPage-CnuBayj3.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-DHFyssU0.js → AchievementsSection-WRU5TxAB.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-ChJYZMSq.js → BadgeDetailModal-B5ikEQik.js} +1 -1
  5. package/dashboard/dist/assets/{BotAnimated-C_qaJy1-.js → BotAnimated-CaMG6j-6.js} +1 -1
  6. package/dashboard/dist/assets/{Card-BhK2nmOu.js → Card-BL3vxENk.js} +1 -1
  7. package/dashboard/dist/assets/{CloudCheckoutPage-Ele1kkut.js → CloudCheckoutPage-CsUb8CJ7.js} +1 -1
  8. package/dashboard/dist/assets/{CloudPage-C5DMqcoV.js → CloudPage-5ZiPFzeC.js} +2 -2
  9. package/dashboard/dist/assets/{CommandPalette-C_hxoIwL.js → CommandPalette-CYd9nXW-.js} +1 -1
  10. package/dashboard/dist/assets/{CommunityStatsModal-BSTqBIEU.js → CommunityStatsModal-BItox7Sz.js} +1 -1
  11. package/dashboard/dist/assets/{ConfirmModal-DI4QK5EV.js → ConfirmModal-BE1ABR4z.js} +1 -1
  12. package/dashboard/dist/assets/{Controls-zK1rzS_8.js → Controls-BRqIsaHT.js} +1 -1
  13. package/dashboard/dist/assets/{DashboardPage-BdDnmc60.js → DashboardPage-Cm0YuBlx.js} +1 -1
  14. package/dashboard/dist/assets/{DateRangePopover-DqwgxpNF.js → DateRangePopover-CRwEu66e.js} +1 -1
  15. package/dashboard/dist/assets/{DevicePage-C0fQKKuI.js → DevicePage-DFUPsGb_.js} +1 -1
  16. package/dashboard/dist/assets/{DialogClose-C_cbyHpb.js → DialogClose-DALl4GmP.js} +1 -1
  17. package/dashboard/dist/assets/DialogDescription-CO0hykis.js +1 -0
  18. package/dashboard/dist/assets/{DialogTitle-BcBpd--_.js → DialogTitle-BSR8ozyn.js} +1 -1
  19. package/dashboard/dist/assets/{ElectricLogo-Dw9gkExE.js → ElectricLogo-BtrycBUW.js} +1 -1
  20. package/dashboard/dist/assets/{FadeIn-Ci69gBGl.js → FadeIn-CfhgvLVd.js} +1 -1
  21. package/dashboard/dist/assets/{HeaderGithubStar-pb03c0kb.js → HeaderGithubStar-N8wqAdOf.js} +1 -1
  22. package/dashboard/dist/assets/{HoverTooltip-Cqd0D789.js → HoverTooltip-CwdMYIzZ.js} +1 -1
  23. package/dashboard/dist/assets/{IpCheckPage-qILbvOJn.js → IpCheckPage-DCRor9cb.js} +1 -1
  24. package/dashboard/dist/assets/{LandingPage-CCIwaAeu.js → LandingPage-s9xvTHSp.js} +2 -2
  25. package/dashboard/dist/assets/{LeaderboardPage-BwzABmKU.js → LeaderboardPage-CUj3WLMo.js} +2 -2
  26. package/dashboard/dist/assets/{LeaderboardProBadge-7kdzfw8r.js → LeaderboardProBadge-N4NOKZWj.js} +1 -1
  27. package/dashboard/dist/assets/{LeaderboardProfileModal-uMnN-IGe.js → LeaderboardProfileModal-09HiUyxm.js} +3 -3
  28. package/dashboard/dist/assets/{LeaderboardProfilePage-CQNc4MES.js → LeaderboardProfilePage-Cw9_rodG.js} +1 -1
  29. package/dashboard/dist/assets/{LimitsPage-CJ1LImRm.js → LimitsPage-F8b6kZa0.js} +1 -1
  30. package/dashboard/dist/assets/{LocalOnlyNotice-Dm0l_F0I.js → LocalOnlyNotice-D1eEzDEv.js} +1 -1
  31. package/dashboard/dist/assets/{LoginCard--tUiBVtr.js → LoginCard-0sIovG_J.js} +1 -1
  32. package/dashboard/dist/assets/{LoginModal-B1-5fFMY.js → LoginModal-BT3oMVak.js} +1 -1
  33. package/dashboard/dist/assets/{LoginPage-BYl6NnZ8.js → LoginPage-CDsXdaE5.js} +1 -1
  34. package/dashboard/dist/assets/{PetPage-DJNGJZrR.js → PetPage-BEnC1azp.js} +2 -2
  35. package/dashboard/dist/assets/{PopoverPopup-aRAdIkTs.js → PopoverPopup-Dwzp-hyf.js} +1 -1
  36. package/dashboard/dist/assets/{ProviderIcon-uiA-KBe2.js → ProviderIcon-Cu_lcuuu.js} +1 -1
  37. package/dashboard/dist/assets/{ResetPasswordPage-BZT353_T.js → ResetPasswordPage-DrpEscm1.js} +1 -1
  38. package/dashboard/dist/assets/{Select-CPC04j0g.js → Select-wIR4qvEI.js} +1 -1
  39. package/dashboard/dist/assets/{SelectItemText-D1vJVrFO.js → SelectItemText-CNuJgYps.js} +1 -1
  40. package/dashboard/dist/assets/{SelfHostPage-FL3EDqmb.js → SelfHostPage-CWNLygpL.js} +1 -1
  41. package/dashboard/dist/assets/{ServiceStatusPage-BohSSNai.js → ServiceStatusPage-BId9t16Q.js} +1 -1
  42. package/dashboard/dist/assets/{SessionsPage-Cf_HASvt.js → SessionsPage-C_klx7d8.js} +1 -1
  43. package/dashboard/dist/assets/{SettingsPage-B4ztKHnj.js → SettingsPage-nC73JaZ4.js} +3 -3
  44. package/dashboard/dist/assets/{SkillsPage-BFiQkY7O.js → SkillsPage-B2agou8Y.js} +1 -1
  45. package/dashboard/dist/assets/{TokenGalaxy-BHVdTJAP.js → TokenGalaxy-BNXUWRwa.js} +1 -1
  46. package/dashboard/dist/assets/{TooltipPopup-BMtQyFn6.js → TooltipPopup--X2kExQ0.js} +1 -1
  47. package/dashboard/dist/assets/{TrendMonitor-DN-g3CNm.js → TrendMonitor-BELHsTnz.js} +1 -1
  48. package/dashboard/dist/assets/{WidgetsPage-Bv32yu8M.js → WidgetsPage-DWp0SMXA.js} +1 -1
  49. package/dashboard/dist/assets/{WrappedPage-HVTOehNb.js → WrappedPage-DgwczKHe.js} +1 -1
  50. package/dashboard/dist/assets/{__vite-browser-external-DnYJww7c.js → __vite-browser-external-BHphZRSC.js} +2 -2
  51. package/dashboard/dist/assets/{agent-logos-BOwAJQqL.js → agent-logos-Cfx5WzU5.js} +1 -1
  52. package/dashboard/dist/assets/{arrow-left-CeXuUuES.js → arrow-left-D83SdANw.js} +1 -1
  53. package/dashboard/dist/assets/{arrow-right-DOdBG1FZ.js → arrow-right-D7ye9bmJ.js} +1 -1
  54. package/dashboard/dist/assets/{arrow-up-right-Dponssa4.js → arrow-up-right-CLmwEwwC.js} +1 -1
  55. package/dashboard/dist/assets/{calendar-days-CsxE6ot0.js → calendar-days-D1qLlvh4.js} +1 -1
  56. package/dashboard/dist/assets/{check-BroNG_vu.js → check-BHJvi4ey.js} +1 -1
  57. package/dashboard/dist/assets/{chevron-down-C5ZpGeO1.js → chevron-down-6RBPKNpt.js} +1 -1
  58. package/dashboard/dist/assets/{cloud-checkout-BtS-hZTc.js → cloud-checkout-DgWcF-at.js} +1 -1
  59. package/dashboard/dist/assets/{composite-fgJbx88k.js → composite-D-0pfyMY.js} +1 -1
  60. package/dashboard/dist/assets/{copy-CHBuwNUs.js → copy-B6mJSQsD.js} +1 -1
  61. package/dashboard/dist/assets/{download-EKd2MHkv.js → download-D31Nn-bI.js} +1 -1
  62. package/dashboard/dist/assets/{external-link-GkSo9C78.js → external-link-C_lK9S3p.js} +1 -1
  63. package/dashboard/dist/assets/{flame-Cg96qJrE.js → flame-1ZxhHshT.js} +1 -1
  64. package/dashboard/dist/assets/{format-tokens-ChG2_n4h.js → format-tokens-DJk25SkD.js} +1 -1
  65. package/dashboard/dist/assets/{gift-B3QDbWPE.js → gift-BUZzFh-1.js} +1 -1
  66. package/dashboard/dist/assets/{icons-BEAyMjau.js → icons-BVPThCa4.js} +1 -1
  67. package/dashboard/dist/assets/{index-B9a1fK8I.js → index-B-mxrdIz.js} +1 -1
  68. package/dashboard/dist/assets/{index-DFcQyAnr.js → index-C0zFsOWh.js} +1 -1
  69. package/dashboard/dist/assets/{index-B0kZThUB.js → index-CjPsyTl7.js} +1 -1
  70. package/dashboard/dist/assets/{index-Cuc8ZXgl.js → index-CzstoU6d.js} +1 -1
  71. package/dashboard/dist/assets/{info-796bNttV.js → info-DmU-vlJ6.js} +1 -1
  72. package/dashboard/dist/assets/{link-2-LYRIPLwz.js → link-2-BlBM7uw7.js} +1 -1
  73. package/dashboard/dist/assets/{loader-circle-DpKC9Uel.js → loader-circle-Dhrb_Ocy.js} +1 -1
  74. package/dashboard/dist/assets/{local-usage-transfer-Ctbuc6U4.js → local-usage-transfer-BudmMtkd.js} +1 -1
  75. package/dashboard/dist/assets/{modifiers.esm-C5YTeNW1.js → modifiers.esm-kmw1VM8W.js} +1 -1
  76. package/dashboard/dist/assets/{monitor-smartphone-DJUCfEvG.js → monitor-smartphone-DtjXNC_x.js} +1 -1
  77. package/dashboard/dist/assets/{play-LC-lnw6w.js → play-9w46lbjy.js} +1 -1
  78. package/dashboard/dist/assets/{plus-CDF0S5xd.js → plus-DAf_WGKT.js} +1 -1
  79. package/dashboard/dist/assets/{provider-display-CkITUZgR.js → provider-display-BZ0XlB2q.js} +1 -1
  80. package/dashboard/dist/assets/{react-BIpM-G7K.js → react-B01ZJuZY.js} +1 -1
  81. package/dashboard/dist/assets/{safePolygon-C3Ea423q.js → safePolygon-B6_x12Gb.js} +1 -1
  82. package/dashboard/dist/assets/{search-C-tN1mTM.js → search-D3dW9GbV.js} +1 -1
  83. package/dashboard/dist/assets/{server-DJ4gq_fT.js → server-C2XZBar7.js} +1 -1
  84. package/dashboard/dist/assets/{shield-check-f75hry9N.js → shield-check-4FX5vbS7.js} +1 -1
  85. package/dashboard/dist/assets/{skills-api-CZ_vfnRL.js → skills-api-BvW_MUyg.js} +1 -1
  86. package/dashboard/dist/assets/{sortable.esm-mLJSvq3M.js → sortable.esm-CAa_BJgh.js} +1 -1
  87. package/dashboard/dist/assets/{store-CZV1KXw2.js → store-2AX2MNkz.js} +1 -1
  88. package/dashboard/dist/assets/{terminal-B9vOwCl9.js → terminal-CON7n_ir.js} +1 -1
  89. package/dashboard/dist/assets/{trash-BN9GbynA.js → trash-C2f88jSi.js} +1 -1
  90. package/dashboard/dist/assets/{use-community-stats-Bc2d8tqa.js → use-community-stats-CgQu43xV.js} +1 -1
  91. package/dashboard/dist/assets/{use-latest-request-guard-_0H4Rk58.js → use-latest-request-guard-lShM46-3.js} +1 -1
  92. package/dashboard/dist/assets/{use-limits-display-prefs-Dln-Rxps.js → use-limits-display-prefs-8o7pbEg0.js} +1 -1
  93. package/dashboard/dist/assets/{use-native-settings-CpkcGzXp.js → use-native-settings-dN8shG3M.js} +1 -1
  94. package/dashboard/dist/assets/{use-ordered-list-DpGA5U4S.js → use-ordered-list-beJy2IqQ.js} +1 -1
  95. package/dashboard/dist/assets/use-reduced-motion-S0bR_G1B.js +1 -0
  96. package/dashboard/dist/assets/{use-transform-JXJt1W74.js → use-transform-cJpAQr2d.js} +1 -1
  97. package/dashboard/dist/assets/{use-usage-limits-BK_H1-b7.js → use-usage-limits-CuusZA6b.js} +1 -1
  98. package/dashboard/dist/assets/{useCurrency-ilbtKs16.js → useCurrency-Cx9UsKJK.js} +1 -1
  99. package/dashboard/dist/assets/{usePositioner-M35LtCGR.js → usePositioner-TEClK6mF.js} +1 -1
  100. package/dashboard/dist/assets/{useTokenFormat-BtTQjv7z.js → useTokenFormat-BNwTLiac.js} +1 -1
  101. package/dashboard/dist/assets/{zap-5xemt9EH.js → zap-CFgbVEhC.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/pricing/seed-snapshot.json +1 -1
  109. package/src/lib/rollout.js +218 -55
  110. package/dashboard/dist/assets/DialogDescription--_wg9JdV.js +0 -1
  111. package/dashboard/dist/assets/use-reduced-motion-Cy21hg3t.js +0 -1
@@ -24394,6 +24394,9 @@ async function parseTraeCnApiIncremental({
24394
24394
  const DSH_SESSION_LOG_MAX_BYTES = 64 * 1024 * 1024;
24395
24395
  const DSH_SESSION_TEXT_MAX_BYTES = 128 * 1024 * 1024;
24396
24396
  const DSH_SOURCE = "dsh";
24397
+ // v2 excludes fork seeds and replayed assistant identities. Old ledgers need
24398
+ // a verified replacement, not a reset of the provider-wide hourly totals.
24399
+ const DSH_ACCOUNTING_VERSION = 2;
24397
24400
 
24398
24401
  // Precedence mirrors the harness's own resolveDshHome: an explicit
24399
24402
  // TokenTracker override, then the harness's $DSH_HOME, then `~/.dsh`. Tests
@@ -24760,20 +24763,48 @@ function legacyDshContributionsFromSnapshot(snapshot, previousState) {
24760
24763
  previousState.lastSeq < -1 ||
24761
24764
  previousState.inode !== snapshot.inode ||
24762
24765
  !Number.isFinite(previousState.size) ||
24763
- snapshot.size < previousState.size
24766
+ snapshot.size < previousState.size ||
24767
+ (snapshot.size === previousState.size && snapshot.mtimeMs !== previousState.mtimeMs)
24764
24768
  ) {
24765
24769
  return null;
24766
24770
  }
24767
24771
  const parsed = extractDshSessionUsage(snapshot.text, -1);
24768
- if (!parsed.complete || parsed.sessionId !== previousState.sessionId) return null;
24772
+ if (!parsed.complete || parsed.sessionId !== previousState.sessionId ||
24773
+ parsed.maxSeq < previousState.lastSeq) return null;
24769
24774
  // The pre-ledger parser replayed unknown-sequence usage on every pass, so
24770
24775
  // there is no safe prefix boundary for such a record. Defer instead.
24771
- if (parsed.deltas.some((delta) => !Number.isSafeInteger(delta.seq))) return null;
24776
+ const priorDeltas = previousState.accountingVersion === DSH_ACCOUNTING_VERSION
24777
+ ? parsed.deltas : parsed.rawDeltas;
24778
+ if (priorDeltas.some((delta) => !Number.isSafeInteger(delta.seq))) return null;
24772
24779
  return dshContributionsFromDeltas(
24773
- parsed.deltas.filter((delta) => delta.seq <= previousState.lastSeq),
24780
+ priorDeltas.filter((delta) => delta.seq <= previousState.lastSeq),
24774
24781
  );
24775
24782
  }
24776
24783
 
24784
+ // When the old and new artifacts differ, aggregate equality alone does not
24785
+ // establish that the replacement contains the historical events. Match the
24786
+ // count-only metadata as a multiset, allowing seq rewrites but no lost events.
24787
+ function dshRawUsageCoversPrefix(candidate, snapshot, previousState) {
24788
+ if (!snapshot?.text || !previousState) return false;
24789
+ const prefix = extractDshSessionUsage(snapshot.text).rawDeltas
24790
+ .filter((delta) => delta.seq != null && delta.seq <= previousState.lastSeq);
24791
+ const key = (delta) => JSON.stringify([
24792
+ delta.messageId, delta.model, delta.timeMs, delta.totals,
24793
+ ]);
24794
+ const available = new Map();
24795
+ for (const delta of candidate || []) {
24796
+ const identity = key(delta);
24797
+ available.set(identity, (available.get(identity) || 0) + 1);
24798
+ }
24799
+ for (const delta of prefix) {
24800
+ const identity = key(delta);
24801
+ const count = available.get(identity) || 0;
24802
+ if (!count) return false;
24803
+ available.set(identity, count - 1);
24804
+ }
24805
+ return true;
24806
+ }
24807
+
24777
24808
  function dshContributionsCoverPrior(prior, candidate) {
24778
24809
  for (const [key, previous] of Object.entries(prior || {})) {
24779
24810
  const current = candidate?.[key];
@@ -25167,17 +25198,20 @@ function isCompleteDshJsonLine(raw) {
25167
25198
  return index === text.length;
25168
25199
  }
25169
25200
 
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 = [];
25201
+ // Read routing metadata and counts only; never materialize message content.
25202
+ // rawDeltas preserves the former seq-only interpretation for migration proof.
25203
+ // deltas excludes the inherited prefix and repeated assistant message ids.
25204
+ function extractDshSessionUsage(text, lastSeq = -1, lastLine = -1) {
25205
+ const rawDeltas = [];
25176
25206
  const watermark = Number.isFinite(lastSeq) ? lastSeq : -1;
25177
25207
  let maxSeq = watermark;
25208
+ let maxLine = -1;
25178
25209
  let sessionId = null;
25179
25210
  let headerModel = null;
25180
25211
  let complete = true;
25212
+ let isSeeded = false;
25213
+ let seedLength = null;
25214
+ let inheritedThrough = -1;
25181
25215
 
25182
25216
  const lines = String(text || "").split("\n");
25183
25217
  for (let i = 0; i < lines.length; i++) {
@@ -25185,29 +25219,37 @@ function extractDshSessionUsage(text, lastSeq = -1) {
25185
25219
  if (!line || !line.trim()) continue;
25186
25220
  if (!isCompleteDshJsonLine(line)) {
25187
25221
  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.
25222
+ // Do not acknowledge records after a corrupt/torn interior line.
25190
25223
  break;
25191
25224
  }
25225
+ maxLine = i;
25192
25226
  const eventType = parseDshJsonString(findDshJsonProperty(line, "type"));
25193
25227
  if (!eventType) continue;
25194
-
25228
+ const data = findDshJsonProperty(line, "data");
25195
25229
  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"));
25230
+ const id = parseDshJsonString(findDshJsonProperty(line, "id")) ||
25231
+ parseDshJsonString(findDshJsonProperty(data, "id")) ||
25232
+ parseDshJsonString(findDshJsonProperty(data, "sessionId"));
25200
25233
  if (id) sessionId = id;
25234
+ isSeeded = findDshJsonProperty(line, "isSeeded") === "true" ||
25235
+ findDshJsonProperty(data, "isSeeded") === "true";
25236
+ const topLength = parseDshJsonNumber(findDshJsonProperty(line, "seedLength"));
25237
+ const length = Number.isFinite(topLength) ? topLength :
25238
+ parseDshJsonNumber(findDshJsonProperty(data, "seedLength"));
25239
+ if (Number.isSafeInteger(length) && length >= 0) seedLength = length;
25201
25240
  continue;
25202
25241
  }
25203
25242
 
25204
25243
  const seq = parseDshJsonNumber(findDshJsonProperty(line, "seq"));
25205
25244
  const seqKnown = Number.isFinite(seq) && seq >= 0;
25206
25245
  if (seqKnown && seq > maxSeq) maxSeq = seq;
25207
-
25208
- const data = findDshJsonProperty(line, "data");
25209
25246
  if (!data) continue;
25210
-
25247
+ if (eventType === "session/end-seed") {
25248
+ // Nested forks contain earlier markers. The LAST explicitly inherited
25249
+ // marker is the boundary; ordinary resume markers (data:{}) are not.
25250
+ if (findDshJsonProperty(data, "inherited") === "true") inheritedThrough = i;
25251
+ continue;
25252
+ }
25211
25253
  if (eventType === "request/header") {
25212
25254
  const header = findDshJsonProperty(data, "header");
25213
25255
  const config = findDshJsonProperty(header, "config");
@@ -25216,39 +25258,46 @@ function extractDshSessionUsage(text, lastSeq = -1) {
25216
25258
  if (normalized) headerModel = normalized;
25217
25259
  continue;
25218
25260
  }
25219
-
25220
25261
  if (eventType !== "assistant/message" && eventType !== "message/assistant") continue;
25221
- if (seqKnown && seq <= watermark) continue;
25222
-
25223
25262
  const message = findDshJsonProperty(data, "message");
25224
25263
  const source = message ? findDshJsonProperty(message, "source") : null;
25225
25264
  const model = normalizeDshModelName(
25226
25265
  (source && parseDshJsonString(findDshJsonProperty(source, "model"))) ||
25227
25266
  parseDshJsonString(findDshJsonProperty(data, "model")),
25228
25267
  ) || headerModel;
25229
- const totals = dshUsageToTotals(
25230
- parseDshUsageSlice(
25231
- findDshJsonProperty(data, "usage") || findDshJsonProperty(line, "usage"),
25232
- ),
25233
- );
25268
+ const totals = dshUsageToTotals(parseDshUsageSlice(
25269
+ findDshJsonProperty(data, "usage") || findDshJsonProperty(line, "usage"),
25270
+ ));
25234
25271
  if (!model || !totals) continue;
25235
-
25236
- const timeMs =
25237
- parseDshJsonNumber(findDshJsonProperty(line, "time")) ||
25272
+ const timeMs = parseDshJsonNumber(findDshJsonProperty(line, "time")) ||
25238
25273
  parseDshJsonNumber(findDshJsonProperty(line, "timestamp")) ||
25239
25274
  parseDshJsonNumber(findDshJsonProperty(data, "time")) ||
25240
25275
  parseDshJsonNumber(findDshJsonProperty(data, "timestamp"));
25241
25276
  if (!Number.isFinite(timeMs) || timeMs <= 0) continue;
25242
-
25243
- deltas.push({ seq: seqKnown ? seq : null, model, timeMs, totals });
25277
+ const id = parseDshJsonString(findDshJsonProperty(message, "id"));
25278
+ rawDeltas.push({ seq: seqKnown ? seq : null, line: i,
25279
+ messageId: id?.trim() || null, model, timeMs, totals });
25244
25280
  }
25245
25281
 
25246
- return { deltas, maxSeq, sessionId, complete };
25282
+ const seedComplete = !isSeeded || inheritedThrough >= 0 || seedLength != null;
25283
+ const seen = new Set();
25284
+ const deltas = rawDeltas.filter((delta) => {
25285
+ if (delta.messageId) {
25286
+ if (seen.has(delta.messageId)) return false;
25287
+ // Claim before applying the watermark: a replay at a newer seq is not new.
25288
+ seen.add(delta.messageId);
25289
+ }
25290
+ if (!seedComplete || delta.line <= inheritedThrough) return false;
25291
+ // Legacy seedLength is an exclusive seq boundary, not a line count.
25292
+ if (seedLength != null && (delta.seq == null || delta.seq < seedLength)) return false;
25293
+ return delta.seq != null ? delta.seq > watermark : delta.line > lastLine;
25294
+ });
25295
+ return { deltas, rawDeltas, maxSeq, maxLine, sessionId, complete, seedComplete };
25247
25296
  }
25248
25297
 
25249
25298
  // Incremental parser entrypoint. Mirrors the other passive JSONL readers:
25250
25299
  // 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
25300
+ // changed, dedup via message ownership plus seq/line watermarks, accumulate into hourly
25252
25301
  // buckets, then flush touched buckets to the queue. Session contribution ledgers
25253
25302
  // make artifact-path migrations replace one session instead of resetting DSH.
25254
25303
  async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgress }) {
@@ -25262,6 +25311,9 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25262
25311
  const storedSessionState =
25263
25312
  dshState.sessions && typeof dshState.sessions === "object" ? dshState.sessions : {};
25264
25313
  const sessionState = { ...storedSessionState };
25314
+ // Never prune ownership when an origin transcript disappears. Only opaque
25315
+ // message ids and session identities are persisted, not conversation text.
25316
+ const messageOwners = { ...(dshState.messageOwners || {}) };
25265
25317
 
25266
25318
  const hourlyState = normalizeHourlyState(cursors?.hourly);
25267
25319
  // normalizeHourlyState preserves bucket objects for the other incremental
@@ -25287,6 +25339,22 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25287
25339
  const presentFiles = new Set(files);
25288
25340
  const total = files.length;
25289
25341
  const deferredFilePaths = new Set();
25342
+ const legacyAliases = new Map();
25343
+ for (const [filePath, state] of Object.entries(fileState)) {
25344
+ if (!state?.sessionId || state.accountingVersion === DSH_ACCOUNTING_VERSION) continue;
25345
+ const aliases = legacyAliases.get(state.sessionId) || [];
25346
+ aliases.push(filePath);
25347
+ legacyAliases.set(state.sessionId, aliases);
25348
+ }
25349
+ for (const aliases of legacyAliases.values()) {
25350
+ if (aliases.length < 2) continue;
25351
+ // Older cursors cannot prove whether these alias paths were counted once
25352
+ // or independently. Keep both baselines instead of guessing a subtraction.
25353
+ for (const filePath of aliases) {
25354
+ deferredFilePaths.add(filePath);
25355
+ deferredMigrationPaths.set(filePath, "ambiguous-legacy-session-aliases");
25356
+ }
25357
+ }
25290
25358
  let recordsProcessed = 0;
25291
25359
  let eventsAggregated = 0;
25292
25360
 
@@ -25344,6 +25412,9 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25344
25412
  const needsLedgerBackfill = Boolean(
25345
25413
  previousSessionId && !previousSessionContributions && !previousFileContributions,
25346
25414
  );
25415
+ const needsAccountingUpgrade = Boolean(
25416
+ prev && prev.accountingVersion !== DSH_ACCOUNTING_VERSION,
25417
+ );
25347
25418
  let snapshot;
25348
25419
  let parsed;
25349
25420
  let fullParsed = null;
@@ -25352,7 +25423,7 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25352
25423
 
25353
25424
  try {
25354
25425
  snapshot = await readDshSessionSnapshot(filePath, {
25355
- previous: needsLedgerBackfill ? null : prev,
25426
+ previous: needsLedgerBackfill || needsAccountingUpgrade ? null : prev,
25356
25427
  });
25357
25428
  if (!snapshot) {
25358
25429
  // A resolver-selected replacement can disappear between discovery and
@@ -25360,10 +25431,11 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25360
25431
  // contribution. This is deliberately separate from a thrown read
25361
25432
  // error because a missing path returns null from the snapshot helper.
25362
25433
  deferStaleMigrationPaths(filePath, "replacement-missing");
25434
+ if (needsAccountingUpgrade) deferredMigrationPaths.set(filePath, "history-missing");
25363
25435
  reportProgress(idx, recordsProcessed, eventsAggregated, total);
25364
25436
  continue;
25365
25437
  }
25366
- if (snapshot.unchanged && !needsLedgerBackfill) {
25438
+ if (snapshot.unchanged && !needsLedgerBackfill && !needsAccountingUpgrade) {
25367
25439
  reportProgress(idx, recordsProcessed, eventsAggregated, total);
25368
25440
  continue;
25369
25441
  }
@@ -25385,18 +25457,18 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25385
25457
  ),
25386
25458
  );
25387
25459
  const lastSeq = fileReset || !Number.isFinite(prev?.lastSeq) ? -1 : prev.lastSeq;
25388
- parsed = extractDshSessionUsage(snapshot.text, lastSeq);
25460
+ parsed = extractDshSessionUsage(snapshot.text, lastSeq, fileReset ? -1 : prev?.lastLine);
25389
25461
  sessionChanged = Boolean(
25390
25462
  prev &&
25391
25463
  parsed.sessionId &&
25392
25464
  parsed.sessionId !== previousSessionId,
25393
25465
  );
25394
25466
  if (sessionChanged) parsed = extractDshSessionUsage(snapshot.text, -1);
25395
- if (!prev || needsLedgerBackfill || sessionChanged || fileReset) {
25467
+ if (!prev || needsLedgerBackfill || needsAccountingUpgrade || sessionChanged || fileReset) {
25396
25468
  fullParsed = extractDshSessionUsage(snapshot.text, -1);
25397
- if (!prev || sessionChanged || fileReset) parsed = fullParsed;
25469
+ if (!prev || needsAccountingUpgrade || sessionChanged || fileReset) parsed = fullParsed;
25398
25470
  }
25399
- if (!parsed.complete) snapshot.mtimeMs = -1;
25471
+ if (!parsed.complete || !parsed.seedComplete) snapshot.mtimeMs = -1;
25400
25472
  } catch (error) {
25401
25473
  if (process.env.TOKENTRACKER_DEBUG) {
25402
25474
  process.stderr.write(`[dsh] skipped ${filePath}: ${error?.message || error}\n`);
@@ -25405,11 +25477,22 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25405
25477
  // selected file list. Keep that cursor until the replacement is readable
25406
25478
  // so a failed migration cannot turn into an untracked double-count.
25407
25479
  deferStaleMigrationPaths(filePath, "replacement-read-failed");
25480
+ if (needsAccountingUpgrade) deferredMigrationPaths.set(filePath, "history-read-failed");
25408
25481
  reportProgress(idx, recordsProcessed, eventsAggregated, total);
25409
25482
  continue;
25410
25483
  }
25411
25484
 
25412
25485
  const sessionId = typeof parsed.sessionId === "string" ? parsed.sessionId : null;
25486
+ if ((legacyAliases.get(sessionId)?.length || 0) > 1) {
25487
+ reportProgress(idx, recordsProcessed, eventsAggregated, total);
25488
+ continue;
25489
+ }
25490
+ if (prev && !sessionId && (previousSessionId || needsAccountingUpgrade)) {
25491
+ deferredFilePaths.add(filePath);
25492
+ deferredMigrationPaths.set(filePath, "replacement-session-id-missing");
25493
+ reportProgress(idx, recordsProcessed, eventsAggregated, total);
25494
+ continue;
25495
+ }
25413
25496
  const stalePathWithoutId = sessionId
25414
25497
  ? null
25415
25498
  : Object.keys(fileState).find(
@@ -25427,8 +25510,15 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25427
25510
  const stalePath = staleFileForSession(sessionId, filePath);
25428
25511
  const previousPath =
25429
25512
  session?.lastPath && session.lastPath !== filePath ? session.lastPath : stalePath;
25513
+ if (previousPath && previousPath !== filePath) {
25514
+ // Another artifact currently owns this session ledger. Replacing that
25515
+ // ledger needs the full candidate, even if this alias has its own cursor.
25516
+ fullParsed = extractDshSessionUsage(snapshot.text);
25517
+ parsed = fullParsed;
25518
+ }
25430
25519
  const resetCurrentFile = Boolean(fileReset && !sessionChanged);
25431
- const replacementPath = previousPath || (resetCurrentFile ? filePath : null);
25520
+ const upgradeCurrentFile = needsAccountingUpgrade && !sessionChanged;
25521
+ const replacementPath = previousPath || (resetCurrentFile || upgradeCurrentFile ? filePath : null);
25432
25522
  const sessionContributions = storedDshContributions(session);
25433
25523
  const staleContributions = storedDshContributions(stalePath && fileState[stalePath]);
25434
25524
  const fileContributions = storedDshContributions(prev);
@@ -25451,7 +25541,9 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25451
25541
  // A pure rename preserves the same bytes. Reusing the parsed full
25452
25542
  // contribution is safe here because replacement below subtracts and
25453
25543
  // re-adds it; no sequence watermark is transferred.
25454
- oldContributions = dshContributionsFromDeltas(parsed.deltas);
25544
+ oldContributions = dshContributionsFromDeltas(
25545
+ previousState.accountingVersion === DSH_ACCOUNTING_VERSION ? parsed.deltas : parsed.rawDeltas,
25546
+ );
25455
25547
  }
25456
25548
 
25457
25549
  // Cursors written before the contribution ledger existed can still be
@@ -25464,10 +25556,13 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25464
25556
  const oldSnapshot = await readDshSessionSnapshot(previousPath).catch(() => null);
25465
25557
  oldContributions = legacyDshContributionsFromSnapshot(oldSnapshot, oldState);
25466
25558
  }
25559
+ if (sessionId && upgradeCurrentFile && !oldContributions) {
25560
+ oldContributions = legacyDshContributionsFromSnapshot(snapshot, prev);
25561
+ }
25467
25562
  const replaceSession = Boolean(
25468
25563
  sessionId &&
25469
25564
  oldContributions &&
25470
- (!prev || resetCurrentFile || (previousPath && previousPath !== filePath)),
25565
+ (!prev || resetCurrentFile || upgradeCurrentFile || (previousPath && previousPath !== filePath)),
25471
25566
  );
25472
25567
  const oldContributionCount = Object.keys(oldContributions || {}).length;
25473
25568
  if (sessionId && replacementPath && !oldContributions) {
@@ -25479,20 +25574,25 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25479
25574
  }
25480
25575
  if (
25481
25576
  replaceSession &&
25482
- (!parsed.complete || (oldContributionCount > 0 && parsed.deltas.length === 0))
25577
+ (!parsed.complete || !parsed.seedComplete || (oldContributionCount > 0 && parsed.rawDeltas.length === 0))
25483
25578
  ) {
25484
25579
  // A readable header-only or torn replacement is not authoritative. Keep
25485
25580
  // the old contribution until a complete replacement with usage arrives.
25486
25581
  deferredFilePaths.add(replacementPath);
25487
25582
  deferredMigrationPaths.set(
25488
25583
  replacementPath,
25489
- parsed.complete ? "replacement-has-no-usage" : "replacement-incomplete",
25584
+ !parsed.complete ? "replacement-incomplete" :
25585
+ !parsed.seedComplete ? "seed-boundary-unavailable" : "replacement-has-no-usage",
25490
25586
  );
25491
25587
  if (resetCurrentFile) fileState[filePath] = { ...prev, mtimeMs: -1 };
25492
25588
  reportProgress(idx, recordsProcessed, eventsAggregated, total);
25493
25589
  continue;
25494
25590
  }
25495
- const candidateContributions = dshContributionsFromDeltas(parsed.deltas);
25591
+ // A correction may legitimately be lower or zero. Verify that the FULL
25592
+ // raw log still accounts for the old ledger before subtracting anything.
25593
+ // Comparing corrected usage to the old totals would reject the very repair
25594
+ // this migration must make; accepting a cropped log would erase history.
25595
+ const candidateContributions = dshContributionsFromDeltas(parsed.rawDeltas);
25496
25596
  if (
25497
25597
  replaceSession &&
25498
25598
  oldContributionCount > 0 &&
@@ -25518,6 +25618,46 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25518
25618
  continue;
25519
25619
  }
25520
25620
 
25621
+ const owner = sessionId || dshSessionDirectoryKey(filePath);
25622
+ const acceptedDeltas = parsed.deltas.filter((delta) => {
25623
+ if (!delta.messageId) return true;
25624
+ const priorOwner = messageOwners[`a:${delta.messageId}`];
25625
+ return !priorOwner || (replaceSession && priorOwner === owner);
25626
+ });
25627
+ // Known ownership is stronger evidence than bucket totals. An equal or
25628
+ // larger divergent alias can still omit a previously counted message.
25629
+ const ownedIds = new Set(parsed.deltas.map((delta) => delta.messageId).filter(Boolean));
25630
+ const dropsOwnedId = replaceSession && Object.entries(messageOwners).some(([key, value]) =>
25631
+ value === owner && !ownedIds.has(key.slice(2)),
25632
+ );
25633
+ if (dropsOwnedId) {
25634
+ deferredFilePaths.add(replacementPath);
25635
+ deferredMigrationPaths.set(replacementPath, "replacement-drops-owned-message");
25636
+ reportProgress(idx, recordsProcessed, eventsAggregated, total);
25637
+ continue;
25638
+ }
25639
+ const correctedContributions = dshContributionsFromDeltas(acceptedDeltas);
25640
+ if (replaceSession && !dshContributionsCoverPrior(oldContributions, correctedContributions)) {
25641
+ // Aggregate >= is not proof: a cropped transcript can substitute larger
25642
+ // unrelated/replayed usage for a lost event. Reconstruct EXACTLY the
25643
+ // acknowledged prefix before allowing a downward/zero correction.
25644
+ const baselineState = previousState || prev;
25645
+ let baselineSnapshot = snapshot;
25646
+ if (previousPath && !sameDshSessionMetadata(baselineState, snapshot)) {
25647
+ baselineSnapshot = await readDshSessionSnapshot(previousPath).catch(() => null);
25648
+ }
25649
+ const proven = legacyDshContributionsFromSnapshot(baselineSnapshot, baselineState);
25650
+ if (baselineState?.accountingVersion === DSH_ACCOUNTING_VERSION ||
25651
+ !proven || !dshContributionsCoverPrior(oldContributions, proven) ||
25652
+ !dshContributionsCoverPrior(proven, oldContributions) ||
25653
+ !dshRawUsageCoversPrefix(parsed.rawDeltas, baselineSnapshot, baselineState)) {
25654
+ deferredFilePaths.add(replacementPath);
25655
+ deferredMigrationPaths.set(replacementPath, "historical-prefix-unverified");
25656
+ reportProgress(idx, recordsProcessed, eventsAggregated, total);
25657
+ continue;
25658
+ }
25659
+ }
25660
+
25521
25661
  if (replaceSession) {
25522
25662
  applyDshContributions({
25523
25663
  hourlyState,
@@ -25530,18 +25670,24 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25530
25670
 
25531
25671
  let nextContributions = replaceSession
25532
25672
  ? {}
25533
- : sessionContributions || fileContributions || {};
25534
- if (fullParsed) {
25535
- nextContributions = dshContributionsFromDeltas(fullParsed.deltas);
25536
- }
25537
-
25538
- for (const delta of parsed.deltas) {
25673
+ : sessionChanged ? {} : sessionContributions || fileContributions || {};
25674
+ // Backfill missing ledgers without re-adding the acknowledged prefix.
25675
+ if (fullParsed && !replaceSession && prev && !sessionChanged && !fileReset) {
25676
+ nextContributions = dshContributionsFromDeltas(fullParsed.deltas.filter((delta) =>
25677
+ (!delta.messageId || !messageOwners[`a:${delta.messageId}`] ||
25678
+ messageOwners[`a:${delta.messageId}`] === owner) &&
25679
+ (delta.seq != null ? delta.seq <= prev.lastSeq : delta.line <= prev.lastLine),
25680
+ ));
25681
+ }
25682
+
25683
+ for (const delta of acceptedDeltas) {
25684
+ if (delta.messageId) messageOwners[`a:${delta.messageId}`] = owner;
25539
25685
  const bucketStart = toUtcHalfHourStart(delta.timeMs);
25540
25686
  if (!bucketStart) continue;
25541
25687
  const bucket = getHourlyBucket(hourlyState, DSH_SOURCE, delta.model, bucketStart);
25542
25688
  addTotals(bucket.totals, delta.totals);
25543
25689
  touchedBuckets.add(bucketKey(DSH_SOURCE, delta.model, bucketStart));
25544
- if (!fullParsed) addDshContribution(nextContributions, delta.model, bucketStart, delta.totals);
25690
+ addDshContribution(nextContributions, delta.model, bucketStart, delta.totals);
25545
25691
  eventsAggregated += 1;
25546
25692
  }
25547
25693
 
@@ -25552,12 +25698,15 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25552
25698
  mtimeMs: snapshot.mtimeMs,
25553
25699
  sessionId,
25554
25700
  lastSeq: parsed.maxSeq,
25701
+ lastLine: parsed.maxLine,
25702
+ accountingVersion: DSH_ACCOUNTING_VERSION,
25555
25703
  contributions: nextContributions,
25556
25704
  updatedAt,
25557
25705
  };
25558
25706
  if (sessionId) {
25559
25707
  sessionState[sessionId] = {
25560
25708
  lastPath: filePath,
25709
+ accountingVersion: DSH_ACCOUNTING_VERSION,
25561
25710
  contributions: nextContributions,
25562
25711
  updatedAt,
25563
25712
  };
@@ -25575,7 +25724,11 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25575
25724
  // Retain pre-ledger identity until a replacement is verified. Its counted
25576
25725
  // usage survives file deletion, so elapsed time cannot authorize replay.
25577
25726
  // Current ledgers retain the same identity in sessionState instead.
25578
- if (legacySessionId && !storedDshContributions(state) && !sessionState[legacySessionId]) {
25727
+ if ((legacySessionId && !storedDshContributions(state) && !sessionState[legacySessionId]) ||
25728
+ (!legacySessionId && state.accountingVersion !== DSH_ACCOUNTING_VERSION)) {
25729
+ if (state.accountingVersion !== DSH_ACCOUNTING_VERSION) {
25730
+ deferredMigrationPaths.set(filePath, "history-unverified");
25731
+ }
25579
25732
  if (!Number.isFinite(Number(state?.missingSince))) {
25580
25733
  fileState[filePath] = { ...state, missingSince: nowMs };
25581
25734
  }
@@ -25584,10 +25737,20 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
25584
25737
  delete fileState[filePath];
25585
25738
  }
25586
25739
 
25740
+ // Missing/corrupt historical transcripts retain their ledger and are made
25741
+ // visible as deferrals, never treated as a provider-wide reset opportunity.
25742
+ for (const [id, session] of Object.entries(sessionState)) {
25743
+ if (session?.accountingVersion !== DSH_ACCOUNTING_VERSION) {
25744
+ const key = session?.lastPath || `session:${id}`;
25745
+ if (!deferredMigrationPaths.has(key)) deferredMigrationPaths.set(key, "history-unverified");
25746
+ }
25747
+ }
25587
25748
  const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
25588
25749
  const updatedAt = new Date().toISOString();
25589
25750
  hourlyState.updatedAt = updatedAt;
25590
25751
  cursors.hourly = hourlyState;
25752
+ dshState.accountingVersion = DSH_ACCOUNTING_VERSION;
25753
+ dshState.messageOwners = messageOwners;
25591
25754
  dshState.files = fileState;
25592
25755
  dshState.sessions = sessionState;
25593
25756
  if (deferredMigrationPaths.size > 0) {
@@ -1 +0,0 @@
1
- import{r as n,J as a}from"./__vite-browser-external-DnYJww7c.js";import{u as p}from"./DialogTitle-BcBpd--_.js";import{u as c}from"./composite-fgJbx88k.js";const x=n.forwardRef(function(e,r){const{render:l,className:u,style:m,id:s,...t}=e,i=p(),o=c(s);return i.useSyncedValueWithCleanup("descriptionElementId",o),a("p",e,{ref:r,props:[{id:o},t]})});export{x as D};
@@ -1 +0,0 @@
1
- import{r as o}from"./__vite-browser-external-DnYJww7c.js";import{h as r,i as t,p as s}from"./react-BIpM-G7K.js";function i(){!r.current&&t();const[e]=o.useState(s.current);return e}export{i as u};