tokentracker-cli 0.83.1 → 0.83.3
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-tMxPvIPA.js → AchievementBadge-BEk-hvfD.js} +1 -1
- package/dashboard/dist/assets/AchievementsPage-kLeXpzIX.js +2 -0
- package/dashboard/dist/assets/AchievementsSection-BtDWuZ8m.js +1 -0
- package/dashboard/dist/assets/BadgeDetailModal-BEgI_98J.js +1 -0
- package/dashboard/dist/assets/{Card-D479hjUR.js → Card-DvOwwAzG.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-ChoWfPva.js → ClawdAnimated-B_T5A9tJ.js} +1 -1
- package/dashboard/dist/assets/CommandPalette-BqAHVsZU.js +1 -0
- package/dashboard/dist/assets/CommunityStatsModal-g6EL03ZU.js +1 -0
- package/dashboard/dist/assets/{ConfirmModal-CCFe2duP.js → ConfirmModal-C7nLLif7.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-riwGRfNO.js +1 -0
- package/dashboard/dist/assets/{DevicePage-Bk9mryIp.js → DevicePage-DMs07QPT.js} +1 -1
- package/dashboard/dist/assets/DialogClose-Dlh20IoO.js +1 -0
- package/dashboard/dist/assets/DialogDescription-BkWNYqG-.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-vIrqSKer.js → DialogTitle-C6n2pohw.js} +1 -1
- package/dashboard/dist/assets/FadeIn-9jciDZsH.js +1 -0
- package/dashboard/dist/assets/{HeaderGithubStar-CFt_WMjW.js → HeaderGithubStar-DWOEAlWe.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-CSWXPm3B.js → IpCheckPage-BrsvUoQe.js} +1 -1
- package/dashboard/dist/assets/LandingPage-rlam9dyu.js +2 -0
- package/dashboard/dist/assets/{LeaderboardAvatar-CQkIM_Gb.js → LeaderboardAvatar-BtMM-6cC.js} +1 -1
- package/dashboard/dist/assets/LeaderboardPage-AhwAfNL9.js +2 -0
- package/dashboard/dist/assets/{LeaderboardProfileModal-DRPXqVvC.js → LeaderboardProfileModal-DBMJkIPl.js} +4 -4
- package/dashboard/dist/assets/{LeaderboardProfilePage-Bef_4_ug.js → LeaderboardProfilePage-B2S8pmDd.js} +3 -3
- package/dashboard/dist/assets/{LimitsPage-bSZCJNkv.js → LimitsPage-iwfTTvqG.js} +3 -3
- package/dashboard/dist/assets/{LocalOnlyNotice-CIEE7t0-.js → LocalOnlyNotice-DL72G1p9.js} +1 -1
- package/dashboard/dist/assets/LoginCard-6lOxJlws.js +1 -0
- package/dashboard/dist/assets/LoginModal-C1ldxgZv.js +1 -0
- package/dashboard/dist/assets/LoginPage-Cz6lrheW.js +1 -0
- package/dashboard/dist/assets/PetPage-CfFyzGFY.js +1 -0
- package/dashboard/dist/assets/PopoverPopup-qAngi4Xc.js +1 -0
- package/dashboard/dist/assets/ProviderIcon-I5z4CMY3.js +1 -0
- package/dashboard/dist/assets/ResetPasswordPage-A5bHmTUi.js +1 -0
- package/dashboard/dist/assets/{Select-Ch5kjPZu.js → Select-DDInV6Vc.js} +1 -1
- package/dashboard/dist/assets/SelectItemText-DIsD6lWR.js +1 -0
- package/dashboard/dist/assets/SettingsPage-BUCnJfRg.js +1 -0
- package/dashboard/dist/assets/SkillsPage-BhsGmLpr.js +1 -0
- package/dashboard/dist/assets/TokenGalaxy-j5bHVfYv.js +4314 -0
- package/dashboard/dist/assets/TrendMonitor-ifxuzprQ.js +19 -0
- package/dashboard/dist/assets/WidgetsPage-QZS8jTR0.js +1 -0
- package/dashboard/dist/assets/{WrappedPage-CPAtRfkp.js → WrappedPage-CzlxH4gJ.js} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-hJyUffEJ.js +1350 -0
- package/dashboard/dist/assets/agent-logos-WXnblKNu.js +1 -0
- package/dashboard/dist/assets/{arrow-up-right-Bm0oL1jH.js → arrow-up-right-WGuMZ7yP.js} +1 -1
- package/dashboard/dist/assets/{check-DR7E_A1J.js → check-DQswISeD.js} +1 -1
- package/dashboard/dist/assets/{copy-BtstNk2s.js → copy-6aPUSNsw.js} +1 -1
- package/dashboard/dist/assets/{download-zOVMvQXI.js → download-DL3wQKU1.js} +1 -1
- package/dashboard/dist/assets/{flame-DQaixlOI.js → flame-BY3LUnQc.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-Cbac-ZGw.js → format-tokens-DUXqRlZ8.js} +1 -1
- package/dashboard/dist/assets/{icons-iL7TjE7A.js → icons-CT3DYBKd.js} +1 -1
- package/dashboard/dist/assets/index-BcTWurhn.js +1 -0
- package/dashboard/dist/assets/index-BsgZcvQd.js +1 -0
- package/dashboard/dist/assets/index-pza5Zl32.js +9 -0
- package/dashboard/dist/assets/{info-BtnxfCu0.js → info-CuyReqZo.js} +1 -1
- package/dashboard/dist/assets/{limits-providers-DkoPB8B2.js → limits-providers-FnlIwbBy.js} +1 -1
- package/dashboard/dist/assets/{link-2-Bh0vUBU6.js → link-2-dgMFFrBX.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-BVF4Sglg.js → loader-circle-D2Erh-me.js} +1 -1
- package/dashboard/dist/assets/module-kH5GsDis.js +2 -0
- package/dashboard/dist/assets/provider-display-DXMQRWSr.js +42 -0
- package/dashboard/dist/assets/react-w-68N3xj.js +1 -0
- package/dashboard/dist/assets/skills-api-CkMNAsEg.js +1 -0
- package/dashboard/dist/assets/{terminal-BUoPNPs5.js → terminal-CJMbTjNB.js} +1 -1
- package/dashboard/dist/assets/{trash-2-DrGpFJsR.js → trash-2-VaWsdH7y.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-D6Kxm_DN.js → use-community-stats-DdAUa2P5.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-BCh-4-Bo.js → use-limits-display-prefs-DJUffL2_.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-o-TL7MGQ.js → use-native-settings-CiYsT_Ip.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-sMvpJjOK.js → use-ordered-list-B12VdEap.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-C6xgTm9r.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-BsG2CJqB.js → use-session-efficiency-pref-rHhQnzAS.js} +1 -1
- package/dashboard/dist/assets/use-transform-uFbh6Odm.js +1 -0
- package/dashboard/dist/assets/{use-usage-limits-DVhgZkTD.js → use-usage-limits-DmT_wC1b.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-DqxQMZty.js → useCurrency-D3_YrrTw.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-DZpDnNt6.js → useScrollLock-JHdC5cyV.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-CdYh0d_c.js → useTokenFormat-CMYJB7gb.js} +1 -1
- package/dashboard/dist/assets/{zap-9gvAtu1o.js → zap-Cr4tl-1M.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +1 -1
- package/src/commands/serve.js +21 -4
- package/src/commands/sync.js +142 -73
- package/src/lib/cursor-store.js +52 -2
- package/src/lib/local-api.js +143 -35
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/runtime-config.js +19 -1
- package/dashboard/dist/assets/AchievementsPage-qqu8DWOa.js +0 -2
- package/dashboard/dist/assets/AchievementsSection-iRzgGWeO.js +0 -1
- package/dashboard/dist/assets/BadgeDetailModal-D3lYNgOJ.js +0 -1
- package/dashboard/dist/assets/CommunityStatsModal-CR-VAT3e.js +0 -1
- package/dashboard/dist/assets/DashboardPage-DGzKf5-y.js +0 -1
- package/dashboard/dist/assets/DialogClose-8Fw7R9Ma.js +0 -1
- package/dashboard/dist/assets/DialogDescription-DDeAH6I9.js +0 -1
- package/dashboard/dist/assets/FadeIn-DdYC9vT8.js +0 -1
- package/dashboard/dist/assets/LandingPage-D7WEaged.js +0 -4314
- package/dashboard/dist/assets/LeaderboardPage-nRXBc0bw.js +0 -2
- package/dashboard/dist/assets/LoginPage-2c5mYQJ1.js +0 -1
- package/dashboard/dist/assets/PetPage-BMi50Olg.js +0 -1
- package/dashboard/dist/assets/PopoverPopup-ByOC1OEv.js +0 -1
- package/dashboard/dist/assets/ResetPasswordPage-CNeptbJw.js +0 -1
- package/dashboard/dist/assets/SelectItemText-D4laqPg3.js +0 -1
- package/dashboard/dist/assets/SettingsPage-DlFD5hy9.js +0 -1
- package/dashboard/dist/assets/SkillsPage-ChdsShAm.js +0 -1
- package/dashboard/dist/assets/TrendMonitor-ueTTQIih.js +0 -19
- package/dashboard/dist/assets/WidgetsPage-a6S6gqVf.js +0 -1
- package/dashboard/dist/assets/__vite-browser-external-Djkun5S2.js +0 -1354
- package/dashboard/dist/assets/agent-logos-CwuIej9M.js +0 -1
- package/dashboard/dist/assets/provider-display-BgCflQrF.js +0 -42
- package/dashboard/dist/assets/use-transform-Bjy8uQ3M.js +0 -1
package/src/lib/cursor-store.js
CHANGED
|
@@ -14,6 +14,7 @@ const STORE_VERSION = 2;
|
|
|
14
14
|
const STORE_DIRNAME = "cursor-store-v2";
|
|
15
15
|
const MANIFEST_FILENAME = "manifest.json";
|
|
16
16
|
const GENERATIONS_DIRNAME = "generations";
|
|
17
|
+
const DEFERRED_CODEX_AUDIT_FILENAME = "deferred-codex-audit.json";
|
|
17
18
|
const DEFAULT_ACTIVATION_BYTES = 16 * 1024 * 1024;
|
|
18
19
|
const PROJECT_ABSENT_CONTEXT_RESCAN_MS = 24 * 60 * 60 * 1000;
|
|
19
20
|
const CURSOR_STORE_RETRY_CODE = "TOKENTRACKER_CURSOR_STORE_RETRY";
|
|
@@ -35,6 +36,7 @@ async function openCursorStore({
|
|
|
35
36
|
const normalizedCodexRoots = normalizeCodexRoots(codexRoots);
|
|
36
37
|
const legacyFingerprint = await fingerprintFile(cursorsPath);
|
|
37
38
|
let manifest = await readJson(manifestPath);
|
|
39
|
+
let migratedDuringOpen = false;
|
|
38
40
|
|
|
39
41
|
if (isManifest(manifest)) {
|
|
40
42
|
const legacyChanged = legacyFingerprint
|
|
@@ -49,6 +51,7 @@ async function openCursorStore({
|
|
|
49
51
|
codexRoots: normalizedCodexRoots,
|
|
50
52
|
failureInjector,
|
|
51
53
|
});
|
|
54
|
+
migratedDuringOpen = true;
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
const opened = await openManifestGeneration({
|
|
@@ -58,7 +61,10 @@ async function openCursorStore({
|
|
|
58
61
|
codexRoots: normalizedCodexRoots,
|
|
59
62
|
failureInjector,
|
|
60
63
|
});
|
|
61
|
-
if (opened)
|
|
64
|
+
if (opened) {
|
|
65
|
+
if (migratedDuringOpen) opened.requiresCommit = true;
|
|
66
|
+
return opened;
|
|
67
|
+
}
|
|
62
68
|
|
|
63
69
|
manifest = await migrateLegacyCursorState({
|
|
64
70
|
cursorsPath,
|
|
@@ -68,6 +74,7 @@ async function openCursorStore({
|
|
|
68
74
|
codexRoots: normalizedCodexRoots,
|
|
69
75
|
failureInjector,
|
|
70
76
|
});
|
|
77
|
+
migratedDuringOpen = true;
|
|
71
78
|
const recovered = await openManifestGeneration({
|
|
72
79
|
cursorsPath,
|
|
73
80
|
storeRoot,
|
|
@@ -75,7 +82,10 @@ async function openCursorStore({
|
|
|
75
82
|
codexRoots: normalizedCodexRoots,
|
|
76
83
|
failureInjector,
|
|
77
84
|
});
|
|
78
|
-
if (recovered)
|
|
85
|
+
if (recovered) {
|
|
86
|
+
recovered.requiresCommit = true;
|
|
87
|
+
return recovered;
|
|
88
|
+
}
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
const legacySize = Number(legacyFingerprint?.size || 0);
|
|
@@ -97,6 +107,7 @@ async function openCursorStore({
|
|
|
97
107
|
codexRoots: normalizedCodexRoots,
|
|
98
108
|
failureInjector,
|
|
99
109
|
});
|
|
110
|
+
migratedDuringOpen = true;
|
|
100
111
|
const opened = await openManifestGeneration({
|
|
101
112
|
cursorsPath,
|
|
102
113
|
storeRoot,
|
|
@@ -105,6 +116,7 @@ async function openCursorStore({
|
|
|
105
116
|
failureInjector,
|
|
106
117
|
});
|
|
107
118
|
if (!opened) throw new Error("Unable to open migrated cursor store");
|
|
119
|
+
if (migratedDuringOpen) opened.requiresCommit = true;
|
|
108
120
|
return opened;
|
|
109
121
|
}
|
|
110
122
|
|
|
@@ -180,6 +192,14 @@ class LegacyCursorStore {
|
|
|
180
192
|
return false;
|
|
181
193
|
}
|
|
182
194
|
|
|
195
|
+
async readDeferredCodexAuditSyncs() {
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async writeDeferredCodexAuditSyncs() {}
|
|
200
|
+
|
|
201
|
+
async clearDeferredCodexAuditSyncs() {}
|
|
202
|
+
|
|
183
203
|
async commit(cursors = this.cursors) {
|
|
184
204
|
await writeJson(this.cursorsPath, cursors);
|
|
185
205
|
}
|
|
@@ -205,12 +225,17 @@ class V2CursorStore {
|
|
|
205
225
|
this.codexRoots = codexRoots;
|
|
206
226
|
this.cursors = generation.core;
|
|
207
227
|
this.failureInjector = failureInjector;
|
|
228
|
+
this.deferredCodexAuditPath = path.join(
|
|
229
|
+
storeRoot,
|
|
230
|
+
DEFERRED_CODEX_AUDIT_FILENAME,
|
|
231
|
+
);
|
|
208
232
|
this.loadedFileShards = new Map();
|
|
209
233
|
this.loadedEventSets = new Map();
|
|
210
234
|
this.pendingEventKeys = new Map();
|
|
211
235
|
this.skipValidationCache = new Map();
|
|
212
236
|
this.fileShardLoadCount = 0;
|
|
213
237
|
this.materializedCodexHashes = false;
|
|
238
|
+
this.requiresCommit = generation.metadata.id !== manifest.current;
|
|
214
239
|
|
|
215
240
|
if (!this.cursors.files || typeof this.cursors.files !== "object") {
|
|
216
241
|
this.cursors.files = {};
|
|
@@ -377,6 +402,29 @@ class V2CursorStore {
|
|
|
377
402
|
return valid;
|
|
378
403
|
}
|
|
379
404
|
|
|
405
|
+
async readDeferredCodexAuditSyncs() {
|
|
406
|
+
const state = await readJson(this.deferredCodexAuditPath);
|
|
407
|
+
const count = Number(state?.syncsSinceFullScan || 0);
|
|
408
|
+
return Number.isSafeInteger(count) && count > 0 ? count : 0;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async writeDeferredCodexAuditSyncs(count) {
|
|
412
|
+
const normalized = Math.max(0, Math.floor(Number(count) || 0));
|
|
413
|
+
if (normalized === 0) {
|
|
414
|
+
await this.clearDeferredCodexAuditSyncs();
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
await writeJson(this.deferredCodexAuditPath, {
|
|
418
|
+
version: 1,
|
|
419
|
+
syncsSinceFullScan: normalized,
|
|
420
|
+
updatedAt: new Date().toISOString(),
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async clearDeferredCodexAuditSyncs() {
|
|
425
|
+
await fs.unlink(this.deferredCodexAuditPath).catch(() => {});
|
|
426
|
+
}
|
|
427
|
+
|
|
380
428
|
hasCodexEvent(key) {
|
|
381
429
|
const shardKey = codexEventShardKey(key);
|
|
382
430
|
if (this.pendingEventKeys.get(shardKey)?.has(key)) return true;
|
|
@@ -464,6 +512,7 @@ class V2CursorStore {
|
|
|
464
512
|
this.pendingEventKeys.clear();
|
|
465
513
|
this.skipValidationCache.clear();
|
|
466
514
|
this.materializedCodexHashes = false;
|
|
515
|
+
this.requiresCommit = true;
|
|
467
516
|
return true;
|
|
468
517
|
}
|
|
469
518
|
|
|
@@ -594,6 +643,7 @@ class V2CursorStore {
|
|
|
594
643
|
this.pendingEventKeys.clear();
|
|
595
644
|
this.skipValidationCache.clear();
|
|
596
645
|
this.materializedCodexHashes = false;
|
|
646
|
+
this.requiresCommit = false;
|
|
597
647
|
await cleanupGenerations(this.storeRoot, new Set([
|
|
598
648
|
nextManifest.current,
|
|
599
649
|
nextManifest.previous,
|
package/src/lib/local-api.js
CHANGED
|
@@ -118,7 +118,32 @@ function readProjectUsageContext(qp, url) {
|
|
|
118
118
|
return { from, to, timeZoneContext, hasRange, dayInRange, projectRows };
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
// Same signature-based caching as readQueueData: the project-usage endpoints
|
|
122
|
+
// all fan out from one dashboard refresh, so without this every request
|
|
123
|
+
// re-read and re-parsed the whole append-only project queue.
|
|
124
|
+
let projectQueueDataCache = null;
|
|
125
|
+
|
|
121
126
|
function readProjectQueueData(projectQueuePath) {
|
|
127
|
+
let signature;
|
|
128
|
+
try {
|
|
129
|
+
signature = queueFileSignature(projectQueuePath);
|
|
130
|
+
} catch (e) {
|
|
131
|
+
if (projectQueueDataCache?.queuePath === projectQueuePath) {
|
|
132
|
+
projectQueueDataCache = null;
|
|
133
|
+
}
|
|
134
|
+
if (e?.code !== "ENOENT") {
|
|
135
|
+
console.error("[LocalAPI] readProjectQueueData: failed to stat:", e?.message || e);
|
|
136
|
+
}
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (
|
|
141
|
+
projectQueueDataCache?.queuePath === projectQueuePath &&
|
|
142
|
+
projectQueueDataCache.signature === signature
|
|
143
|
+
) {
|
|
144
|
+
return projectQueueDataCache.rows;
|
|
145
|
+
}
|
|
146
|
+
|
|
122
147
|
let raw;
|
|
123
148
|
try {
|
|
124
149
|
raw = fs.readFileSync(projectQueuePath, "utf8");
|
|
@@ -141,7 +166,11 @@ function readProjectQueueData(projectQueuePath) {
|
|
|
141
166
|
// skip malformed
|
|
142
167
|
}
|
|
143
168
|
}
|
|
144
|
-
|
|
169
|
+
// Callers treat the result as read-only (sortByHour copies before sorting),
|
|
170
|
+
// so the cached array can be shared across requests.
|
|
171
|
+
const rows = Array.from(seen.values());
|
|
172
|
+
projectQueueDataCache = { queuePath: projectQueuePath, signature, rows };
|
|
173
|
+
return rows;
|
|
145
174
|
}
|
|
146
175
|
|
|
147
176
|
function isLegacyInclusiveCodexRow(row) {
|
|
@@ -188,9 +217,9 @@ function normalizeQueueRow(row) {
|
|
|
188
217
|
}
|
|
189
218
|
|
|
190
219
|
function readQueueData(queuePath) {
|
|
191
|
-
let
|
|
220
|
+
let stat;
|
|
192
221
|
try {
|
|
193
|
-
|
|
222
|
+
stat = fs.statSync(queuePath, { bigint: true });
|
|
194
223
|
} catch (e) {
|
|
195
224
|
if (queueDataCache?.queuePath === queuePath) queueDataCache = null;
|
|
196
225
|
if (e?.code !== "ENOENT") {
|
|
@@ -198,49 +227,119 @@ function readQueueData(queuePath) {
|
|
|
198
227
|
}
|
|
199
228
|
return [];
|
|
200
229
|
}
|
|
230
|
+
const signature = `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
201
231
|
|
|
202
|
-
|
|
203
|
-
|
|
232
|
+
const cached = queueDataCache;
|
|
233
|
+
if (cached?.queuePath === queuePath && cached.signature === signature) {
|
|
234
|
+
return cached.rows;
|
|
204
235
|
}
|
|
205
236
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
237
|
+
// The queue is append-only, so on a same-file append only the new tail is
|
|
238
|
+
// read and parsed; the deduped row set carries over. A replaced, truncated,
|
|
239
|
+
// or first-seen file falls back to a full read.
|
|
240
|
+
const devIno = `${stat.dev}:${stat.ino}`;
|
|
241
|
+
const fileSize = Number(stat.size);
|
|
242
|
+
const canAppend =
|
|
243
|
+
cached != null &&
|
|
244
|
+
cached.queuePath === queuePath &&
|
|
245
|
+
cached.devIno === devIno &&
|
|
246
|
+
fileSize >= cached.consumedBytes;
|
|
247
|
+
let seen = canAppend ? cached.seen : new Map();
|
|
248
|
+
let offset = canAppend ? cached.consumedBytes : 0;
|
|
249
|
+
|
|
250
|
+
// Integrity probe for the append assumption: repo writers only append or
|
|
251
|
+
// atomically replace (inode change), but an EXTERNAL in-place rewrite
|
|
252
|
+
// (cp over the file, shell redirect) keeps the inode with a same/larger
|
|
253
|
+
// size while changing the prefix. The byte before our offset must be the
|
|
254
|
+
// newline that terminated the last consumed line — anything else means the
|
|
255
|
+
// prefix is no longer ours, so fall back to a full read.
|
|
256
|
+
if (offset > 0) {
|
|
257
|
+
try {
|
|
258
|
+
const fd = fs.openSync(queuePath, "r");
|
|
259
|
+
try {
|
|
260
|
+
const probe = Buffer.allocUnsafe(1);
|
|
261
|
+
if (fs.readSync(fd, probe, 0, 1, offset - 1) !== 1 || probe[0] !== 0x0a) {
|
|
262
|
+
seen = new Map();
|
|
263
|
+
offset = 0;
|
|
264
|
+
}
|
|
265
|
+
} finally {
|
|
266
|
+
fs.closeSync(fd);
|
|
267
|
+
}
|
|
268
|
+
} catch {
|
|
269
|
+
seen = new Map();
|
|
270
|
+
offset = 0;
|
|
215
271
|
}
|
|
216
|
-
return [];
|
|
217
272
|
}
|
|
218
|
-
|
|
273
|
+
|
|
274
|
+
let raw = "";
|
|
275
|
+
if (fileSize > offset) {
|
|
276
|
+
try {
|
|
277
|
+
if (offset === 0) {
|
|
278
|
+
raw = fs.readFileSync(queuePath, "utf8");
|
|
279
|
+
} else {
|
|
280
|
+
const fd = fs.openSync(queuePath, "r");
|
|
281
|
+
try {
|
|
282
|
+
const length = fileSize - offset;
|
|
283
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
284
|
+
let read = 0;
|
|
285
|
+
while (read < length) {
|
|
286
|
+
// A concurrent truncation between stat and open makes readSync hit
|
|
287
|
+
// EOF early — without the 0-byte break this loop never terminates.
|
|
288
|
+
const n = fs.readSync(fd, buffer, read, length - read, offset + read);
|
|
289
|
+
if (n === 0) break;
|
|
290
|
+
read += n;
|
|
291
|
+
}
|
|
292
|
+
raw = buffer.toString("utf8", 0, read);
|
|
293
|
+
} finally {
|
|
294
|
+
fs.closeSync(fd);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
} catch (e) {
|
|
298
|
+
// ENOENT is legitimate (queue deleted between stat and read); anything
|
|
299
|
+
// else is a signal we don't want to hide behind an empty array forever —
|
|
300
|
+
// the dashboard would otherwise render "0 tokens" with no clue the queue
|
|
301
|
+
// was unreadable.
|
|
302
|
+
if (e?.code !== "ENOENT") {
|
|
303
|
+
console.error("[LocalAPI] readQueueData: failed to read queue:", e?.message || e);
|
|
304
|
+
}
|
|
305
|
+
return canAppend ? cached.rows : [];
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
219
309
|
// Parse row-by-row so a single corrupted line (partial write, disk-full
|
|
220
|
-
// truncation, …) does not wipe out every other row with it.
|
|
221
|
-
|
|
310
|
+
// truncation, …) does not wipe out every other row with it. An unterminated
|
|
311
|
+
// tail is attempted (legacy writers may omit the final newline) but NOT
|
|
312
|
+
// marked consumed — a mid-append partial line is re-read once it completes.
|
|
313
|
+
// "\n" (0x0A) never appears inside a multi-byte UTF-8 sequence, so cutting
|
|
314
|
+
// on the last newline is byte-safe.
|
|
315
|
+
let consumedBytes = offset;
|
|
222
316
|
let malformed = 0;
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
317
|
+
if (raw) {
|
|
318
|
+
const lastNewline = raw.lastIndexOf("\n");
|
|
319
|
+
if (lastNewline !== -1) {
|
|
320
|
+
consumedBytes += Buffer.byteLength(raw.slice(0, lastNewline), "utf8") + 1;
|
|
321
|
+
}
|
|
322
|
+
const lines = raw.split("\n");
|
|
323
|
+
for (const line of lines) {
|
|
324
|
+
if (!line.trim()) continue;
|
|
325
|
+
try {
|
|
326
|
+
const row = JSON.parse(line);
|
|
327
|
+
// Deduplicate: each sync appends cumulative totals per bucket, so for
|
|
328
|
+
// each (source, model, hour_start) keep only the latest (last) entry.
|
|
329
|
+
const key = `${row.source || ""}|${row.model || ""}|${row.hour_start || ""}`;
|
|
330
|
+
seen.set(key, normalizeQueueRow(row));
|
|
331
|
+
} catch {
|
|
332
|
+
malformed += 1;
|
|
333
|
+
}
|
|
228
334
|
}
|
|
229
335
|
}
|
|
230
336
|
if (malformed > 0) {
|
|
231
337
|
console.error(
|
|
232
|
-
`[LocalAPI] readQueueData: skipped ${malformed}
|
|
338
|
+
`[LocalAPI] readQueueData: skipped ${malformed} malformed line(s) in ${queuePath}`,
|
|
233
339
|
);
|
|
234
340
|
}
|
|
235
|
-
// Deduplicate: each sync appends cumulative totals per bucket, so for
|
|
236
|
-
// each (source, model, hour_start) keep only the latest (last) entry.
|
|
237
|
-
const seen = new Map();
|
|
238
|
-
for (const row of parsed) {
|
|
239
|
-
const key = `${row.source || ""}|${row.model || ""}|${row.hour_start || ""}`;
|
|
240
|
-
seen.set(key, normalizeQueueRow(row));
|
|
241
|
-
}
|
|
242
341
|
const rows = Array.from(seen.values());
|
|
243
|
-
queueDataCache = { queuePath, signature, rows };
|
|
342
|
+
queueDataCache = { queuePath, signature, devIno, consumedBytes, seen, rows };
|
|
244
343
|
return rows;
|
|
245
344
|
}
|
|
246
345
|
|
|
@@ -802,9 +901,18 @@ function runSyncCommand(extraEnv = {}, opts = {}) {
|
|
|
802
901
|
});
|
|
803
902
|
child.on("close", (code) => {
|
|
804
903
|
const r = { code: code ?? 1, stdout: trimOutput(stdout), stderr: trimOutput(stderr) };
|
|
805
|
-
code === 0
|
|
806
|
-
|
|
807
|
-
|
|
904
|
+
if (code === 0) {
|
|
905
|
+
finish(resolve, r);
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
const error = Object.assign(
|
|
909
|
+
new Error(r.stderr || r.stdout || `exit ${r.code}`),
|
|
910
|
+
r,
|
|
911
|
+
);
|
|
912
|
+
if (/\bSYNC_BUSY\b/.test(`${r.stderr}\n${r.stdout}`)) {
|
|
913
|
+
error.code = "SYNC_BUSY";
|
|
914
|
+
}
|
|
915
|
+
finish(reject, error);
|
|
808
916
|
});
|
|
809
917
|
});
|
|
810
918
|
}
|