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.
- package/dashboard/dist/assets/{AchievementBadge-Ct0vfdYb.js → AchievementBadge-CwjI28oT.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-dfoRxqOv.js → AchievementsPage-Dq544fjX.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-DHFyssU0.js → AchievementsSection-DufV6nod.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-ChJYZMSq.js → BadgeDetailModal-vw8xydKu.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-C_qaJy1-.js → BotAnimated-De0anwsh.js} +1 -1
- package/dashboard/dist/assets/{Card-BhK2nmOu.js → Card-CuSeWBDM.js} +1 -1
- package/dashboard/dist/assets/{CloudCheckoutPage-Ele1kkut.js → CloudCheckoutPage-CxFcOwAj.js} +1 -1
- package/dashboard/dist/assets/{CloudPage-C5DMqcoV.js → CloudPage-Beb7_JLW.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-C_hxoIwL.js → CommandPalette-PsUq2iRS.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-BSTqBIEU.js → CommunityStatsModal-b54WaPbK.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-DI4QK5EV.js → ConfirmModal-_NdunIh5.js} +1 -1
- package/dashboard/dist/assets/{Controls-zK1rzS_8.js → Controls-5Is6MWln.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-BdDnmc60.js → DashboardPage-C0n0qC7h.js} +1 -1
- package/dashboard/dist/assets/{DateRangePopover-DqwgxpNF.js → DateRangePopover-C3WfGy-0.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-C0fQKKuI.js → DevicePage-Vn6k-mGd.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-C_cbyHpb.js → DialogClose-CXttnna-.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-C3KGos_G.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-BcBpd--_.js → DialogTitle-B0cNIo0E.js} +1 -1
- package/dashboard/dist/assets/{ElectricLogo-Dw9gkExE.js → ElectricLogo-DC3XSfIu.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-Ci69gBGl.js → FadeIn-H7S3TMHi.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-pb03c0kb.js → HeaderGithubStar-xgbIo5OP.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-Cqd0D789.js → HoverTooltip-CpzM5Dju.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-qILbvOJn.js → IpCheckPage-BdV7S4dY.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-CCIwaAeu.js → LandingPage-D9f28ryO.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardPage-BwzABmKU.js → LeaderboardPage--WUFpkBI.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProBadge-7kdzfw8r.js → LeaderboardProBadge-CEvrQ75Y.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-uMnN-IGe.js → LeaderboardProfileModal-Bn0HFr5h.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-CQNc4MES.js → LeaderboardProfilePage-DJ7fQI68.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-CJ1LImRm.js → LimitsPage-7tPDheuX.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-Dm0l_F0I.js → LocalOnlyNotice-DlGbVQS8.js} +1 -1
- package/dashboard/dist/assets/{LoginCard--tUiBVtr.js → LoginCard-BZZmzUGh.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-B1-5fFMY.js → LoginModal-BT5_bIxU.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-BYl6NnZ8.js → LoginPage-DMjDUPPi.js} +1 -1
- package/dashboard/dist/assets/{PetPage-DJNGJZrR.js → PetPage-Dk3j6ITn.js} +2 -2
- package/dashboard/dist/assets/{PopoverPopup-aRAdIkTs.js → PopoverPopup-pugRu3RC.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-uiA-KBe2.js → ProviderIcon-CtmL1CmY.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-BZT353_T.js → ResetPasswordPage-BB6U7t8P.js} +1 -1
- package/dashboard/dist/assets/{Select-CPC04j0g.js → Select-7w1su8lw.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-D1vJVrFO.js → SelectItemText-CaEUmWkj.js} +1 -1
- package/dashboard/dist/assets/{SelfHostPage-FL3EDqmb.js → SelfHostPage-DctWoF-A.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-BohSSNai.js → ServiceStatusPage-Dg3ljEXC.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-Cf_HASvt.js → SessionsPage-B1dvW-be.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-B4ztKHnj.js → SettingsPage-DuXSX1R2.js} +3 -3
- package/dashboard/dist/assets/{SkillsPage-BFiQkY7O.js → SkillsPage-CvXQFgc9.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-BHVdTJAP.js → TokenGalaxy-DKC8Rk_Y.js} +1 -1
- package/dashboard/dist/assets/{TooltipPopup-BMtQyFn6.js → TooltipPopup-Dp9EpHLG.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-DN-g3CNm.js → TrendMonitor-DWnlBn3H.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-Bv32yu8M.js → WidgetsPage-GECNmb1Q.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-HVTOehNb.js → WrappedPage-Dq_1lOnb.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-DnYJww7c.js → __vite-browser-external-CkF0pdTJ.js} +2 -2
- package/dashboard/dist/assets/{agent-logos-BOwAJQqL.js → agent-logos-Dqe3Vrah.js} +1 -1
- package/dashboard/dist/assets/{arrow-left-CeXuUuES.js → arrow-left-Cr_2QnDu.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-DOdBG1FZ.js → arrow-right-BsK2sJgt.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-Dponssa4.js → arrow-up-right-DkWMGqXo.js} +1 -1
- package/dashboard/dist/assets/{calendar-days-CsxE6ot0.js → calendar-days-BB9VV438.js} +1 -1
- package/dashboard/dist/assets/{check-BroNG_vu.js → check-bpSCJZWh.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-C5ZpGeO1.js → chevron-down-CtECtDmx.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-BtS-hZTc.js → cloud-checkout-C4PrzKhN.js} +1 -1
- package/dashboard/dist/assets/{composite-fgJbx88k.js → composite-BHisG82k.js} +1 -1
- package/dashboard/dist/assets/{copy-CHBuwNUs.js → copy-Dmxzd-Sw.js} +1 -1
- package/dashboard/dist/assets/{download-EKd2MHkv.js → download-CkdQzw2w.js} +1 -1
- package/dashboard/dist/assets/{external-link-GkSo9C78.js → external-link-C971FjvC.js} +1 -1
- package/dashboard/dist/assets/{flame-Cg96qJrE.js → flame-mKbp7kxw.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-ChG2_n4h.js → format-tokens-C35FrJnH.js} +1 -1
- package/dashboard/dist/assets/{gift-B3QDbWPE.js → gift-BX2lSOmc.js} +1 -1
- package/dashboard/dist/assets/{icons-BEAyMjau.js → icons-ov92u52T.js} +1 -1
- package/dashboard/dist/assets/{index-B0kZThUB.js → index-9OCL-BL8.js} +1 -1
- package/dashboard/dist/assets/{index-DFcQyAnr.js → index-Bgm38oHe.js} +1 -1
- package/dashboard/dist/assets/{index-Cuc8ZXgl.js → index-Ca5xkNrK.js} +1 -1
- package/dashboard/dist/assets/{index-B9a1fK8I.js → index-DF03nIHb.js} +1 -1
- package/dashboard/dist/assets/{info-796bNttV.js → info-B-f9hWcw.js} +1 -1
- package/dashboard/dist/assets/{link-2-LYRIPLwz.js → link-2-l1wuezTo.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-DpKC9Uel.js → loader-circle-Lmjrs8XX.js} +1 -1
- package/dashboard/dist/assets/{local-usage-transfer-Ctbuc6U4.js → local-usage-transfer-Ds6cF41d.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-C5YTeNW1.js → modifiers.esm-tVlltJXO.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-DJUCfEvG.js → monitor-smartphone-XRiYiY8u.js} +1 -1
- package/dashboard/dist/assets/{play-LC-lnw6w.js → play-DaD9EMjk.js} +1 -1
- package/dashboard/dist/assets/{plus-CDF0S5xd.js → plus-D30fbsKQ.js} +1 -1
- package/dashboard/dist/assets/{provider-display-CkITUZgR.js → provider-display-CKdhBOwz.js} +1 -1
- package/dashboard/dist/assets/{react-BIpM-G7K.js → react-BY0y0K4x.js} +1 -1
- package/dashboard/dist/assets/{safePolygon-C3Ea423q.js → safePolygon-CSG_P7rG.js} +1 -1
- package/dashboard/dist/assets/{search-C-tN1mTM.js → search-Kiq8wVHA.js} +1 -1
- package/dashboard/dist/assets/{server-DJ4gq_fT.js → server-CjovGBGz.js} +1 -1
- package/dashboard/dist/assets/{shield-check-f75hry9N.js → shield-check-DF9MmMCq.js} +1 -1
- package/dashboard/dist/assets/{skills-api-CZ_vfnRL.js → skills-api-CEzr6p92.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-mLJSvq3M.js → sortable.esm-D7jd9mE_.js} +1 -1
- package/dashboard/dist/assets/{store-CZV1KXw2.js → store-D9_jo_do.js} +1 -1
- package/dashboard/dist/assets/{terminal-B9vOwCl9.js → terminal-CLOlvzR-.js} +1 -1
- package/dashboard/dist/assets/{trash-BN9GbynA.js → trash-DIe0fsDD.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-Bc2d8tqa.js → use-community-stats-gDkdiRBz.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-_0H4Rk58.js → use-latest-request-guard-tXGuKvua.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-Dln-Rxps.js → use-limits-display-prefs-CCuLFy00.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-CpkcGzXp.js → use-native-settings-hgYqaiR7.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-DpGA5U4S.js → use-ordered-list-D6-YzYJI.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-DfbDxs5a.js +1 -0
- package/dashboard/dist/assets/{use-transform-JXJt1W74.js → use-transform-BEiXvAuE.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-BK_H1-b7.js → use-usage-limits-BlwtyPtT.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-ilbtKs16.js → useCurrency-CwCGJLHb.js} +1 -1
- package/dashboard/dist/assets/{usePositioner-M35LtCGR.js → usePositioner-BqHtRIrE.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-BtTQjv7z.js → useTokenFormat-BUNBUX-x.js} +1 -1
- package/dashboard/dist/assets/{zap-5xemt9EH.js → zap-C0RFs2qQ.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/ip-check.html +1 -1
- package/dashboard/dist/leaderboard.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +1 -1
- package/src/commands/sync.js +2 -2
- package/src/lib/limits-cache.js +475 -0
- package/src/lib/pricing/curated-overrides.json +24 -0
- package/src/lib/pricing/matcher.js +4 -1
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +261 -102
- package/src/lib/usage-limits.js +370 -1063
- package/dashboard/dist/assets/DialogDescription--_wg9JdV.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-Cy21hg3t.js +0 -1
package/src/lib/rollout.js
CHANGED
|
@@ -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
|
-
|
|
11645
|
-
|
|
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")
|
|
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
|
-
|
|
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)
|
|
11692
|
-
if (seenIds.has(messageId))
|
|
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
|
-
|
|
11687
|
+
return;
|
|
11700
11688
|
}
|
|
11701
11689
|
|
|
11702
11690
|
if (tsMs == null) {
|
|
11703
11691
|
seenIds.add(messageId);
|
|
11704
|
-
|
|
11692
|
+
return;
|
|
11705
11693
|
}
|
|
11706
11694
|
const tsIso = new Date(tsMs).toISOString();
|
|
11707
11695
|
const bucketStart = toUtcHalfHourStart(tsIso);
|
|
11708
|
-
if (!bucketStart)
|
|
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
|
-
|
|
11741
|
-
|
|
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:
|
|
11744
|
-
mtimeMs:
|
|
11745
|
-
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
|
-
|
|
16904
|
-
|
|
16905
|
-
|
|
16906
|
-
|
|
16907
|
-
|
|
16908
|
-
|
|
16909
|
-
|
|
16910
|
-
|
|
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 {
|
|
16909
|
+
try { entry = JSON.parse(record.line); } catch { break; }
|
|
16913
16910
|
onEntry(entry);
|
|
16914
|
-
|
|
16911
|
+
committedEndOffset = scannedEndOffset;
|
|
16912
|
+
break;
|
|
16915
16913
|
}
|
|
16916
|
-
|
|
16917
|
-
|
|
16918
|
-
|
|
16919
|
-
|
|
16920
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
25171
|
-
//
|
|
25172
|
-
//
|
|
25173
|
-
|
|
25174
|
-
|
|
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
|
-
//
|
|
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(
|
|
25198
|
-
parseDshJsonString(findDshJsonProperty(
|
|
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
|
-
|
|
25231
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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.
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
25535
|
-
|
|
25536
|
-
|
|
25537
|
-
|
|
25538
|
-
|
|
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
|
-
|
|
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) {
|