tokentracker-cli 0.84.4 → 0.84.5
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-C29PwsK_.js → AchievementBadge-DJvRfvu1.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-BlZODLp8.js → AchievementsPage-Cr8WToxS.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-Bc6NMnIa.js → AchievementsSection-BCPYB46e.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-BUy4636q.js → BadgeDetailModal-D-12Ek-H.js} +1 -1
- package/dashboard/dist/assets/{Card-CqxuFMAN.js → Card-Ha3x9zIj.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-BG3m8BgW.js → ClawdAnimated-DoGgAppT.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-Dcwtom2b.js → CommandPalette-DIKocEAD.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-dBjHrgjE.js → CommunityStatsModal-BNuMbEWy.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-DncQkptq.js → ConfirmModal-F2gPIIS9.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-CdF5WNHN.js → DashboardPage-YvoCEGG9.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-B-PO4s-r.js → DevicePage-CbC_J22s.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-DGtj17VY.js → DialogClose-VEynQCYg.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-B9bb9Ar0.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-DLkuu1Iw.js → DialogTitle-BbAVHI_v.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-Dd5RBmXd.js → FadeIn-CZVqSnis.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-BBCOpHqO.js → HeaderGithubStar-Cs_h332Y.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-BbigYxdC.js → HoverTooltip-DvvHTphK.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-BBpZYjSC.js → IpCheckPage-DPVmqSS_.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-ttXnt_s9.js → LandingPage-BCvNh3zE.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-DOl_cOO9.js → LeaderboardAvatar-BAPZ2ROX.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-BYIyoLkB.js → LeaderboardPage-DZSxazHP.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-BO2n4LSQ.js → LeaderboardProfileModal-r03akfe2.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-CqwW5yMw.js → LeaderboardProfilePage-CU63YW2M.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-B-w771Tl.js → LimitsPage-JWZDa5_Y.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-Cqc_diwF.js → LocalOnlyNotice-BHlcPCZk.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-D6zc1JKY.js → LoginCard-C-dqUumR.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-udN3TqoF.js → LoginModal-BFzRg4Iy.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-BucPr3_X.js → LoginPage-QtlnGF1S.js} +1 -1
- package/dashboard/dist/assets/{PetPage-6_nmMXWp.js → PetPage-C6UMY2qD.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-DntczBmH.js → PopoverPopup-CltmqcrT.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-Z6qlwSj1.js → ProviderIcon-BqOI9EiD.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-WqQwsQC1.js → ResetPasswordPage-BsfCBUZD.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-D3NM3dOv.js → SegmentedControl-D4biXw_Q.js} +1 -1
- package/dashboard/dist/assets/{Select-DCpwSFK7.js → Select-DTw-14iE.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-DICaclqa.js → SelectItemText-3tUrnu3f.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-COHoSjPP.js → SessionsPage-DYYlNqHg.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-nzbrBo_U.js → SettingsPage-CYlEboSl.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-DE8mK1lS.js → SkillsPage-Bzq8V5Pd.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-BSWGbByr.js → TokenGalaxy-DU77e8sM.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-CxxXTuTT.js → ToolbarRootContext-DV3KGf-a.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-DillSTMV.js → TrendMonitor-D6QYKmbl.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-LqyxKYAt.js → WidgetsPage-DSP4jCsH.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-DNDuRKjf.js → WrappedPage-B0x6O_DU.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-DEhUaWQY.js → __vite-browser-external-D3llGq6p.js} +2 -2
- package/dashboard/dist/assets/{agent-logos-D34B25op.js → agent-logos-B2S_cdOL.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-BYWbL4WV.js → arrow-up-right-DNUVuhx2.js} +1 -1
- package/dashboard/dist/assets/{check-DkfaR6JX.js → check-C3ARiiE_.js} +1 -1
- package/dashboard/dist/assets/{chevron-right-D2cpt_J3.js → chevron-right-3bY7H8jO.js} +1 -1
- package/dashboard/dist/assets/{copy-Cv5Ix0Pn.js → copy-C-UIMKoO.js} +1 -1
- package/dashboard/dist/assets/{download-5nr9Tl80.js → download-DtRzJfcr.js} +1 -1
- package/dashboard/dist/assets/{flame-B6M-msYH.js → flame-Bas3ry-g.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-DiRRwgY8.js → format-tokens-DFtRUIH8.js} +1 -1
- package/dashboard/dist/assets/{icons-DQHoG48j.js → icons-CHBt4Tmo.js} +1 -1
- package/dashboard/dist/assets/{index-hSxFduER.js → index-4unZlm2w.js} +1 -1
- package/dashboard/dist/assets/{index-BoH_HaYH.js → index-C1ADnvzm.js} +1 -1
- package/dashboard/dist/assets/{index-vFee2-sp.js → index-CZRlrK7w.js} +1 -1
- package/dashboard/dist/assets/{info-CM5MR8lk.js → info-BkrO-fg3.js} +1 -1
- package/dashboard/dist/assets/{limits-providers-BV4Y9nI4.js → limits-providers-BtwYmYHA.js} +1 -1
- package/dashboard/dist/assets/{link-2-Bd5jF09Z.js → link-2-DTLFz8uG.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-B5oZJSYV.js → loader-circle-BqQW_Ipq.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-CiYy9Kri.js → maximize-2-YdpeE4dq.js} +1 -1
- package/dashboard/dist/assets/{provider-display-lzLynfde.js → provider-display-CgBOpjq0.js} +1 -1
- package/dashboard/dist/assets/{react-CllryaXi.js → react-DD9jpv--.js} +1 -1
- package/dashboard/dist/assets/{search-DDdsotHp.js → search--TvX-fig.js} +1 -1
- package/dashboard/dist/assets/{skills-api-487vZmwI.js → skills-api-C5C8a1at.js} +1 -1
- package/dashboard/dist/assets/{store-CYaTRd_k.js → store-CRA8gPnP.js} +1 -1
- package/dashboard/dist/assets/{terminal-OyeOY0tb.js → terminal-BXZyl2ek.js} +1 -1
- package/dashboard/dist/assets/{trash-2-COI9stXJ.js → trash-2-IFdBQ65v.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-DkzinngC.js → use-community-stats-LyJ5aMpX.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-CiMETU5Y.js → use-limits-display-prefs-CKvFS1Nn.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-qzpxm9Bh.js → use-native-settings-Cg-4j_0z.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-BI-Qh9xR.js → use-ordered-list-2pX5PXZO.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-BCR01kRg.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-1WyhIjeG.js → use-session-efficiency-pref-DilJOODA.js} +1 -1
- package/dashboard/dist/assets/{use-transform-D5i_ArzG.js → use-transform-DiyYTLpI.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-Drcnt7__.js → use-usage-limits-DkUmp_nW.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-KWJo1GIr.js → useCurrency-lm7KjGoD.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-K0FPky1Y.js → useScrollLock-DVSyJ-GE.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-CwG4ck8v.js → useTokenFormat-Li8BANKO.js} +1 -1
- package/dashboard/dist/assets/{zap-wZ6IQAyU.js → zap-CKZt1_i5.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/lib/pricing/curated-overrides.json +536 -101
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +373 -72
- package/dashboard/dist/assets/DialogDescription-wStLk8Br.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-DrsPn1ux.js +0 -1
package/src/lib/rollout.js
CHANGED
|
@@ -6204,7 +6204,7 @@ async function parseKimiCodeIncremental({ wireFiles, cursors, queuePath, onProgr
|
|
|
6204
6204
|
// each sync (passive scan only, same shape as Kimi's wire.jsonl reader).
|
|
6205
6205
|
//
|
|
6206
6206
|
// Per-line record types: message, reasoning, topic, file-history-snapshot.
|
|
6207
|
-
// Only `type=="message"
|
|
6207
|
+
// Only `type=="message" and role=="assistant"` carry token usage. The shape:
|
|
6208
6208
|
//
|
|
6209
6209
|
// providerData.rawUsage = {
|
|
6210
6210
|
// prompt_tokens: 22223, // OpenAI-style — INCLUDES cached
|
|
@@ -6734,7 +6734,7 @@ async function parseCodebuddyIncremental({
|
|
|
6734
6734
|
// (each verified against real ~/.workbuddy logs, NOT assumed from CodeBuddy):
|
|
6735
6735
|
//
|
|
6736
6736
|
// 1. Usage lives on `function_call` records too — not only on
|
|
6737
|
-
// `type=="message"
|
|
6737
|
+
// `type=="message" and role=="assistant"`. Each LLM round-trip (whether it
|
|
6738
6738
|
// ends in a tool call or a text reply) carries its own providerData.rawUsage.
|
|
6739
6739
|
// We therefore aggregate EVERY record that has providerData.rawUsage and
|
|
6740
6740
|
// dedup per response id, instead of filtering by record type.
|
|
@@ -12387,7 +12387,8 @@ async function parseCopilotAppDbIncremental({
|
|
|
12387
12387
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
12388
12388
|
|
|
12389
12389
|
const GROK_ESTIMATED_INPUT_RATIO = 0.8;
|
|
12390
|
-
|
|
12390
|
+
// v4: bill from turn_completed.usage (true cumulative API usage), not context-window totalTokens.
|
|
12391
|
+
const GROK_CURSOR_VERSION = 4;
|
|
12391
12392
|
|
|
12392
12393
|
function resolveGrokBuildHome(env = process.env) {
|
|
12393
12394
|
if (env.TOKENTRACKER_GROK_HOME) return env.TOKENTRACKER_GROK_HOME;
|
|
@@ -12540,6 +12541,9 @@ function grokMessageCountFromSignals(signals) {
|
|
|
12540
12541
|
);
|
|
12541
12542
|
}
|
|
12542
12543
|
|
|
12544
|
+
// Context-window telemetry only. Prefer turn_completed.usage when available —
|
|
12545
|
+
// signals.totalTokens / contextTokensUsed track the live window, not billable
|
|
12546
|
+
// cumulative spend across a session (especially after compaction).
|
|
12543
12547
|
function grokEffectiveTotalFromSignals(signals) {
|
|
12544
12548
|
if (!signals || typeof signals !== "object") return 0;
|
|
12545
12549
|
const beforeCompaction = normalizeNonNegativeNumber(signals.totalTokensBeforeCompaction);
|
|
@@ -12612,28 +12616,135 @@ function grokFileEndsWithNewline(filePath, size) {
|
|
|
12612
12616
|
}
|
|
12613
12617
|
}
|
|
12614
12618
|
|
|
12615
|
-
|
|
12616
|
-
|
|
12619
|
+
|
|
12620
|
+
function canonicalizeGrokUsageModel(model) {
|
|
12621
|
+
const raw = normalizeModelInput(model) || "grok-build";
|
|
12622
|
+
const lower = raw.toLowerCase();
|
|
12623
|
+
// Free Build SKU must not fuzzy-match paid grok-4.5 rates in native clients.
|
|
12624
|
+
if (lower.includes("build-free") || lower.endsWith("-free") || lower.includes("free-tier")) {
|
|
12625
|
+
return "grok-build-free";
|
|
12626
|
+
}
|
|
12627
|
+
if (lower === "grok-4.5-build" || lower === "grok-4-5-build") {
|
|
12628
|
+
return "grok-4.5-build";
|
|
12629
|
+
}
|
|
12630
|
+
return raw;
|
|
12631
|
+
}
|
|
12632
|
+
|
|
12633
|
+
function normalizeGrokTurnUsage(usage, model, timestamp, eventId) {
|
|
12634
|
+
if (!usage || typeof usage !== "object") return null;
|
|
12635
|
+
const inputRaw = normalizeNonNegativeNumber(
|
|
12636
|
+
usage.inputTokens ?? usage.input_tokens,
|
|
12637
|
+
);
|
|
12638
|
+
const output = normalizeNonNegativeNumber(
|
|
12639
|
+
usage.outputTokens ?? usage.output_tokens,
|
|
12640
|
+
);
|
|
12641
|
+
const cached = normalizeNonNegativeNumber(
|
|
12642
|
+
usage.cachedReadTokens ??
|
|
12643
|
+
usage.cache_read_input_tokens ??
|
|
12644
|
+
usage.cached_input_tokens,
|
|
12645
|
+
);
|
|
12646
|
+
const reasoning = normalizeNonNegativeNumber(
|
|
12647
|
+
usage.reasoningTokens ?? usage.reasoning_output_tokens,
|
|
12648
|
+
);
|
|
12649
|
+
// Grok reports inputTokens as the full prompt (including cache hits). Split
|
|
12650
|
+
// so pricing can apply cache_read rates correctly.
|
|
12651
|
+
const nonCachedInput = Math.max(0, inputRaw - cached);
|
|
12652
|
+
let total = normalizeNonNegativeNumber(usage.totalTokens ?? usage.total_tokens);
|
|
12653
|
+
if (total <= 0) {
|
|
12654
|
+
total = inputRaw + output + reasoning;
|
|
12655
|
+
}
|
|
12656
|
+
if (total <= 0 && nonCachedInput <= 0 && cached <= 0 && output <= 0) return null;
|
|
12657
|
+
return {
|
|
12658
|
+
input_tokens: nonCachedInput,
|
|
12659
|
+
cached_input_tokens: cached,
|
|
12660
|
+
cache_creation_input_tokens: 0,
|
|
12661
|
+
output_tokens: output,
|
|
12662
|
+
reasoning_output_tokens: reasoning,
|
|
12663
|
+
total_tokens: total > 0 ? total : nonCachedInput + cached + output + reasoning,
|
|
12664
|
+
billable_total_tokens: total > 0 ? total : nonCachedInput + cached + output + reasoning,
|
|
12665
|
+
conversation_count: 1,
|
|
12666
|
+
model: canonicalizeGrokUsageModel(model),
|
|
12667
|
+
timestamp,
|
|
12668
|
+
eventId,
|
|
12669
|
+
};
|
|
12670
|
+
}
|
|
12671
|
+
|
|
12672
|
+
function extractGrokTurnUsageEvents(record, fallbackTimestamp, fallbackModel, lineIndex) {
|
|
12673
|
+
const update = record?.params?.update;
|
|
12674
|
+
if (!update || typeof update !== "object") return [];
|
|
12675
|
+
if (update.sessionUpdate !== "turn_completed") return [];
|
|
12676
|
+
const usage = update.usage;
|
|
12677
|
+
if (!usage || typeof usage !== "object") return [];
|
|
12678
|
+
|
|
12679
|
+
const meta = record?.params?._meta || record?._meta || {};
|
|
12680
|
+
const timestamp = grokTimestampFromUpdate(meta, record, fallbackTimestamp);
|
|
12681
|
+
const baseEventId = grokEventId(
|
|
12682
|
+
meta.eventId ?? record?.eventId ?? record?.id ?? update.prompt_id,
|
|
12683
|
+
String(lineIndex),
|
|
12684
|
+
);
|
|
12685
|
+
|
|
12686
|
+
const modelUsage =
|
|
12687
|
+
usage.modelUsage && typeof usage.modelUsage === "object" ? usage.modelUsage : null;
|
|
12688
|
+
const events = [];
|
|
12689
|
+
if (modelUsage && Object.keys(modelUsage).length > 0) {
|
|
12690
|
+
for (const [modelName, modelUsageEntry] of Object.entries(modelUsage)) {
|
|
12691
|
+
if (!modelUsageEntry || typeof modelUsageEntry !== "object") continue;
|
|
12692
|
+
const event = normalizeGrokTurnUsage(
|
|
12693
|
+
modelUsageEntry,
|
|
12694
|
+
modelName,
|
|
12695
|
+
timestamp,
|
|
12696
|
+
`${baseEventId}|${modelName}`,
|
|
12697
|
+
);
|
|
12698
|
+
if (event) events.push(event);
|
|
12699
|
+
}
|
|
12700
|
+
}
|
|
12701
|
+
if (events.length === 0) {
|
|
12702
|
+
const event = normalizeGrokTurnUsage(usage, fallbackModel, timestamp, baseEventId);
|
|
12703
|
+
if (event) events.push(event);
|
|
12704
|
+
}
|
|
12705
|
+
return events;
|
|
12706
|
+
}
|
|
12707
|
+
|
|
12708
|
+
// Context-window watermark events (legacy / fallback only).
|
|
12709
|
+
function extractGrokContextTokenEvent(record, fallbackTimestamp, lineIndex) {
|
|
12710
|
+
const meta = record?.params?._meta || record?._meta;
|
|
12711
|
+
if (!meta || typeof meta !== "object") return null;
|
|
12712
|
+
const totalTokens = normalizeNonNegativeNumber(meta.totalTokens);
|
|
12713
|
+
if (totalTokens <= 0) return null;
|
|
12714
|
+
return {
|
|
12715
|
+
totalTokens,
|
|
12716
|
+
timestamp: grokTimestampFromUpdate(meta, record, fallbackTimestamp),
|
|
12717
|
+
eventId: grokEventId(meta.eventId ?? record?.eventId ?? record?.id, String(lineIndex)),
|
|
12718
|
+
};
|
|
12719
|
+
}
|
|
12720
|
+
|
|
12721
|
+
async function readGrokUpdateTokenEvents(updatesPath, fallbackTimestamp, prevOffsetEntry, options = {}) {
|
|
12722
|
+
const fallbackModel = options.fallbackModel || "grok-build";
|
|
12723
|
+
if (!updatesPath) {
|
|
12724
|
+
return { turnEvents: [], contextEvents: [], offsetEntry: null };
|
|
12725
|
+
}
|
|
12617
12726
|
let stat;
|
|
12618
12727
|
try {
|
|
12619
12728
|
stat = fssync.statSync(updatesPath);
|
|
12620
|
-
if (!stat.isFile()) return {
|
|
12729
|
+
if (!stat.isFile()) return { turnEvents: [], contextEvents: [], offsetEntry: null };
|
|
12621
12730
|
} catch {
|
|
12622
|
-
return {
|
|
12731
|
+
return { turnEvents: [], contextEvents: [], offsetEntry: null };
|
|
12623
12732
|
}
|
|
12624
12733
|
|
|
12625
|
-
// updates.jsonl is append-only
|
|
12626
|
-
//
|
|
12627
|
-
//
|
|
12628
|
-
// bytes a write race leaves unparsed are covered by the next event's
|
|
12629
|
-
// cumulative total. Re-read from 0 on truncation or inode change.
|
|
12734
|
+
// updates.jsonl is append-only. Turn usage is additive per turn_completed, so
|
|
12735
|
+
// resuming from the last consumed byte is safe. Re-read from 0 on truncation
|
|
12736
|
+
// or inode change.
|
|
12630
12737
|
const prevSize = Number(prevOffsetEntry?.size) || 0;
|
|
12631
12738
|
const prevIno = prevOffsetEntry?.ino;
|
|
12632
12739
|
const inodeChanged = typeof prevIno === "number" && prevIno !== stat.ino;
|
|
12633
12740
|
const startOffset = stat.size < prevSize || inodeChanged ? 0 : prevSize;
|
|
12634
12741
|
const baseOffset = { mtimeMs: stat.mtimeMs, ino: stat.ino };
|
|
12635
12742
|
if (stat.size <= startOffset) {
|
|
12636
|
-
return {
|
|
12743
|
+
return {
|
|
12744
|
+
turnEvents: [],
|
|
12745
|
+
contextEvents: [],
|
|
12746
|
+
offsetEntry: { size: startOffset, ...baseOffset },
|
|
12747
|
+
};
|
|
12637
12748
|
}
|
|
12638
12749
|
|
|
12639
12750
|
// Only advance the stored offset to the end of the last newline-terminated
|
|
@@ -12642,7 +12753,8 @@ async function readGrokUpdateTokenEvents(updatesPath, fallbackTimestamp, prevOff
|
|
|
12642
12753
|
// complete instead of skipping it forever (which would undercount tokens).
|
|
12643
12754
|
const endsWithNewline = grokFileEndsWithNewline(updatesPath, stat.size);
|
|
12644
12755
|
|
|
12645
|
-
const
|
|
12756
|
+
const turnEvents = [];
|
|
12757
|
+
const contextEvents = [];
|
|
12646
12758
|
let lineIndex = 0;
|
|
12647
12759
|
let lastLine = "";
|
|
12648
12760
|
const input = fssync.createReadStream(updatesPath, {
|
|
@@ -12662,21 +12774,24 @@ async function readGrokUpdateTokenEvents(updatesPath, fallbackTimestamp, prevOff
|
|
|
12662
12774
|
} catch {
|
|
12663
12775
|
continue;
|
|
12664
12776
|
}
|
|
12665
|
-
const
|
|
12666
|
-
|
|
12667
|
-
|
|
12668
|
-
|
|
12669
|
-
|
|
12670
|
-
|
|
12671
|
-
|
|
12672
|
-
|
|
12673
|
-
|
|
12674
|
-
}
|
|
12777
|
+
const turns = extractGrokTurnUsageEvents(
|
|
12778
|
+
record,
|
|
12779
|
+
fallbackTimestamp,
|
|
12780
|
+
fallbackModel,
|
|
12781
|
+
lineIndex,
|
|
12782
|
+
);
|
|
12783
|
+
if (turns.length > 0) {
|
|
12784
|
+
turnEvents.push(...turns);
|
|
12785
|
+
continue;
|
|
12786
|
+
}
|
|
12787
|
+
const contextEvent = extractGrokContextTokenEvent(record, fallbackTimestamp, lineIndex);
|
|
12788
|
+
if (contextEvent) contextEvents.push(contextEvent);
|
|
12675
12789
|
}
|
|
12676
12790
|
} catch {
|
|
12677
|
-
// Stream error mid-read:
|
|
12678
|
-
// offset so the next sync
|
|
12679
|
-
|
|
12791
|
+
// Stream error mid-read: discard partial events and do not advance the
|
|
12792
|
+
// offset, so the next sync re-extracts from the same range exactly once
|
|
12793
|
+
// instead of double-counting already-parsed turn events.
|
|
12794
|
+
return { turnEvents: [], contextEvents: [], offsetEntry: prevOffsetEntry || null };
|
|
12680
12795
|
}
|
|
12681
12796
|
|
|
12682
12797
|
// When the file does not end on a newline, the final emitted line is a
|
|
@@ -12684,7 +12799,11 @@ async function readGrokUpdateTokenEvents(updatesPath, fallbackTimestamp, prevOff
|
|
|
12684
12799
|
// stays on a complete-line boundary and the line is re-read once finished.
|
|
12685
12800
|
const trailingPartialBytes = endsWithNewline ? 0 : Buffer.byteLength(lastLine, "utf8");
|
|
12686
12801
|
const committedSize = Math.max(startOffset, stat.size - trailingPartialBytes);
|
|
12687
|
-
return {
|
|
12802
|
+
return {
|
|
12803
|
+
turnEvents,
|
|
12804
|
+
contextEvents,
|
|
12805
|
+
offsetEntry: { size: committedSize, ...baseOffset },
|
|
12806
|
+
};
|
|
12688
12807
|
}
|
|
12689
12808
|
|
|
12690
12809
|
function estimateGrokTokenDelta(totalTokens, conversationCount, options = {}) {
|
|
@@ -12706,6 +12825,70 @@ function estimateGrokTokenDelta(totalTokens, conversationCount, options = {}) {
|
|
|
12706
12825
|
};
|
|
12707
12826
|
}
|
|
12708
12827
|
|
|
12828
|
+
function clearGrokHourlyBuckets(hourlyState) {
|
|
12829
|
+
if (!hourlyState || typeof hourlyState !== "object") return;
|
|
12830
|
+
const buckets = hourlyState.buckets && typeof hourlyState.buckets === "object" ? hourlyState.buckets : null;
|
|
12831
|
+
if (buckets) {
|
|
12832
|
+
for (const key of Object.keys(buckets)) {
|
|
12833
|
+
if (key.startsWith("grok|")) delete buckets[key];
|
|
12834
|
+
}
|
|
12835
|
+
}
|
|
12836
|
+
const groupQueued =
|
|
12837
|
+
hourlyState.groupQueued && typeof hourlyState.groupQueued === "object"
|
|
12838
|
+
? hourlyState.groupQueued
|
|
12839
|
+
: null;
|
|
12840
|
+
if (groupQueued) {
|
|
12841
|
+
for (const key of Object.keys(groupQueued)) {
|
|
12842
|
+
if (key.startsWith("grok|")) delete groupQueued[key];
|
|
12843
|
+
}
|
|
12844
|
+
}
|
|
12845
|
+
}
|
|
12846
|
+
|
|
12847
|
+
async function retractStaleGrokQueueRows(queuePath, keepKeys) {
|
|
12848
|
+
if (!queuePath) return 0;
|
|
12849
|
+
let raw = "";
|
|
12850
|
+
try {
|
|
12851
|
+
raw = fssync.readFileSync(queuePath, "utf8");
|
|
12852
|
+
} catch (error) {
|
|
12853
|
+
if (error?.code === "ENOENT") return 0;
|
|
12854
|
+
throw error;
|
|
12855
|
+
}
|
|
12856
|
+
|
|
12857
|
+
const latestGrok = new Map();
|
|
12858
|
+
for (const line of raw.split("\n")) {
|
|
12859
|
+
if (!line.trim()) continue;
|
|
12860
|
+
let row;
|
|
12861
|
+
try {
|
|
12862
|
+
row = JSON.parse(line);
|
|
12863
|
+
} catch {
|
|
12864
|
+
continue;
|
|
12865
|
+
}
|
|
12866
|
+
if ((row?.source || "") !== "grok") continue;
|
|
12867
|
+
const model = normalizeModelInput(row.model) || DEFAULT_MODEL;
|
|
12868
|
+
const hourStart = typeof row.hour_start === "string" ? row.hour_start : null;
|
|
12869
|
+
if (!hourStart) continue;
|
|
12870
|
+
latestGrok.set(bucketKey("grok", model, hourStart), { model, hour_start: hourStart, row });
|
|
12871
|
+
}
|
|
12872
|
+
|
|
12873
|
+
const zero = initTotals();
|
|
12874
|
+
const lines = [];
|
|
12875
|
+
for (const [key, entry] of latestGrok.entries()) {
|
|
12876
|
+
if (keepKeys.has(key)) continue;
|
|
12877
|
+
if (totalsKey(entry.row) === totalsKey(zero)) continue;
|
|
12878
|
+
lines.push(
|
|
12879
|
+
JSON.stringify({
|
|
12880
|
+
source: "grok",
|
|
12881
|
+
model: entry.model,
|
|
12882
|
+
hour_start: entry.hour_start,
|
|
12883
|
+
...zero,
|
|
12884
|
+
}),
|
|
12885
|
+
);
|
|
12886
|
+
}
|
|
12887
|
+
if (lines.length === 0) return 0;
|
|
12888
|
+
await fs.appendFile(queuePath, `${lines.join("\n")}\n`, "utf8");
|
|
12889
|
+
return lines.length;
|
|
12890
|
+
}
|
|
12891
|
+
|
|
12709
12892
|
async function parseGrokBuildIncremental({
|
|
12710
12893
|
sessions,
|
|
12711
12894
|
cursors = {},
|
|
@@ -12716,11 +12899,44 @@ async function parseGrokBuildIncremental({
|
|
|
12716
12899
|
if (queuePath) await ensureDir(path.dirname(queuePath));
|
|
12717
12900
|
const hourlyState = normalizeHourlyState(cursors?.hourly);
|
|
12718
12901
|
const grokState = cursors.grok && typeof cursors.grok === "object" ? { ...cursors.grok } : {};
|
|
12719
|
-
|
|
12902
|
+
const prevVersion = Number(grokState.version) || 0;
|
|
12903
|
+
const needsTurnUsageMigration = prevVersion < GROK_CURSOR_VERSION;
|
|
12904
|
+
|
|
12905
|
+
// v3 and earlier treated context-window totalTokens as cumulative spend, which
|
|
12906
|
+
// undercounts heavily (often 10-50x) and mis-splits input/output. Rebuild from
|
|
12907
|
+
// turn_completed.usage when migrating to v4.
|
|
12908
|
+
//
|
|
12909
|
+
// Drop prior watermark totals / updateOffsets so files are re-read from byte 0,
|
|
12910
|
+
// but keep legacySeen markers from seenSessions so the one-shot baseline
|
|
12911
|
+
// (sessions already counted under the old scanner) still applies.
|
|
12912
|
+
let sessionSnapshots;
|
|
12913
|
+
if (needsTurnUsageMigration) {
|
|
12914
|
+
const normalized = normalizeGrokSessionSnapshots(grokState);
|
|
12915
|
+
sessionSnapshots = {};
|
|
12916
|
+
for (const [sessionId, snapshot] of Object.entries(normalized)) {
|
|
12917
|
+
if (!snapshot?.legacySeen) continue;
|
|
12918
|
+
sessionSnapshots[sessionId] = {
|
|
12919
|
+
totalTokens: 0,
|
|
12920
|
+
messageCount: 0,
|
|
12921
|
+
model: null,
|
|
12922
|
+
source: null,
|
|
12923
|
+
lastEventId: null,
|
|
12924
|
+
lastEventTimestamp: null,
|
|
12925
|
+
updatedAt: snapshot.updatedAt || null,
|
|
12926
|
+
legacySeen: true,
|
|
12927
|
+
};
|
|
12928
|
+
}
|
|
12929
|
+
clearGrokHourlyBuckets(hourlyState);
|
|
12930
|
+
} else {
|
|
12931
|
+
sessionSnapshots = normalizeGrokSessionSnapshots(grokState);
|
|
12932
|
+
}
|
|
12720
12933
|
const prevUpdateOffsets =
|
|
12721
|
-
|
|
12934
|
+
!needsTurnUsageMigration &&
|
|
12935
|
+
grokState.updateOffsets &&
|
|
12936
|
+
typeof grokState.updateOffsets === "object"
|
|
12722
12937
|
? grokState.updateOffsets
|
|
12723
12938
|
: {};
|
|
12939
|
+
|
|
12724
12940
|
// Rebuilt from the sessions seen this scan, so entries for deleted session
|
|
12725
12941
|
// dirs are pruned and the cursor stays bounded by the on-disk session count.
|
|
12726
12942
|
const updateOffsets = {};
|
|
@@ -12755,83 +12971,142 @@ async function parseGrokBuildIncremental({
|
|
|
12755
12971
|
const model = grokModelFromSignals(safeSignals);
|
|
12756
12972
|
const lastActive = grokLastActiveFromSignals(safeSignals, summary);
|
|
12757
12973
|
|
|
12758
|
-
let
|
|
12759
|
-
let observedTotal = previousTotal;
|
|
12974
|
+
let cumulativeTotal = previousTotal;
|
|
12760
12975
|
let tokenDeltaForSession = 0;
|
|
12761
12976
|
let finalTouchedHourStart = null;
|
|
12762
12977
|
let source = previous.source || null;
|
|
12763
12978
|
let lastEventId = previous.lastEventId || null;
|
|
12764
12979
|
let lastEventTimestamp = previous.lastEventTimestamp || null;
|
|
12765
|
-
|
|
12766
|
-
|
|
12767
|
-
|
|
12768
|
-
|
|
12769
|
-
|
|
12770
|
-
pendingTokenDeltas.push({ deltaTokens, hourStartStr });
|
|
12771
|
-
tokenDeltaForSession += deltaTokens;
|
|
12772
|
-
finalTouchedHourStart = hourStartStr;
|
|
12773
|
-
source = deltaSource;
|
|
12774
|
-
lastEventTimestamp = timestamp || lastEventTimestamp;
|
|
12775
|
-
return true;
|
|
12776
|
-
};
|
|
12980
|
+
let lastModel = previous.model || model;
|
|
12981
|
+
let sawTurnUsage = source === "turn_usage" || previous.source === "turn_usage";
|
|
12982
|
+
// Defer bucket writes until after we know whether this is a legacy baseline
|
|
12983
|
+
// pass (first sighting of a session already counted under an older scanner).
|
|
12984
|
+
const pendingBucketDeltas = [];
|
|
12777
12985
|
|
|
12778
12986
|
const updatesPath = grokUpdatesPathForSession(sess);
|
|
12779
12987
|
const updates = await readGrokUpdateTokenEvents(
|
|
12780
12988
|
updatesPath,
|
|
12781
12989
|
lastActive,
|
|
12782
12990
|
updatesPath ? prevUpdateOffsets[updatesPath] : null,
|
|
12991
|
+
{ fallbackModel: model },
|
|
12783
12992
|
);
|
|
12784
12993
|
if (updatesPath && updates.offsetEntry) {
|
|
12785
12994
|
updateOffsets[updatesPath] = updates.offsetEntry;
|
|
12786
12995
|
}
|
|
12787
|
-
|
|
12788
|
-
|
|
12996
|
+
|
|
12997
|
+
// Preferred path: each turn_completed carries true cumulative API usage for
|
|
12998
|
+
// that turn (input/output/cache/reasoning). Sum them.
|
|
12999
|
+
for (const event of updates.turnEvents) {
|
|
13000
|
+
sawTurnUsage = true;
|
|
13001
|
+
const hourStartStr = toUtcHalfHourStart(event.timestamp) || toUtcHalfHourStart(lastActive) || toUtcHalfHourStart(Date.now());
|
|
13002
|
+
if (!hourStartStr) continue;
|
|
13003
|
+
const eventModel = event.model || model;
|
|
13004
|
+
const delta = {
|
|
13005
|
+
input_tokens: event.input_tokens,
|
|
13006
|
+
cached_input_tokens: event.cached_input_tokens,
|
|
13007
|
+
cache_creation_input_tokens: event.cache_creation_input_tokens,
|
|
13008
|
+
output_tokens: event.output_tokens,
|
|
13009
|
+
reasoning_output_tokens: event.reasoning_output_tokens,
|
|
13010
|
+
total_tokens: event.total_tokens,
|
|
13011
|
+
billable_total_tokens: event.billable_total_tokens,
|
|
13012
|
+
conversation_count: event.conversation_count || 1,
|
|
13013
|
+
};
|
|
13014
|
+
pendingBucketDeltas.push({ model: eventModel, hourStartStr, delta });
|
|
13015
|
+
cumulativeTotal += event.total_tokens;
|
|
13016
|
+
tokenDeltaForSession += event.total_tokens;
|
|
13017
|
+
finalTouchedHourStart = hourStartStr;
|
|
13018
|
+
source = "turn_usage";
|
|
12789
13019
|
lastEventId = event.eventId || lastEventId;
|
|
12790
13020
|
lastEventTimestamp = event.timestamp || lastEventTimestamp;
|
|
12791
|
-
|
|
12792
|
-
|
|
12793
|
-
|
|
12794
|
-
|
|
12795
|
-
|
|
13021
|
+
lastModel = eventModel;
|
|
13022
|
+
}
|
|
13023
|
+
|
|
13024
|
+
// Fallback only when this session never emitted turn_completed usage
|
|
13025
|
+
// (older logs / partial sessions). Context watermark is a lower-bound
|
|
13026
|
+
// estimate and must not run on top of turn usage.
|
|
13027
|
+
if (!sawTurnUsage) {
|
|
13028
|
+
let highWatermark = previousTotal;
|
|
13029
|
+
for (const event of updates.contextEvents) {
|
|
13030
|
+
lastEventId = event.eventId || lastEventId;
|
|
13031
|
+
lastEventTimestamp = event.timestamp || lastEventTimestamp;
|
|
13032
|
+
if (event.totalTokens <= highWatermark) continue;
|
|
13033
|
+
const deltaTokens = event.totalTokens - highWatermark;
|
|
13034
|
+
highWatermark = event.totalTokens;
|
|
13035
|
+
const hourStartStr =
|
|
13036
|
+
toUtcHalfHourStart(event.timestamp) ||
|
|
13037
|
+
toUtcHalfHourStart(lastActive) ||
|
|
13038
|
+
toUtcHalfHourStart(Date.now());
|
|
13039
|
+
if (!hourStartStr) continue;
|
|
13040
|
+
const delta = estimateGrokTokenDelta(deltaTokens, 0, { allowZeroConversationCount: true });
|
|
13041
|
+
pendingBucketDeltas.push({ model, hourStartStr, delta });
|
|
13042
|
+
tokenDeltaForSession += deltaTokens;
|
|
13043
|
+
finalTouchedHourStart = hourStartStr;
|
|
13044
|
+
source = "updates";
|
|
13045
|
+
}
|
|
12796
13046
|
|
|
12797
|
-
|
|
12798
|
-
|
|
12799
|
-
|
|
12800
|
-
|
|
12801
|
-
|
|
12802
|
-
|
|
13047
|
+
const effectiveSignalTotal = grokEffectiveTotalFromSignals(safeSignals);
|
|
13048
|
+
if (effectiveSignalTotal > highWatermark) {
|
|
13049
|
+
const deltaTokens = effectiveSignalTotal - highWatermark;
|
|
13050
|
+
highWatermark = effectiveSignalTotal;
|
|
13051
|
+
const hourStartStr = toUtcHalfHourStart(lastActive) || toUtcHalfHourStart(Date.now());
|
|
13052
|
+
if (hourStartStr) {
|
|
13053
|
+
const delta = estimateGrokTokenDelta(deltaTokens, 0, { allowZeroConversationCount: true });
|
|
13054
|
+
pendingBucketDeltas.push({ model, hourStartStr, delta });
|
|
13055
|
+
tokenDeltaForSession += deltaTokens;
|
|
13056
|
+
finalTouchedHourStart = hourStartStr;
|
|
13057
|
+
source = "signals";
|
|
13058
|
+
}
|
|
13059
|
+
}
|
|
13060
|
+
cumulativeTotal = Math.max(previousTotal, highWatermark);
|
|
12803
13061
|
}
|
|
12804
13062
|
|
|
12805
|
-
const finalTotal = Math.max(previousTotal,
|
|
13063
|
+
const finalTotal = Math.max(previousTotal, cumulativeTotal);
|
|
13064
|
+
// Sessions already observed under an older scanner must establish a watermark
|
|
13065
|
+
// without backfilling historical tokens as brand-new usage.
|
|
12806
13066
|
const legacyBaselineOnly = previous.legacySeen && previousTotal === 0 && finalTotal > 0;
|
|
13067
|
+
|
|
12807
13068
|
if (!legacyBaselineOnly) {
|
|
12808
|
-
for (const pending of
|
|
12809
|
-
const
|
|
12810
|
-
|
|
12811
|
-
|
|
12812
|
-
touchedBuckets.add(bucketKey("grok", model, pending.hourStartStr));
|
|
13069
|
+
for (const pending of pendingBucketDeltas) {
|
|
13070
|
+
const bucket = getHourlyBucket(hourlyState, "grok", pending.model, pending.hourStartStr);
|
|
13071
|
+
addTotals(bucket.totals, pending.delta);
|
|
13072
|
+
touchedBuckets.add(bucketKey("grok", pending.model, pending.hourStartStr));
|
|
12813
13073
|
eventsAggregated++;
|
|
12814
13074
|
}
|
|
12815
|
-
}
|
|
12816
13075
|
|
|
12817
|
-
|
|
12818
|
-
|
|
12819
|
-
|
|
12820
|
-
|
|
12821
|
-
|
|
12822
|
-
|
|
13076
|
+
// Message/conversation count for fallback-only sessions (turn path already
|
|
13077
|
+
// counts each turn_completed as one conversation).
|
|
13078
|
+
if (!sawTurnUsage && tokenDeltaForSession > 0 && finalTouchedHourStart) {
|
|
13079
|
+
const deltaMessageCount =
|
|
13080
|
+
messageCount > previousMessageCount ? messageCount - previousMessageCount : 1;
|
|
13081
|
+
const bucket = getHourlyBucket(hourlyState, "grok", lastModel || model, finalTouchedHourStart);
|
|
13082
|
+
addTotals(bucket.totals, { conversation_count: deltaMessageCount });
|
|
13083
|
+
touchedBuckets.add(bucketKey("grok", lastModel || model, finalTouchedHourStart));
|
|
13084
|
+
}
|
|
12823
13085
|
}
|
|
12824
13086
|
|
|
12825
13087
|
if (finalTotal > 0 && (tokenDeltaForSession > 0 || previousTotal > 0 || legacyBaselineOnly)) {
|
|
12826
13088
|
sessionSnapshots[sessionId] = {
|
|
12827
13089
|
totalTokens: finalTotal,
|
|
12828
13090
|
messageCount: Math.max(previousMessageCount, messageCount),
|
|
12829
|
-
model,
|
|
13091
|
+
model: lastModel || model,
|
|
12830
13092
|
source: source || previous.source || null,
|
|
12831
13093
|
lastEventId,
|
|
12832
13094
|
lastEventTimestamp,
|
|
12833
13095
|
updatedAt: new Date().toISOString(),
|
|
12834
13096
|
};
|
|
13097
|
+
} else if (previous.legacySeen && finalTotal === 0) {
|
|
13098
|
+
// Keep the baseline marker across empty syncs so later growth is still
|
|
13099
|
+
// baselined once instead of backfilled as brand-new usage.
|
|
13100
|
+
sessionSnapshots[sessionId] = {
|
|
13101
|
+
totalTokens: 0,
|
|
13102
|
+
messageCount: Math.max(previousMessageCount, messageCount),
|
|
13103
|
+
model: lastModel || model,
|
|
13104
|
+
source: previous.source || null,
|
|
13105
|
+
lastEventId: lastEventId || previous.lastEventId || null,
|
|
13106
|
+
lastEventTimestamp: lastEventTimestamp || previous.lastEventTimestamp || null,
|
|
13107
|
+
updatedAt: new Date().toISOString(),
|
|
13108
|
+
legacySeen: true,
|
|
13109
|
+
};
|
|
12835
13110
|
}
|
|
12836
13111
|
|
|
12837
13112
|
if (onProgress) {
|
|
@@ -12839,19 +13114,45 @@ async function parseGrokBuildIncremental({
|
|
|
12839
13114
|
}
|
|
12840
13115
|
}
|
|
12841
13116
|
|
|
12842
|
-
|
|
13117
|
+
let bucketsQueued = queuePath
|
|
12843
13118
|
? await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
|
|
12844
13119
|
: 0;
|
|
13120
|
+
|
|
13121
|
+
// After a semantics migration, retract stale grok queue keys that the full
|
|
13122
|
+
// rescan no longer produces so dashboard "latest per key" no longer keeps
|
|
13123
|
+
// the old undercounted rows.
|
|
13124
|
+
if (needsTurnUsageMigration && queuePath) {
|
|
13125
|
+
const keepKeys = new Set();
|
|
13126
|
+
for (const [key, bucket] of Object.entries(hourlyState.buckets || {})) {
|
|
13127
|
+
if (!key.startsWith("grok|") || !bucket?.totals) continue;
|
|
13128
|
+
keepKeys.add(key);
|
|
13129
|
+
}
|
|
13130
|
+
const retracted = await retractStaleGrokQueueRows(queuePath, keepKeys);
|
|
13131
|
+
bucketsQueued += retracted;
|
|
13132
|
+
}
|
|
13133
|
+
|
|
12845
13134
|
hourlyState.updatedAt = new Date().toISOString();
|
|
12846
13135
|
cursors.hourly = hourlyState;
|
|
12847
13136
|
sessionSnapshots = capGrokSessionSnapshots(sessionSnapshots);
|
|
12848
13137
|
|
|
13138
|
+
const migrations = grokState.migrations && typeof grokState.migrations === "object"
|
|
13139
|
+
? { ...grokState.migrations }
|
|
13140
|
+
: {};
|
|
13141
|
+
if (needsTurnUsageMigration) {
|
|
13142
|
+
migrations.turnUsageV4 = {
|
|
13143
|
+
appliedAt: new Date().toISOString(),
|
|
13144
|
+
fromVersion: prevVersion,
|
|
13145
|
+
toVersion: GROK_CURSOR_VERSION,
|
|
13146
|
+
};
|
|
13147
|
+
}
|
|
13148
|
+
|
|
12849
13149
|
cursors.grok = {
|
|
12850
13150
|
...grokState,
|
|
12851
13151
|
version: GROK_CURSOR_VERSION,
|
|
12852
13152
|
sessionSnapshots,
|
|
12853
13153
|
seenSessions: Object.keys(sessionSnapshots),
|
|
12854
13154
|
updateOffsets,
|
|
13155
|
+
migrations,
|
|
12855
13156
|
updatedAt: new Date().toISOString()
|
|
12856
13157
|
};
|
|
12857
13158
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{r as n,z as a}from"./__vite-browser-external-DEhUaWQY.js";import{u as p}from"./DialogTitle-DLkuu1Iw.js";import{u as c}from"./useScrollLock-K0FPky1Y.js";const x=n.forwardRef(function(e,r){const{render:l,className:u,style:m,id:s,...t}=e,{store:i}=p(),o=c(s);return i.useSyncedValueWithCleanup("descriptionElementId",o),a("p",e,{ref:r,props:[{id:o},t]})});export{x as D};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{r as o}from"./__vite-browser-external-DEhUaWQY.js";import{h as r,i as t,p as s}from"./react-CllryaXi.js";function i(){!r.current&&t();const[e]=o.useState(s.current);return e}export{i as u};
|