tokentracker-cli 0.87.0 → 0.87.2
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-CU9m5POo.js → AchievementBadge-C468-Rsw.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-ESzayVR5.js → AchievementsPage-CZWzSdiH.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-DUVRqw19.js → AchievementsSection-DXt6VbwH.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-CxNQcN6_.js → BadgeDetailModal-BtGvpqQm.js} +1 -1
- package/dashboard/dist/assets/{Card-DXIoSOtO.js → Card-DZ-_fXkw.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-BsuxMkNd.js → ClawdAnimated-9rXaHo1-.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-C-amBp21.js → CommandPalette-sqIvZNsH.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-ByFOQube.js → CommunityStatsModal-c8VohIqQ.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-BnUyidbn.js → ConfirmModal-Ct2HLdds.js} +1 -1
- package/dashboard/dist/assets/Controls-DPtXGUGi.js +1 -0
- package/dashboard/dist/assets/DashboardPage-C65hbl8o.js +1 -0
- package/dashboard/dist/assets/{DevicePage-B_o-jhku.js → DevicePage-Ds4GEuqY.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-BSd7Jiag.js → DialogClose-B2IAbJG4.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-DeRza-LI.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-XBpneDtQ.js → DialogTitle-BIwLH6gA.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-Dg0v4V9p.js → FadeIn-DPS7_DOB.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-BIsQD1OK.js → HeaderGithubStar-CsSgr9Hd.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-CtmRa8ne.js → HoverTooltip-CLGZW2d-.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-BUoLz5sL.js → IpCheckPage-DEM_uk69.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-BheA-lw8.js → LandingPage-BQ3YzZ68.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-DAy9SOdL.js → LeaderboardAvatar-ySznr4Ml.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-CwLqtEUN.js → LeaderboardPage-Br4JCoVE.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-BZ9yGyjB.js → LeaderboardProfileModal-Drvsb5xF.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-Cs-LTfvS.js → LeaderboardProfilePage-BD0P-zZF.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-BQle8e8N.js → LimitsPage-Mbn2mj6S.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-Bp4CqObQ.js → LocalOnlyNotice-BsKgtoVa.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-BIXfWkR2.js → LoginCard-CTQDdHcx.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-B-ubPXdr.js → LoginModal-BxCyLDEW.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-9arjWT-s.js → LoginPage-ftxnX8g1.js} +1 -1
- package/dashboard/dist/assets/PetPage-CQsfqsm8.js +1 -0
- package/dashboard/dist/assets/{PopoverPopup-DMg7XE1h.js → PopoverPopup-C16r8jQC.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-BG3oqeST.js → ProviderIcon-7Tkq9_m6.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-LQ9rfcRE.js → ResetPasswordPage-D-MPpQd1.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-GY2tRk1u.js → SegmentedControl-CYFrzu9w.js} +1 -1
- package/dashboard/dist/assets/{Select-CCngFwmz.js → Select-Dp4V9p6t.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-B9Zm8WTR.js → SelectItemText-CxhBeoxx.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-BxPvk1-W.js → ServiceStatusPage-BT6hvvLW.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-DGo6yAwP.js → SessionsPage-BopeZBiH.js} +1 -1
- package/dashboard/dist/assets/SettingsPage-D03GJJBd.js +1 -0
- package/dashboard/dist/assets/{SkillsPage-D0PB3WR5.js → SkillsPage-Cr8O6eOH.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-Chh_fPIv.js → TokenGalaxy-BoUsrUEs.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-BXacVvrF.js → ToolbarRootContext-CwRaHhMN.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-CjiCwNmO.js → TrendMonitor-BV2ugEsw.js} +1 -1
- package/dashboard/dist/assets/WidgetsPage-B7r4mvrw.js +1 -0
- package/dashboard/dist/assets/{WrappedPage-CEpBAMZR.js → WrappedPage-CFU9l22i.js} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-CzQk3FUc.css +1 -0
- package/dashboard/dist/assets/{__vite-browser-external-Dyag-P6u.js → __vite-browser-external-Db9onipF.js} +10 -3
- package/dashboard/dist/assets/{agent-logos-CLQGE0AM.js → agent-logos-DSSn_SPi.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-CU2HDjoG.js → arrow-up-right-COvrArNz.js} +1 -1
- package/dashboard/dist/assets/{check-CucGM5lh.js → check-CasSqiev.js} +1 -1
- package/dashboard/dist/assets/{chevron-right-DYzUvcID.js → chevron-right-B1PMeNoD.js} +1 -1
- package/dashboard/dist/assets/{copy-BFeQ31Tr.js → copy-D67vycfp.js} +1 -1
- package/dashboard/dist/assets/{download-Coq_llCX.js → download-adOCFO2v.js} +1 -1
- package/dashboard/dist/assets/{external-link-BJiDBW3d.js → external-link-DVanKEuy.js} +1 -1
- package/dashboard/dist/assets/{flame-5stsnhc0.js → flame-RbiHZM9b.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-CpTJbo-2.js → format-tokens-DhMp1e1L.js} +1 -1
- package/dashboard/dist/assets/{icons-jsuvmnnO.js → icons-BC0p_aGr.js} +1 -1
- package/dashboard/dist/assets/{index-DSD3RLQ4.js → index-A41xjc83.js} +1 -1
- package/dashboard/dist/assets/{index-BU6GsTbx.js → index-CaRyTJ6K.js} +1 -1
- package/dashboard/dist/assets/{index-Bs97feNM.js → index-DrgBqR3o.js} +1 -1
- package/dashboard/dist/assets/{info-DNNUzeV_.js → info-D9LwqlnC.js} +1 -1
- package/dashboard/dist/assets/{limits-providers-DKTqNO55.js → limits-providers-CRHpXhmW.js} +1 -1
- package/dashboard/dist/assets/{link-2-DC1hnQcE.js → link-2-B5f4qFR0.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-DtInM4Io.js → loader-circle-DCmwZEdS.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-DxpsaYXp.js → maximize-2-BMalEDCZ.js} +1 -1
- package/dashboard/dist/assets/{provider-display-hkyvfEJF.js → provider-display-qMFRUZHm.js} +1 -1
- package/dashboard/dist/assets/{react-TbJoDSP4.js → react-BnaPNGj8.js} +1 -1
- package/dashboard/dist/assets/{search-C7bnqB9b.js → search-BrqBkLs2.js} +1 -1
- package/dashboard/dist/assets/{skills-api-D7glOWg7.js → skills-api-vo3iM6yD.js} +1 -1
- package/dashboard/dist/assets/{store-CCVlzuqv.js → store-BwMVXuz3.js} +1 -1
- package/dashboard/dist/assets/{terminal-uQlsmzGi.js → terminal-BiyUh_8k.js} +1 -1
- package/dashboard/dist/assets/{trash-2-B1yuX-oD.js → trash-2-DAr4sqkX.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-0Fs5nY1G.js → use-community-stats-Cg48Zphy.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-CrftAn62.js → use-limits-display-prefs-DfKtH1Zn.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-DAdPjLb5.js → use-ordered-list-CnXfrWeF.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-BIgYrbON.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-Cc52HiK0.js → use-session-efficiency-pref-CMDTCAAd.js} +1 -1
- package/dashboard/dist/assets/{use-transform-D76wHhX0.js → use-transform-B2d9Txt5.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-B7Prb8rk.js → use-usage-limits-CJbTUl9W.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-DWoM_A3s.js → useCurrency-B0hJte5f.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-Bp6ddiQo.js → useScrollLock-DuosKdSB.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-DZOYz5HT.js → useTokenFormat-DB8pKLFM.js} +1 -1
- package/dashboard/dist/assets/{zap-DyeAeO8l.js → zap-rbzC4XgP.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dashboard/dist/ip-check.html +2 -2
- package/dashboard/dist/leaderboard.html +2 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +1 -1
- package/src/commands/device-login.js +12 -7
- package/src/commands/init.js +3 -0
- package/src/commands/sync.js +115 -20
- package/src/lib/grok-context-breakdown.js +124 -19
- package/src/lib/grok-limits.js +374 -9
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/runtime-config.js +23 -3
- package/dashboard/dist/assets/DashboardPage-GyXkKYW0.js +0 -1
- package/dashboard/dist/assets/DialogDescription-BFBVGbUT.js +0 -1
- package/dashboard/dist/assets/PetPage-BzAVsvh6.js +0 -1
- package/dashboard/dist/assets/SettingsPage-ConPEiwO.js +0 -1
- package/dashboard/dist/assets/WidgetsPage-CBw9uBCO.js +0 -1
- package/dashboard/dist/assets/__vite-browser-external-hXV_ZYTG.css +0 -1
- package/dashboard/dist/assets/use-native-settings-CVqB9eoO.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-DVGYs3-k.js +0 -1
package/src/commands/sync.js
CHANGED
|
@@ -8,7 +8,13 @@ const readline = require("node:readline");
|
|
|
8
8
|
const { resolveInstallPaths, ensureFlatCursor } = require("../lib/install-resolver");
|
|
9
9
|
const { multiInstallParse, mergeBothFileSources } = require("../lib/multi-install-parser");
|
|
10
10
|
const wsl = require("../lib/wsl-probe");
|
|
11
|
-
const {
|
|
11
|
+
const {
|
|
12
|
+
ensureDir,
|
|
13
|
+
readJson,
|
|
14
|
+
writeJson,
|
|
15
|
+
chmod600IfPossible,
|
|
16
|
+
openLock,
|
|
17
|
+
} = require("../lib/fs");
|
|
12
18
|
const { physicalJsonlRecords } = require("../lib/jsonl-lines");
|
|
13
19
|
const {
|
|
14
20
|
listRolloutFiles,
|
|
@@ -125,7 +131,7 @@ const {
|
|
|
125
131
|
openCursorStore,
|
|
126
132
|
} = require("../lib/cursor-store");
|
|
127
133
|
const { resolveTrackerPaths } = require("../lib/tracker-paths");
|
|
128
|
-
const { resolveRuntimeConfig } = require("../lib/runtime-config");
|
|
134
|
+
const { resolveRuntimeConfig, isLegacyInsforgeBaseUrl } = require("../lib/runtime-config");
|
|
129
135
|
const { extractTokenCount } = require("../lib/codex-rollout-parser");
|
|
130
136
|
const {
|
|
131
137
|
consumeUsageDelta,
|
|
@@ -215,6 +221,7 @@ const CODEX_FORK_REPLAY_REPAIR_KEY = "codexForkReplayRepair_2026_07";
|
|
|
215
221
|
// contributing rollout before committing the rebuild. The new key is required
|
|
216
222
|
// so installs that finalized the original migration on a false negative retry.
|
|
217
223
|
const CODEX_USAGE_LINEAGE_REPAIR_KEY = "codexUsageLineageRepair_2026_07_v2";
|
|
224
|
+
const LEGACY_BASE_URL_MIGRATION_NOTE = "reset_after_legacy_baseurl_migration_2026_07";
|
|
218
225
|
// Keep the one escalated desktop refresh bounded; explicit full syncs can retry
|
|
219
226
|
// the same migration without this ceiling when the history needs a deeper scan.
|
|
220
227
|
const CODEX_BACKGROUND_LINEAGE_SCAN_MAX_BYTES = 1024 * 1024;
|
|
@@ -430,6 +437,53 @@ async function cmdSync(argv, context = {}) {
|
|
|
430
437
|
}
|
|
431
438
|
|
|
432
439
|
const config = await readJson(configPath);
|
|
440
|
+
let legacyBaseUrlMigration = null;
|
|
441
|
+
// One-time repair for installs initialized before the 2026-04-19 InsForge
|
|
442
|
+
// project migration (0.5.67): init preserved any persisted config.baseUrl,
|
|
443
|
+
// so those installs kept uploading to the retired b46ug8xu project until
|
|
444
|
+
// its backend went dark on 2026-07-27 — every upload since 503s while the
|
|
445
|
+
// dashboard shows no error (silent cloud outage). Reset the upload offset
|
|
446
|
+
// so the whole local queue replays into the current project, clear the 503
|
|
447
|
+
// backoff, then remove the retired URL as the final commit step. Keeping the
|
|
448
|
+
// URL until every prerequisite succeeds makes an interrupted migration
|
|
449
|
+
// retryable on the next sync. Device tokens are opaque random values looked
|
|
450
|
+
// up by SHA-256 hash in tokentracker_device_tokens (not JWT_SECRET-signed);
|
|
451
|
+
// the database was restored into the current project, so preserve the old
|
|
452
|
+
// token unless the local API has already minted a replacement from the
|
|
453
|
+
// user's current login session and passed it through the environment.
|
|
454
|
+
// Ingest is a whole-row upsert per bucket key, making replay idempotent.
|
|
455
|
+
if (config && isLegacyInsforgeBaseUrl(config.baseUrl)) {
|
|
456
|
+
const priorQueueState = (await readJson(queueStatePath)) || {};
|
|
457
|
+
// Prepare the replay exactly once. The note survives successful batches,
|
|
458
|
+
// so retryable failures retain both their committed offset and backoff
|
|
459
|
+
// instead of restarting history and hammering the backend on every hook.
|
|
460
|
+
if (priorQueueState.note !== LEGACY_BASE_URL_MIGRATION_NOTE) {
|
|
461
|
+
await writeJson(queueStatePath, {
|
|
462
|
+
...priorQueueState,
|
|
463
|
+
offset: 0,
|
|
464
|
+
updatedAt: new Date().toISOString(),
|
|
465
|
+
note: LEGACY_BASE_URL_MIGRATION_NOTE,
|
|
466
|
+
});
|
|
467
|
+
try {
|
|
468
|
+
await fs.unlink(uploadThrottlePath);
|
|
469
|
+
} catch (error) {
|
|
470
|
+
if (error?.code !== "ENOENT") throw error;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const replacementDeviceToken =
|
|
474
|
+
typeof process.env.TOKENTRACKER_DEVICE_TOKEN === "string"
|
|
475
|
+
? process.env.TOKENTRACKER_DEVICE_TOKEN.trim()
|
|
476
|
+
: "";
|
|
477
|
+
const previousDeviceToken =
|
|
478
|
+
typeof config.deviceToken === "string" ? config.deviceToken.trim() : "";
|
|
479
|
+
// Do not commit either the replacement token or URL removal yet. The
|
|
480
|
+
// current backend must accept a token first; otherwise a stale environment
|
|
481
|
+
// token could destroy a valid restored credential and make recovery worse.
|
|
482
|
+
legacyBaseUrlMigration = {
|
|
483
|
+
previousDeviceToken,
|
|
484
|
+
replacementDeviceToken,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
433
487
|
const codexCursorRoots = [process.env.CODEX_HOME || path.join(home, ".codex")];
|
|
434
488
|
const cursorStore = await openCursorStore({
|
|
435
489
|
trackerDir,
|
|
@@ -2386,24 +2440,28 @@ async function cmdSync(argv, context = {}) {
|
|
|
2386
2440
|
|
|
2387
2441
|
progress?.stop();
|
|
2388
2442
|
|
|
2389
|
-
const
|
|
2443
|
+
const runtimeConfig = config ? { ...config } : {};
|
|
2444
|
+
if (legacyBaseUrlMigration?.replacementDeviceToken) {
|
|
2445
|
+
runtimeConfig.deviceToken = legacyBaseUrlMigration.replacementDeviceToken;
|
|
2446
|
+
}
|
|
2447
|
+
const runtime = resolveRuntimeConfig({ config: runtimeConfig, env: process.env });
|
|
2390
2448
|
|
|
2391
2449
|
let uploadResult = { inserted: 0, skipped: 0 };
|
|
2392
2450
|
let uploadAttempted = false;
|
|
2393
|
-
let
|
|
2451
|
+
let autoUploadDecision = null;
|
|
2394
2452
|
|
|
2395
|
-
if (opts.publishAccount) {
|
|
2453
|
+
if (opts.publishAccount || (legacyBaseUrlMigration && opts.auto)) {
|
|
2396
2454
|
const uploadStateBefore = (await readJson(queueStatePath)) || { offset: 0 };
|
|
2397
2455
|
const queueSizeBefore = await safeStatSize(queuePath);
|
|
2398
2456
|
const pendingBytesBefore = Math.max(
|
|
2399
2457
|
0,
|
|
2400
2458
|
queueSizeBefore - Number(uploadStateBefore.offset || 0),
|
|
2401
2459
|
);
|
|
2402
|
-
//
|
|
2403
|
-
//
|
|
2404
|
-
//
|
|
2405
|
-
//
|
|
2406
|
-
|
|
2460
|
+
// Native publication and every auto-triggered legacy migration share the
|
|
2461
|
+
// failure-backoff gate. Intentionally ignore the 30-minute success
|
|
2462
|
+
// throttle: native refresh owns its own cadence, while a pending migration
|
|
2463
|
+
// should complete as soon as a credential becomes usable.
|
|
2464
|
+
autoUploadDecision = decideAutoUpload({
|
|
2407
2465
|
nowMs: Date.now(),
|
|
2408
2466
|
pendingBytes: pendingBytesBefore,
|
|
2409
2467
|
state: {
|
|
@@ -2420,7 +2478,7 @@ async function cmdSync(argv, context = {}) {
|
|
|
2420
2478
|
|
|
2421
2479
|
if (runtime.deviceToken && runtime.baseUrl &&
|
|
2422
2480
|
(!isBackgroundLightweightSync || opts.publishAccount) &&
|
|
2423
|
-
(!
|
|
2481
|
+
(!autoUploadDecision || autoUploadDecision.allowed)) {
|
|
2424
2482
|
uploadAttempted = true;
|
|
2425
2483
|
// Mirror the machine identity into the purge-surviving seed file so a
|
|
2426
2484
|
// future `uninstall --purge` + reinstall recovers the same cloud device
|
|
@@ -2432,14 +2490,49 @@ async function cmdSync(argv, context = {}) {
|
|
|
2432
2490
|
// best effort — upload below must not be blocked by identity mirroring
|
|
2433
2491
|
}
|
|
2434
2492
|
try {
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2493
|
+
let successfulDeviceToken = runtime.deviceToken;
|
|
2494
|
+
const drainWithToken = (deviceToken) =>
|
|
2495
|
+
drainQueueToCloud({
|
|
2496
|
+
baseUrl: runtime.baseUrl,
|
|
2497
|
+
deviceToken,
|
|
2498
|
+
queuePath,
|
|
2499
|
+
queueStatePath,
|
|
2500
|
+
maxBatches: opts.drain ? 100 : (autoUploadDecision?.maxBatches || 5),
|
|
2501
|
+
batchSize: autoUploadDecision?.batchSize || 200,
|
|
2502
|
+
});
|
|
2503
|
+
try {
|
|
2504
|
+
uploadResult = await drainWithToken(successfulDeviceToken);
|
|
2505
|
+
} catch (error) {
|
|
2506
|
+
const fallbackDeviceToken = legacyBaseUrlMigration?.previousDeviceToken;
|
|
2507
|
+
const replacementDeviceToken = legacyBaseUrlMigration?.replacementDeviceToken;
|
|
2508
|
+
const stateAfterFailure = (await readJson(queueStatePath)) || { offset: 0 };
|
|
2509
|
+
const canFallbackWithoutSplittingHistory =
|
|
2510
|
+
(error?.status === 401 || error?.status === 403) &&
|
|
2511
|
+
replacementDeviceToken &&
|
|
2512
|
+
fallbackDeviceToken &&
|
|
2513
|
+
fallbackDeviceToken !== replacementDeviceToken &&
|
|
2514
|
+
Number(stateAfterFailure.offset || 0) === 0;
|
|
2515
|
+
if (!canFallbackWithoutSplittingHistory) throw error;
|
|
2516
|
+
successfulDeviceToken = fallbackDeviceToken;
|
|
2517
|
+
uploadResult = await drainWithToken(successfulDeviceToken);
|
|
2518
|
+
}
|
|
2519
|
+
// A successful ingest response proves which credential belongs to the
|
|
2520
|
+
// current backend. Only now commit the token and remove the retry marker.
|
|
2521
|
+
// Empty queues make no request, so keep the marker until future data can
|
|
2522
|
+
// validate a credential instead of persisting an unverified token.
|
|
2523
|
+
if (legacyBaseUrlMigration && uploadResult.batches > 0) {
|
|
2524
|
+
// device-login does not share the sync lock and may have written a
|
|
2525
|
+
// fresh current-backend config while the scan/upload was running.
|
|
2526
|
+
// Re-read before committing, merge only while the legacy marker still
|
|
2527
|
+
// exists, and never clobber a concurrently completed login.
|
|
2528
|
+
const latestConfig = (await readJson(configPath)) || config;
|
|
2529
|
+
if (isLegacyInsforgeBaseUrl(latestConfig.baseUrl)) {
|
|
2530
|
+
latestConfig.deviceToken = successfulDeviceToken;
|
|
2531
|
+
delete latestConfig.baseUrl;
|
|
2532
|
+
await writeJson(configPath, latestConfig);
|
|
2533
|
+
await chmod600IfPossible(configPath);
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2443
2536
|
// Record success so the exponential backoff step resets — otherwise
|
|
2444
2537
|
// a single past failure keeps us pessimistically throttled forever.
|
|
2445
2538
|
uploadThrottleState = recordUploadSuccess({
|
|
@@ -3005,6 +3098,7 @@ async function drainQueueToCloud({ baseUrl, deviceToken, queuePath, queueStatePa
|
|
|
3005
3098
|
let offset = Number(state.offset || 0);
|
|
3006
3099
|
let inserted = 0;
|
|
3007
3100
|
let skipped = 0;
|
|
3101
|
+
let batches = 0;
|
|
3008
3102
|
|
|
3009
3103
|
const queueSize = await safeStatSize(queuePath);
|
|
3010
3104
|
const limit = Math.min(Math.max(1, Math.floor(Number(batchSize || 200))), MAX_INGEST_BUCKETS);
|
|
@@ -3042,6 +3136,7 @@ async function drainQueueToCloud({ baseUrl, deviceToken, queuePath, queueStatePa
|
|
|
3042
3136
|
|
|
3043
3137
|
inserted += Number(data?.inserted || 0);
|
|
3044
3138
|
skipped += Number(data?.skipped || 0);
|
|
3139
|
+
batches += 1;
|
|
3045
3140
|
|
|
3046
3141
|
offset = result.nextOffset;
|
|
3047
3142
|
state.offset = offset;
|
|
@@ -3049,7 +3144,7 @@ async function drainQueueToCloud({ baseUrl, deviceToken, queuePath, queueStatePa
|
|
|
3049
3144
|
await writeJson(queueStatePath, state);
|
|
3050
3145
|
}
|
|
3051
3146
|
|
|
3052
|
-
return { inserted, skipped };
|
|
3147
|
+
return { inserted, skipped, batches };
|
|
3053
3148
|
}
|
|
3054
3149
|
|
|
3055
3150
|
async function readQueueBatch(queuePath, startOffset, maxBuckets) {
|
|
@@ -27,9 +27,16 @@ const {
|
|
|
27
27
|
getExecutableName,
|
|
28
28
|
} = require("./categorizer-utils");
|
|
29
29
|
|
|
30
|
-
const CACHE_SCHEMA_VERSION = "grok-context-
|
|
30
|
+
const CACHE_SCHEMA_VERSION = "grok-context-v2";
|
|
31
31
|
const CACHE = new Map();
|
|
32
|
-
|
|
32
|
+
// Result-level TTL. Per-file parse cache is the main win when flipping tabs.
|
|
33
|
+
const CACHE_TTL_MS = 5 * 60_000;
|
|
34
|
+
// Deduplicate concurrent scans when the dashboard remounts the panel quickly.
|
|
35
|
+
const IN_FLIGHT = new Map();
|
|
36
|
+
// Per updates.jsonl parse cache — mirrors Codex PARSED_GROUP_CACHE so switching
|
|
37
|
+
// back to Grok does not re-read every session log on each open.
|
|
38
|
+
const FILE_PARSE_CACHE = new Map();
|
|
39
|
+
const MAX_FILE_PARSE_CACHE_ENTRIES = 512;
|
|
33
40
|
|
|
34
41
|
// ---------------------------------------------------------------------------
|
|
35
42
|
// Time helpers
|
|
@@ -316,7 +323,45 @@ function finalizeExecRows(map) {
|
|
|
316
323
|
.sort((a, b) => (b.totals?.total_tokens || 0) - (a.totals?.total_tokens || 0));
|
|
317
324
|
}
|
|
318
325
|
|
|
326
|
+
function fileParseCacheKey(updatesPath, { from, to, timeZoneContext } = {}, stat) {
|
|
327
|
+
return [
|
|
328
|
+
CACHE_SCHEMA_VERSION,
|
|
329
|
+
updatesPath,
|
|
330
|
+
from || "",
|
|
331
|
+
to || "",
|
|
332
|
+
timeZoneContext?.timeZone || "",
|
|
333
|
+
timeZoneContext?.offsetMinutes ?? "",
|
|
334
|
+
Number(stat?.size || 0),
|
|
335
|
+
Math.floor(Number(stat?.mtimeMs || 0)),
|
|
336
|
+
].join("|");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function rememberFileParse(key, value) {
|
|
340
|
+
FILE_PARSE_CACHE.delete(key);
|
|
341
|
+
FILE_PARSE_CACHE.set(key, value);
|
|
342
|
+
while (FILE_PARSE_CACHE.size > MAX_FILE_PARSE_CACHE_ENTRIES) {
|
|
343
|
+
const oldest = FILE_PARSE_CACHE.keys().next().value;
|
|
344
|
+
if (oldest == null) break;
|
|
345
|
+
FILE_PARSE_CACHE.delete(oldest);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
319
349
|
async function parseGrokUpdatesFile(updatesPath, { from, to, timeZoneContext } = {}) {
|
|
350
|
+
let stat = null;
|
|
351
|
+
try {
|
|
352
|
+
stat = fs.statSync(updatesPath);
|
|
353
|
+
} catch {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
if (!stat.isFile() || stat.size <= 0) return null;
|
|
357
|
+
|
|
358
|
+
const cacheKey = fileParseCacheKey(updatesPath, { from, to, timeZoneContext }, stat);
|
|
359
|
+
const hit = FILE_PARSE_CACHE.get(cacheKey);
|
|
360
|
+
if (hit) {
|
|
361
|
+
rememberFileParse(cacheKey, hit);
|
|
362
|
+
return hit;
|
|
363
|
+
}
|
|
364
|
+
|
|
320
365
|
const byTool = new Map();
|
|
321
366
|
const byExecKind = new Map();
|
|
322
367
|
const byExecExit = new Map();
|
|
@@ -512,7 +557,7 @@ async function parseGrokUpdatesFile(updatesPath, { from, to, timeZoneContext } =
|
|
|
512
557
|
input.destroy?.();
|
|
513
558
|
}
|
|
514
559
|
|
|
515
|
-
|
|
560
|
+
const parsedResult = {
|
|
516
561
|
totals: roundTotals(totals),
|
|
517
562
|
turnCount,
|
|
518
563
|
toolBreakdown: { tool_rows: finalizeToolRows(byTool) },
|
|
@@ -525,6 +570,8 @@ async function parseGrokUpdatesFile(updatesPath, { from, to, timeZoneContext } =
|
|
|
525
570
|
by_exit: finalizeExecRows(byExecExit),
|
|
526
571
|
},
|
|
527
572
|
};
|
|
573
|
+
rememberFileParse(cacheKey, parsedResult);
|
|
574
|
+
return parsedResult;
|
|
528
575
|
}
|
|
529
576
|
|
|
530
577
|
function emptySessionResult() {
|
|
@@ -580,38 +627,96 @@ function mergeExecRows(map, rows) {
|
|
|
580
627
|
}
|
|
581
628
|
}
|
|
582
629
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
to = null,
|
|
586
|
-
top = 50,
|
|
587
|
-
timeZoneContext = null,
|
|
588
|
-
env = process.env,
|
|
589
|
-
} = {}) {
|
|
590
|
-
const limitedTop = Number.isFinite(top) && top > 0 ? Math.min(Math.floor(top), 200) : 50;
|
|
591
|
-
const sessions = discoverGrokSessionFiles(env);
|
|
592
|
-
const inventorySig = crypto
|
|
630
|
+
function buildGrokInventorySignature(sessions) {
|
|
631
|
+
return crypto
|
|
593
632
|
.createHash("sha256")
|
|
594
633
|
.update(
|
|
595
|
-
sessions
|
|
634
|
+
(sessions || [])
|
|
596
635
|
.map((s) => `${s.updatesPath}:${s.size}:${Math.floor(s.mtimeMs)}`)
|
|
597
636
|
.join("|"),
|
|
598
637
|
)
|
|
599
638
|
.digest("hex");
|
|
600
|
-
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function buildGrokResultCacheKey({ from, to, top, timeZoneContext, inventorySig }) {
|
|
642
|
+
// Include inventorySig so a changed session file cannot serve a stale aggregate.
|
|
643
|
+
// Unchanged files still skip re-parse via FILE_PARSE_CACHE (Codex-style).
|
|
644
|
+
return [
|
|
601
645
|
CACHE_SCHEMA_VERSION,
|
|
602
646
|
from || "",
|
|
603
647
|
to || "",
|
|
604
|
-
|
|
648
|
+
top,
|
|
605
649
|
timeZoneContext?.timeZone || "",
|
|
606
650
|
timeZoneContext?.offsetMinutes ?? "",
|
|
607
|
-
inventorySig,
|
|
651
|
+
inventorySig || "",
|
|
608
652
|
].join("|");
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
async function computeGrokContextBreakdown(options = {}) {
|
|
656
|
+
const {
|
|
657
|
+
from = null,
|
|
658
|
+
to = null,
|
|
659
|
+
top = 50,
|
|
660
|
+
timeZoneContext = null,
|
|
661
|
+
env = process.env,
|
|
662
|
+
} = options;
|
|
663
|
+
const limitedTop = Number.isFinite(top) && top > 0 ? Math.min(Math.floor(top), 200) : 50;
|
|
664
|
+
const sessions = discoverGrokSessionFiles(env);
|
|
665
|
+
const inventorySig = buildGrokInventorySignature(sessions);
|
|
666
|
+
const cacheKey = buildGrokResultCacheKey({
|
|
667
|
+
from,
|
|
668
|
+
to,
|
|
669
|
+
top: limitedTop,
|
|
670
|
+
timeZoneContext,
|
|
671
|
+
inventorySig,
|
|
672
|
+
});
|
|
609
673
|
|
|
610
674
|
const cached = CACHE.get(cacheKey);
|
|
611
675
|
if (cached && Date.now() - cached.at < CACHE_TTL_MS) {
|
|
612
676
|
return cached.value;
|
|
613
677
|
}
|
|
614
678
|
|
|
679
|
+
const existing = IN_FLIGHT.get(cacheKey);
|
|
680
|
+
if (existing) return existing;
|
|
681
|
+
|
|
682
|
+
const run = computeGrokContextBreakdownUncached({
|
|
683
|
+
from,
|
|
684
|
+
to,
|
|
685
|
+
top: limitedTop,
|
|
686
|
+
timeZoneContext,
|
|
687
|
+
env,
|
|
688
|
+
sessions,
|
|
689
|
+
cacheKey,
|
|
690
|
+
});
|
|
691
|
+
IN_FLIGHT.set(cacheKey, run);
|
|
692
|
+
try {
|
|
693
|
+
return await run;
|
|
694
|
+
} finally {
|
|
695
|
+
if (IN_FLIGHT.get(cacheKey) === run) IN_FLIGHT.delete(cacheKey);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
async function computeGrokContextBreakdownUncached({
|
|
700
|
+
from = null,
|
|
701
|
+
to = null,
|
|
702
|
+
top = 50,
|
|
703
|
+
timeZoneContext = null,
|
|
704
|
+
env = process.env,
|
|
705
|
+
sessions = null,
|
|
706
|
+
cacheKey = null,
|
|
707
|
+
} = {}) {
|
|
708
|
+
const limitedTop = Number.isFinite(top) && top > 0 ? Math.min(Math.floor(top), 200) : 50;
|
|
709
|
+
const sessionList = sessions || discoverGrokSessionFiles(env);
|
|
710
|
+
const resultCacheKey =
|
|
711
|
+
cacheKey ||
|
|
712
|
+
buildGrokResultCacheKey({
|
|
713
|
+
from,
|
|
714
|
+
to,
|
|
715
|
+
top: limitedTop,
|
|
716
|
+
timeZoneContext,
|
|
717
|
+
inventorySig: buildGrokInventorySignature(sessionList),
|
|
718
|
+
});
|
|
719
|
+
|
|
615
720
|
const grand = emptyTotals();
|
|
616
721
|
const byTool = new Map();
|
|
617
722
|
const byExecKind = new Map();
|
|
@@ -623,7 +728,7 @@ async function computeGrokContextBreakdown({
|
|
|
623
728
|
let messageCount = 0;
|
|
624
729
|
let sessionCount = 0;
|
|
625
730
|
|
|
626
|
-
for (const sess of
|
|
731
|
+
for (const sess of sessionList) {
|
|
627
732
|
const parsed = await parseGrokUpdatesFile(sess.updatesPath, {
|
|
628
733
|
from,
|
|
629
734
|
to,
|
|
@@ -776,7 +881,7 @@ async function computeGrokContextBreakdown({
|
|
|
776
881
|
},
|
|
777
882
|
};
|
|
778
883
|
|
|
779
|
-
CACHE.set(
|
|
884
|
+
CACHE.set(resultCacheKey, { at: Date.now(), value: result });
|
|
780
885
|
while (CACHE.size > 32) {
|
|
781
886
|
const oldest = [...CACHE.entries()].sort((a, b) => a[1].at - b[1].at)[0];
|
|
782
887
|
if (oldest) CACHE.delete(oldest[0]);
|