tokentracker-cli 1.5.1 → 1.5.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-mB7HZSGE.js → AchievementBadge-uuDGO9Mq.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-DLzeq_eo.js → AchievementsPage-BCTVT7Jo.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-DrXQyDns.js → AchievementsSection-B6fJSJSr.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-WrgKRybi.js → BadgeDetailModal-FjUN__9G.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-DFRaixVK.js → BotAnimated-B-KqY_Wr.js} +1 -1
- package/dashboard/dist/assets/{Card-bFq2tlO8.js → Card-DS8T8piP.js} +1 -1
- package/dashboard/dist/assets/{CloudCheckoutPage-gwhGxavy.js → CloudCheckoutPage-_0UjsFPu.js} +1 -1
- package/dashboard/dist/assets/{CloudPage-BD4iBYeZ.js → CloudPage-BstgVBaV.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-C_KlPl0U.js → CommandPalette-DtVsYZgc.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-Cl1Bgv8M.js → CommunityStatsModal-DTpEJqV3.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-NWoYevYC.js → ConfirmModal-Lfdh2vr7.js} +1 -1
- package/dashboard/dist/assets/{Controls-D2-D2cfA.js → Controls-DpwQ7pkq.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-DUFWejkE.js +1 -0
- package/dashboard/dist/assets/{DateRangePopover-8EMVXuH6.js → DateRangePopover-CrWgrzT2.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-DbP8d6bM.js → DevicePage-DbK5KWJS.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-CEstgLa1.js → DialogClose-Dd2B_o03.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-CLy0arbt.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-DP5TLLbz.js → DialogTitle-3JQBVha3.js} +1 -1
- package/dashboard/dist/assets/{ElectricLogo-Uvi3yMum.js → ElectricLogo-LYAqG8pG.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-Dpzihg6f.js → FadeIn-DhDvUor7.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-DllqemKr.js → HeaderGithubStar-9f1XmgYC.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-Dc-TAH6l.js → HoverTooltip-BXhHAkP7.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-jd1D5ZKX.js → IpCheckPage-ChPdDeMl.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-CB1ZVw-a.js → LandingPage-DnJ7Y9zw.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardPage-Bt2cRRmE.js → LeaderboardPage-BF9isLzu.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProBadge-BVlyVOFf.js → LeaderboardProBadge-BM4JKgkC.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-CwQFRJg_.js → LeaderboardProfileModal-DfZABOpN.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-CeqcTbUG.js → LeaderboardProfilePage-BaZ38iT5.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-C7LbG3LX.js +6 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-B2_HoCVD.js → LocalOnlyNotice-DvhlYoqQ.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-nw9JOc2d.js → LoginCard-g_Gbnzhs.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-BHNLTK1D.js → LoginModal-DkJrI6b7.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-LI2GZKhM.js → LoginPage-CqCKSo0n.js} +1 -1
- package/dashboard/dist/assets/{PetPage-CK6sPeI0.js → PetPage-DbF7ROsj.js} +2 -2
- package/dashboard/dist/assets/{PopoverPopup-BsXchEVa.js → PopoverPopup-4WuH41ZT.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-vgCEy0dh.js → ProviderIcon-6j0A2KD9.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-B4CPj2vX.js → ResetPasswordPage-DBd-iwuM.js} +1 -1
- package/dashboard/dist/assets/{Select-BkAy6P2r.js → Select-BBEpLeiG.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-C0RNqDrD.js → SelectItemText-Cn_U_rrO.js} +1 -1
- package/dashboard/dist/assets/{SelfHostPage-B-_w-Z-a.js → SelfHostPage-2WZc6Rml.js} +1 -1
- package/dashboard/dist/assets/ServiceStatusPage-B-g4MDl0.js +1 -0
- package/dashboard/dist/assets/{SessionsPage-CmWXBqD2.js → SessionsPage-CWS0iQ1L.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-CA70zP-t.js → SettingsPage-rXNr96i-.js} +3 -3
- package/dashboard/dist/assets/{SkillsPage-CNamhfzU.js → SkillsPage-CudTvbZL.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-CXrVBSk6.js → TokenGalaxy-BRKrbTsJ.js} +1 -1
- package/dashboard/dist/assets/{TooltipPopup-CR4MqElX.js → TooltipPopup-CsLKXhWC.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-DQfcrEve.js → TrendMonitor-8onUTbkk.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-BBXxtZbP.js → WidgetsPage-CcwE0lz8.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-C8i2KlMF.js → WrappedPage-Cj9LLYqP.js} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-07djSGmy.js +45 -0
- package/dashboard/dist/assets/__vite-browser-external-BDldtsD2.css +1 -0
- package/dashboard/dist/assets/{agent-logos-CxQb8vir.js → agent-logos-ChBA1nAH.js} +1 -1
- package/dashboard/dist/assets/{arrow-left-D6zkcLVt.js → arrow-left-Bb5mfoPc.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-BGb-XVn_.js → arrow-right-D1Ti2XUI.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-XTStagDJ.js → arrow-up-right-B_CjA_y8.js} +1 -1
- package/dashboard/dist/assets/{calendar-days-Cz3NGZiS.js → calendar-days-DdfrAqie.js} +1 -1
- package/dashboard/dist/assets/{check-1z_vmHPD.js → check-29zGw1MK.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-CkSBY3bE.js → chevron-down-DAzlFX7g.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-pD5C4r_S.js → cloud-checkout-C2SeCjFM.js} +1 -1
- package/dashboard/dist/assets/{composite-BtDzx3a2.js → composite-DIf735Hq.js} +1 -1
- package/dashboard/dist/assets/{copy-Ctv_wxuD.js → copy-BOuOM0HQ.js} +1 -1
- package/dashboard/dist/assets/{download-DKbKMGSR.js → download-50tQRpGe.js} +1 -1
- package/dashboard/dist/assets/{external-link-g9cU017t.js → external-link-BhlQClVe.js} +1 -1
- package/dashboard/dist/assets/{flame-DZq3kXUt.js → flame-CeGfA-0L.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-CXQfx0PC.js → format-tokens-Cq7YiAkJ.js} +1 -1
- package/dashboard/dist/assets/{gift-CKlKjGP6.js → gift-Do-UFhiT.js} +1 -1
- package/dashboard/dist/assets/{icons-xPGMnBjl.js → icons-C6EZ_HmP.js} +1 -1
- package/dashboard/dist/assets/{index-fVbP5wOv.js → index-BFrbVqsm.js} +1 -1
- package/dashboard/dist/assets/{index-CDBOKcDh.js → index-BWhbBAd9.js} +1 -1
- package/dashboard/dist/assets/{index-Dl0oFLNr.js → index-D9kwYWLX.js} +1 -1
- package/dashboard/dist/assets/{index-BwOjMtB2.js → index-c0crpe9Z.js} +1 -1
- package/dashboard/dist/assets/{info-D9xNIuGh.js → info-B4MXmsNE.js} +1 -1
- package/dashboard/dist/assets/{link-2-Bk5wYt2n.js → link-2-DuWCslzl.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-CcN3pHWb.js → loader-circle-C2I3NZAO.js} +1 -1
- package/dashboard/dist/assets/{local-usage-transfer-CYcy1ici.js → local-usage-transfer-BQasvsUF.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-DbpEzkJe.js → modifiers.esm-DbeNS6FT.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-WkWv1qIS.js → monitor-smartphone-C6Y4ktsc.js} +1 -1
- package/dashboard/dist/assets/{play-2yZGWMes.js → play-CwgcDJC2.js} +1 -1
- package/dashboard/dist/assets/{plus-mOF8tG1F.js → plus-B796wsUO.js} +1 -1
- package/dashboard/dist/assets/{provider-display-7gaOvgMp.js → provider-display-Cw26RFDm.js} +1 -1
- package/dashboard/dist/assets/{react-CdPiPDhk.js → react-d7Bk1-_K.js} +1 -1
- package/dashboard/dist/assets/{safePolygon-C7FJLAlh.js → safePolygon-CDE0dNv_.js} +1 -1
- package/dashboard/dist/assets/{search-C63nCrjo.js → search-DtKvsdRs.js} +1 -1
- package/dashboard/dist/assets/{server-BaG1rnuv.js → server-gAURHUyz.js} +1 -1
- package/dashboard/dist/assets/{shield-check-D_4-Cm6L.js → shield-check-DFVdDTwh.js} +1 -1
- package/dashboard/dist/assets/{skills-api-DfbzwpDv.js → skills-api-2rdPUrJI.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-D-v8FRX5.js → sortable.esm-DFlfwnJr.js} +1 -1
- package/dashboard/dist/assets/{store-Dv_FYrXK.js → store-BKPCk_de.js} +1 -1
- package/dashboard/dist/assets/{terminal-FQi6MaYt.js → terminal-BtWFakvS.js} +1 -1
- package/dashboard/dist/assets/{trash-TbK96dEz.js → trash-bpr9zTAM.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-DGoWVZuk.js → use-community-stats-DMnGgLne.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-Dg6uSaYt.js → use-latest-request-guard-BSBlJJHe.js} +1 -1
- package/dashboard/dist/assets/use-limits-display-prefs-eYlvnUKn.js +1 -0
- package/dashboard/dist/assets/{use-native-settings-DIk0AF3b.js → use-native-settings-OthqiF6g.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-CUqV3Zgl.js → use-ordered-list-DpAtr0R-.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-DF0grwHs.js +1 -0
- package/dashboard/dist/assets/{use-transform-D_-w22c8.js → use-transform-BRz9dz_2.js} +1 -1
- package/dashboard/dist/assets/use-usage-limits-CwmhudAX.js +1 -0
- package/dashboard/dist/assets/{useCurrency-B_AuLCsL.js → useCurrency-CAA-tQ27.js} +1 -1
- package/dashboard/dist/assets/{usePositioner-DfxvIif8.js → usePositioner-Cj2DPmwM.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-bbO13V4s.js → useTokenFormat-Bf5-f7Hx.js} +1 -1
- package/dashboard/dist/assets/{zap-k07-EXTK.js → zap-B8Fg6PaC.js} +1 -1
- package/dashboard/dist/index.html +6 -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/serve.js +22 -13
- package/src/lib/codex-rollout-parser.js +43 -6
- package/src/lib/codex-usage-record.js +21 -0
- package/src/lib/deepseek-status.js +157 -0
- package/src/lib/local-api.js +44 -4
- package/src/lib/pricing/curated-overrides.json +94 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/provider-status.js +15 -2
- package/src/lib/qoder-limits.js +95 -23
- package/src/lib/rollout.js +68 -14
- package/src/lib/session-analytics.js +10 -3
- package/src/lib/usage-limits.js +187 -43
- package/src/lib/workbuddy-limits.js +292 -27
- package/src/lib/zcode-limits.js +457 -42
- package/dashboard/dist/assets/DashboardPage-vITMnPXx.js +0 -1
- package/dashboard/dist/assets/DialogDescription-0wcygxtH.js +0 -1
- package/dashboard/dist/assets/LimitsPage-Bp4ZORmR.js +0 -6
- package/dashboard/dist/assets/ServiceStatusPage-5YevsGGD.js +0 -1
- package/dashboard/dist/assets/__vite-browser-external-BEEzY7mq.js +0 -45
- package/dashboard/dist/assets/__vite-browser-external-Br-KAXh6.css +0 -1
- package/dashboard/dist/assets/use-limits-display-prefs-Djbdp7fi.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-DMCok-vZ.js +0 -1
- package/dashboard/dist/assets/use-usage-limits-h7aGsIQr.js +0 -1
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
// Statuspage.io contract (`/api/v2/status.json`):
|
|
10
10
|
// { "page": { "updated_at": ISO }, "status": { "indicator": "none|minor|major|critical", "description": "..." } }
|
|
11
11
|
|
|
12
|
+
const { parseDeepSeekStatusPage } = require("./deepseek-status");
|
|
13
|
+
|
|
12
14
|
const STATUS_PROBE_TIMEOUT_MS = 6_000;
|
|
13
15
|
// Status pages change on incident timescales; 5 minutes keeps the limits
|
|
14
16
|
// poll (2-min TTL upstream) from hammering statuspage.io every cycle.
|
|
@@ -23,6 +25,13 @@ const STATUS_PAGE_SPECS = {
|
|
|
23
25
|
apiUrl: "https://status.claude.com/api/v2/status.json",
|
|
24
26
|
pageUrl: "https://status.claude.com",
|
|
25
27
|
},
|
|
28
|
+
deepseek: {
|
|
29
|
+
// Flashcat-hosted Next.js page: no public JSON API and no CORS. Relayed
|
|
30
|
+
// server-side here; `format` switches the fetch to HTML + RSC parsing.
|
|
31
|
+
apiUrl: "https://status.deepseek.com/",
|
|
32
|
+
pageUrl: "https://status.deepseek.com",
|
|
33
|
+
format: "deepseek-rsc",
|
|
34
|
+
},
|
|
26
35
|
};
|
|
27
36
|
|
|
28
37
|
const VALID_INDICATORS = new Set(["none", "minor", "major", "critical"]);
|
|
@@ -68,13 +77,17 @@ async function fetchProviderServiceStatus(providerId, { fetchImpl = fetch, nowMs
|
|
|
68
77
|
const controller = new AbortController();
|
|
69
78
|
const timer = setTimeout(() => controller.abort(), STATUS_PROBE_TIMEOUT_MS);
|
|
70
79
|
try {
|
|
80
|
+
const isDeepSeek = spec.format === "deepseek-rsc";
|
|
71
81
|
const res = await fetchImpl(spec.apiUrl, {
|
|
72
82
|
method: "GET",
|
|
73
|
-
headers: { Accept: "application/json" },
|
|
83
|
+
headers: { Accept: isDeepSeek ? "text/html" : "application/json" },
|
|
74
84
|
signal: controller.signal,
|
|
75
85
|
});
|
|
76
86
|
if (res.ok) {
|
|
77
|
-
|
|
87
|
+
const payload = isDeepSeek
|
|
88
|
+
? parseDeepSeekStatusPage(await res.text())
|
|
89
|
+
: await res.json();
|
|
90
|
+
data = normalizeStatusPayload(payload, spec, nowMs);
|
|
78
91
|
}
|
|
79
92
|
} finally {
|
|
80
93
|
clearTimeout(timer);
|
package/src/lib/qoder-limits.js
CHANGED
|
@@ -40,6 +40,9 @@ const QODER_ACTIVITY_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
|
40
40
|
const QODER_LIMITS_CACHE_FILE = "qoder-usage-limits-cache.json";
|
|
41
41
|
const QODER_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
42
42
|
const QODER_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS = 12 * 60 * 60 * 1000;
|
|
43
|
+
// Align with usage-limits provenance aging (10 min): a cache hit fresher than
|
|
44
|
+
// this inherits its written source and is not flagged stale (#955 self-lock).
|
|
45
|
+
const QODER_LIMITS_CACHE_FRESH_TTL_MS = 10 * 60 * 1000;
|
|
43
46
|
// Qoder Desktop 0.4+ writes its account usage summary to main.log, which can
|
|
44
47
|
// accumulate a whole session before the quota is read. Scan a bounded window
|
|
45
48
|
// backwards instead of assuming the summary is near EOF.
|
|
@@ -105,7 +108,10 @@ function normalizeQuotaSummary(summary) {
|
|
|
105
108
|
if (total === 0 && (used !== 0 || remaining !== 0)) {
|
|
106
109
|
throw new Error("Qoder zero total quota must have zero usage and remaining.");
|
|
107
110
|
}
|
|
108
|
-
|
|
111
|
+
// An empty 0/0 quota (no active plan) renders as 0%, matching Qoder's own
|
|
112
|
+
// Credits UI and the desktop/CLI paths; a server-provided percentage still
|
|
113
|
+
// wins when present.
|
|
114
|
+
const usagePercentage = providedPercentage ?? (total > 0 ? (used / total) * 100 : 0);
|
|
109
115
|
return {
|
|
110
116
|
used,
|
|
111
117
|
total,
|
|
@@ -131,7 +137,7 @@ function normalizeQoderUsageResponse(response) {
|
|
|
131
137
|
const usedPercent = shared
|
|
132
138
|
? total > 0
|
|
133
139
|
? (used / total) * 100
|
|
134
|
-
:
|
|
140
|
+
: 0
|
|
135
141
|
: base.usagePercentage;
|
|
136
142
|
|
|
137
143
|
return {
|
|
@@ -279,16 +285,17 @@ function normalizeQoderRpcUsage(response) {
|
|
|
279
285
|
const used = quota ? finiteNumber(quota.used, "userQuota.used") : 0;
|
|
280
286
|
const total = quota ? finiteNumber(quota.total, "userQuota.total") : 0;
|
|
281
287
|
const remaining = quota ? finiteNumber(quota.remaining, "userQuota.remaining") : 0;
|
|
282
|
-
|
|
288
|
+
// totalUsagePercentage aggregates the plan and add-on buckets and reaches
|
|
289
|
+
// this function on mixed scales (the OpenAPI returns fractions, the desktop
|
|
290
|
+
// IPC returns percents), so the primary window always derives its percent
|
|
291
|
+
// from its own quota numbers.
|
|
283
292
|
const usedPercent = total === 0
|
|
284
293
|
? 0
|
|
285
294
|
: addOnWindow
|
|
286
295
|
? (used / total) * 100
|
|
287
296
|
: response.isQuotaExceeded === true
|
|
288
297
|
? 100
|
|
289
|
-
:
|
|
290
|
-
? reportedPercent
|
|
291
|
-
: (used / total) * 100;
|
|
298
|
+
: (used / total) * 100;
|
|
292
299
|
const normalizedExpiry = normalizeResetAt(response.expiresAt);
|
|
293
300
|
const expiryMs = normalizedExpiry ? Date.parse(normalizedExpiry) : NaN;
|
|
294
301
|
return {
|
|
@@ -431,6 +438,15 @@ function readQoderLimitsCache({
|
|
|
431
438
|
if (nowMs - cachedAtMs > QODER_LIMITS_CACHE_MAX_AGE_MS && !hasUnexpiredDatedWindow) {
|
|
432
439
|
return null;
|
|
433
440
|
}
|
|
441
|
+
// Provenance must describe the data's age, not the transport (#955): a
|
|
442
|
+
// successful refresh writes this cache and immediately floors later log
|
|
443
|
+
// scans, so the next refresh re-serves the same payload. Marking every
|
|
444
|
+
// hit stale/disk-cache made the Limits page permanently partial.
|
|
445
|
+
const ageMs = nowMs - cachedAtMs;
|
|
446
|
+
const fresh = ageMs <= QODER_LIMITS_CACHE_FRESH_TTL_MS;
|
|
447
|
+
const writtenSource = typeof raw.source === "string" && raw.source.trim()
|
|
448
|
+
? raw.source.trim()
|
|
449
|
+
: null;
|
|
434
450
|
return {
|
|
435
451
|
configured: true,
|
|
436
452
|
error: null,
|
|
@@ -440,9 +456,9 @@ function readQoderLimitsCache({
|
|
|
440
456
|
secondary_window: secondaryWindow,
|
|
441
457
|
...(tertiaryWindow ? { tertiary_window: tertiaryWindow } : {}),
|
|
442
458
|
cached: true,
|
|
443
|
-
stale:
|
|
459
|
+
stale: !fresh,
|
|
444
460
|
cached_at: raw.cached_at,
|
|
445
|
-
source: "disk-cache",
|
|
461
|
+
source: fresh && writtenSource ? writtenSource : "disk-cache",
|
|
446
462
|
};
|
|
447
463
|
} catch (_error) {
|
|
448
464
|
return null;
|
|
@@ -463,6 +479,9 @@ function writeQoderLimitsCache(limits, {
|
|
|
463
479
|
primary_window: limits.primary_window || null,
|
|
464
480
|
secondary_window: limits.secondary_window || null,
|
|
465
481
|
...(limits.tertiary_window ? { tertiary_window: limits.tertiary_window } : {}),
|
|
482
|
+
// Remember the winning source so a fresh re-serve keeps honest
|
|
483
|
+
// provenance instead of always claiming "disk-cache" (#955).
|
|
484
|
+
source: typeof limits.source === "string" ? limits.source : null,
|
|
466
485
|
cached_at: new Date(nowMs).toISOString(),
|
|
467
486
|
},
|
|
468
487
|
};
|
|
@@ -727,6 +746,12 @@ function parseQoderAccountUsageLog(text, { fallbackObservedAtMs = null } = {}) {
|
|
|
727
746
|
return {
|
|
728
747
|
limits,
|
|
729
748
|
observedAtMs: Number.isFinite(lineObservedAtMs) ? lineObservedAtMs : fallbackObservedAtMs,
|
|
749
|
+
// The summary's own timestamp, or null when the line carries no parseable
|
|
750
|
+
// one and observedAtMs fell back to the file mtime. Callers that order
|
|
751
|
+
// this snapshot against other sources must use it, not observedAtMs: an
|
|
752
|
+
// mtime bump from an unrelated append would otherwise masquerade as a
|
|
753
|
+
// fresh observation.
|
|
754
|
+
lineObservedAtMs: Number.isFinite(lineObservedAtMs) ? lineObservedAtMs : null,
|
|
730
755
|
};
|
|
731
756
|
}
|
|
732
757
|
return null;
|
|
@@ -772,7 +797,7 @@ function parseQoderQuotaLog(text) {
|
|
|
772
797
|
// one; pruning to the current candidate set keeps the map bounded.
|
|
773
798
|
const qoderNoSummaryScans = new Map();
|
|
774
799
|
|
|
775
|
-
function
|
|
800
|
+
function readQoderLocalQuotaWithAge({
|
|
776
801
|
home = os.homedir(),
|
|
777
802
|
platform = process.platform,
|
|
778
803
|
env = process.env,
|
|
@@ -780,17 +805,23 @@ function readQoderLocalQuota({
|
|
|
780
805
|
envPrefix = "QODER",
|
|
781
806
|
nowMs = Date.now(),
|
|
782
807
|
maxAgeMs = QODER_LOG_MAX_AGE_MS,
|
|
808
|
+
olderThanMs = -Infinity,
|
|
783
809
|
} = {}) {
|
|
784
810
|
const candidates = [];
|
|
785
811
|
for (const logRoot of qoderLogRoots({ home, platform, env, appDir, envPrefix })) {
|
|
786
812
|
for (const entry of listRecentQoderLogs(logRoot)) {
|
|
787
813
|
if (nowMs - entry.mtimeMs > maxAgeMs) continue;
|
|
814
|
+
// A file last written at or before the cache snapshot cannot hold an
|
|
815
|
+
// observation newer than it (a line's time never exceeds the file
|
|
816
|
+
// mtime), so scanning it could never win the timestamp comparison.
|
|
817
|
+
if (entry.mtimeMs <= olderThanMs) continue;
|
|
788
818
|
candidates.push(entry);
|
|
789
819
|
}
|
|
790
820
|
}
|
|
791
821
|
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
792
822
|
const scanned = candidates.slice(0, QODER_LOG_MAX_FILES);
|
|
793
823
|
const seenPaths = new Set();
|
|
824
|
+
let best = null;
|
|
794
825
|
for (const { full, mtimeMs, size } of scanned) {
|
|
795
826
|
seenPaths.add(full);
|
|
796
827
|
const signature = `${size}:${mtimeMs}`;
|
|
@@ -804,7 +835,7 @@ function readQoderLocalQuota({
|
|
|
804
835
|
return accountUsage;
|
|
805
836
|
}
|
|
806
837
|
const legacy = parseQoderQuotaLog(text);
|
|
807
|
-
return legacy ? { limits: legacy, observedAtMs: mtimeMs } : null;
|
|
838
|
+
return legacy ? { limits: legacy, observedAtMs: mtimeMs, lineObservedAtMs: null } : null;
|
|
808
839
|
});
|
|
809
840
|
if (parsed === undefined) continue; // Scan failed (I/O); retry on the next call.
|
|
810
841
|
if (!parsed) {
|
|
@@ -813,12 +844,27 @@ function readQoderLocalQuota({
|
|
|
813
844
|
}
|
|
814
845
|
const observedAtMs = parsed.observedAtMs ?? mtimeMs;
|
|
815
846
|
if (nowMs - observedAtMs > maxAgeMs) continue;
|
|
816
|
-
|
|
847
|
+
// A summary that cannot beat the cache cutoff is not eligible: keep
|
|
848
|
+
// scanning, a deeper file may still hold a newer line that can.
|
|
849
|
+
if ((parsed.lineObservedAtMs ?? 0) <= olderThanMs) continue;
|
|
850
|
+
// A file's mtime order need not match its summary-line order: an append
|
|
851
|
+
// without a fresh summary leaves an old line in a freshly touched file.
|
|
852
|
+
// Keep the newest eligible line across the whole scan instead of the
|
|
853
|
+
// first hit — advancing the cache cutoff afterwards must not lock out a
|
|
854
|
+
// summary that was newer all along.
|
|
855
|
+
const lineObservedAtMs = parsed.lineObservedAtMs ?? null;
|
|
856
|
+
if (!best || (lineObservedAtMs ?? 0) > (best.lineObservedAtMs ?? 0)) {
|
|
857
|
+
best = { limits: parsed.limits, lineObservedAtMs };
|
|
858
|
+
}
|
|
817
859
|
}
|
|
818
860
|
for (const cachedPath of qoderNoSummaryScans.keys()) {
|
|
819
861
|
if (!seenPaths.has(cachedPath)) qoderNoSummaryScans.delete(cachedPath);
|
|
820
862
|
}
|
|
821
|
-
return
|
|
863
|
+
return best;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function readQoderLocalQuota(options = {}) {
|
|
867
|
+
return readQoderLocalQuotaWithAge(options)?.limits ?? null;
|
|
822
868
|
}
|
|
823
869
|
|
|
824
870
|
function siteFromEnv(value) {
|
|
@@ -899,9 +945,34 @@ async function fetchQoderLimits({
|
|
|
899
945
|
}
|
|
900
946
|
}
|
|
901
947
|
|
|
902
|
-
//
|
|
903
|
-
// the
|
|
948
|
+
// A 0.4.x desktop app refreshes its main.log summary whenever the account
|
|
949
|
+
// page reloads, so the newest line can be newer than the last-good cache.
|
|
950
|
+
// Compare the summary's own timestamp against the cache instead of always
|
|
951
|
+
// trusting the cache: only a parseable line timestamp may win, and files
|
|
952
|
+
// last written before the cache are skipped without a scan. Without a valid
|
|
953
|
+
// cache there is nothing to compare against, so the summary defers to the
|
|
954
|
+
// live manual-cookie session below and only serves as its fallback.
|
|
904
955
|
const cachedLimits = readQoderLimitsCache({ home, nowMs, namespace: cacheNamespace });
|
|
956
|
+
const cachedAtMs = Date.parse(cachedLimits?.cached_at || "");
|
|
957
|
+
const cacheFloorMs = Number.isFinite(cachedAtMs) ? cachedAtMs : -Infinity;
|
|
958
|
+
const localScan = readQoderLocalQuotaWithAge({
|
|
959
|
+
home,
|
|
960
|
+
platform,
|
|
961
|
+
env,
|
|
962
|
+
appDir,
|
|
963
|
+
envPrefix,
|
|
964
|
+
nowMs,
|
|
965
|
+
olderThanMs: cacheFloorMs,
|
|
966
|
+
});
|
|
967
|
+
if (localScan && Number.isFinite(cacheFloorMs) && (localScan.lineObservedAtMs ?? 0) > cacheFloorMs) {
|
|
968
|
+
const local = localScan.limits;
|
|
969
|
+
if (activityWindow) {
|
|
970
|
+
local.secondary_window = activityWindow;
|
|
971
|
+
local.source += activitySource === "disk-cache" ? "+cached-activity" : "+activity";
|
|
972
|
+
}
|
|
973
|
+
writeQoderLimitsCache(local, { home, nowMs, namespace: cacheNamespace });
|
|
974
|
+
return local;
|
|
975
|
+
}
|
|
905
976
|
if (cachedLimits) {
|
|
906
977
|
if (activityWindow) cachedLimits.secondary_window = activityWindow;
|
|
907
978
|
return cachedLimits;
|
|
@@ -930,15 +1001,15 @@ async function fetchQoderLimits({
|
|
|
930
1001
|
// Qoder 1.18 can return an all-zero active plan with usage_percentage=0
|
|
931
1002
|
// while the desktop client separately marks the account quota-exceeded.
|
|
932
1003
|
// Preserve that status as metadata, but match Qoder's Credits UI by
|
|
933
|
-
// rendering an empty 0/0 bucket as 0%, not a full red 100% bar.
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
if (
|
|
938
|
-
result.quota_exceeded =
|
|
1004
|
+
// rendering an empty 0/0 bucket as 0%, not a full red 100% bar. With no
|
|
1005
|
+
// valid cache the log scan above only ran as a companion — it lost its
|
|
1006
|
+
// spot ahead of this session — so its result is still available here.
|
|
1007
|
+
const zeroPlanLocal = result.primary_window?.limit_credits === 0 ? localScan : null;
|
|
1008
|
+
if (zeroPlanLocal) {
|
|
1009
|
+
result.quota_exceeded = zeroPlanLocal.limits.quota_exceeded === true;
|
|
939
1010
|
result.primary_window.reset_at = null;
|
|
940
1011
|
result.source = "provider-api+local-log";
|
|
941
|
-
result.plan_label =
|
|
1012
|
+
result.plan_label = zeroPlanLocal.limits.plan_label || null;
|
|
942
1013
|
}
|
|
943
1014
|
if (activityWindow) {
|
|
944
1015
|
result.secondary_window = activityWindow;
|
|
@@ -956,8 +1027,8 @@ async function fetchQoderLimits({
|
|
|
956
1027
|
}
|
|
957
1028
|
}
|
|
958
1029
|
|
|
959
|
-
|
|
960
|
-
|
|
1030
|
+
if (localScan) {
|
|
1031
|
+
const local = localScan.limits;
|
|
961
1032
|
if (activityWindow) {
|
|
962
1033
|
local.secondary_window = activityWindow;
|
|
963
1034
|
local.source += activitySource === "disk-cache" ? "+cached-activity" : "+activity";
|
|
@@ -965,6 +1036,7 @@ async function fetchQoderLimits({
|
|
|
965
1036
|
writeQoderLimitsCache(local, { home, nowMs, namespace: cacheNamespace });
|
|
966
1037
|
return local;
|
|
967
1038
|
}
|
|
1039
|
+
|
|
968
1040
|
if (activityWindow) {
|
|
969
1041
|
return {
|
|
970
1042
|
configured: true,
|
package/src/lib/rollout.js
CHANGED
|
@@ -35,6 +35,7 @@ const {
|
|
|
35
35
|
createCompactionResponseIds,
|
|
36
36
|
createUsageRecordState,
|
|
37
37
|
extractTokenUsageRecord,
|
|
38
|
+
isCodexTotalOnlyResetSentinel,
|
|
38
39
|
isCodexTurnEndEvent,
|
|
39
40
|
isRecordOnly,
|
|
40
41
|
noteLineAfterUsageRecord,
|
|
@@ -3606,6 +3607,67 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
|
|
|
3606
3607
|
|
|
3607
3608
|
const toAppend = [];
|
|
3608
3609
|
for (const group of groupedBuckets.values()) {
|
|
3610
|
+
// A local reconstruction may opt into observed-only attribution. Preserve
|
|
3611
|
+
// every model bucket, including unknown, instead of guessing from the
|
|
3612
|
+
// busiest model in the same half-hour. The policy lives in cursor state
|
|
3613
|
+
// so hooks, desktop refreshes, and restarted CLI processes agree.
|
|
3614
|
+
if (hourlyState.modelAttributionPolicy?.[group.source] === "observed") {
|
|
3615
|
+
// A group last aligned under the default policy must not keep the stale
|
|
3616
|
+
// dominant attribution downstream. Mirror the default path's alignment
|
|
3617
|
+
// correction — one zero row for the old model — then observed mode
|
|
3618
|
+
// re-emits every bucket, unknown included, and the alignment state is
|
|
3619
|
+
// dropped so the correction fires at most once per group.
|
|
3620
|
+
const alignedModelBucket = group.buckets.get(DEFAULT_MODEL) || null;
|
|
3621
|
+
const staleAlignedModel = (() => {
|
|
3622
|
+
if (!alignedModelBucket?.alignedModel) return null;
|
|
3623
|
+
const normalized = normalizeModelInput(alignedModelBucket.alignedModel);
|
|
3624
|
+
return normalized && normalized !== DEFAULT_MODEL && !group.buckets.has(normalized)
|
|
3625
|
+
? normalized
|
|
3626
|
+
: null;
|
|
3627
|
+
})();
|
|
3628
|
+
if (staleAlignedModel) {
|
|
3629
|
+
const zeroTotals = initTotals();
|
|
3630
|
+
toAppend.push(JSON.stringify({
|
|
3631
|
+
source: group.source,
|
|
3632
|
+
model: staleAlignedModel,
|
|
3633
|
+
hour_start: group.hourStart,
|
|
3634
|
+
input_tokens: zeroTotals.input_tokens,
|
|
3635
|
+
cached_input_tokens: zeroTotals.cached_input_tokens,
|
|
3636
|
+
cache_creation_input_tokens: zeroTotals.cache_creation_input_tokens,
|
|
3637
|
+
output_tokens: zeroTotals.output_tokens,
|
|
3638
|
+
reasoning_output_tokens: zeroTotals.reasoning_output_tokens,
|
|
3639
|
+
total_tokens: zeroTotals.total_tokens,
|
|
3640
|
+
billable_total_tokens: zeroTotals.billable_total_tokens,
|
|
3641
|
+
total_cost_usd: zeroTotals.total_cost_usd,
|
|
3642
|
+
conversation_count: zeroTotals.conversation_count,
|
|
3643
|
+
}));
|
|
3644
|
+
}
|
|
3645
|
+
for (const bucket of group.buckets.values()) bucket.alignedModel = null;
|
|
3646
|
+
for (const [model, bucket] of group.buckets.entries()) {
|
|
3647
|
+
const totals = bucket.totals;
|
|
3648
|
+
const usagePrecision = bucket.usage_precision || null;
|
|
3649
|
+
const key = usagePrecision ? `${totalsKey(totals)}|${usagePrecision}` : totalsKey(totals);
|
|
3650
|
+
if (bucket.queuedKey === key) continue;
|
|
3651
|
+
toAppend.push(JSON.stringify({
|
|
3652
|
+
source: group.source,
|
|
3653
|
+
model,
|
|
3654
|
+
hour_start: group.hourStart,
|
|
3655
|
+
input_tokens: totals.input_tokens,
|
|
3656
|
+
cached_input_tokens: totals.cached_input_tokens,
|
|
3657
|
+
cache_creation_input_tokens: totals.cache_creation_input_tokens,
|
|
3658
|
+
output_tokens: totals.output_tokens,
|
|
3659
|
+
reasoning_output_tokens: totals.reasoning_output_tokens,
|
|
3660
|
+
total_tokens: totals.total_tokens,
|
|
3661
|
+
billable_total_tokens: totals.billable_total_tokens ?? totals.total_tokens,
|
|
3662
|
+
total_cost_usd: totals.total_cost_usd || 0,
|
|
3663
|
+
usage_precision: usagePrecision || undefined,
|
|
3664
|
+
...prioritySubsetOf(totals),
|
|
3665
|
+
conversation_count: totals.conversation_count,
|
|
3666
|
+
}));
|
|
3667
|
+
bucket.queuedKey = key;
|
|
3668
|
+
}
|
|
3669
|
+
continue;
|
|
3670
|
+
}
|
|
3609
3671
|
const unknownBucket = group.buckets.get(DEFAULT_MODEL) || null;
|
|
3610
3672
|
const dominantModel = pickDominantModel(group.buckets);
|
|
3611
3673
|
let alignedModel = null;
|
|
@@ -3977,6 +4039,10 @@ function normalizeHourlyState(raw) {
|
|
|
3977
4039
|
const rawBuckets = state.buckets && typeof state.buckets === "object" ? state.buckets : {};
|
|
3978
4040
|
const buckets = {};
|
|
3979
4041
|
const groupQueued = {};
|
|
4042
|
+
const modelAttributionPolicy = Object.fromEntries(
|
|
4043
|
+
Object.entries(state.modelAttributionPolicy || {})
|
|
4044
|
+
.filter(([, policy]) => policy === "observed"),
|
|
4045
|
+
);
|
|
3980
4046
|
|
|
3981
4047
|
if (!Number.isFinite(version) || version < 2) {
|
|
3982
4048
|
for (const [key, value] of Object.entries(rawBuckets)) {
|
|
@@ -3994,6 +4060,7 @@ function normalizeHourlyState(raw) {
|
|
|
3994
4060
|
version: 3,
|
|
3995
4061
|
buckets,
|
|
3996
4062
|
groupQueued,
|
|
4063
|
+
...(Object.keys(modelAttributionPolicy).length > 0 ? { modelAttributionPolicy } : {}),
|
|
3997
4064
|
updatedAt: typeof state.updatedAt === "string" ? state.updatedAt : null,
|
|
3998
4065
|
};
|
|
3999
4066
|
}
|
|
@@ -4013,6 +4080,7 @@ function normalizeHourlyState(raw) {
|
|
|
4013
4080
|
version: 3,
|
|
4014
4081
|
buckets,
|
|
4015
4082
|
groupQueued: version >= 3 ? existingGroupQueued : {},
|
|
4083
|
+
...(Object.keys(modelAttributionPolicy).length > 0 ? { modelAttributionPolicy } : {}),
|
|
4016
4084
|
updatedAt: typeof state.updatedAt === "string" ? state.updatedAt : null,
|
|
4017
4085
|
};
|
|
4018
4086
|
}
|
|
@@ -5246,20 +5314,6 @@ function normalizeUsage(u) {
|
|
|
5246
5314
|
return out;
|
|
5247
5315
|
}
|
|
5248
5316
|
|
|
5249
|
-
function isCodexTotalOnlyResetSentinel(lastUsage, totalUsage) {
|
|
5250
|
-
const componentTotal = (usage) => [
|
|
5251
|
-
usage?.input_tokens,
|
|
5252
|
-
usage?.cached_input_tokens,
|
|
5253
|
-
usage?.cache_creation_input_tokens ?? usage?.cache_write_input_tokens,
|
|
5254
|
-
usage?.output_tokens,
|
|
5255
|
-
usage?.reasoning_output_tokens,
|
|
5256
|
-
].reduce((sum, value) => sum + Math.max(0, Number(value) || 0), 0);
|
|
5257
|
-
return Number(lastUsage?.total_tokens || 0) > 0
|
|
5258
|
-
&& Number(totalUsage?.total_tokens || 0) === 0
|
|
5259
|
-
&& componentTotal(lastUsage) === 0
|
|
5260
|
-
&& componentTotal(totalUsage) === 0;
|
|
5261
|
-
}
|
|
5262
|
-
|
|
5263
5317
|
// Stable dedup key for one Claude jsonl entry. Anthropic's official protocol
|
|
5264
5318
|
// guarantees `message.id` is globally unique per response, so msgId alone is a
|
|
5265
5319
|
// valid dedup key. Older code required both msgId AND requestId, which short-
|
|
@@ -93,6 +93,8 @@ const {
|
|
|
93
93
|
// date-filtered session views and cached sidecars keep the 6x premium.
|
|
94
94
|
// v27 adds oh-my-pi (omp) sessions from ~/.omp/agent/sessions/**/*.jsonl.
|
|
95
95
|
const SIDECAR_VERSION = 27;
|
|
96
|
+
// Invalidate only Codex token summaries; other sources keep their cached rows.
|
|
97
|
+
const CODEX_USAGE_VERSION = 1;
|
|
96
98
|
const EDIT_TOOLS = new Set([
|
|
97
99
|
"apply_patch",
|
|
98
100
|
"edit",
|
|
@@ -803,6 +805,7 @@ async function scanCodexSession(filePath) {
|
|
|
803
805
|
seenTokenEvents: new Set(),
|
|
804
806
|
collectBreakdowns: false,
|
|
805
807
|
collectModelUsage: true,
|
|
808
|
+
ledgerUsage: true,
|
|
806
809
|
onObject(obj, model) {
|
|
807
810
|
signalCollector.consume(obj, model);
|
|
808
811
|
performanceCollector.consume(obj, model);
|
|
@@ -2273,7 +2276,7 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
|
|
|
2273
2276
|
previousMeta = JSON.parse(fs.readFileSync(metaPath, "utf8"));
|
|
2274
2277
|
const checkedAt = Date.parse(previousMeta.checked_at || previousMeta.generated_at || "");
|
|
2275
2278
|
if (
|
|
2276
|
-
!retryZcodeScan && !retryOpencodeScan && !retryKimiScan && !retryOmpScan && !retryDshScan && previousMeta.version === SIDECAR_VERSION &&
|
|
2279
|
+
!retryZcodeScan && !retryOpencodeScan && !retryKimiScan && !retryOmpScan && !retryDshScan && previousMeta.version === SIDECAR_VERSION && previousMeta.codex_usage_version === CODEX_USAGE_VERSION &&
|
|
2277
2280
|
Number.isFinite(checkedAt) &&
|
|
2278
2281
|
Date.now() - checkedAt < Math.max(0, Number(cacheTtlMs) || 0)
|
|
2279
2282
|
) {
|
|
@@ -2314,7 +2317,7 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
|
|
|
2314
2317
|
// db + its WAL sidecars: the trio is one logical file for change detection.
|
|
2315
2318
|
...[...(discovered.zcode || []), ...(discovered.opencode || [])].flatMap((dbPath) => [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]),
|
|
2316
2319
|
]);
|
|
2317
|
-
if (!force && !retryZcodeScan && !retryOpencodeScan && !retryKimiScan && !retryOmpScan && !retryDshScan && previousMeta?.version === SIDECAR_VERSION && previousMeta.signature === signature) {
|
|
2320
|
+
if (!force && !retryZcodeScan && !retryOpencodeScan && !retryKimiScan && !retryOmpScan && !retryDshScan && previousMeta?.version === SIDECAR_VERSION && previousMeta.codex_usage_version === CODEX_USAGE_VERSION && previousMeta.signature === signature) {
|
|
2318
2321
|
await writeAtomic(metaPath, `${JSON.stringify({ ...previousMeta, checked_at: new Date().toISOString() })}\n`);
|
|
2319
2322
|
return readSidecar(sidecarPath);
|
|
2320
2323
|
}
|
|
@@ -2344,6 +2347,7 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
|
|
|
2344
2347
|
// vanished mid-scan). Swallowing these silently made sessions disappear with
|
|
2345
2348
|
// no signal at all; count them so the API can say so.
|
|
2346
2349
|
let skippedFiles = 0;
|
|
2350
|
+
let incompleteCodexUsage = false;
|
|
2347
2351
|
let incompleteZcodeScan = false;
|
|
2348
2352
|
let incompleteOpencodeScan = false;
|
|
2349
2353
|
let incompleteKimiScan = false;
|
|
@@ -2354,6 +2358,7 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
|
|
|
2354
2358
|
const statKey = analyticsEntryStatKey(entry.source, entry.filePath);
|
|
2355
2359
|
if (!statKey) {
|
|
2356
2360
|
skippedFiles += 1;
|
|
2361
|
+
if (entry.source === "codex") incompleteCodexUsage = true;
|
|
2357
2362
|
if (entry.source === "zcode") incompleteZcodeScan = true;
|
|
2358
2363
|
if (entry.source === "opencode") incompleteOpencodeScan = true;
|
|
2359
2364
|
if (entry.source === "kimi") incompleteKimiScan = true;
|
|
@@ -2362,7 +2367,7 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
|
|
|
2362
2367
|
continue;
|
|
2363
2368
|
}
|
|
2364
2369
|
let rows = null;
|
|
2365
|
-
if (!force && !(entry.source === "zcode" && retryZcodeScan) && !(entry.source === "opencode" && retryOpencodeScan) && !(entry.source === "kimi" && retryKimiScan) && !(entry.source === "omp" && retryOmpScan) && !(entry.source === "dsh" && retryDshScan) && previousFiles[cacheKey]?.stat_key === statKey) {
|
|
2370
|
+
if (!force && !(entry.source === "codex" && previousMeta?.codex_usage_version !== CODEX_USAGE_VERSION) && !(entry.source === "zcode" && retryZcodeScan) && !(entry.source === "opencode" && retryOpencodeScan) && !(entry.source === "kimi" && retryKimiScan) && !(entry.source === "omp" && retryOmpScan) && !(entry.source === "dsh" && retryDshScan) && previousFiles[cacheKey]?.stat_key === statKey) {
|
|
2366
2371
|
if (entry.source === "zcode" || entry.source === "opencode" || entry.source === "kimi" || entry.source === "omp" || entry.source === "dsh") {
|
|
2367
2372
|
// One db scans to many cached rows; recover them all from the previous
|
|
2368
2373
|
// pass. An empty recovery means the sidecar lost its rows — rescan.
|
|
@@ -2392,6 +2397,7 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
|
|
|
2392
2397
|
// One active/partial session must not poison the sidecar, but do not
|
|
2393
2398
|
// pretend it never existed either.
|
|
2394
2399
|
skippedFiles += 1;
|
|
2400
|
+
if (entry.source === "codex") incompleteCodexUsage = true;
|
|
2395
2401
|
if (entry.source === "zcode") incompleteZcodeScan = true;
|
|
2396
2402
|
if (entry.source === "opencode") incompleteOpencodeScan = true;
|
|
2397
2403
|
if (entry.source === "kimi") incompleteKimiScan = true;
|
|
@@ -2455,6 +2461,7 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
|
|
|
2455
2461
|
const generatedAt = new Date().toISOString();
|
|
2456
2462
|
await writeAtomic(metaPath, `${JSON.stringify({
|
|
2457
2463
|
version: SIDECAR_VERSION,
|
|
2464
|
+
codex_usage_version: incompleteCodexUsage ? 0 : CODEX_USAGE_VERSION,
|
|
2458
2465
|
signature,
|
|
2459
2466
|
generated_at: generatedAt,
|
|
2460
2467
|
checked_at: generatedAt,
|