tokentracker-cli 1.3.11 → 1.3.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-CjXQPC-W.js → AchievementBadge-DJK8AWBF.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-xb-g6Jws.js → AchievementsPage-Cle78oKH.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-DI4NOt9D.js → AchievementsSection-BPqXDoPP.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-Cn2rV5Bk.js → BadgeDetailModal-HsH_Pwsu.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-CFb-gvO7.js → BotAnimated-BWy2-z4Q.js} +1 -1
- package/dashboard/dist/assets/{Card-CB2-QAqP.js → Card-DPGBuBDb.js} +1 -1
- package/dashboard/dist/assets/{CloudCheckoutPage-beBzOiJA.js → CloudCheckoutPage-ohIQnIhe.js} +1 -1
- package/dashboard/dist/assets/{CloudPage-COYm4wdR.js → CloudPage-OYiiZ4c4.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-kCa9Mfpj.js → CommandPalette-BmDZmpMi.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-BeUZhxxn.js → CommunityStatsModal-L86uJgyp.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-DiCm2JXX.js → ConfirmModal-COQfPXos.js} +1 -1
- package/dashboard/dist/assets/{Controls-BkRVScXl.js → Controls-BkKlT6Dz.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-DiV0ynQc.js → DashboardPage-3dlQBOjz.js} +1 -1
- package/dashboard/dist/assets/{DateRangePopover-DdmjfdFj.js → DateRangePopover-D0UeP8ig.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-D7E9uy4l.js → DevicePage-K3CdWKaf.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-VDWLqY0o.js → DialogClose-DKQ2ZVA0.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-MbuaPFlj.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-DfBUUBYN.js → DialogTitle-ufFnN-8D.js} +1 -1
- package/dashboard/dist/assets/{ElectricLogo-Cr-WDplA.js → ElectricLogo-Bb79UnTo.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-DPdMW40K.js → FadeIn-DlAKIU4I.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-Ulqd6uC1.js → HeaderGithubStar-CzwSyhEY.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-By6YzN89.js → HoverTooltip-BEpwekkd.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-BLvWuVmL.js → IpCheckPage-C7kpZ9JX.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-BfgBmSGV.js → LandingPage-CfYk0Jzy.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardPage-D7C91BEb.js → LeaderboardPage-XgjAHwpy.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProBadge-Mj_-HH8L.js → LeaderboardProBadge-wSlqVjBb.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-BfHTrqHf.js → LeaderboardProfileModal-DaQrNYnu.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-CggAvx-w.js → LeaderboardProfilePage-3SWjG8FQ.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-BR8hlK7t.js → LimitsPage-DvM57ri3.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-yW1orlU8.js → LocalOnlyNotice-CY6eDJEk.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-B_7mpZ6Y.js → LoginCard-4bdpPQeD.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-D1ZtIkRD.js → LoginModal-Bmw7-b3f.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-DRThDeqD.js → LoginPage-CEI4nnlT.js} +1 -1
- package/dashboard/dist/assets/{PetPage-CsIkqyiR.js → PetPage-7Udtc9GY.js} +2 -2
- package/dashboard/dist/assets/{PopoverPopup-dySzR9xR.js → PopoverPopup-r2BvRKLO.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-oBKAZ-WS.js → ProviderIcon-Cy_pKLdw.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-Bf0-zIl2.js → ResetPasswordPage-QKUaLtO3.js} +1 -1
- package/dashboard/dist/assets/{Select-xyNGq71G.js → Select-BsUrn8Ui.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-C8ZB19C7.js → SelectItemText-Nis0uGZT.js} +1 -1
- package/dashboard/dist/assets/{SelfHostPage-Cf7pLoVz.js → SelfHostPage-DF8EzBEV.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-BH3W_Wxq.js → ServiceStatusPage-BYmKzI_v.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-u8RIKjx9.js → SessionsPage-BsCVe75l.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-Bnh-AxM9.js → SettingsPage-CIZ6WmNR.js} +3 -3
- package/dashboard/dist/assets/{SkillsPage-CIP3Av7z.js → SkillsPage-cwHyK0Hp.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-DnNhsfa-.js → TokenGalaxy-2S-ud67v.js} +1 -1
- package/dashboard/dist/assets/{TooltipPopup-BstAYPIH.js → TooltipPopup-GG7xjDOP.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-D4CVzczb.js → TrendMonitor-CrbXrVMe.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-A_gMzap_.js → WidgetsPage-HrhnI3C-.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-Bmm_WjA5.js → WrappedPage-sm7UVFon.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-DS-dnBDP.js → __vite-browser-external-C7JgMYNl.js} +2 -2
- package/dashboard/dist/assets/{agent-logos-BdU0GjQq.js → agent-logos-DGrrmPth.js} +1 -1
- package/dashboard/dist/assets/{arrow-left-Cl8AYaOh.js → arrow-left-DOzsdmqR.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-Cfi2pR1p.js → arrow-right-CHb0kJou.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-DH7vnAf4.js → arrow-up-right-CH8vEEN1.js} +1 -1
- package/dashboard/dist/assets/{calendar-days-BJjjx5Vu.js → calendar-days-_D5qnyH_.js} +1 -1
- package/dashboard/dist/assets/{check-DGTejNYt.js → check-DZLesnNy.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-Brz4a22U.js → chevron-down-QL4xkUtL.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-DZbiSnVr.js → cloud-checkout-DiCcWJVl.js} +1 -1
- package/dashboard/dist/assets/{composite-rfjkpEna.js → composite-DObGcQmb.js} +1 -1
- package/dashboard/dist/assets/{download-axaJjbTW.js → download-BKp8pusR.js} +1 -1
- package/dashboard/dist/assets/{external-link-D9dvv4DN.js → external-link-B3p_TVGc.js} +1 -1
- package/dashboard/dist/assets/{flame-DuivUwnP.js → flame-Df9OTUym.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-Dg_-K3cD.js → format-tokens-DXrEDEYs.js} +1 -1
- package/dashboard/dist/assets/{gift-CgE1uDg4.js → gift-DtWubiuR.js} +1 -1
- package/dashboard/dist/assets/{host-mode-C2QCLKjw.js → host-mode-C069A_hB.js} +1 -1
- package/dashboard/dist/assets/{icons-PNlyYW8C.js → icons-lB1kfROy.js} +1 -1
- package/dashboard/dist/assets/{index-DFWE1vbu.js → index-D356L-jw.js} +1 -1
- package/dashboard/dist/assets/{index-BbroxaKr.js → index-DsXIWrJx.js} +1 -1
- package/dashboard/dist/assets/{index-PiRGMAPd.js → index-M5BiC0jx.js} +1 -1
- package/dashboard/dist/assets/{info-BjRBvuNu.js → info-CSG8OkG4.js} +1 -1
- package/dashboard/dist/assets/{link-2-CCqYoK3V.js → link-2-BkBGu7FX.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-IiB0vjDJ.js → loader-circle-BERDkbsP.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-DbjjBgLM.js → modifiers.esm-Ce5IvVw2.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-BUSQ3-U3.js → monitor-smartphone-zKuoJldz.js} +1 -1
- package/dashboard/dist/assets/{play-CDU4bWoE.js → play-C0x-pxft.js} +1 -1
- package/dashboard/dist/assets/{plus-BY7jwS0R.js → plus-DJvks-CX.js} +1 -1
- package/dashboard/dist/assets/{provider-display-endIONDM.js → provider-display-C0e4tAFu.js} +1 -1
- package/dashboard/dist/assets/{react-C0JpiKb9.js → react-Bg0_YfGs.js} +1 -1
- package/dashboard/dist/assets/{safePolygon-B5pqmyki.js → safePolygon-CptquMWZ.js} +1 -1
- package/dashboard/dist/assets/{search-BWQCqCrK.js → search-CLESf2Tg.js} +1 -1
- package/dashboard/dist/assets/{server-DkDn4DQU.js → server-Cdwhp-2m.js} +1 -1
- package/dashboard/dist/assets/{shield-check-BPOMBy-3.js → shield-check-8U4XqmW3.js} +1 -1
- package/dashboard/dist/assets/{skills-api-Btor2iNW.js → skills-api-ioDaxLLo.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-qA98E6si.js → sortable.esm-CEohF5EK.js} +1 -1
- package/dashboard/dist/assets/{store-DRcycxJw.js → store-CyOeiGFE.js} +1 -1
- package/dashboard/dist/assets/{terminal-BWOI_yxH.js → terminal-BjgwEAH-.js} +1 -1
- package/dashboard/dist/assets/{trash-DgWVzyKs.js → trash-Skj6z7D1.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-odyPsCDi.js → use-community-stats-Cworqjzy.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-CBocPZjh.js → use-latest-request-guard-DMVWXXHE.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-uKB639iI.js → use-limits-display-prefs-DKJZcYfE.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-EoDW6Dgk.js → use-native-settings-CQF4BLQl.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-B4Gychu4.js → use-ordered-list-B9yoUGUU.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-DPUViCfp.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-C0goPBNl.js → use-session-efficiency-pref-K00bK2sQ.js} +1 -1
- package/dashboard/dist/assets/{use-transform-hwE19i1Y.js → use-transform-Cfw11yOU.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-DnsGayPX.js → use-usage-limits-BE2dJXFd.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-DiN1Jj_t.js → useCurrency-BW7y9Tdv.js} +1 -1
- package/dashboard/dist/assets/{usePositioner-CM1Isf5e.js → usePositioner-DhtQcPOC.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-CGhMrRps.js → useTokenFormat-BnvzUc-t.js} +1 -1
- package/dashboard/dist/assets/{zap-Bc2wd9eQ.js → zap-BQ3BR9On.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/status.js +5 -1
- package/src/commands/sync.js +28 -1
- package/src/lib/async-queue-reader.js +117 -0
- package/src/lib/jsonl-lines.js +34 -4
- package/src/lib/local-api.js +88 -226
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +148 -52
- package/src/lib/session-analytics.js +11 -10
- package/src/lib/upload-queue.js +1 -1
- package/dashboard/dist/assets/DialogDescription-BQ5VSHQi.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-B-JhzSwL.js +0 -1
package/src/lib/local-api.js
CHANGED
|
@@ -3,6 +3,8 @@ const os = require("node:os");
|
|
|
3
3
|
const path = require("node:path");
|
|
4
4
|
const { spawn } = require("node:child_process");
|
|
5
5
|
const crypto = require("node:crypto");
|
|
6
|
+
const { setImmediate: yieldToRequests } = require("node:timers/promises");
|
|
7
|
+
const { createAsyncQueueReader } = require("./async-queue-reader");
|
|
6
8
|
const { DEFAULT_BASE_URL, resolveRuntimeConfig, assertInsforgeRuntime, normalizeInstanceBaseUrl, clearDeviceIdentity, resetInstanceState } = require("./runtime-config");
|
|
7
9
|
const {
|
|
8
10
|
filterRowsByUsageScope,
|
|
@@ -92,13 +94,13 @@ const CLAUDE_MEM_OBSERVER_PROJECT_KEY = deriveProjectKeyFromRef(
|
|
|
92
94
|
);
|
|
93
95
|
const PROJECT_USAGE_MAX_ENTRIES = 10;
|
|
94
96
|
const MAX_TIME_ZONE_CACHE_ENTRIES = 16;
|
|
97
|
+
const AGGREGATION_BATCH_ROWS = 256;
|
|
95
98
|
|
|
96
99
|
// The native dashboard fans one refresh out across several local endpoints.
|
|
97
100
|
// Keep one immutable, deduped view of the current queue so those endpoints do
|
|
98
101
|
// not all read and JSON.parse the same append-only file. A file identity +
|
|
99
102
|
// nanosecond timestamp signature invalidates the cache immediately on append,
|
|
100
103
|
// in-place rewrite, or atomic replacement.
|
|
101
|
-
let queueDataCache = null;
|
|
102
104
|
const dailyAggregationCache = new WeakMap();
|
|
103
105
|
const zonedPartsFormatters = new Map();
|
|
104
106
|
|
|
@@ -110,11 +112,6 @@ function boundedCacheSet(cache, key, value, maxEntries) {
|
|
|
110
112
|
}
|
|
111
113
|
}
|
|
112
114
|
|
|
113
|
-
function queueFileSignature(queuePath) {
|
|
114
|
-
const stat = fs.statSync(queuePath, { bigint: true });
|
|
115
|
-
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
115
|
function timeZoneContextKey({ timeZone, offsetMinutes } = {}) {
|
|
119
116
|
return `${timeZone || ""}|${Number.isFinite(offsetMinutes) ? offsetMinutes : ""}|p${getPricingRevision()}`;
|
|
120
117
|
}
|
|
@@ -125,7 +122,7 @@ function timeZoneContextKey({ timeZone, offsetMinutes } = {}) {
|
|
|
125
122
|
// key ONCE (rowDayKey constructs an Intl.DateTimeFormat per call — the
|
|
126
123
|
// dominant per-row cost) and reuse it for both the range check and any
|
|
127
124
|
// day bucketing.
|
|
128
|
-
function readProjectUsageContext(qp, url) {
|
|
125
|
+
async function readProjectUsageContext(qp, url) {
|
|
129
126
|
const from = url.searchParams.get("from") || "";
|
|
130
127
|
const to = url.searchParams.get("to") || "";
|
|
131
128
|
const timeZoneContext = getTimeZoneContext(url);
|
|
@@ -136,7 +133,7 @@ function readProjectUsageContext(qp, url) {
|
|
|
136
133
|
return (!from || day >= from) && (!to || day <= to);
|
|
137
134
|
};
|
|
138
135
|
const projectQueuePath = path.join(path.dirname(qp), "project.queue.jsonl");
|
|
139
|
-
const projectRows = readProjectQueueData(projectQueuePath).filter(
|
|
136
|
+
const projectRows = (await readProjectQueueData(projectQueuePath)).filter(
|
|
140
137
|
(row) => row.project_key !== CLAUDE_MEM_OBSERVER_PROJECT_KEY,
|
|
141
138
|
);
|
|
142
139
|
return { from, to, timeZoneContext, hasRange, dayInRange, projectRows };
|
|
@@ -145,57 +142,15 @@ function readProjectUsageContext(qp, url) {
|
|
|
145
142
|
// Same signature-based caching as readQueueData: the project-usage endpoints
|
|
146
143
|
// all fan out from one dashboard refresh, so without this every request
|
|
147
144
|
// re-read and re-parsed the whole append-only project queue.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
if (e?.code !== "ENOENT") {
|
|
159
|
-
console.error("[LocalAPI] readProjectQueueData: failed to stat:", e?.message || e);
|
|
160
|
-
}
|
|
161
|
-
return [];
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (
|
|
165
|
-
projectQueueDataCache?.queuePath === projectQueuePath &&
|
|
166
|
-
projectQueueDataCache.signature === signature
|
|
167
|
-
) {
|
|
168
|
-
return projectQueueDataCache.rows;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
let raw;
|
|
172
|
-
try {
|
|
173
|
-
raw = fs.readFileSync(projectQueuePath, "utf8");
|
|
174
|
-
} catch (e) {
|
|
175
|
-
if (e?.code !== "ENOENT") {
|
|
176
|
-
console.error("[LocalAPI] readProjectQueueData: failed to read:", e?.message || e);
|
|
177
|
-
}
|
|
178
|
-
return [];
|
|
179
|
-
}
|
|
180
|
-
const lines = raw.split("\n").filter((l) => l.trim());
|
|
181
|
-
const seen = new Map();
|
|
182
|
-
for (const line of lines) {
|
|
183
|
-
try {
|
|
184
|
-
const row = JSON.parse(line);
|
|
185
|
-
const key = `${row.project_key || ""}|${row.source || ""}|${row.hour_start || ""}`;
|
|
186
|
-
// Same legacy-row corrections as the main queue (codex inclusive-input,
|
|
187
|
-
// cursor billable=0) so both read paths report identical numbers.
|
|
188
|
-
seen.set(key, normalizeQueueRow(row));
|
|
189
|
-
} catch {
|
|
190
|
-
// skip malformed
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
// Callers treat the result as read-only (sortByHour copies before sorting),
|
|
194
|
-
// so the cached array can be shared across requests.
|
|
195
|
-
const rows = Array.from(seen.values());
|
|
196
|
-
projectQueueDataCache = { queuePath: projectQueuePath, signature, rows };
|
|
197
|
-
return rows;
|
|
198
|
-
}
|
|
145
|
+
const readProjectQueueData = createAsyncQueueReader({
|
|
146
|
+
label: "readProjectQueueData",
|
|
147
|
+
parseRow(row) {
|
|
148
|
+
return {
|
|
149
|
+
key: `${row.project_key || ""}|${row.source || ""}|${row.hour_start || ""}`,
|
|
150
|
+
row: normalizeQueueRow(row),
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
});
|
|
199
154
|
|
|
200
155
|
function isLegacyInclusiveCodexRow(row) {
|
|
201
156
|
if (!row || (row.source !== "codex" && row.source !== "every-code")) return false;
|
|
@@ -244,136 +199,16 @@ function normalizeQueueRow(row) {
|
|
|
244
199
|
return normalized;
|
|
245
200
|
}
|
|
246
201
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
const signature = `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
259
|
-
|
|
260
|
-
const cached = queueDataCache;
|
|
261
|
-
if (cached?.queuePath === queuePath && cached.signature === signature) {
|
|
262
|
-
return cached.rows;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// The queue is append-only, so on a same-file append only the new tail is
|
|
266
|
-
// read and parsed; the deduped row set carries over. A replaced, truncated,
|
|
267
|
-
// or first-seen file falls back to a full read.
|
|
268
|
-
const devIno = `${stat.dev}:${stat.ino}`;
|
|
269
|
-
const fileSize = Number(stat.size);
|
|
270
|
-
const canAppend =
|
|
271
|
-
cached != null &&
|
|
272
|
-
cached.queuePath === queuePath &&
|
|
273
|
-
cached.devIno === devIno &&
|
|
274
|
-
fileSize >= cached.consumedBytes;
|
|
275
|
-
let seen = canAppend ? cached.seen : new Map();
|
|
276
|
-
let offset = canAppend ? cached.consumedBytes : 0;
|
|
277
|
-
|
|
278
|
-
// Integrity probe for the append assumption: repo writers only append or
|
|
279
|
-
// atomically replace (inode change), but an EXTERNAL in-place rewrite
|
|
280
|
-
// (cp over the file, shell redirect) keeps the inode with a same/larger
|
|
281
|
-
// size while changing the prefix. The byte before our offset must be the
|
|
282
|
-
// newline that terminated the last consumed line — anything else means the
|
|
283
|
-
// prefix is no longer ours, so fall back to a full read.
|
|
284
|
-
if (offset > 0) {
|
|
285
|
-
try {
|
|
286
|
-
const fd = fs.openSync(queuePath, "r");
|
|
287
|
-
try {
|
|
288
|
-
const probe = Buffer.allocUnsafe(1);
|
|
289
|
-
if (fs.readSync(fd, probe, 0, 1, offset - 1) !== 1 || probe[0] !== 0x0a) {
|
|
290
|
-
seen = new Map();
|
|
291
|
-
offset = 0;
|
|
292
|
-
}
|
|
293
|
-
} finally {
|
|
294
|
-
fs.closeSync(fd);
|
|
295
|
-
}
|
|
296
|
-
} catch {
|
|
297
|
-
seen = new Map();
|
|
298
|
-
offset = 0;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
let raw = "";
|
|
303
|
-
if (fileSize > offset) {
|
|
304
|
-
try {
|
|
305
|
-
if (offset === 0) {
|
|
306
|
-
raw = fs.readFileSync(queuePath, "utf8");
|
|
307
|
-
} else {
|
|
308
|
-
const fd = fs.openSync(queuePath, "r");
|
|
309
|
-
try {
|
|
310
|
-
const length = fileSize - offset;
|
|
311
|
-
const buffer = Buffer.allocUnsafe(length);
|
|
312
|
-
let read = 0;
|
|
313
|
-
while (read < length) {
|
|
314
|
-
// A concurrent truncation between stat and open makes readSync hit
|
|
315
|
-
// EOF early — without the 0-byte break this loop never terminates.
|
|
316
|
-
const n = fs.readSync(fd, buffer, read, length - read, offset + read);
|
|
317
|
-
if (n === 0) break;
|
|
318
|
-
read += n;
|
|
319
|
-
}
|
|
320
|
-
raw = buffer.toString("utf8", 0, read);
|
|
321
|
-
} finally {
|
|
322
|
-
fs.closeSync(fd);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
} catch (e) {
|
|
326
|
-
// ENOENT is legitimate (queue deleted between stat and read); anything
|
|
327
|
-
// else is a signal we don't want to hide behind an empty array forever —
|
|
328
|
-
// the dashboard would otherwise render "0 tokens" with no clue the queue
|
|
329
|
-
// was unreadable.
|
|
330
|
-
if (e?.code !== "ENOENT") {
|
|
331
|
-
console.error("[LocalAPI] readQueueData: failed to read queue:", e?.message || e);
|
|
332
|
-
}
|
|
333
|
-
return canAppend ? cached.rows : [];
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Parse row-by-row so a single corrupted line (partial write, disk-full
|
|
338
|
-
// truncation, …) does not wipe out every other row with it. An unterminated
|
|
339
|
-
// tail is attempted (legacy writers may omit the final newline) but NOT
|
|
340
|
-
// marked consumed — a mid-append partial line is re-read once it completes.
|
|
341
|
-
// "\n" (0x0A) never appears inside a multi-byte UTF-8 sequence, so cutting
|
|
342
|
-
// on the last newline is byte-safe.
|
|
343
|
-
let consumedBytes = offset;
|
|
344
|
-
let malformed = 0;
|
|
345
|
-
if (raw) {
|
|
346
|
-
const lastNewline = raw.lastIndexOf("\n");
|
|
347
|
-
if (lastNewline !== -1) {
|
|
348
|
-
consumedBytes += Buffer.byteLength(raw.slice(0, lastNewline), "utf8") + 1;
|
|
349
|
-
}
|
|
350
|
-
const lines = raw.split("\n");
|
|
351
|
-
for (const line of lines) {
|
|
352
|
-
if (!line.trim()) continue;
|
|
353
|
-
try {
|
|
354
|
-
const row = JSON.parse(line);
|
|
355
|
-
// Account session states (and the pre-release watermark records
|
|
356
|
-
// this branch may still hold in a dev queue) are cloud-side control
|
|
357
|
-
// records, not usage rows - never surface them locally.
|
|
358
|
-
if (row?.kind === "account_session_state" || row?.kind === "account_sync_watermark") continue;
|
|
359
|
-
// Deduplicate: each sync appends cumulative totals per bucket, so for
|
|
360
|
-
// each (source, model, hour_start) keep only the latest (last) entry.
|
|
361
|
-
const key = `${row.source || ""}|${row.model || ""}|${row.hour_start || ""}`;
|
|
362
|
-
seen.set(key, normalizeQueueRow(row));
|
|
363
|
-
} catch {
|
|
364
|
-
malformed += 1;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
if (malformed > 0) {
|
|
369
|
-
console.error(
|
|
370
|
-
`[LocalAPI] readQueueData: skipped ${malformed} malformed line(s) in ${queuePath}`,
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
|
-
const rows = Array.from(seen.values());
|
|
374
|
-
queueDataCache = { queuePath, signature, devIno, consumedBytes, seen, rows };
|
|
375
|
-
return rows;
|
|
376
|
-
}
|
|
202
|
+
const readQueueData = createAsyncQueueReader({
|
|
203
|
+
label: "readQueueData",
|
|
204
|
+
parseRow(row) {
|
|
205
|
+
if (row?.kind === "account_session_state" || row?.kind === "account_sync_watermark") return null;
|
|
206
|
+
return {
|
|
207
|
+
key: `${row.source || ""}|${row.model || ""}|${row.hour_start || ""}`,
|
|
208
|
+
row: normalizeQueueRow(row),
|
|
209
|
+
};
|
|
210
|
+
},
|
|
211
|
+
});
|
|
377
212
|
|
|
378
213
|
function rowDayKey(row, timeZoneContext) {
|
|
379
214
|
const hs = row.hour_start;
|
|
@@ -401,8 +236,32 @@ function aggregateByDay(rows, timeZoneContext = null) {
|
|
|
401
236
|
return cached;
|
|
402
237
|
}
|
|
403
238
|
|
|
239
|
+
if (!cachedByTimeZone) {
|
|
240
|
+
cachedByTimeZone = new Map();
|
|
241
|
+
dailyAggregationCache.set(normalizedRows, cachedByTimeZone);
|
|
242
|
+
}
|
|
243
|
+
const pending = buildDailyAggregates(normalizedRows, timeZoneContext).then((daily) => {
|
|
244
|
+
// Pricing can refresh while a large aggregation yields. Rebuild consistently
|
|
245
|
+
// with the current catalog instead of publishing a mix of two revisions.
|
|
246
|
+
if (cacheKey !== timeZoneContextKey(timeZoneContext || {})) {
|
|
247
|
+
cachedByTimeZone.delete(cacheKey);
|
|
248
|
+
return aggregateByDay(normalizedRows, timeZoneContext);
|
|
249
|
+
}
|
|
250
|
+
return daily;
|
|
251
|
+
}).catch((error) => {
|
|
252
|
+
if (cachedByTimeZone.get(cacheKey) === pending) cachedByTimeZone.delete(cacheKey);
|
|
253
|
+
throw error;
|
|
254
|
+
});
|
|
255
|
+
// Share both completed and in-flight aggregates across summary/daily/heatmap.
|
|
256
|
+
boundedCacheSet(cachedByTimeZone, cacheKey, pending, MAX_TIME_ZONE_CACHE_ENTRIES);
|
|
257
|
+
return pending;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function buildDailyAggregates(normalizedRows, timeZoneContext) {
|
|
404
261
|
const byDay = new Map();
|
|
262
|
+
let processed = 0;
|
|
405
263
|
for (const row of normalizedRows) {
|
|
264
|
+
if (++processed % AGGREGATION_BATCH_ROWS === 0) await yieldToRequests();
|
|
406
265
|
if (!row.hour_start) continue;
|
|
407
266
|
const day = rowDayKey(row, timeZoneContext);
|
|
408
267
|
if (!day) continue;
|
|
@@ -438,11 +297,6 @@ function aggregateByDay(rows, timeZoneContext = null) {
|
|
|
438
297
|
a.models[model] = (a.models[model] || 0) + (row.total_tokens || 0);
|
|
439
298
|
}
|
|
440
299
|
const daily = Array.from(byDay.values()).sort((a, b) => a.day.localeCompare(b.day));
|
|
441
|
-
if (!cachedByTimeZone) {
|
|
442
|
-
cachedByTimeZone = new Map();
|
|
443
|
-
dailyAggregationCache.set(normalizedRows, cachedByTimeZone);
|
|
444
|
-
}
|
|
445
|
-
boundedCacheSet(cachedByTimeZone, cacheKey, daily, MAX_TIME_ZONE_CACHE_ENTRIES);
|
|
446
300
|
return daily;
|
|
447
301
|
}
|
|
448
302
|
|
|
@@ -546,9 +400,9 @@ function getRequestedUsageScope(url) {
|
|
|
546
400
|
return normalizeUsageScope(url.searchParams.get("scope"));
|
|
547
401
|
}
|
|
548
402
|
|
|
549
|
-
function scopedQueueRows(queuePath, url) {
|
|
403
|
+
async function scopedQueueRows(queuePath, url) {
|
|
550
404
|
const scope = getRequestedUsageScope(url);
|
|
551
|
-
const allRows = readQueueData(queuePath);
|
|
405
|
+
const allRows = await readQueueData(queuePath);
|
|
552
406
|
return {
|
|
553
407
|
scope,
|
|
554
408
|
allRows,
|
|
@@ -743,9 +597,11 @@ function formatPartsDayKey(parts) {
|
|
|
743
597
|
return `${parts.year}-${String(parts.month).padStart(2, "0")}-${String(parts.day).padStart(2, "0")}`;
|
|
744
598
|
}
|
|
745
599
|
|
|
746
|
-
function aggregateHourlyByDay(rows, dayKey, timeZoneContext) {
|
|
600
|
+
async function aggregateHourlyByDay(rows, dayKey, timeZoneContext) {
|
|
747
601
|
const byHour = new Map();
|
|
602
|
+
let processed = 0;
|
|
748
603
|
for (const row of rows) {
|
|
604
|
+
if (++processed % AGGREGATION_BATCH_ROWS === 0) await yieldToRequests();
|
|
749
605
|
if (!row.hour_start) continue;
|
|
750
606
|
const parts = getZonedParts(new Date(row.hour_start), timeZoneContext);
|
|
751
607
|
if (!parts) continue;
|
|
@@ -2474,7 +2330,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
2474
2330
|
if (p === "/functions/tokentracker-wrapped") {
|
|
2475
2331
|
const yearParam = url.searchParams.get("year");
|
|
2476
2332
|
const year = yearParam ? Number(yearParam) : null;
|
|
2477
|
-
const { rows, scope, excludedSources } = scopedQueueRows(qp, url);
|
|
2333
|
+
const { rows, scope, excludedSources } = await scopedQueueRows(qp, url);
|
|
2478
2334
|
const { aggregateWrapped } = require("./wrapped-aggregator");
|
|
2479
2335
|
const summary = aggregateWrapped(rows, year ? { year } : {});
|
|
2480
2336
|
json(res, { scope, excluded_sources: excludedSources, ...summary });
|
|
@@ -2503,8 +2359,8 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
2503
2359
|
const from = url.searchParams.get("from") || "";
|
|
2504
2360
|
const to = url.searchParams.get("to") || "";
|
|
2505
2361
|
const timeZoneContext = getTimeZoneContext(url);
|
|
2506
|
-
const { rows, scope, excludedSources } = scopedQueueRows(qp, url);
|
|
2507
|
-
const allDaily = aggregateByDay(rows, timeZoneContext);
|
|
2362
|
+
const { rows, scope, excludedSources } = await scopedQueueRows(qp, url);
|
|
2363
|
+
const allDaily = await aggregateByDay(rows, timeZoneContext);
|
|
2508
2364
|
const daily = allDaily.filter((d) => d.day >= from && d.day <= to);
|
|
2509
2365
|
const totals = daily.reduce(
|
|
2510
2366
|
(acc, r) => {
|
|
@@ -2570,8 +2426,8 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
2570
2426
|
const from = url.searchParams.get("from") || "";
|
|
2571
2427
|
const to = url.searchParams.get("to") || "";
|
|
2572
2428
|
const timeZoneContext = getTimeZoneContext(url);
|
|
2573
|
-
const { rows, scope, excludedSources } = scopedQueueRows(qp, url);
|
|
2574
|
-
const daily = aggregateByDay(rows, timeZoneContext).filter((d) => d.day >= from && d.day <= to);
|
|
2429
|
+
const { rows, scope, excludedSources } = await scopedQueueRows(qp, url);
|
|
2430
|
+
const daily = (await aggregateByDay(rows, timeZoneContext)).filter((d) => d.day >= from && d.day <= to);
|
|
2575
2431
|
json(res, { from, to, scope, excluded_sources: excludedSources, data: daily });
|
|
2576
2432
|
return true;
|
|
2577
2433
|
}
|
|
@@ -2604,7 +2460,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
2604
2460
|
json(res, { available: false, from, to, by_model: [], by_tool: [], totals: null });
|
|
2605
2461
|
return true;
|
|
2606
2462
|
}
|
|
2607
|
-
const { rows, scope, excludedSources } = scopedQueueRows(qp, url);
|
|
2463
|
+
const { rows, scope, excludedSources } = await scopedQueueRows(qp, url);
|
|
2608
2464
|
const result = computeQualityPerDollar(rows, outcomes, { from, to });
|
|
2609
2465
|
json(res, { from, to, scope, excluded_sources: excludedSources, ...result });
|
|
2610
2466
|
return true;
|
|
@@ -2677,8 +2533,8 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
2677
2533
|
if (p === "/functions/tokentracker-usage-heatmap") {
|
|
2678
2534
|
const weeks = parseInt(url.searchParams.get("weeks") || "52", 10);
|
|
2679
2535
|
const timeZoneContext = getTimeZoneContext(url);
|
|
2680
|
-
const { rows, scope, excludedSources } = scopedQueueRows(qp, url);
|
|
2681
|
-
const daily = aggregateByDay(rows, timeZoneContext);
|
|
2536
|
+
const { rows, scope, excludedSources } = await scopedQueueRows(qp, url);
|
|
2537
|
+
const daily = await aggregateByDay(rows, timeZoneContext);
|
|
2682
2538
|
const todayParts = getZonedParts(new Date(), timeZoneContext);
|
|
2683
2539
|
const todayStr = formatPartsDayKey(todayParts) || new Date().toISOString().slice(0, 10);
|
|
2684
2540
|
const end = new Date(`${todayStr}T00:00:00Z`);
|
|
@@ -2741,15 +2597,15 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
2741
2597
|
const from = url.searchParams.get("from") || "";
|
|
2742
2598
|
const to = url.searchParams.get("to") || "";
|
|
2743
2599
|
const timeZoneContext = getTimeZoneContext(url);
|
|
2744
|
-
const { rows: scopedRows, scope, excludedSources } = scopedQueueRows(qp, url);
|
|
2745
|
-
const
|
|
2746
|
-
if (!r.hour_start) return false;
|
|
2747
|
-
const d = rowDayKey(r, timeZoneContext);
|
|
2748
|
-
return d >= from && d <= to;
|
|
2749
|
-
});
|
|
2750
|
-
|
|
2600
|
+
const { rows: scopedRows, scope, excludedSources } = await scopedQueueRows(qp, url);
|
|
2601
|
+
const pricingRevision = getPricingRevision();
|
|
2751
2602
|
const bySource = new Map();
|
|
2752
|
-
|
|
2603
|
+
let processed = 0;
|
|
2604
|
+
for (const row of scopedRows) {
|
|
2605
|
+
if (++processed % AGGREGATION_BATCH_ROWS === 0) await yieldToRequests();
|
|
2606
|
+
if (!row.hour_start) continue;
|
|
2607
|
+
const day = rowDayKey(row, timeZoneContext);
|
|
2608
|
+
if (day < from || day > to) continue;
|
|
2753
2609
|
const src = row.source || "unknown";
|
|
2754
2610
|
const mdl = row.model || "unknown";
|
|
2755
2611
|
if (!bySource.has(src))
|
|
@@ -2783,6 +2639,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
2783
2639
|
// dropped every premium — same tokens, fewer dollars than the hero.
|
|
2784
2640
|
ma.totals.total_cost_usd = Number(ma.totals.total_cost_usd || 0) + computeRowCost(row);
|
|
2785
2641
|
}
|
|
2642
|
+
if (pricingRevision !== getPricingRevision()) return handleLocalApi(req, res, url);
|
|
2786
2643
|
|
|
2787
2644
|
const sources = Array.from(bySource.values()).map((s) => {
|
|
2788
2645
|
s.models = Array.from(s.models.values())
|
|
@@ -2833,7 +2690,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
2833
2690
|
timeZoneContext,
|
|
2834
2691
|
});
|
|
2835
2692
|
if (!Number(result?.totals?.total_tokens || 0)) {
|
|
2836
|
-
const fallback = buildCodexCategoryFallbackFromQueue(readQueueData(qp), {
|
|
2693
|
+
const fallback = buildCodexCategoryFallbackFromQueue(await readQueueData(qp), {
|
|
2837
2694
|
from,
|
|
2838
2695
|
to,
|
|
2839
2696
|
timeZoneContext,
|
|
@@ -2882,11 +2739,13 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
2882
2739
|
? Math.min(Math.floor(limitParam), PROJECT_USAGE_MAX_ENTRIES)
|
|
2883
2740
|
: PROJECT_USAGE_MAX_ENTRIES;
|
|
2884
2741
|
const { timeZoneContext, hasRange, dayInRange, projectRows } =
|
|
2885
|
-
readProjectUsageContext(qp, url);
|
|
2742
|
+
await readProjectUsageContext(qp, url);
|
|
2886
2743
|
|
|
2887
|
-
const aggregateEntries = (rows, keyOf, refOf) => {
|
|
2744
|
+
const aggregateEntries = async (rows, keyOf, refOf) => {
|
|
2888
2745
|
const byKey = new Map();
|
|
2746
|
+
let processed = 0;
|
|
2889
2747
|
for (const row of rows) {
|
|
2748
|
+
if (++processed % AGGREGATION_BATCH_ROWS === 0) await yieldToRequests();
|
|
2890
2749
|
if (hasRange && !dayInRange(rowDayKey(row, timeZoneContext))) continue;
|
|
2891
2750
|
const key = keyOf(row);
|
|
2892
2751
|
if (!byKey.has(key)) {
|
|
@@ -2935,7 +2794,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
2935
2794
|
}));
|
|
2936
2795
|
};
|
|
2937
2796
|
|
|
2938
|
-
let entries = aggregateEntries(
|
|
2797
|
+
let entries = await aggregateEntries(
|
|
2939
2798
|
projectRows,
|
|
2940
2799
|
(row) => row.project_key || "unknown",
|
|
2941
2800
|
(row) => row.project_ref || "",
|
|
@@ -2947,8 +2806,8 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
2947
2806
|
// totally empty. This path used to also exist for the non-empty case
|
|
2948
2807
|
// and produce wrong numbers; keep it only as the empty fallback.
|
|
2949
2808
|
if (entries.length === 0 && projectRows.length === 0) {
|
|
2950
|
-
entries = aggregateEntries(
|
|
2951
|
-
readQueueData(qp),
|
|
2809
|
+
entries = await aggregateEntries(
|
|
2810
|
+
await readQueueData(qp),
|
|
2952
2811
|
(row) => row.source || "unknown",
|
|
2953
2812
|
// Synthetic source-only row: leave project_ref empty rather than
|
|
2954
2813
|
// fabricating `https://${src}.ai`, which resolves to unrelated
|
|
@@ -2973,7 +2832,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
2973
2832
|
return true;
|
|
2974
2833
|
}
|
|
2975
2834
|
const { from, to, timeZoneContext, dayInRange, projectRows } =
|
|
2976
|
-
readProjectUsageContext(qp, url);
|
|
2835
|
+
await readProjectUsageContext(qp, url);
|
|
2977
2836
|
|
|
2978
2837
|
const emptyMeasures = () => ({
|
|
2979
2838
|
total_tokens: 0,
|
|
@@ -3003,8 +2862,9 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
3003
2862
|
const byDay = new Map();
|
|
3004
2863
|
const bySource = new Map();
|
|
3005
2864
|
let rangeTotalTokens = 0;
|
|
3006
|
-
|
|
2865
|
+
let processed = 0;
|
|
3007
2866
|
for (const row of projectRows) {
|
|
2867
|
+
if (++processed % AGGREGATION_BATCH_ROWS === 0) await yieldToRequests();
|
|
3008
2868
|
const day = rowDayKey(row, timeZoneContext);
|
|
3009
2869
|
if (!dayInRange(day)) continue;
|
|
3010
2870
|
rangeTotalTokens += Number(row.total_tokens || 0);
|
|
@@ -3286,8 +3146,8 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
3286
3146
|
if (p === "/functions/tokentracker-usage-hourly") {
|
|
3287
3147
|
const day = url.searchParams.get("day") || new Date().toISOString().slice(0, 10);
|
|
3288
3148
|
const timeZoneContext = getTimeZoneContext(url);
|
|
3289
|
-
const { rows, scope, excludedSources } = scopedQueueRows(qp, url);
|
|
3290
|
-
const data = aggregateHourlyByDay(rows, day, timeZoneContext);
|
|
3149
|
+
const { rows, scope, excludedSources } = await scopedQueueRows(qp, url);
|
|
3150
|
+
const data = await aggregateHourlyByDay(rows, day, timeZoneContext);
|
|
3291
3151
|
json(res, { day, scope, excluded_sources: excludedSources, data });
|
|
3292
3152
|
return true;
|
|
3293
3153
|
}
|
|
@@ -3297,9 +3157,11 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
3297
3157
|
const from = url.searchParams.get("from") || "";
|
|
3298
3158
|
const to = url.searchParams.get("to") || "";
|
|
3299
3159
|
const timeZoneContext = getTimeZoneContext(url);
|
|
3300
|
-
const { rows, scope, excludedSources } = scopedQueueRows(qp, url);
|
|
3160
|
+
const { rows, scope, excludedSources } = await scopedQueueRows(qp, url);
|
|
3301
3161
|
const byMonth = new Map();
|
|
3162
|
+
let processed = 0;
|
|
3302
3163
|
for (const row of rows) {
|
|
3164
|
+
if (++processed % AGGREGATION_BATCH_ROWS === 0) await yieldToRequests();
|
|
3303
3165
|
if (!row.hour_start) continue;
|
|
3304
3166
|
const day = rowDayKey(row, timeZoneContext);
|
|
3305
3167
|
if (!day || day < from || day > to) continue;
|
|
@@ -3607,8 +3469,8 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
3607
3469
|
// --- achievements (local badges) ---
|
|
3608
3470
|
if (p === "/functions/tokentracker-achievements") {
|
|
3609
3471
|
const timeZoneContext = getTimeZoneContext(url);
|
|
3610
|
-
const queueRows = readQueueData(qp);
|
|
3611
|
-
const { projectRows } = readProjectUsageContext(qp, url);
|
|
3472
|
+
const queueRows = await readQueueData(qp);
|
|
3473
|
+
const { projectRows } = await readProjectUsageContext(qp, url);
|
|
3612
3474
|
json(res, {
|
|
3613
3475
|
generated_at: new Date().toISOString(),
|
|
3614
3476
|
achievements: computeLocalAchievements(queueRows, projectRows, { timeZoneContext }),
|