tokentracker-cli 1.5.12 → 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.
- package/dashboard/dist/assets/{AchievementBadge-DvcPjqX7.js → AchievementBadge-DqJWve1h.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-C9AkvdHe.js → AchievementsPage-CnuBayj3.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-g0JGNGfT.js → AchievementsSection-WRU5TxAB.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-BHxQ8umD.js → BadgeDetailModal-B5ikEQik.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-C5Byp7Ml.js → BotAnimated-CaMG6j-6.js} +1 -1
- package/dashboard/dist/assets/{Card-BV1iTPCd.js → Card-BL3vxENk.js} +1 -1
- package/dashboard/dist/assets/{CloudCheckoutPage-DcAvPHXS.js → CloudCheckoutPage-CsUb8CJ7.js} +1 -1
- package/dashboard/dist/assets/{CloudPage-pw82pCP6.js → CloudPage-5ZiPFzeC.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-B88IfMHd.js → CommandPalette-CYd9nXW-.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-DwtEZ96Z.js → CommunityStatsModal-BItox7Sz.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-Cx1-1xUk.js → ConfirmModal-BE1ABR4z.js} +1 -1
- package/dashboard/dist/assets/{Controls-hHP6yz1h.js → Controls-BRqIsaHT.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-Cz98astU.js → DashboardPage-Cm0YuBlx.js} +1 -1
- package/dashboard/dist/assets/{DateRangePopover-BjsUociY.js → DateRangePopover-CRwEu66e.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-C0ZyCHAD.js → DevicePage-DFUPsGb_.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-eeerEmfs.js → DialogClose-DALl4GmP.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-CO0hykis.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-Bn14t_Xd.js → DialogTitle-BSR8ozyn.js} +1 -1
- package/dashboard/dist/assets/{ElectricLogo-OyoU_h0I.js → ElectricLogo-BtrycBUW.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-BbC4-H2A.js → FadeIn-CfhgvLVd.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-B2CCV29J.js → HeaderGithubStar-N8wqAdOf.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-DiD_l4GJ.js → HoverTooltip-CwdMYIzZ.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-1HwHzeVT.js → IpCheckPage-DCRor9cb.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-Cx_xIavm.js → LandingPage-s9xvTHSp.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardPage-BsP635cI.js → LeaderboardPage-CUj3WLMo.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProBadge-B7uhXYjD.js → LeaderboardProBadge-N4NOKZWj.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-_QOUdhsi.js → LeaderboardProfileModal-09HiUyxm.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-CSpx6Rw5.js → LeaderboardProfilePage-Cw9_rodG.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-DOj_2vh6.js → LimitsPage-F8b6kZa0.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-DnkG5DmI.js → LocalOnlyNotice-D1eEzDEv.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-Ds9kOBpV.js → LoginCard-0sIovG_J.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-B-7fS0gK.js → LoginModal-BT3oMVak.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-B2K2tXvd.js → LoginPage-CDsXdaE5.js} +1 -1
- package/dashboard/dist/assets/{PetPage-DwsDJxX_.js → PetPage-BEnC1azp.js} +2 -2
- package/dashboard/dist/assets/{PopoverPopup-CqUafQx4.js → PopoverPopup-Dwzp-hyf.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-BcE1tBpR.js → ProviderIcon-Cu_lcuuu.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-CP17McXH.js → ResetPasswordPage-DrpEscm1.js} +1 -1
- package/dashboard/dist/assets/{Select-BcFrd0ex.js → Select-wIR4qvEI.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-C-FBw54p.js → SelectItemText-CNuJgYps.js} +1 -1
- package/dashboard/dist/assets/{SelfHostPage--4KcyhvD.js → SelfHostPage-CWNLygpL.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-C4GQ-8Tm.js → ServiceStatusPage-BId9t16Q.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-DMW3U0i3.js → SessionsPage-C_klx7d8.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-sYLabRGu.js → SettingsPage-nC73JaZ4.js} +3 -3
- package/dashboard/dist/assets/{SkillsPage-CkjmvAds.js → SkillsPage-B2agou8Y.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-BfJtQKzg.js → TokenGalaxy-BNXUWRwa.js} +1 -1
- package/dashboard/dist/assets/{TooltipPopup-BxvShS92.js → TooltipPopup--X2kExQ0.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-C9cHT4G_.js → TrendMonitor-BELHsTnz.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-BqlH3M7h.js → WidgetsPage-DWp0SMXA.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-D2A1zcGs.js → WrappedPage-DgwczKHe.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-RPZeXrAv.js → __vite-browser-external-BHphZRSC.js} +2 -2
- package/dashboard/dist/assets/{agent-logos-CKAwJGNX.js → agent-logos-Cfx5WzU5.js} +1 -1
- package/dashboard/dist/assets/{arrow-left-sB5X7dRY.js → arrow-left-D83SdANw.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-Cvwg139b.js → arrow-right-D7ye9bmJ.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-H5sP370z.js → arrow-up-right-CLmwEwwC.js} +1 -1
- package/dashboard/dist/assets/{calendar-days-B6p2eInB.js → calendar-days-D1qLlvh4.js} +1 -1
- package/dashboard/dist/assets/{check-DgFRXvUP.js → check-BHJvi4ey.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-D-ny8IBp.js → chevron-down-6RBPKNpt.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-CT_OgBOA.js → cloud-checkout-DgWcF-at.js} +1 -1
- package/dashboard/dist/assets/{composite-BJuoijSA.js → composite-D-0pfyMY.js} +1 -1
- package/dashboard/dist/assets/{copy-JdswLd9u.js → copy-B6mJSQsD.js} +1 -1
- package/dashboard/dist/assets/{download-BI-n3YEd.js → download-D31Nn-bI.js} +1 -1
- package/dashboard/dist/assets/{external-link-CH7tksO4.js → external-link-C_lK9S3p.js} +1 -1
- package/dashboard/dist/assets/{flame-CP7hkBXq.js → flame-1ZxhHshT.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-Biw6wloc.js → format-tokens-DJk25SkD.js} +1 -1
- package/dashboard/dist/assets/{gift-Dz9MeN8q.js → gift-BUZzFh-1.js} +1 -1
- package/dashboard/dist/assets/{icons-DP4Ni8VA.js → icons-BVPThCa4.js} +1 -1
- package/dashboard/dist/assets/{index-kQETJkOr.js → index-B-mxrdIz.js} +1 -1
- package/dashboard/dist/assets/{index-SXavLLcl.js → index-C0zFsOWh.js} +1 -1
- package/dashboard/dist/assets/{index-CIfh9hNs.js → index-CjPsyTl7.js} +1 -1
- package/dashboard/dist/assets/{index-BkeoJvCA.js → index-CzstoU6d.js} +1 -1
- package/dashboard/dist/assets/{info-Bba3RZJA.js → info-DmU-vlJ6.js} +1 -1
- package/dashboard/dist/assets/{link-2-B2C7-GOt.js → link-2-BlBM7uw7.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-Dld8yUT7.js → loader-circle-Dhrb_Ocy.js} +1 -1
- package/dashboard/dist/assets/{local-usage-transfer-CAuRHyVW.js → local-usage-transfer-BudmMtkd.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-CtFVyyHC.js → modifiers.esm-kmw1VM8W.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-iFNG6h_M.js → monitor-smartphone-DtjXNC_x.js} +1 -1
- package/dashboard/dist/assets/{play-C7GLNAdX.js → play-9w46lbjy.js} +1 -1
- package/dashboard/dist/assets/{plus-CSKQToSA.js → plus-DAf_WGKT.js} +1 -1
- package/dashboard/dist/assets/{provider-display-BwVQXnb2.js → provider-display-BZ0XlB2q.js} +1 -1
- package/dashboard/dist/assets/{react-Cy_Eczdb.js → react-B01ZJuZY.js} +1 -1
- package/dashboard/dist/assets/{safePolygon-DiPfgohh.js → safePolygon-B6_x12Gb.js} +1 -1
- package/dashboard/dist/assets/{search-DKyVdlXG.js → search-D3dW9GbV.js} +1 -1
- package/dashboard/dist/assets/{server-DaU0lAOr.js → server-C2XZBar7.js} +1 -1
- package/dashboard/dist/assets/{shield-check-DWiYKLWx.js → shield-check-4FX5vbS7.js} +1 -1
- package/dashboard/dist/assets/{skills-api-CXotXIRt.js → skills-api-BvW_MUyg.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-ON-92Wzk.js → sortable.esm-CAa_BJgh.js} +1 -1
- package/dashboard/dist/assets/{store-C91MN0gm.js → store-2AX2MNkz.js} +1 -1
- package/dashboard/dist/assets/{terminal-CZUta1FC.js → terminal-CON7n_ir.js} +1 -1
- package/dashboard/dist/assets/{trash-dOvHm24W.js → trash-C2f88jSi.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-BD3O7D_G.js → use-community-stats-CgQu43xV.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-Geg2sygc.js → use-latest-request-guard-lShM46-3.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-Ck2CuPqb.js → use-limits-display-prefs-8o7pbEg0.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-3DH5xldB.js → use-native-settings-dN8shG3M.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-BonkP7AB.js → use-ordered-list-beJy2IqQ.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-S0bR_G1B.js +1 -0
- package/dashboard/dist/assets/{use-transform-C32HObUN.js → use-transform-cJpAQr2d.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-6GFRuUdV.js → use-usage-limits-CuusZA6b.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-BsGBVGb5.js → useCurrency-Cx9UsKJK.js} +1 -1
- package/dashboard/dist/assets/{usePositioner-DvFrrUs0.js → usePositioner-TEClK6mF.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-Dryg74BA.js → useTokenFormat-BNwTLiac.js} +1 -1
- package/dashboard/dist/assets/{zap-BL_hxkkj.js → zap-CFgbVEhC.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/init.js +30 -16
- package/src/commands/serve.js +28 -25
- package/src/commands/sync.js +3 -3
- package/src/lib/cursor-store.js +5 -1
- package/src/lib/local-api.js +25 -11
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +262 -59
- package/src/lib/runtime-copy.js +77 -0
- package/dashboard/dist/assets/DialogDescription-DG_4qSj0.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-uryC1l51.js +0 -1
package/src/lib/rollout.js
CHANGED
|
@@ -3526,32 +3526,72 @@ async function parseOpencodeMessageFile({
|
|
|
3526
3526
|
// Re-emit stored buckets once on upgrade, including idle historical groups:
|
|
3527
3527
|
// their totals still retain the original attribution, so no log replay is
|
|
3528
3528
|
// needed. Queue readers replace rows by (source, model, hour_start).
|
|
3529
|
+
//
|
|
3530
|
+
// Older repairs sometimes flattened per-model keys into `codex|unknown|<hour>`
|
|
3531
|
+
// while leaving `groupQueued` pointing at the already-correct named queue
|
|
3532
|
+
// rows (#1052). Re-emitting that flattened unknown invents an `unknown`
|
|
3533
|
+
// sibling (and can zero a stale alignedModel) beside the named rows. Skip
|
|
3534
|
+
// re-emit for those flattened legacy groups; clear the marker only.
|
|
3529
3535
|
function preserveCodexModelBuckets(hourlyState, touchedBuckets) {
|
|
3530
3536
|
const upgrading = hourlyState.modelAttributionPolicy?.codex !== "observed";
|
|
3531
|
-
|
|
3537
|
+
const groupQueued =
|
|
3538
|
+
hourlyState.groupQueued && typeof hourlyState.groupQueued === "object"
|
|
3539
|
+
? hourlyState.groupQueued
|
|
3540
|
+
: {};
|
|
3541
|
+
if (!upgrading && !Object.keys(groupQueued).some(
|
|
3532
3542
|
key => key.startsWith(`${DEFAULT_SOURCE}${BUCKET_SEPARATOR}`),
|
|
3533
3543
|
)) return;
|
|
3544
|
+
|
|
3545
|
+
const groupModels = new Map();
|
|
3546
|
+
for (const [key, bucket] of Object.entries(hourlyState.buckets || {})) {
|
|
3547
|
+
const parsed = parseBucketKey(key);
|
|
3548
|
+
if (parsed.source !== DEFAULT_SOURCE || !bucket?.totals) continue;
|
|
3549
|
+
const groupKey = groupBucketKey(parsed.source, parsed.hourStart);
|
|
3550
|
+
const model = normalizeModelInput(parsed.model) || DEFAULT_MODEL;
|
|
3551
|
+
let entry = groupModels.get(groupKey);
|
|
3552
|
+
if (!entry) {
|
|
3553
|
+
entry = { named: false };
|
|
3554
|
+
groupModels.set(groupKey, entry);
|
|
3555
|
+
}
|
|
3556
|
+
if (model !== DEFAULT_MODEL) entry.named = true;
|
|
3557
|
+
}
|
|
3558
|
+
|
|
3534
3559
|
const legacyGroups = new Set();
|
|
3560
|
+
const flattenedLegacyGroups = new Set();
|
|
3561
|
+
for (const groupKey of Object.keys(groupQueued)) {
|
|
3562
|
+
if (!groupKey.startsWith(`${DEFAULT_SOURCE}${BUCKET_SEPARATOR}`)) continue;
|
|
3563
|
+
legacyGroups.add(groupKey);
|
|
3564
|
+
const entry = groupModels.get(groupKey);
|
|
3565
|
+
// No named model buckets left: the hour was flattened to unknown (or the
|
|
3566
|
+
// buckets were dropped). Named queue rows from the repair are the truth.
|
|
3567
|
+
if (!entry || !entry.named) flattenedLegacyGroups.add(groupKey);
|
|
3568
|
+
}
|
|
3569
|
+
|
|
3535
3570
|
for (const [key, bucket] of Object.entries(hourlyState.buckets || {})) {
|
|
3536
3571
|
const parsed = parseBucketKey(key);
|
|
3537
3572
|
if (parsed.source !== DEFAULT_SOURCE || !bucket?.totals) continue;
|
|
3538
3573
|
const groupKey = groupBucketKey(parsed.source, parsed.hourStart);
|
|
3539
|
-
const legacy =
|
|
3540
|
-
if (
|
|
3574
|
+
const legacy = legacyGroups.has(groupKey);
|
|
3575
|
+
if (flattenedLegacyGroups.has(groupKey)) continue;
|
|
3541
3576
|
if (!upgrading && !legacy) continue;
|
|
3542
3577
|
bucket.queuedKey = null;
|
|
3543
3578
|
bucket.retractedUnknownKey = null;
|
|
3544
3579
|
touchedBuckets.add(key);
|
|
3545
3580
|
}
|
|
3546
3581
|
for (const groupKey of legacyGroups) {
|
|
3582
|
+
if (flattenedLegacyGroups.has(groupKey)) {
|
|
3583
|
+
delete groupQueued[groupKey];
|
|
3584
|
+
continue;
|
|
3585
|
+
}
|
|
3547
3586
|
const hourStart = groupKey.slice(DEFAULT_SOURCE.length + BUCKET_SEPARATOR.length);
|
|
3548
3587
|
// Legacy groups emitted a combined unknown row for multiple named models.
|
|
3549
3588
|
// Replace it with the actual unknown total (or zero) before splitting.
|
|
3550
3589
|
const unknown = getHourlyBucket(hourlyState, DEFAULT_SOURCE, DEFAULT_MODEL, hourStart);
|
|
3551
3590
|
unknown.queuedKey = null;
|
|
3552
3591
|
touchedBuckets.add(bucketKey(DEFAULT_SOURCE, DEFAULT_MODEL, hourStart));
|
|
3553
|
-
delete
|
|
3592
|
+
delete groupQueued[groupKey];
|
|
3554
3593
|
}
|
|
3594
|
+
hourlyState.groupQueued = groupQueued;
|
|
3555
3595
|
hourlyState.modelAttributionPolicy = { ...hourlyState.modelAttributionPolicy, codex: "observed" };
|
|
3556
3596
|
}
|
|
3557
3597
|
|
|
@@ -24354,6 +24394,9 @@ async function parseTraeCnApiIncremental({
|
|
|
24354
24394
|
const DSH_SESSION_LOG_MAX_BYTES = 64 * 1024 * 1024;
|
|
24355
24395
|
const DSH_SESSION_TEXT_MAX_BYTES = 128 * 1024 * 1024;
|
|
24356
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;
|
|
24357
24400
|
|
|
24358
24401
|
// Precedence mirrors the harness's own resolveDshHome: an explicit
|
|
24359
24402
|
// TokenTracker override, then the harness's $DSH_HOME, then `~/.dsh`. Tests
|
|
@@ -24720,20 +24763,48 @@ function legacyDshContributionsFromSnapshot(snapshot, previousState) {
|
|
|
24720
24763
|
previousState.lastSeq < -1 ||
|
|
24721
24764
|
previousState.inode !== snapshot.inode ||
|
|
24722
24765
|
!Number.isFinite(previousState.size) ||
|
|
24723
|
-
snapshot.size < previousState.size
|
|
24766
|
+
snapshot.size < previousState.size ||
|
|
24767
|
+
(snapshot.size === previousState.size && snapshot.mtimeMs !== previousState.mtimeMs)
|
|
24724
24768
|
) {
|
|
24725
24769
|
return null;
|
|
24726
24770
|
}
|
|
24727
24771
|
const parsed = extractDshSessionUsage(snapshot.text, -1);
|
|
24728
|
-
if (!parsed.complete || parsed.sessionId !== previousState.sessionId
|
|
24772
|
+
if (!parsed.complete || parsed.sessionId !== previousState.sessionId ||
|
|
24773
|
+
parsed.maxSeq < previousState.lastSeq) return null;
|
|
24729
24774
|
// The pre-ledger parser replayed unknown-sequence usage on every pass, so
|
|
24730
24775
|
// there is no safe prefix boundary for such a record. Defer instead.
|
|
24731
|
-
|
|
24776
|
+
const priorDeltas = previousState.accountingVersion === DSH_ACCOUNTING_VERSION
|
|
24777
|
+
? parsed.deltas : parsed.rawDeltas;
|
|
24778
|
+
if (priorDeltas.some((delta) => !Number.isSafeInteger(delta.seq))) return null;
|
|
24732
24779
|
return dshContributionsFromDeltas(
|
|
24733
|
-
|
|
24780
|
+
priorDeltas.filter((delta) => delta.seq <= previousState.lastSeq),
|
|
24734
24781
|
);
|
|
24735
24782
|
}
|
|
24736
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
|
+
|
|
24737
24808
|
function dshContributionsCoverPrior(prior, candidate) {
|
|
24738
24809
|
for (const [key, previous] of Object.entries(prior || {})) {
|
|
24739
24810
|
const current = candidate?.[key];
|
|
@@ -25127,17 +25198,20 @@ function isCompleteDshJsonLine(raw) {
|
|
|
25127
25198
|
return index === text.length;
|
|
25128
25199
|
}
|
|
25129
25200
|
|
|
25130
|
-
//
|
|
25131
|
-
//
|
|
25132
|
-
//
|
|
25133
|
-
|
|
25134
|
-
|
|
25135
|
-
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 = [];
|
|
25136
25206
|
const watermark = Number.isFinite(lastSeq) ? lastSeq : -1;
|
|
25137
25207
|
let maxSeq = watermark;
|
|
25208
|
+
let maxLine = -1;
|
|
25138
25209
|
let sessionId = null;
|
|
25139
25210
|
let headerModel = null;
|
|
25140
25211
|
let complete = true;
|
|
25212
|
+
let isSeeded = false;
|
|
25213
|
+
let seedLength = null;
|
|
25214
|
+
let inheritedThrough = -1;
|
|
25141
25215
|
|
|
25142
25216
|
const lines = String(text || "").split("\n");
|
|
25143
25217
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -25145,29 +25219,37 @@ function extractDshSessionUsage(text, lastSeq = -1) {
|
|
|
25145
25219
|
if (!line || !line.trim()) continue;
|
|
25146
25220
|
if (!isCompleteDshJsonLine(line)) {
|
|
25147
25221
|
complete = false;
|
|
25148
|
-
//
|
|
25149
|
-
// complete prefix so a repaired record is retried before seq advances.
|
|
25222
|
+
// Do not acknowledge records after a corrupt/torn interior line.
|
|
25150
25223
|
break;
|
|
25151
25224
|
}
|
|
25225
|
+
maxLine = i;
|
|
25152
25226
|
const eventType = parseDshJsonString(findDshJsonProperty(line, "type"));
|
|
25153
25227
|
if (!eventType) continue;
|
|
25154
|
-
|
|
25228
|
+
const data = findDshJsonProperty(line, "data");
|
|
25155
25229
|
if (eventType === "session" || eventType === "session/start") {
|
|
25156
|
-
const id =
|
|
25157
|
-
parseDshJsonString(findDshJsonProperty(
|
|
25158
|
-
parseDshJsonString(findDshJsonProperty(
|
|
25159
|
-
parseDshJsonString(findDshJsonProperty(findDshJsonProperty(line, "data"), "sessionId"));
|
|
25230
|
+
const id = parseDshJsonString(findDshJsonProperty(line, "id")) ||
|
|
25231
|
+
parseDshJsonString(findDshJsonProperty(data, "id")) ||
|
|
25232
|
+
parseDshJsonString(findDshJsonProperty(data, "sessionId"));
|
|
25160
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;
|
|
25161
25240
|
continue;
|
|
25162
25241
|
}
|
|
25163
25242
|
|
|
25164
25243
|
const seq = parseDshJsonNumber(findDshJsonProperty(line, "seq"));
|
|
25165
25244
|
const seqKnown = Number.isFinite(seq) && seq >= 0;
|
|
25166
25245
|
if (seqKnown && seq > maxSeq) maxSeq = seq;
|
|
25167
|
-
|
|
25168
|
-
const data = findDshJsonProperty(line, "data");
|
|
25169
25246
|
if (!data) continue;
|
|
25170
|
-
|
|
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
|
+
}
|
|
25171
25253
|
if (eventType === "request/header") {
|
|
25172
25254
|
const header = findDshJsonProperty(data, "header");
|
|
25173
25255
|
const config = findDshJsonProperty(header, "config");
|
|
@@ -25176,39 +25258,46 @@ function extractDshSessionUsage(text, lastSeq = -1) {
|
|
|
25176
25258
|
if (normalized) headerModel = normalized;
|
|
25177
25259
|
continue;
|
|
25178
25260
|
}
|
|
25179
|
-
|
|
25180
25261
|
if (eventType !== "assistant/message" && eventType !== "message/assistant") continue;
|
|
25181
|
-
if (seqKnown && seq <= watermark) continue;
|
|
25182
|
-
|
|
25183
25262
|
const message = findDshJsonProperty(data, "message");
|
|
25184
25263
|
const source = message ? findDshJsonProperty(message, "source") : null;
|
|
25185
25264
|
const model = normalizeDshModelName(
|
|
25186
25265
|
(source && parseDshJsonString(findDshJsonProperty(source, "model"))) ||
|
|
25187
25266
|
parseDshJsonString(findDshJsonProperty(data, "model")),
|
|
25188
25267
|
) || headerModel;
|
|
25189
|
-
const totals = dshUsageToTotals(
|
|
25190
|
-
|
|
25191
|
-
|
|
25192
|
-
),
|
|
25193
|
-
);
|
|
25268
|
+
const totals = dshUsageToTotals(parseDshUsageSlice(
|
|
25269
|
+
findDshJsonProperty(data, "usage") || findDshJsonProperty(line, "usage"),
|
|
25270
|
+
));
|
|
25194
25271
|
if (!model || !totals) continue;
|
|
25195
|
-
|
|
25196
|
-
const timeMs =
|
|
25197
|
-
parseDshJsonNumber(findDshJsonProperty(line, "time")) ||
|
|
25272
|
+
const timeMs = parseDshJsonNumber(findDshJsonProperty(line, "time")) ||
|
|
25198
25273
|
parseDshJsonNumber(findDshJsonProperty(line, "timestamp")) ||
|
|
25199
25274
|
parseDshJsonNumber(findDshJsonProperty(data, "time")) ||
|
|
25200
25275
|
parseDshJsonNumber(findDshJsonProperty(data, "timestamp"));
|
|
25201
25276
|
if (!Number.isFinite(timeMs) || timeMs <= 0) continue;
|
|
25202
|
-
|
|
25203
|
-
|
|
25277
|
+
const id = parseDshJsonString(findDshJsonProperty(message, "id"));
|
|
25278
|
+
rawDeltas.push({ seq: seqKnown ? seq : null, line: i,
|
|
25279
|
+
messageId: id?.trim() || null, model, timeMs, totals });
|
|
25204
25280
|
}
|
|
25205
25281
|
|
|
25206
|
-
|
|
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 };
|
|
25207
25296
|
}
|
|
25208
25297
|
|
|
25209
25298
|
// Incremental parser entrypoint. Mirrors the other passive JSONL readers:
|
|
25210
25299
|
// identity-check each file (inode/size/mtime), re-read + re-parse only when it
|
|
25211
|
-
// changed, dedup via
|
|
25300
|
+
// changed, dedup via message ownership plus seq/line watermarks, accumulate into hourly
|
|
25212
25301
|
// buckets, then flush touched buckets to the queue. Session contribution ledgers
|
|
25213
25302
|
// make artifact-path migrations replace one session instead of resetting DSH.
|
|
25214
25303
|
async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgress }) {
|
|
@@ -25222,6 +25311,9 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
|
|
|
25222
25311
|
const storedSessionState =
|
|
25223
25312
|
dshState.sessions && typeof dshState.sessions === "object" ? dshState.sessions : {};
|
|
25224
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 || {}) };
|
|
25225
25317
|
|
|
25226
25318
|
const hourlyState = normalizeHourlyState(cursors?.hourly);
|
|
25227
25319
|
// normalizeHourlyState preserves bucket objects for the other incremental
|
|
@@ -25247,6 +25339,22 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
|
|
|
25247
25339
|
const presentFiles = new Set(files);
|
|
25248
25340
|
const total = files.length;
|
|
25249
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
|
+
}
|
|
25250
25358
|
let recordsProcessed = 0;
|
|
25251
25359
|
let eventsAggregated = 0;
|
|
25252
25360
|
|
|
@@ -25304,6 +25412,9 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
|
|
|
25304
25412
|
const needsLedgerBackfill = Boolean(
|
|
25305
25413
|
previousSessionId && !previousSessionContributions && !previousFileContributions,
|
|
25306
25414
|
);
|
|
25415
|
+
const needsAccountingUpgrade = Boolean(
|
|
25416
|
+
prev && prev.accountingVersion !== DSH_ACCOUNTING_VERSION,
|
|
25417
|
+
);
|
|
25307
25418
|
let snapshot;
|
|
25308
25419
|
let parsed;
|
|
25309
25420
|
let fullParsed = null;
|
|
@@ -25312,7 +25423,7 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
|
|
|
25312
25423
|
|
|
25313
25424
|
try {
|
|
25314
25425
|
snapshot = await readDshSessionSnapshot(filePath, {
|
|
25315
|
-
previous: needsLedgerBackfill ? null : prev,
|
|
25426
|
+
previous: needsLedgerBackfill || needsAccountingUpgrade ? null : prev,
|
|
25316
25427
|
});
|
|
25317
25428
|
if (!snapshot) {
|
|
25318
25429
|
// A resolver-selected replacement can disappear between discovery and
|
|
@@ -25320,10 +25431,11 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
|
|
|
25320
25431
|
// contribution. This is deliberately separate from a thrown read
|
|
25321
25432
|
// error because a missing path returns null from the snapshot helper.
|
|
25322
25433
|
deferStaleMigrationPaths(filePath, "replacement-missing");
|
|
25434
|
+
if (needsAccountingUpgrade) deferredMigrationPaths.set(filePath, "history-missing");
|
|
25323
25435
|
reportProgress(idx, recordsProcessed, eventsAggregated, total);
|
|
25324
25436
|
continue;
|
|
25325
25437
|
}
|
|
25326
|
-
if (snapshot.unchanged && !needsLedgerBackfill) {
|
|
25438
|
+
if (snapshot.unchanged && !needsLedgerBackfill && !needsAccountingUpgrade) {
|
|
25327
25439
|
reportProgress(idx, recordsProcessed, eventsAggregated, total);
|
|
25328
25440
|
continue;
|
|
25329
25441
|
}
|
|
@@ -25345,18 +25457,18 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
|
|
|
25345
25457
|
),
|
|
25346
25458
|
);
|
|
25347
25459
|
const lastSeq = fileReset || !Number.isFinite(prev?.lastSeq) ? -1 : prev.lastSeq;
|
|
25348
|
-
parsed = extractDshSessionUsage(snapshot.text, lastSeq);
|
|
25460
|
+
parsed = extractDshSessionUsage(snapshot.text, lastSeq, fileReset ? -1 : prev?.lastLine);
|
|
25349
25461
|
sessionChanged = Boolean(
|
|
25350
25462
|
prev &&
|
|
25351
25463
|
parsed.sessionId &&
|
|
25352
25464
|
parsed.sessionId !== previousSessionId,
|
|
25353
25465
|
);
|
|
25354
25466
|
if (sessionChanged) parsed = extractDshSessionUsage(snapshot.text, -1);
|
|
25355
|
-
if (!prev || needsLedgerBackfill || sessionChanged || fileReset) {
|
|
25467
|
+
if (!prev || needsLedgerBackfill || needsAccountingUpgrade || sessionChanged || fileReset) {
|
|
25356
25468
|
fullParsed = extractDshSessionUsage(snapshot.text, -1);
|
|
25357
|
-
if (!prev || sessionChanged || fileReset) parsed = fullParsed;
|
|
25469
|
+
if (!prev || needsAccountingUpgrade || sessionChanged || fileReset) parsed = fullParsed;
|
|
25358
25470
|
}
|
|
25359
|
-
if (!parsed.complete) snapshot.mtimeMs = -1;
|
|
25471
|
+
if (!parsed.complete || !parsed.seedComplete) snapshot.mtimeMs = -1;
|
|
25360
25472
|
} catch (error) {
|
|
25361
25473
|
if (process.env.TOKENTRACKER_DEBUG) {
|
|
25362
25474
|
process.stderr.write(`[dsh] skipped ${filePath}: ${error?.message || error}\n`);
|
|
@@ -25365,11 +25477,22 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
|
|
|
25365
25477
|
// selected file list. Keep that cursor until the replacement is readable
|
|
25366
25478
|
// so a failed migration cannot turn into an untracked double-count.
|
|
25367
25479
|
deferStaleMigrationPaths(filePath, "replacement-read-failed");
|
|
25480
|
+
if (needsAccountingUpgrade) deferredMigrationPaths.set(filePath, "history-read-failed");
|
|
25368
25481
|
reportProgress(idx, recordsProcessed, eventsAggregated, total);
|
|
25369
25482
|
continue;
|
|
25370
25483
|
}
|
|
25371
25484
|
|
|
25372
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
|
+
}
|
|
25373
25496
|
const stalePathWithoutId = sessionId
|
|
25374
25497
|
? null
|
|
25375
25498
|
: Object.keys(fileState).find(
|
|
@@ -25387,8 +25510,15 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
|
|
|
25387
25510
|
const stalePath = staleFileForSession(sessionId, filePath);
|
|
25388
25511
|
const previousPath =
|
|
25389
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
|
+
}
|
|
25390
25519
|
const resetCurrentFile = Boolean(fileReset && !sessionChanged);
|
|
25391
|
-
const
|
|
25520
|
+
const upgradeCurrentFile = needsAccountingUpgrade && !sessionChanged;
|
|
25521
|
+
const replacementPath = previousPath || (resetCurrentFile || upgradeCurrentFile ? filePath : null);
|
|
25392
25522
|
const sessionContributions = storedDshContributions(session);
|
|
25393
25523
|
const staleContributions = storedDshContributions(stalePath && fileState[stalePath]);
|
|
25394
25524
|
const fileContributions = storedDshContributions(prev);
|
|
@@ -25411,7 +25541,9 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
|
|
|
25411
25541
|
// A pure rename preserves the same bytes. Reusing the parsed full
|
|
25412
25542
|
// contribution is safe here because replacement below subtracts and
|
|
25413
25543
|
// re-adds it; no sequence watermark is transferred.
|
|
25414
|
-
oldContributions = dshContributionsFromDeltas(
|
|
25544
|
+
oldContributions = dshContributionsFromDeltas(
|
|
25545
|
+
previousState.accountingVersion === DSH_ACCOUNTING_VERSION ? parsed.deltas : parsed.rawDeltas,
|
|
25546
|
+
);
|
|
25415
25547
|
}
|
|
25416
25548
|
|
|
25417
25549
|
// Cursors written before the contribution ledger existed can still be
|
|
@@ -25424,10 +25556,13 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
|
|
|
25424
25556
|
const oldSnapshot = await readDshSessionSnapshot(previousPath).catch(() => null);
|
|
25425
25557
|
oldContributions = legacyDshContributionsFromSnapshot(oldSnapshot, oldState);
|
|
25426
25558
|
}
|
|
25559
|
+
if (sessionId && upgradeCurrentFile && !oldContributions) {
|
|
25560
|
+
oldContributions = legacyDshContributionsFromSnapshot(snapshot, prev);
|
|
25561
|
+
}
|
|
25427
25562
|
const replaceSession = Boolean(
|
|
25428
25563
|
sessionId &&
|
|
25429
25564
|
oldContributions &&
|
|
25430
|
-
(!prev || resetCurrentFile || (previousPath && previousPath !== filePath)),
|
|
25565
|
+
(!prev || resetCurrentFile || upgradeCurrentFile || (previousPath && previousPath !== filePath)),
|
|
25431
25566
|
);
|
|
25432
25567
|
const oldContributionCount = Object.keys(oldContributions || {}).length;
|
|
25433
25568
|
if (sessionId && replacementPath && !oldContributions) {
|
|
@@ -25439,20 +25574,25 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
|
|
|
25439
25574
|
}
|
|
25440
25575
|
if (
|
|
25441
25576
|
replaceSession &&
|
|
25442
|
-
(!parsed.complete || (oldContributionCount > 0 && parsed.
|
|
25577
|
+
(!parsed.complete || !parsed.seedComplete || (oldContributionCount > 0 && parsed.rawDeltas.length === 0))
|
|
25443
25578
|
) {
|
|
25444
25579
|
// A readable header-only or torn replacement is not authoritative. Keep
|
|
25445
25580
|
// the old contribution until a complete replacement with usage arrives.
|
|
25446
25581
|
deferredFilePaths.add(replacementPath);
|
|
25447
25582
|
deferredMigrationPaths.set(
|
|
25448
25583
|
replacementPath,
|
|
25449
|
-
parsed.complete ? "replacement-
|
|
25584
|
+
!parsed.complete ? "replacement-incomplete" :
|
|
25585
|
+
!parsed.seedComplete ? "seed-boundary-unavailable" : "replacement-has-no-usage",
|
|
25450
25586
|
);
|
|
25451
25587
|
if (resetCurrentFile) fileState[filePath] = { ...prev, mtimeMs: -1 };
|
|
25452
25588
|
reportProgress(idx, recordsProcessed, eventsAggregated, total);
|
|
25453
25589
|
continue;
|
|
25454
25590
|
}
|
|
25455
|
-
|
|
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);
|
|
25456
25596
|
if (
|
|
25457
25597
|
replaceSession &&
|
|
25458
25598
|
oldContributionCount > 0 &&
|
|
@@ -25478,6 +25618,46 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
|
|
|
25478
25618
|
continue;
|
|
25479
25619
|
}
|
|
25480
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
|
+
|
|
25481
25661
|
if (replaceSession) {
|
|
25482
25662
|
applyDshContributions({
|
|
25483
25663
|
hourlyState,
|
|
@@ -25490,18 +25670,24 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
|
|
|
25490
25670
|
|
|
25491
25671
|
let nextContributions = replaceSession
|
|
25492
25672
|
? {}
|
|
25493
|
-
: sessionContributions || fileContributions || {};
|
|
25494
|
-
|
|
25495
|
-
|
|
25496
|
-
|
|
25497
|
-
|
|
25498
|
-
|
|
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;
|
|
25499
25685
|
const bucketStart = toUtcHalfHourStart(delta.timeMs);
|
|
25500
25686
|
if (!bucketStart) continue;
|
|
25501
25687
|
const bucket = getHourlyBucket(hourlyState, DSH_SOURCE, delta.model, bucketStart);
|
|
25502
25688
|
addTotals(bucket.totals, delta.totals);
|
|
25503
25689
|
touchedBuckets.add(bucketKey(DSH_SOURCE, delta.model, bucketStart));
|
|
25504
|
-
|
|
25690
|
+
addDshContribution(nextContributions, delta.model, bucketStart, delta.totals);
|
|
25505
25691
|
eventsAggregated += 1;
|
|
25506
25692
|
}
|
|
25507
25693
|
|
|
@@ -25512,12 +25698,15 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
|
|
|
25512
25698
|
mtimeMs: snapshot.mtimeMs,
|
|
25513
25699
|
sessionId,
|
|
25514
25700
|
lastSeq: parsed.maxSeq,
|
|
25701
|
+
lastLine: parsed.maxLine,
|
|
25702
|
+
accountingVersion: DSH_ACCOUNTING_VERSION,
|
|
25515
25703
|
contributions: nextContributions,
|
|
25516
25704
|
updatedAt,
|
|
25517
25705
|
};
|
|
25518
25706
|
if (sessionId) {
|
|
25519
25707
|
sessionState[sessionId] = {
|
|
25520
25708
|
lastPath: filePath,
|
|
25709
|
+
accountingVersion: DSH_ACCOUNTING_VERSION,
|
|
25521
25710
|
contributions: nextContributions,
|
|
25522
25711
|
updatedAt,
|
|
25523
25712
|
};
|
|
@@ -25535,7 +25724,11 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
|
|
|
25535
25724
|
// Retain pre-ledger identity until a replacement is verified. Its counted
|
|
25536
25725
|
// usage survives file deletion, so elapsed time cannot authorize replay.
|
|
25537
25726
|
// Current ledgers retain the same identity in sessionState instead.
|
|
25538
|
-
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
|
+
}
|
|
25539
25732
|
if (!Number.isFinite(Number(state?.missingSince))) {
|
|
25540
25733
|
fileState[filePath] = { ...state, missingSince: nowMs };
|
|
25541
25734
|
}
|
|
@@ -25544,10 +25737,20 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
|
|
|
25544
25737
|
delete fileState[filePath];
|
|
25545
25738
|
}
|
|
25546
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
|
+
}
|
|
25547
25748
|
const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
|
|
25548
25749
|
const updatedAt = new Date().toISOString();
|
|
25549
25750
|
hourlyState.updatedAt = updatedAt;
|
|
25550
25751
|
cursors.hourly = hourlyState;
|
|
25752
|
+
dshState.accountingVersion = DSH_ACCOUNTING_VERSION;
|
|
25753
|
+
dshState.messageOwners = messageOwners;
|
|
25551
25754
|
dshState.files = fileState;
|
|
25552
25755
|
dshState.sessions = sessionState;
|
|
25553
25756
|
if (deferredMigrationPaths.size > 0) {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const fs = require("node:fs/promises");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const { writeFileAtomic } = require("./fs");
|
|
4
|
+
|
|
5
|
+
async function statIfPresent(file) {
|
|
6
|
+
try {
|
|
7
|
+
return await fs.lstat(file);
|
|
8
|
+
} catch (error) {
|
|
9
|
+
if (error.code === "ENOENT") return null;
|
|
10
|
+
throw error;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function ensureRuntimeDirectory(dir) {
|
|
15
|
+
const target = await statIfPresent(dir);
|
|
16
|
+
if (target?.isDirectory() && !target.isSymbolicLink()) return;
|
|
17
|
+
if (target) await fs.rm(dir, { recursive: true, force: true });
|
|
18
|
+
await fs.mkdir(dir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Runtime refreshes run on every desktop launch. Compare bytes instead of
|
|
22
|
+
// deleting the installation: identical versions, interrupted copies, and local
|
|
23
|
+
// builds with unchanged version numbers all behave correctly. Never follow a
|
|
24
|
+
// destination symlink when replacing files or pruning obsolete entries.
|
|
25
|
+
async function copyRuntimeTree(from, to) {
|
|
26
|
+
const source = await fs.lstat(from);
|
|
27
|
+
let target = await statIfPresent(to);
|
|
28
|
+
if (source.isSymbolicLink()) {
|
|
29
|
+
const link = await fs.readlink(from);
|
|
30
|
+
const resolved = path.resolve(path.dirname(from), link);
|
|
31
|
+
if (target?.isSymbolicLink()
|
|
32
|
+
&& path.resolve(path.dirname(to), await fs.readlink(to)) === resolved) return;
|
|
33
|
+
if (target) await fs.rm(to, { recursive: true, force: true });
|
|
34
|
+
await fs.mkdir(path.dirname(to), { recursive: true });
|
|
35
|
+
// Keep fs.cp's existing default link semantics for npm/pnpm installations.
|
|
36
|
+
await fs.cp(from, to);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (target && (target.isSymbolicLink() || source.isDirectory() !== target.isDirectory())) {
|
|
41
|
+
await fs.rm(to, { recursive: true, force: true });
|
|
42
|
+
target = null;
|
|
43
|
+
}
|
|
44
|
+
if (source.isDirectory()) {
|
|
45
|
+
if (!target) await fs.mkdir(to, { recursive: true });
|
|
46
|
+
const names = await fs.readdir(from);
|
|
47
|
+
const retained = new Set(names);
|
|
48
|
+
for (const name of names) {
|
|
49
|
+
await copyRuntimeTree(path.join(from, name), path.join(to, name));
|
|
50
|
+
}
|
|
51
|
+
for (const name of await fs.readdir(to)) {
|
|
52
|
+
if (!retained.has(name)) {
|
|
53
|
+
await fs.rm(path.join(to, name), { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!target) {
|
|
60
|
+
await fs.mkdir(path.dirname(to), { recursive: true });
|
|
61
|
+
await fs.copyFile(from, to);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const content = await fs.readFile(from);
|
|
65
|
+
if (target?.isFile() && target.size === source.size
|
|
66
|
+
&& content.equals(await fs.readFile(to))) {
|
|
67
|
+
if (process.platform !== "win32" && (source.mode & 0o777) !== (target.mode & 0o777)) {
|
|
68
|
+
await fs.chmod(to, source.mode & 0o777);
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Hooks may read the installed runtime while it is refreshed. Publish each
|
|
73
|
+
// changed file atomically so they cannot read a truncated module.
|
|
74
|
+
await writeFileAtomic(to, content, { mode: source.mode & 0o777 });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { copyRuntimeTree, ensureRuntimeDirectory };
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{r as n,J as a}from"./__vite-browser-external-RPZeXrAv.js";import{u as p}from"./DialogTitle-Bn14t_Xd.js";import{u as c}from"./composite-BJuoijSA.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};
|