tokentracker-cli 0.90.2 → 0.91.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/README.de.md +7 -5
- package/README.ja.md +9 -5
- package/README.ko.md +9 -5
- package/README.md +7 -5
- package/README.zh-CN.md +6 -4
- package/dashboard/dist/assets/{AchievementBadge-BJl0ibLg.js → AchievementBadge-LHOuekQ3.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-CV6aNe3g.js → AchievementsPage-DNzeYWBl.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-DxLHu9EV.js → AchievementsSection-Ch5dImt8.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-ConoHhIL.js → BadgeDetailModal-CuikWkUy.js} +1 -1
- package/dashboard/dist/assets/{Card-C_VQxG6G.js → Card-Chwjmxjz.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-_Mfn4MnU.js → ClawdAnimated-BdB7aycS.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-CCRCnYY9.js → CommandPalette-BMwuSEWM.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-DuICwpdM.js → CommunityStatsModal-eEDF-xn8.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-CI27robt.js → ConfirmModal-Be7f6XdL.js} +1 -1
- package/dashboard/dist/assets/{Controls-Cwu0gtgj.js → Controls-CtBFPCBY.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-GsFvPie1.js → DashboardPage-M0p-SU-f.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-BSK76UQE.js → DevicePage-CAfcUond.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-BXQlPs02.js → DialogClose-kd1wIljN.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-CjJLpfCL.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-DyvhHV1-.js → DialogTitle-DhB-tbnZ.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-DaP6F4f8.js → FadeIn-B8eRWBdy.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-CKB5puCd.js → HeaderGithubStar-DmWep8uP.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-BG6k61dF.js → HoverTooltip-DDyTZli0.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-Oqt1J9S2.js → IpCheckPage-CN8hYm4o.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-45qoUn79.js → LandingPage-B3cHASqK.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-ZgXnaI_N.js → LeaderboardAvatar-CEcLXrJL.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-C7E_LrFk.js → LeaderboardPage-H6HW9hon.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-D6PUcj3b.js → LeaderboardProfileModal-B_5PyjRA.js} +4 -4
- package/dashboard/dist/assets/{LeaderboardProfilePage-VcC9DK_B.js → LeaderboardProfilePage-DR9KWLWv.js} +3 -3
- package/dashboard/dist/assets/{LimitsPage-B9K0RFQD.js → LimitsPage-Da0hRxO_.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-BGsfKEM9.js → LocalOnlyNotice-DuTdva_n.js} +1 -1
- package/dashboard/dist/assets/LoginCard-DOL3osrj.js +1 -0
- package/dashboard/dist/assets/{LoginModal-N3pWGciC.js → LoginModal-DBmaQL2L.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-TGgL0aXF.js → LoginPage-Dk08VrkC.js} +1 -1
- package/dashboard/dist/assets/{PetPage-DPi7OT_4.js → PetPage-BbCIdpzx.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-BkptZaPd.js → PopoverPopup-C1fJZ4qt.js} +1 -1
- package/dashboard/dist/assets/ProviderIcon-FfLD-f73.js +1 -0
- package/dashboard/dist/assets/{ResetPasswordPage-DQ4qdjBD.js → ResetPasswordPage-Bg7eVc6L.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-CuTrxHXS.js → SegmentedControl-CFNHcUz7.js} +1 -1
- package/dashboard/dist/assets/{Select-_Txi2j1D.js → Select-B0Q0eDcJ.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-DVKhj-YN.js → SelectItemText-BVbLYxSQ.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-ChflNSK0.js → ServiceStatusPage-DCZH2kz5.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-CHv6xK2J.js → SessionsPage-DO5Et7ss.js} +2 -2
- package/dashboard/dist/assets/{SettingsPage-DkWAYALG.js → SettingsPage-afgxGfzT.js} +2 -2
- package/dashboard/dist/assets/{SkillsPage-DnTSlBl-.js → SkillsPage-DDKiHCF7.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-yAMCaU-k.js → TokenGalaxy-DV1Xoe5z.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-BeDIqB0k.js → ToolbarRootContext-DNckd9Qz.js} +1 -1
- package/dashboard/dist/assets/TrendMonitor-CliMJxCC.js +19 -0
- package/dashboard/dist/assets/{WidgetsPage-y_qKZ9Gv.js → WidgetsPage-BrwG6YMM.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-BGUGI6vk.js → WrappedPage-Ft9YJd0D.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-CRvi_9hb.js → __vite-browser-external-C6_BtvDo.js} +4 -3
- package/dashboard/dist/assets/agent-logos-yYNu1ZUy.js +1 -0
- package/dashboard/dist/assets/{arrow-up-right-CjEesyJI.js → arrow-up-right-B_K57Vbg.js} +1 -1
- package/dashboard/dist/assets/{check-KDrH1gwE.js → check-xH63o2xJ.js} +1 -1
- package/dashboard/dist/assets/{chevron-right-DseqFm-D.js → chevron-right-D3F2rAfr.js} +1 -1
- package/dashboard/dist/assets/{copy-CFJ1QKAQ.js → copy-WPfJGGI0.js} +1 -1
- package/dashboard/dist/assets/{download-BQUNGUdh.js → download-BgtWtEXI.js} +1 -1
- package/dashboard/dist/assets/{external-link-BCEOLLH2.js → external-link-DSHGoZRM.js} +1 -1
- package/dashboard/dist/assets/{flame-BigtoGg2.js → flame-CNGl6Qac.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-CS1GUpAV.js → format-tokens-jVm2UX7Y.js} +1 -1
- package/dashboard/dist/assets/{icons-DpZ1a8Ct.js → icons-B1JZg-Id.js} +1 -1
- package/dashboard/dist/assets/{index-DqkwD6eO.js → index-BTZibjLi.js} +1 -1
- package/dashboard/dist/assets/{index-lGAAw1Rv.js → index-BjD81Qce.js} +1 -1
- package/dashboard/dist/assets/{index-DFU_yVc5.js → index-CUFHknAy.js} +1 -1
- package/dashboard/dist/assets/{info-Br_jtGtO.js → info-CLM460KM.js} +1 -1
- package/dashboard/dist/assets/{limits-providers-CkqO1LMQ.js → limits-providers-CFO4O1qh.js} +1 -1
- package/dashboard/dist/assets/{link-2-D-nAhkk-.js → link-2-BHOsUnAI.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-CuNPuDRR.js → loader-circle-CgcIQi92.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-dlvwitfg.js → maximize-2-DvqfuJEA.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-ORha_lPI.js → modifiers.esm-CNiwIkPh.js} +1 -1
- package/dashboard/dist/assets/module-CV1mYFsv.js +3 -0
- package/dashboard/dist/assets/{monitor-smartphone-CI2qvlMB.js → monitor-smartphone-BLutpxrG.js} +1 -1
- package/dashboard/dist/assets/provider-display-BxIgWUEa.js +42 -0
- package/dashboard/dist/assets/{react-Dy9iWvqT.js → react-CM-ozVnh.js} +1 -1
- package/dashboard/dist/assets/{search-Do7tzc3k.js → search-4T07INKt.js} +1 -1
- package/dashboard/dist/assets/{skills-api-CJ-8EFac.js → skills-api-EXMD3LD1.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-BKQg8NHC.js → sortable.esm-jC6mPJiL.js} +1 -1
- package/dashboard/dist/assets/{store-SUT-n3k3.js → store-CvSEdgf2.js} +1 -1
- package/dashboard/dist/assets/{terminal-BX4DOxd6.js → terminal-D9fyE2pP.js} +1 -1
- package/dashboard/dist/assets/{trash-2-VERlG247.js → trash-2-QLUsAY3l.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-BX6_XUdu.js → use-community-stats-Chrt3kpU.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-BpC3tYRw.js → use-limits-display-prefs-BJJd750e.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-CX-LREPo.js → use-ordered-list-BBJp5lFv.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-BvXZCGFC.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-B1G8hzVM.js → use-session-efficiency-pref-BXbvx2Ht.js} +1 -1
- package/dashboard/dist/assets/{use-transform-DNCqLbUD.js → use-transform-BVp_MUsU.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-Cvbvf30m.js → use-usage-limits-Dk3EkeIs.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-EciQmJkm.js → useCurrency-Bi88Sd__.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-sLkmam8C.js → useScrollLock-if34g2H-.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-nb9na-2-.js → useTokenFormat-C2cz-5qJ.js} +1 -1
- package/dashboard/dist/assets/{zap-BQOWCMov.js → zap-D1yHmcQR.js} +1 -1
- package/dashboard/dist/index.html +28 -24
- package/dashboard/dist/ip-check.html +1 -1
- package/dashboard/dist/leaderboard.html +1 -1
- package/dashboard/dist/llms.txt +4 -2
- package/dashboard/dist/share.html +1 -1
- package/package.json +2 -2
- package/src/commands/init.js +12 -1
- package/src/commands/status.js +59 -0
- package/src/commands/sync.js +220 -4
- package/src/lib/account-usage-dedup.js +267 -0
- package/src/lib/local-api.js +4 -0
- package/src/lib/pricing/curated-overrides.json +15 -9
- package/src/lib/pricing/index.js +38 -2
- package/src/lib/pricing/matcher.js +1 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +853 -14
- package/src/lib/source-metadata.js +1 -1
- package/src/lib/trae-cn-config.js +539 -0
- package/dashboard/dist/assets/DialogDescription-M_lg2jhD.js +0 -1
- package/dashboard/dist/assets/LoginCard-j5HVFRdP.js +0 -1
- package/dashboard/dist/assets/ProviderIcon-zaz4nwxI.js +0 -1
- package/dashboard/dist/assets/TrendMonitor-BthLuYwy.js +0 -19
- package/dashboard/dist/assets/agent-logos-DwHwn_cb.js +0 -1
- package/dashboard/dist/assets/module-BRJxSJmz.js +0 -3
- package/dashboard/dist/assets/provider-display-BNCdZ83n.js +0 -42
- package/dashboard/dist/assets/use-reduced-motion-DbebVWBw.js +0 -1
package/src/lib/rollout.js
CHANGED
|
@@ -12397,6 +12397,57 @@ function resolvePiDefaultModel() {
|
|
|
12397
12397
|
return "pi-unknown";
|
|
12398
12398
|
}
|
|
12399
12399
|
|
|
12400
|
+
// Prime Agent (PrimeIntellect-ai/prime-agent) persists the same metadata-only
|
|
12401
|
+
// assistant usage envelope as pi, but uses a flat sessions directory:
|
|
12402
|
+
// ~/.prime/agent/sessions/<session-id>.jsonl
|
|
12403
|
+
// Keep its path and cursor namespace independent from pi so installations of
|
|
12404
|
+
// both tools can never suppress or double-count each other.
|
|
12405
|
+
function resolvePrimeAgentHome(env = process.env) {
|
|
12406
|
+
if (env.TOKENTRACKER_PRIME_AGENT_HOME) {
|
|
12407
|
+
return expandHomePath(env.TOKENTRACKER_PRIME_AGENT_HOME, env);
|
|
12408
|
+
}
|
|
12409
|
+
const home = env.HOME || require("node:os").homedir();
|
|
12410
|
+
if (process.platform === "win32") {
|
|
12411
|
+
return pickWin32ProviderPath({
|
|
12412
|
+
env,
|
|
12413
|
+
nativeValue: path.join(home, ".prime"),
|
|
12414
|
+
wslProviderDir: ".prime",
|
|
12415
|
+
});
|
|
12416
|
+
}
|
|
12417
|
+
return path.join(home, ".prime");
|
|
12418
|
+
}
|
|
12419
|
+
|
|
12420
|
+
function resolvePrimeAgentDir(env = process.env) {
|
|
12421
|
+
if (env.TOKENTRACKER_PRIME_AGENT_DIR) {
|
|
12422
|
+
return expandHomePath(env.TOKENTRACKER_PRIME_AGENT_DIR, env);
|
|
12423
|
+
}
|
|
12424
|
+
const primeHome = resolvePrimeAgentHome(env);
|
|
12425
|
+
return primeHome ? path.join(primeHome, "agent") : null;
|
|
12426
|
+
}
|
|
12427
|
+
|
|
12428
|
+
function resolvePrimeAgentSessionFiles(env = process.env) {
|
|
12429
|
+
const agentDir = resolvePrimeAgentDir(env);
|
|
12430
|
+
if (!agentDir) return [];
|
|
12431
|
+
const sessionsDir = path.join(agentDir, "sessions");
|
|
12432
|
+
if (!fssync.existsSync(sessionsDir)) return [];
|
|
12433
|
+
const files = [];
|
|
12434
|
+
const walk = (dir) => {
|
|
12435
|
+
let entries;
|
|
12436
|
+
try { entries = fssync.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
12437
|
+
for (const entry of entries) {
|
|
12438
|
+
const full = path.join(dir, entry.name);
|
|
12439
|
+
if (entry.isDirectory()) walk(full);
|
|
12440
|
+
else if (entry.isFile() && entry.name.endsWith(".jsonl")) files.push(full);
|
|
12441
|
+
}
|
|
12442
|
+
};
|
|
12443
|
+
walk(sessionsDir);
|
|
12444
|
+
return files.sort((a, b) => a.localeCompare(b));
|
|
12445
|
+
}
|
|
12446
|
+
|
|
12447
|
+
function resolvePrimeAgentDefaultModel() {
|
|
12448
|
+
return "prime-agent-unknown";
|
|
12449
|
+
}
|
|
12450
|
+
|
|
12400
12451
|
// Pi is a router: the same session can send turns to Anthropic, GitHub
|
|
12401
12452
|
// Copilot, or another backend. Keep provider names in the queue source so
|
|
12402
12453
|
// those turns cannot collapse into one bucket (or inherit the wrong pricing).
|
|
@@ -12413,30 +12464,47 @@ function piSourceForProvider(provider) {
|
|
|
12413
12464
|
return slug ? `pi-${slug}` : "pi";
|
|
12414
12465
|
}
|
|
12415
12466
|
|
|
12416
|
-
|
|
12467
|
+
function primeAgentSourceForProvider(provider) {
|
|
12468
|
+
if (typeof provider !== "string" || !provider.trim()) return "prime-agent";
|
|
12469
|
+
const slug = provider
|
|
12470
|
+
.trim()
|
|
12471
|
+
.toLowerCase()
|
|
12472
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
12473
|
+
.replace(/^-+|-+$/g, "")
|
|
12474
|
+
.slice(0, 64);
|
|
12475
|
+
return slug ? `prime-agent-${slug}` : "prime-agent";
|
|
12476
|
+
}
|
|
12477
|
+
|
|
12478
|
+
async function parsePiLikeIncremental({
|
|
12417
12479
|
sessionFiles,
|
|
12418
12480
|
cursors,
|
|
12419
12481
|
queuePath,
|
|
12420
12482
|
onProgress,
|
|
12421
12483
|
env,
|
|
12422
12484
|
defaultModel,
|
|
12485
|
+
stateKey,
|
|
12486
|
+
resolveSessionFiles,
|
|
12487
|
+
resolveDefaultModel,
|
|
12488
|
+
sourceForProvider,
|
|
12423
12489
|
} = {}) {
|
|
12424
12490
|
await ensureDir(path.dirname(queuePath));
|
|
12425
|
-
const
|
|
12426
|
-
|
|
12491
|
+
const providerState = cursors[stateKey] && typeof cursors[stateKey] === "object"
|
|
12492
|
+
? cursors[stateKey]
|
|
12493
|
+
: {};
|
|
12494
|
+
const seenIds = new Set(Array.isArray(providerState.seenIds) ? providerState.seenIds : []);
|
|
12427
12495
|
const fileOffsets =
|
|
12428
|
-
|
|
12429
|
-
? { ...
|
|
12496
|
+
providerState.fileOffsets && typeof providerState.fileOffsets === "object"
|
|
12497
|
+
? { ...providerState.fileOffsets }
|
|
12430
12498
|
: {};
|
|
12431
12499
|
|
|
12432
12500
|
const files = Array.isArray(sessionFiles)
|
|
12433
12501
|
? sessionFiles
|
|
12434
|
-
:
|
|
12435
|
-
const fallbackModel = defaultModel ||
|
|
12502
|
+
: resolveSessionFiles(env || process.env);
|
|
12503
|
+
const fallbackModel = defaultModel || resolveDefaultModel();
|
|
12436
12504
|
|
|
12437
12505
|
if (files.length === 0) {
|
|
12438
|
-
cursors
|
|
12439
|
-
...
|
|
12506
|
+
cursors[stateKey] = {
|
|
12507
|
+
...providerState,
|
|
12440
12508
|
seenIds: Array.from(seenIds),
|
|
12441
12509
|
fileOffsets,
|
|
12442
12510
|
updatedAt: new Date().toISOString(),
|
|
@@ -12463,12 +12531,29 @@ async function parsePiIncremental({
|
|
|
12463
12531
|
if (stat.size <= startOffset) continue;
|
|
12464
12532
|
|
|
12465
12533
|
let stream;
|
|
12534
|
+
let streamedBytes = 0;
|
|
12535
|
+
let lastCompleteOffset = startOffset;
|
|
12466
12536
|
try {
|
|
12467
12537
|
stream = fssync.createReadStream(filePath, {
|
|
12468
12538
|
encoding: "utf8",
|
|
12469
12539
|
start: startOffset,
|
|
12470
12540
|
});
|
|
12471
12541
|
} catch { continue; }
|
|
12542
|
+
// readline emits an unterminated final fragment as if it were a line. If
|
|
12543
|
+
// the producer is still writing that JSON object, parsing fails; advancing
|
|
12544
|
+
// to stat.size here would then lose the completed record forever. Track the
|
|
12545
|
+
// last physical newline and commit only through that byte boundary.
|
|
12546
|
+
stream.on("data", (chunk) => {
|
|
12547
|
+
let searchFrom = 0;
|
|
12548
|
+
let newlineIndex;
|
|
12549
|
+
while ((newlineIndex = chunk.indexOf("\n", searchFrom)) !== -1) {
|
|
12550
|
+
lastCompleteOffset = startOffset
|
|
12551
|
+
+ streamedBytes
|
|
12552
|
+
+ Buffer.byteLength(chunk.slice(0, newlineIndex + 1), "utf8");
|
|
12553
|
+
searchFrom = newlineIndex + 1;
|
|
12554
|
+
}
|
|
12555
|
+
streamedBytes += Buffer.byteLength(chunk, "utf8");
|
|
12556
|
+
});
|
|
12472
12557
|
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
12473
12558
|
|
|
12474
12559
|
for await (const line of rl) {
|
|
@@ -12529,7 +12614,7 @@ async function parsePiIncremental({
|
|
|
12529
12614
|
: input + output + cacheRead + cacheWrite + reasoningTokens;
|
|
12530
12615
|
|
|
12531
12616
|
const model = normalizeModelInput(msg.model) || fallbackModel;
|
|
12532
|
-
const source =
|
|
12617
|
+
const source = sourceForProvider(msg.provider);
|
|
12533
12618
|
|
|
12534
12619
|
const delta = {
|
|
12535
12620
|
input_tokens: input,
|
|
@@ -12561,7 +12646,7 @@ async function parsePiIncremental({
|
|
|
12561
12646
|
let postStat = stat;
|
|
12562
12647
|
try { postStat = fssync.statSync(filePath); } catch {}
|
|
12563
12648
|
fileOffsets[filePath] = {
|
|
12564
|
-
size: postStat.size,
|
|
12649
|
+
size: Math.min(lastCompleteOffset, postStat.size),
|
|
12565
12650
|
mtimeMs: postStat.mtimeMs,
|
|
12566
12651
|
ino: postStat.ino,
|
|
12567
12652
|
};
|
|
@@ -12579,8 +12664,8 @@ async function parsePiIncremental({
|
|
|
12579
12664
|
const updatedAt = new Date().toISOString();
|
|
12580
12665
|
hourlyState.updatedAt = updatedAt;
|
|
12581
12666
|
cursors.hourly = hourlyState;
|
|
12582
|
-
cursors
|
|
12583
|
-
...
|
|
12667
|
+
cursors[stateKey] = {
|
|
12668
|
+
...providerState,
|
|
12584
12669
|
seenIds: cappedSeen,
|
|
12585
12670
|
fileOffsets,
|
|
12586
12671
|
updatedAt,
|
|
@@ -12589,6 +12674,26 @@ async function parsePiIncremental({
|
|
|
12589
12674
|
return { recordsProcessed, eventsAggregated, bucketsQueued };
|
|
12590
12675
|
}
|
|
12591
12676
|
|
|
12677
|
+
async function parsePiIncremental(options = {}) {
|
|
12678
|
+
return parsePiLikeIncremental({
|
|
12679
|
+
...options,
|
|
12680
|
+
stateKey: "pi",
|
|
12681
|
+
resolveSessionFiles: resolvePiSessionFiles,
|
|
12682
|
+
resolveDefaultModel: resolvePiDefaultModel,
|
|
12683
|
+
sourceForProvider: piSourceForProvider,
|
|
12684
|
+
});
|
|
12685
|
+
}
|
|
12686
|
+
|
|
12687
|
+
async function parsePrimeAgentIncremental(options = {}) {
|
|
12688
|
+
return parsePiLikeIncremental({
|
|
12689
|
+
...options,
|
|
12690
|
+
stateKey: "primeAgent",
|
|
12691
|
+
resolveSessionFiles: resolvePrimeAgentSessionFiles,
|
|
12692
|
+
resolveDefaultModel: resolvePrimeAgentDefaultModel,
|
|
12693
|
+
sourceForProvider: primeAgentSourceForProvider,
|
|
12694
|
+
});
|
|
12695
|
+
}
|
|
12696
|
+
|
|
12592
12697
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
12593
12698
|
// Craft Agents (lukilabs/craft-agents-oss) — passive JSONL reader
|
|
12594
12699
|
//
|
|
@@ -15568,10 +15673,61 @@ async function retractStaleGrokQueueRows(queuePath, keepKeys) {
|
|
|
15568
15673
|
return lines.length;
|
|
15569
15674
|
}
|
|
15570
15675
|
|
|
15676
|
+
function pruneMissingGrokProjectUpdateOffsets(projectUpdateOffsets) {
|
|
15677
|
+
if (!projectUpdateOffsets || typeof projectUpdateOffsets !== "object") return;
|
|
15678
|
+
for (const updatesPath of Object.keys(projectUpdateOffsets)) {
|
|
15679
|
+
if (typeof updatesPath !== "string" || !updatesPath) {
|
|
15680
|
+
delete projectUpdateOffsets[updatesPath];
|
|
15681
|
+
continue;
|
|
15682
|
+
}
|
|
15683
|
+
try {
|
|
15684
|
+
if (!fssync.existsSync(updatesPath)) delete projectUpdateOffsets[updatesPath];
|
|
15685
|
+
} catch {
|
|
15686
|
+
delete projectUpdateOffsets[updatesPath];
|
|
15687
|
+
}
|
|
15688
|
+
}
|
|
15689
|
+
}
|
|
15690
|
+
|
|
15691
|
+
function grokTryDecodeUriComponent(value) {
|
|
15692
|
+
if (typeof value !== "string") return null;
|
|
15693
|
+
const trimmed = value.trim();
|
|
15694
|
+
if (!trimmed) return null;
|
|
15695
|
+
try {
|
|
15696
|
+
const decoded = decodeURIComponent(trimmed).trim();
|
|
15697
|
+
return decoded || null;
|
|
15698
|
+
} catch {
|
|
15699
|
+
return null;
|
|
15700
|
+
}
|
|
15701
|
+
}
|
|
15702
|
+
|
|
15703
|
+
function grokEncodedCwdLooksEncoded(value) {
|
|
15704
|
+
return typeof value === "string" && /%[0-9A-Fa-f]{2}/.test(value);
|
|
15705
|
+
}
|
|
15706
|
+
|
|
15707
|
+
// First non-empty wins: summary.info.cwd, hook sess.cwd, decode(encodedCwd),
|
|
15708
|
+
// then decode(basename(dirname(sessionDir))). Do not guess from updates.jsonl.
|
|
15709
|
+
function resolveGrokSessionCwd(sess, summary) {
|
|
15710
|
+
const summaryCwd = summary?.info?.cwd;
|
|
15711
|
+
if (typeof summaryCwd === "string" && summaryCwd.trim()) return summaryCwd.trim();
|
|
15712
|
+
if (typeof sess?.cwd === "string" && sess.cwd.trim()) return sess.cwd.trim();
|
|
15713
|
+
if (grokEncodedCwdLooksEncoded(sess?.encodedCwd)) {
|
|
15714
|
+
const decoded = grokTryDecodeUriComponent(sess.encodedCwd);
|
|
15715
|
+
if (decoded) return decoded;
|
|
15716
|
+
}
|
|
15717
|
+
const sessionDir = typeof sess?.sessionDir === "string" ? sess.sessionDir.trim() : "";
|
|
15718
|
+
if (sessionDir) {
|
|
15719
|
+
const decoded = grokTryDecodeUriComponent(path.basename(path.dirname(sessionDir)));
|
|
15720
|
+
if (decoded) return decoded;
|
|
15721
|
+
}
|
|
15722
|
+
return null;
|
|
15723
|
+
}
|
|
15724
|
+
|
|
15571
15725
|
async function parseGrokBuildIncremental({
|
|
15572
15726
|
sessions,
|
|
15573
15727
|
cursors = {},
|
|
15574
15728
|
queuePath,
|
|
15729
|
+
projectQueuePath,
|
|
15730
|
+
publicRepoResolver,
|
|
15575
15731
|
onProgress,
|
|
15576
15732
|
env = process.env
|
|
15577
15733
|
} = {}) {
|
|
@@ -15616,6 +15772,25 @@ async function parseGrokBuildIncremental({
|
|
|
15616
15772
|
? grokState.updateOffsets
|
|
15617
15773
|
: {};
|
|
15618
15774
|
|
|
15775
|
+
const projectEnabled = typeof projectQueuePath === "string" && projectQueuePath.length > 0;
|
|
15776
|
+
const projectState = projectEnabled ? normalizeProjectState(cursors?.projectHourly) : null;
|
|
15777
|
+
const projectTouchedBuckets = projectEnabled ? new Set() : null;
|
|
15778
|
+
const projectMetaCache = projectEnabled ? new Map() : null;
|
|
15779
|
+
const publicRepoCache = projectEnabled ? new Map() : null;
|
|
15780
|
+
// Independent of global updateOffsets so an upgrade can backfill historical
|
|
15781
|
+
// turn_completed events into project.queue.jsonl without re-appending totals.
|
|
15782
|
+
const projectUpdateOffsets =
|
|
15783
|
+
projectEnabled &&
|
|
15784
|
+
grokState.projectUpdateOffsets &&
|
|
15785
|
+
typeof grokState.projectUpdateOffsets === "object"
|
|
15786
|
+
? { ...grokState.projectUpdateOffsets }
|
|
15787
|
+
: {};
|
|
15788
|
+
const projectSessionSnapshots = projectEnabled
|
|
15789
|
+
? normalizeGrokSessionSnapshots({
|
|
15790
|
+
sessionSnapshots: grokState.projectSessionSnapshots,
|
|
15791
|
+
})
|
|
15792
|
+
: {};
|
|
15793
|
+
|
|
15619
15794
|
// Rebuilt from the sessions seen this scan, so entries for deleted session
|
|
15620
15795
|
// dirs are pruned and the cursor stays bounded by the on-disk session count.
|
|
15621
15796
|
const updateOffsets = {};
|
|
@@ -15788,11 +15963,167 @@ async function parseGrokBuildIncremental({
|
|
|
15788
15963
|
};
|
|
15789
15964
|
}
|
|
15790
15965
|
|
|
15966
|
+
if (projectEnabled) {
|
|
15967
|
+
const cwd = resolveGrokSessionCwd(sess, summary);
|
|
15968
|
+
if (cwd) {
|
|
15969
|
+
const projectContext = await resolveProjectContextForPath({
|
|
15970
|
+
startDir: wsl.mapWslCwdToUnc(cwd, updatesPath || sess?.sessionDir || cwd),
|
|
15971
|
+
projectMetaCache,
|
|
15972
|
+
publicRepoCache,
|
|
15973
|
+
publicRepoResolver,
|
|
15974
|
+
projectState,
|
|
15975
|
+
});
|
|
15976
|
+
const projectRef = projectContext?.projectRef || null;
|
|
15977
|
+
const projectKey = projectContext?.projectKey || null;
|
|
15978
|
+
// Only advance the project cursor on successful public_verified attribution.
|
|
15979
|
+
// Blocked / missing git / unverified stay unadvanced so a later sync can retry.
|
|
15980
|
+
if (projectKey && projectRef) {
|
|
15981
|
+
const projectPrevious = projectSessionSnapshots[sessionId] || {};
|
|
15982
|
+
const projectPreviousTotal = normalizeNonNegativeNumber(projectPrevious.totalTokens);
|
|
15983
|
+
const projectPreviousMessageCount = normalizeNonNegativeNumber(
|
|
15984
|
+
projectPrevious.messageCount,
|
|
15985
|
+
);
|
|
15986
|
+
let projectSawTurnUsage = projectPrevious.source === "turn_usage";
|
|
15987
|
+
let projectCumulativeTotal = projectPreviousTotal;
|
|
15988
|
+
let projectTokenDelta = 0;
|
|
15989
|
+
let projectFinalTouchedHourStart = null;
|
|
15990
|
+
let projectSource = projectPrevious.source || null;
|
|
15991
|
+
let projectLastModel = projectPrevious.model || model;
|
|
15992
|
+
const projectPendingDeltas = [];
|
|
15993
|
+
|
|
15994
|
+
const projectUpdates = await readGrokUpdateTokenEvents(
|
|
15995
|
+
updatesPath,
|
|
15996
|
+
lastActive,
|
|
15997
|
+
updatesPath ? projectUpdateOffsets[updatesPath] : null,
|
|
15998
|
+
{ fallbackModel: model },
|
|
15999
|
+
);
|
|
16000
|
+
|
|
16001
|
+
for (const event of projectUpdates.turnEvents) {
|
|
16002
|
+
projectSawTurnUsage = true;
|
|
16003
|
+
const hourStartStr =
|
|
16004
|
+
toUtcHalfHourStart(event.timestamp) ||
|
|
16005
|
+
toUtcHalfHourStart(lastActive) ||
|
|
16006
|
+
toUtcHalfHourStart(Date.now());
|
|
16007
|
+
if (!hourStartStr) continue;
|
|
16008
|
+
const eventModel = event.model || model;
|
|
16009
|
+
const delta = {
|
|
16010
|
+
input_tokens: event.input_tokens,
|
|
16011
|
+
cached_input_tokens: event.cached_input_tokens,
|
|
16012
|
+
cache_creation_input_tokens: event.cache_creation_input_tokens,
|
|
16013
|
+
output_tokens: event.output_tokens,
|
|
16014
|
+
reasoning_output_tokens: event.reasoning_output_tokens,
|
|
16015
|
+
total_tokens: event.total_tokens,
|
|
16016
|
+
billable_total_tokens: event.billable_total_tokens,
|
|
16017
|
+
conversation_count: event.conversation_count || 1,
|
|
16018
|
+
};
|
|
16019
|
+
projectPendingDeltas.push({ model: eventModel, hourStartStr, delta });
|
|
16020
|
+
projectCumulativeTotal += event.total_tokens;
|
|
16021
|
+
projectTokenDelta += event.total_tokens;
|
|
16022
|
+
projectFinalTouchedHourStart = hourStartStr;
|
|
16023
|
+
projectSource = "turn_usage";
|
|
16024
|
+
projectLastModel = eventModel;
|
|
16025
|
+
}
|
|
16026
|
+
|
|
16027
|
+
// Watermark fallback uses the independent project snapshot, never
|
|
16028
|
+
// global sessionSnapshots.totalTokens, and never stacks on turn usage.
|
|
16029
|
+
if (!projectSawTurnUsage) {
|
|
16030
|
+
let highWatermark = projectPreviousTotal;
|
|
16031
|
+
for (const event of projectUpdates.contextEvents) {
|
|
16032
|
+
if (event.totalTokens <= highWatermark) continue;
|
|
16033
|
+
const deltaTokens = event.totalTokens - highWatermark;
|
|
16034
|
+
highWatermark = event.totalTokens;
|
|
16035
|
+
const hourStartStr =
|
|
16036
|
+
toUtcHalfHourStart(event.timestamp) ||
|
|
16037
|
+
toUtcHalfHourStart(lastActive) ||
|
|
16038
|
+
toUtcHalfHourStart(Date.now());
|
|
16039
|
+
if (!hourStartStr) continue;
|
|
16040
|
+
const delta = estimateGrokTokenDelta(deltaTokens, 0, {
|
|
16041
|
+
allowZeroConversationCount: true,
|
|
16042
|
+
});
|
|
16043
|
+
projectPendingDeltas.push({ model, hourStartStr, delta });
|
|
16044
|
+
projectTokenDelta += deltaTokens;
|
|
16045
|
+
projectFinalTouchedHourStart = hourStartStr;
|
|
16046
|
+
projectSource = "updates";
|
|
16047
|
+
}
|
|
16048
|
+
|
|
16049
|
+
const effectiveSignalTotal = grokEffectiveTotalFromSignals(safeSignals);
|
|
16050
|
+
if (effectiveSignalTotal > highWatermark) {
|
|
16051
|
+
const deltaTokens = effectiveSignalTotal - highWatermark;
|
|
16052
|
+
highWatermark = effectiveSignalTotal;
|
|
16053
|
+
const hourStartStr =
|
|
16054
|
+
toUtcHalfHourStart(lastActive) || toUtcHalfHourStart(Date.now());
|
|
16055
|
+
if (hourStartStr) {
|
|
16056
|
+
const delta = estimateGrokTokenDelta(deltaTokens, 0, {
|
|
16057
|
+
allowZeroConversationCount: true,
|
|
16058
|
+
});
|
|
16059
|
+
projectPendingDeltas.push({ model, hourStartStr, delta });
|
|
16060
|
+
projectTokenDelta += deltaTokens;
|
|
16061
|
+
projectFinalTouchedHourStart = hourStartStr;
|
|
16062
|
+
projectSource = "signals";
|
|
16063
|
+
}
|
|
16064
|
+
}
|
|
16065
|
+
projectCumulativeTotal = Math.max(projectPreviousTotal, highWatermark);
|
|
16066
|
+
}
|
|
16067
|
+
|
|
16068
|
+
const projectFinalTotal = Math.max(projectPreviousTotal, projectCumulativeTotal);
|
|
16069
|
+
|
|
16070
|
+
// legacyBaselineOnly applies only to global buckets.
|
|
16071
|
+
for (const pending of projectPendingDeltas) {
|
|
16072
|
+
const projectBucket = getProjectBucket(
|
|
16073
|
+
projectState,
|
|
16074
|
+
projectKey,
|
|
16075
|
+
"grok",
|
|
16076
|
+
pending.hourStartStr,
|
|
16077
|
+
projectRef,
|
|
16078
|
+
);
|
|
16079
|
+
addTotals(projectBucket.totals, pending.delta);
|
|
16080
|
+
projectTouchedBuckets.add(
|
|
16081
|
+
projectBucketKey(projectKey, "grok", pending.hourStartStr),
|
|
16082
|
+
);
|
|
16083
|
+
}
|
|
16084
|
+
|
|
16085
|
+
if (!projectSawTurnUsage && projectTokenDelta > 0 && projectFinalTouchedHourStart) {
|
|
16086
|
+
const deltaMessageCount =
|
|
16087
|
+
messageCount > projectPreviousMessageCount
|
|
16088
|
+
? messageCount - projectPreviousMessageCount
|
|
16089
|
+
: 1;
|
|
16090
|
+
const projectBucket = getProjectBucket(
|
|
16091
|
+
projectState,
|
|
16092
|
+
projectKey,
|
|
16093
|
+
"grok",
|
|
16094
|
+
projectFinalTouchedHourStart,
|
|
16095
|
+
projectRef,
|
|
16096
|
+
);
|
|
16097
|
+
addTotals(projectBucket.totals, { conversation_count: deltaMessageCount });
|
|
16098
|
+
projectTouchedBuckets.add(
|
|
16099
|
+
projectBucketKey(projectKey, "grok", projectFinalTouchedHourStart),
|
|
16100
|
+
);
|
|
16101
|
+
}
|
|
16102
|
+
|
|
16103
|
+
if (updatesPath && projectUpdates.offsetEntry) {
|
|
16104
|
+
projectUpdateOffsets[updatesPath] = projectUpdates.offsetEntry;
|
|
16105
|
+
}
|
|
16106
|
+
|
|
16107
|
+
if (projectFinalTotal > 0 && (projectTokenDelta > 0 || projectPreviousTotal > 0)) {
|
|
16108
|
+
projectSessionSnapshots[sessionId] = {
|
|
16109
|
+
totalTokens: projectFinalTotal,
|
|
16110
|
+
messageCount: Math.max(projectPreviousMessageCount, messageCount),
|
|
16111
|
+
model: projectLastModel || model,
|
|
16112
|
+
source: projectSource || projectPrevious.source || null,
|
|
16113
|
+
updatedAt: new Date().toISOString(),
|
|
16114
|
+
};
|
|
16115
|
+
}
|
|
16116
|
+
}
|
|
16117
|
+
}
|
|
16118
|
+
}
|
|
16119
|
+
|
|
15791
16120
|
if (onProgress) {
|
|
15792
16121
|
onProgress({ index: index + 1, total: sessionList.length, bucketsQueued: touchedBuckets.size });
|
|
15793
16122
|
}
|
|
15794
16123
|
}
|
|
15795
16124
|
|
|
16125
|
+
if (projectEnabled) pruneMissingGrokProjectUpdateOffsets(projectUpdateOffsets);
|
|
16126
|
+
|
|
15796
16127
|
let bucketsQueued = queuePath
|
|
15797
16128
|
? await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
|
|
15798
16129
|
: 0;
|
|
@@ -15810,8 +16141,20 @@ async function parseGrokBuildIncremental({
|
|
|
15810
16141
|
bucketsQueued += retracted;
|
|
15811
16142
|
}
|
|
15812
16143
|
|
|
16144
|
+
const projectBucketsQueued = projectEnabled
|
|
16145
|
+
? await enqueueTouchedProjectBuckets({
|
|
16146
|
+
projectQueuePath,
|
|
16147
|
+
projectState,
|
|
16148
|
+
projectTouchedBuckets,
|
|
16149
|
+
})
|
|
16150
|
+
: 0;
|
|
16151
|
+
|
|
15813
16152
|
hourlyState.updatedAt = new Date().toISOString();
|
|
15814
16153
|
cursors.hourly = hourlyState;
|
|
16154
|
+
if (projectState) {
|
|
16155
|
+
projectState.updatedAt = hourlyState.updatedAt;
|
|
16156
|
+
cursors.projectHourly = projectState;
|
|
16157
|
+
}
|
|
15815
16158
|
sessionSnapshots = capGrokSessionSnapshots(sessionSnapshots);
|
|
15816
16159
|
|
|
15817
16160
|
const migrations = grokState.migrations && typeof grokState.migrations === "object"
|
|
@@ -15831,6 +16174,12 @@ async function parseGrokBuildIncremental({
|
|
|
15831
16174
|
sessionSnapshots,
|
|
15832
16175
|
seenSessions: Object.keys(sessionSnapshots),
|
|
15833
16176
|
updateOffsets,
|
|
16177
|
+
...(projectEnabled
|
|
16178
|
+
? {
|
|
16179
|
+
projectUpdateOffsets,
|
|
16180
|
+
projectSessionSnapshots: capGrokSessionSnapshots(projectSessionSnapshots),
|
|
16181
|
+
}
|
|
16182
|
+
: {}),
|
|
15834
16183
|
migrations,
|
|
15835
16184
|
updatedAt: new Date().toISOString()
|
|
15836
16185
|
};
|
|
@@ -15838,7 +16187,8 @@ async function parseGrokBuildIncremental({
|
|
|
15838
16187
|
return {
|
|
15839
16188
|
recordsProcessed: eventsAggregated,
|
|
15840
16189
|
eventsAggregated,
|
|
15841
|
-
bucketsQueued
|
|
16190
|
+
bucketsQueued,
|
|
16191
|
+
projectBucketsQueued,
|
|
15842
16192
|
};
|
|
15843
16193
|
}
|
|
15844
16194
|
|
|
@@ -16378,6 +16728,489 @@ function readTraeEntitlementFromStorage(storagePath) {
|
|
|
16378
16728
|
};
|
|
16379
16729
|
}
|
|
16380
16730
|
|
|
16731
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
16732
|
+
// Trae Work CN (国内版) — usage API incremental parser.
|
|
16733
|
+
//
|
|
16734
|
+
// TRAE Work CN's usage API reports per-session rows that are NOT append-only:
|
|
16735
|
+
// a session can be re-reported with corrected token totals, a different model,
|
|
16736
|
+
// or a shifted time bucket. This parser keeps the last normalized contribution
|
|
16737
|
+
// per session_id under `cursors.traeCn` and reconciles by subtracting the
|
|
16738
|
+
// previous contribution from its old bucket before adding the new one, so
|
|
16739
|
+
// corrections retract the stale tuple instead of stacking. Only the normalized
|
|
16740
|
+
// contribution is persisted — no raw API rows, cost/credits, auth, refresh
|
|
16741
|
+
// tokens, or prompt previews. Source is always `trae-cn`.
|
|
16742
|
+
const TRAE_CN_SOURCE = "trae-cn";
|
|
16743
|
+
const TRAE_CN_STATE_VERSION = 1;
|
|
16744
|
+
const TRAE_CN_UNKNOWN_MODEL = "trae-cn-unknown";
|
|
16745
|
+
|
|
16746
|
+
function normalizeTraeCnState(raw) {
|
|
16747
|
+
if (raw === undefined || raw === null) {
|
|
16748
|
+
return {
|
|
16749
|
+
version: TRAE_CN_STATE_VERSION,
|
|
16750
|
+
sessions: {},
|
|
16751
|
+
prunedBeforeMs: 0,
|
|
16752
|
+
updatedAt: null,
|
|
16753
|
+
};
|
|
16754
|
+
}
|
|
16755
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
16756
|
+
throw new Error("Trae CN cursor state is malformed.");
|
|
16757
|
+
}
|
|
16758
|
+
if (raw.version !== TRAE_CN_STATE_VERSION) {
|
|
16759
|
+
throw new Error(`Trae CN cursor state version ${raw.version} is not supported.`);
|
|
16760
|
+
}
|
|
16761
|
+
if (!raw.sessions || typeof raw.sessions !== "object" || Array.isArray(raw.sessions)) {
|
|
16762
|
+
throw new Error("Trae CN cursor sessions are malformed.");
|
|
16763
|
+
}
|
|
16764
|
+
return {
|
|
16765
|
+
version: TRAE_CN_STATE_VERSION,
|
|
16766
|
+
sessions: { ...raw.sessions },
|
|
16767
|
+
prunedBeforeMs:
|
|
16768
|
+
Number.isFinite(raw.prunedBeforeMs) && raw.prunedBeforeMs > 0 ? raw.prunedBeforeMs : 0,
|
|
16769
|
+
updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : null,
|
|
16770
|
+
};
|
|
16771
|
+
}
|
|
16772
|
+
|
|
16773
|
+
function isValidTraeCnTotals(totals) {
|
|
16774
|
+
if (!totals || typeof totals !== "object") return false;
|
|
16775
|
+
for (const key of [
|
|
16776
|
+
"input_tokens",
|
|
16777
|
+
"cached_input_tokens",
|
|
16778
|
+
"cache_creation_input_tokens",
|
|
16779
|
+
"output_tokens",
|
|
16780
|
+
"reasoning_output_tokens",
|
|
16781
|
+
"total_tokens",
|
|
16782
|
+
"billable_total_tokens",
|
|
16783
|
+
"conversation_count",
|
|
16784
|
+
]) {
|
|
16785
|
+
if (!Number.isSafeInteger(totals[key]) || totals[key] < 0) return false;
|
|
16786
|
+
}
|
|
16787
|
+
return true;
|
|
16788
|
+
}
|
|
16789
|
+
|
|
16790
|
+
// Canonical UTC half-hour bucketStart produced by toUtcHalfHourStart:
|
|
16791
|
+
// "YYYY-MM-DDTHH:00:00.000Z" or "YYYY-MM-DDTHH:30:00.000Z".
|
|
16792
|
+
function isValidTraeCnBucketStart(value) {
|
|
16793
|
+
if (typeof value !== "string") return false;
|
|
16794
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:(00|30):00\.000Z$/.test(value)) return false;
|
|
16795
|
+
const dt = new Date(value);
|
|
16796
|
+
return Number.isFinite(dt.getTime()) && dt.toISOString() === value;
|
|
16797
|
+
}
|
|
16798
|
+
|
|
16799
|
+
// Validate a stored prior contribution before it is ever subtracted, so a
|
|
16800
|
+
// tampered or corrupt cursor fails closed instead of silently rewinding buckets.
|
|
16801
|
+
// Enforces this provider's fixed canonical invariant (all totals safe
|
|
16802
|
+
// nonnegative integers, reasoning=0, conversation_count=1,
|
|
16803
|
+
// total=input+cached+cacheCreation+output, billable=total) plus a canonical
|
|
16804
|
+
// half-hour UTC bucketStart.
|
|
16805
|
+
function validateTraeCnStoredContribution(entry) {
|
|
16806
|
+
if (!entry || typeof entry !== "object") {
|
|
16807
|
+
throw new Error("Trae CN stored session contribution is malformed.");
|
|
16808
|
+
}
|
|
16809
|
+
if (
|
|
16810
|
+
typeof entry.model !== "string" ||
|
|
16811
|
+
!entry.model.trim() ||
|
|
16812
|
+
entry.model.includes("|")
|
|
16813
|
+
) {
|
|
16814
|
+
throw new Error("Trae CN stored session model is malformed.");
|
|
16815
|
+
}
|
|
16816
|
+
if (!isValidTraeCnBucketStart(entry.bucketStart)) {
|
|
16817
|
+
throw new Error("Trae CN stored session bucket is malformed.");
|
|
16818
|
+
}
|
|
16819
|
+
if (!isValidTraeCnTotals(entry.totals)) {
|
|
16820
|
+
throw new Error("Trae CN stored session totals are malformed.");
|
|
16821
|
+
}
|
|
16822
|
+
const { totals } = entry;
|
|
16823
|
+
if (totals.reasoning_output_tokens !== 0 || totals.conversation_count !== 1) {
|
|
16824
|
+
throw new Error("Trae CN stored session totals are malformed.");
|
|
16825
|
+
}
|
|
16826
|
+
const sum =
|
|
16827
|
+
totals.input_tokens +
|
|
16828
|
+
totals.cached_input_tokens +
|
|
16829
|
+
totals.cache_creation_input_tokens +
|
|
16830
|
+
totals.output_tokens;
|
|
16831
|
+
if (totals.total_tokens !== sum || totals.billable_total_tokens !== totals.total_tokens) {
|
|
16832
|
+
throw new Error("Trae CN stored session totals are malformed.");
|
|
16833
|
+
}
|
|
16834
|
+
}
|
|
16835
|
+
|
|
16836
|
+
// Token fields may live in `row.extra_info` (object or JSON string) or at the
|
|
16837
|
+
// top level of the row. Missing values are NOT coerced to zero.
|
|
16838
|
+
function traeCnExtraInfo(row) {
|
|
16839
|
+
const extra = row?.extra_info;
|
|
16840
|
+
if (extra && typeof extra === "object" && !Array.isArray(extra)) return extra;
|
|
16841
|
+
if (typeof extra === "string") {
|
|
16842
|
+
try {
|
|
16843
|
+
const parsed = JSON.parse(extra);
|
|
16844
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
16845
|
+
} catch (_e) {}
|
|
16846
|
+
}
|
|
16847
|
+
return null;
|
|
16848
|
+
}
|
|
16849
|
+
|
|
16850
|
+
function traeCnTokenField(row, extra, key) {
|
|
16851
|
+
if (extra && extra[key] !== undefined && extra[key] !== null) return extra[key];
|
|
16852
|
+
if (row && row[key] !== undefined && row[key] !== null) return row[key];
|
|
16853
|
+
return undefined;
|
|
16854
|
+
}
|
|
16855
|
+
|
|
16856
|
+
// Normalize one raw session row into the canonical contribution, or throw
|
|
16857
|
+
// before any cursor/bucket mutation happens. Returns
|
|
16858
|
+
// { sessionId, model, bucketStart, totals }.
|
|
16859
|
+
function normalizeTraeCnSession(row) {
|
|
16860
|
+
const sessionId = typeof row?.session_id === "string" ? row.session_id.trim() : "";
|
|
16861
|
+
if (!sessionId) {
|
|
16862
|
+
throw new Error("Trae CN session row is missing session_id.");
|
|
16863
|
+
}
|
|
16864
|
+
let model;
|
|
16865
|
+
if (row?.model_name === undefined || row?.model_name === null) {
|
|
16866
|
+
model = TRAE_CN_UNKNOWN_MODEL;
|
|
16867
|
+
} else if (typeof row.model_name === "string") {
|
|
16868
|
+
model = row.model_name.trim() || TRAE_CN_UNKNOWN_MODEL;
|
|
16869
|
+
} else {
|
|
16870
|
+
throw new Error("Trae CN session row has an invalid model_name.");
|
|
16871
|
+
}
|
|
16872
|
+
if (model.includes("|")) {
|
|
16873
|
+
throw new Error("Trae CN session row has an unsupported model name.");
|
|
16874
|
+
}
|
|
16875
|
+
if (!Number.isSafeInteger(row?.usage_time) || row.usage_time <= 0) {
|
|
16876
|
+
throw new Error("Trae CN session row has an invalid usage_time.");
|
|
16877
|
+
}
|
|
16878
|
+
const bucketStart = toUtcHalfHourStart(row.usage_time * 1000);
|
|
16879
|
+
if (!bucketStart) {
|
|
16880
|
+
throw new Error("Trae CN session row has an invalid usage_time.");
|
|
16881
|
+
}
|
|
16882
|
+
const extra = traeCnExtraInfo(row);
|
|
16883
|
+
// Cache fields may be absent for models without a prompt-cache concept
|
|
16884
|
+
// (Doubao / DeepSeek on TRAE CN) — an absent cache field means "no cache
|
|
16885
|
+
// activity", i.e. 0, NOT a malformed row. input/output stay mandatory.
|
|
16886
|
+
//
|
|
16887
|
+
// `input_token` is cache-INCLUSIVE: verifying real rows against TRAE CN's
|
|
16888
|
+
// own credit billing, credits = fresh_input*p + cache_read*(p/4) + output*q
|
|
16889
|
+
// fits every row with zero residual only under this reading (same
|
|
16890
|
+
// convention as Codex / Qoder). Peel the cache subsets off into their own
|
|
16891
|
+
// columns and report only the remainder as ordinary input, otherwise cached
|
|
16892
|
+
// context is double-counted in dashboards and cost calculations. cache_write
|
|
16893
|
+
// was 0 across all observed rows; it is treated as a further subset of the
|
|
16894
|
+
// cache-inclusive input (clamped so the buckets always sum back to it).
|
|
16895
|
+
const cacheRead = traeCnTokenField(row, extra, "cache_read_token");
|
|
16896
|
+
const cacheWrite = traeCnTokenField(row, extra, "cache_write_token");
|
|
16897
|
+
const fields = [
|
|
16898
|
+
["input_token", traeCnTokenField(row, extra, "input_token")],
|
|
16899
|
+
["output_token", traeCnTokenField(row, extra, "output_token")],
|
|
16900
|
+
["cache_read_token", cacheRead === undefined || cacheRead === null ? 0 : cacheRead],
|
|
16901
|
+
["cache_write_token", cacheWrite === undefined || cacheWrite === null ? 0 : cacheWrite],
|
|
16902
|
+
];
|
|
16903
|
+
for (const [label, value] of fields) {
|
|
16904
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
16905
|
+
throw new Error(`Trae CN session row has an invalid ${label}.`);
|
|
16906
|
+
}
|
|
16907
|
+
}
|
|
16908
|
+
const rawInput = fields[0][1];
|
|
16909
|
+
const outputTokens = fields[1][1];
|
|
16910
|
+
const cachedInput = Math.min(rawInput, fields[2][1]);
|
|
16911
|
+
const cacheCreation = Math.min(rawInput - cachedInput, fields[3][1]);
|
|
16912
|
+
const total = rawInput + outputTokens;
|
|
16913
|
+
if (!Number.isSafeInteger(total)) {
|
|
16914
|
+
throw new Error("Trae CN session row token totals overflow.");
|
|
16915
|
+
}
|
|
16916
|
+
return {
|
|
16917
|
+
sessionId,
|
|
16918
|
+
model,
|
|
16919
|
+
bucketStart,
|
|
16920
|
+
totals: {
|
|
16921
|
+
input_tokens: rawInput - cachedInput - cacheCreation,
|
|
16922
|
+
output_tokens: outputTokens,
|
|
16923
|
+
cached_input_tokens: cachedInput,
|
|
16924
|
+
cache_creation_input_tokens: cacheCreation,
|
|
16925
|
+
reasoning_output_tokens: 0,
|
|
16926
|
+
total_tokens: total,
|
|
16927
|
+
billable_total_tokens: total,
|
|
16928
|
+
conversation_count: 1,
|
|
16929
|
+
},
|
|
16930
|
+
};
|
|
16931
|
+
}
|
|
16932
|
+
|
|
16933
|
+
// Fail closed instead of letting subtractTotals clamp a stored prior
|
|
16934
|
+
// contribution that exceeds its source bucket (a corruption signal).
|
|
16935
|
+
function assertTraeCnBucketCovers(bucketTotals, previousTotals) {
|
|
16936
|
+
for (const key of [
|
|
16937
|
+
"input_tokens",
|
|
16938
|
+
"cached_input_tokens",
|
|
16939
|
+
"cache_creation_input_tokens",
|
|
16940
|
+
"output_tokens",
|
|
16941
|
+
"reasoning_output_tokens",
|
|
16942
|
+
"total_tokens",
|
|
16943
|
+
"billable_total_tokens",
|
|
16944
|
+
"conversation_count",
|
|
16945
|
+
]) {
|
|
16946
|
+
if ((bucketTotals[key] || 0) < (previousTotals[key] || 0)) {
|
|
16947
|
+
throw new Error("Trae CN state corruption: stored contribution exceeds its bucket totals.");
|
|
16948
|
+
}
|
|
16949
|
+
}
|
|
16950
|
+
}
|
|
16951
|
+
|
|
16952
|
+
// Account session state: a queue record (kind: "account_session_state")
|
|
16953
|
+
// carrying the CANONICAL observation of ONE provider-side TRAE CN session.
|
|
16954
|
+
// Cloud truth for trae-cn lives at the session level
|
|
16955
|
+
// (tokentracker_account_session_states; ingest edge upserts via
|
|
16956
|
+
// tokentracker_upsert_account_session_states): identity is
|
|
16957
|
+
// (user_id, source, session_id) - device_id is NOT identity (the usage API
|
|
16958
|
+
// request carries no device discriminator). Evidence split (2026-08-17, one
|
|
16959
|
+
// account, three real fetches 137 -> 141 -> 164): repeated-fetch id
|
|
16960
|
+
// stability VERIFIED (137/137 persisted, corrections KEPT ids), cross-window
|
|
16961
|
+
// stability VERIFIED (exact subsets), no duplicate ids OBSERVED. Cross-device
|
|
16962
|
+
// same-account id stability is NOT DIRECTLY VERIFIED - no device
|
|
16963
|
+
// discriminator in the request body is necessary but not sufficient (a
|
|
16964
|
+
// device/login context could ride inside the JWT / server auth context), and
|
|
16965
|
+
// no second independent device/auth experiment was run.
|
|
16966
|
+
//
|
|
16967
|
+
// The three correction classes collapse into ONE whole-row replace:
|
|
16968
|
+
// downward S tokens 100 -> 60
|
|
16969
|
+
// model S model A -> B
|
|
16970
|
+
// bucket S bucket 10:00 -> 10:30
|
|
16971
|
+
// A fresh device with no cursor history uploads every session it observes;
|
|
16972
|
+
// the cloud LWW guard reconciles versions. ABSENCE is NOT PROVEN to mean
|
|
16973
|
+
// deletion, so nothing is ever emitted for sessions missing from a
|
|
16974
|
+
// non-empty snapshot, and an empty payload emits nothing at all.
|
|
16975
|
+
//
|
|
16976
|
+
// snapshot_verified_at is the CLIENT logical fetch stamp (the API exposes no
|
|
16977
|
+
// provider-side ordering signal - headers carry only CDN trace ids, rows
|
|
16978
|
+
// carry no revision). It is stamped once per real fetch and replayed
|
|
16979
|
+
// verbatim by this append-only queue; the cloud upsert applies strictly
|
|
16980
|
+
// newer (>) stamps only, so replays are idempotent and a transport retry of
|
|
16981
|
+
// an older observation cannot displace a newer one. Cross-device ordering
|
|
16982
|
+
// under clock skew is a documented residual risk, NOT strict correctness.
|
|
16983
|
+
//
|
|
16984
|
+
// Appended AFTER the bucket rows of the same sync (queue order guarantees a
|
|
16985
|
+
// device's rows land before the states describing them). Bucket-row readers
|
|
16986
|
+
// skip this record via its kind field (it carries no hour_start and is not
|
|
16987
|
+
// a usage row).
|
|
16988
|
+
async function appendTraeCnSessionStates({ queuePath, observations, verifiedAtMs }) {
|
|
16989
|
+
if (!Array.isArray(observations) || observations.length === 0) return 0;
|
|
16990
|
+
if (!Number.isFinite(verifiedAtMs)) {
|
|
16991
|
+
throw new Error("Trae CN session states require a finite verification stamp.");
|
|
16992
|
+
}
|
|
16993
|
+
const verifiedAt = new Date(verifiedAtMs).toISOString();
|
|
16994
|
+
const lines = [];
|
|
16995
|
+
for (const obs of observations) {
|
|
16996
|
+
const t = obs.totals;
|
|
16997
|
+
lines.push(
|
|
16998
|
+
JSON.stringify({
|
|
16999
|
+
kind: "account_session_state",
|
|
17000
|
+
source: TRAE_CN_SOURCE,
|
|
17001
|
+
session_id: obs.sessionId,
|
|
17002
|
+
model: obs.model,
|
|
17003
|
+
bucket_start: obs.bucketStart,
|
|
17004
|
+
input_tokens: t.input_tokens,
|
|
17005
|
+
output_tokens: t.output_tokens,
|
|
17006
|
+
cached_input_tokens: t.cached_input_tokens,
|
|
17007
|
+
cache_creation_input_tokens: t.cache_creation_input_tokens,
|
|
17008
|
+
reasoning_output_tokens: t.reasoning_output_tokens,
|
|
17009
|
+
total_tokens: t.total_tokens,
|
|
17010
|
+
snapshot_verified_at: verifiedAt,
|
|
17011
|
+
}),
|
|
17012
|
+
);
|
|
17013
|
+
}
|
|
17014
|
+
await fs.appendFile(queuePath, lines.join("\n") + "\n", "utf8");
|
|
17015
|
+
return lines.length;
|
|
17016
|
+
}
|
|
17017
|
+
|
|
17018
|
+
async function parseTraeCnApiIncremental({
|
|
17019
|
+
sessions,
|
|
17020
|
+
cursors,
|
|
17021
|
+
queuePath,
|
|
17022
|
+
onProgress,
|
|
17023
|
+
windowStartMs,
|
|
17024
|
+
windowEndMs,
|
|
17025
|
+
snapshotVerifiedAtMs,
|
|
17026
|
+
} = {}) {
|
|
17027
|
+
if (!Array.isArray(sessions)) {
|
|
17028
|
+
throw new Error("Trae CN sessions must be an array.");
|
|
17029
|
+
}
|
|
17030
|
+
if (!cursors || typeof cursors !== "object" || Array.isArray(cursors)) {
|
|
17031
|
+
throw new Error("Trae CN cursors must be a writable object.");
|
|
17032
|
+
}
|
|
17033
|
+
await ensureDir(path.dirname(queuePath));
|
|
17034
|
+
|
|
17035
|
+
// Validate persisted state up front (fresh absent state initializes version
|
|
17036
|
+
// 1; malformed state / unexpected version fails closed instead of resetting).
|
|
17037
|
+
// Deep-clone the normalized working states so reconciliation and enqueue
|
|
17038
|
+
// mutations never touch `cursors` before the final assignment succeeds.
|
|
17039
|
+
const hourlyState = structuredClone(normalizeHourlyState(cursors?.hourly));
|
|
17040
|
+
const traeCnState = structuredClone(normalizeTraeCnState(cursors?.traeCn));
|
|
17041
|
+
|
|
17042
|
+
if (sessions.length === 0) {
|
|
17043
|
+
// Empty payload is a successful no-op: no usage mutation, no session
|
|
17044
|
+
// state records.
|
|
17045
|
+
//
|
|
17046
|
+
// Evidence check (2026-08-16, two real fetches 17min apart over the same
|
|
17047
|
+
// account: 137 -> 141 sessions, 0 disappeared, 0 rows changed) shows the
|
|
17048
|
+
// session set is stable, but NOTHING proves 'absent from the response'
|
|
17049
|
+
// means 'authoritatively deleted / zero' — so the absence contract is
|
|
17050
|
+
// NOT PROVEN and must stay symmetric everywhere: a session missing from
|
|
17051
|
+
// a non-empty snapshot never emits a retraction, and an empty response
|
|
17052
|
+
// asserts nothing at all. Canonical corrections ride on explicit
|
|
17053
|
+
// observations only.
|
|
17054
|
+
return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
17055
|
+
}
|
|
17056
|
+
|
|
17057
|
+
// Full prevalidation + in-payload dedupe BEFORE any cursor/bucket mutation.
|
|
17058
|
+
//
|
|
17059
|
+
// ANY malformed row fails the whole snapshot closed. Canonical session
|
|
17060
|
+
// states assert this device actually understood the snapshot, so a
|
|
17061
|
+
// partially understood snapshot (99 valid + 1 uninterpretable row) must
|
|
17062
|
+
// never be enqueued: it would become the canonical cloud state. The
|
|
17063
|
+
// provider-level try/catch in cmdSync keeps the failure
|
|
17064
|
+
// isolated (other providers sync on) and the error message carries only
|
|
17065
|
+
// the count + reason - no session ids, tokens, or credentials. Confirmed
|
|
17066
|
+
// LEGAL variations must not land here: absent cache fields are already
|
|
17067
|
+
// coerced to 0 inside normalizeTraeCnSession (models without a prompt
|
|
17068
|
+
// cache concept); only truly uninterpretable rows (bad session_id /
|
|
17069
|
+
// usage_time / token numbers / model, impossible schema) throw.
|
|
17070
|
+
const bySession = new Map();
|
|
17071
|
+
let skippedRows = 0;
|
|
17072
|
+
let firstSkipReason = "";
|
|
17073
|
+
for (const row of sessions) {
|
|
17074
|
+
let normalized;
|
|
17075
|
+
try {
|
|
17076
|
+
normalized = normalizeTraeCnSession(row);
|
|
17077
|
+
} catch (error) {
|
|
17078
|
+
skippedRows += 1;
|
|
17079
|
+
if (!firstSkipReason) firstSkipReason = error?.message || "unknown";
|
|
17080
|
+
continue;
|
|
17081
|
+
}
|
|
17082
|
+
const existing = bySession.get(normalized.sessionId);
|
|
17083
|
+
if (existing) {
|
|
17084
|
+
const identical =
|
|
17085
|
+
existing.model === normalized.model &&
|
|
17086
|
+
existing.bucketStart === normalized.bucketStart &&
|
|
17087
|
+
totalsKey(existing.totals) === totalsKey(normalized.totals);
|
|
17088
|
+
if (!identical) {
|
|
17089
|
+
throw new Error("Trae CN session rows contain conflicting contributions.");
|
|
17090
|
+
}
|
|
17091
|
+
continue; // exact duplicate within one payload is accepted once
|
|
17092
|
+
}
|
|
17093
|
+
bySession.set(normalized.sessionId, normalized);
|
|
17094
|
+
}
|
|
17095
|
+
if (skippedRows > 0) {
|
|
17096
|
+
// Partial snapshots are not authoritative: one uninterpretable row means
|
|
17097
|
+
// the window was NOT fully verified, so fail closed - no bucket rows, no
|
|
17098
|
+
// session states, no cursor commit. The next sync replays from the same
|
|
17099
|
+
// cursor state (idempotent).
|
|
17100
|
+
throw new Error(
|
|
17101
|
+
`Trae CN snapshot is not authoritative: ${skippedRows} malformed row${skippedRows !== 1 ? "s" : ""} (first: ${firstSkipReason}).`,
|
|
17102
|
+
);
|
|
17103
|
+
}
|
|
17104
|
+
const touchedBuckets = new Set();
|
|
17105
|
+
const cb = typeof onProgress === "function" ? onProgress : null;
|
|
17106
|
+
const total = sessions.length;
|
|
17107
|
+
let index = 0;
|
|
17108
|
+
let eventsAggregated = 0;
|
|
17109
|
+
|
|
17110
|
+
// Validate EVERY existing stored contribution up front — not just sessions
|
|
17111
|
+
// present in the incoming payload — so an unrelated malformed entry fails
|
|
17112
|
+
// closed before any reconciliation mutation.
|
|
17113
|
+
for (const sessionId of Object.keys(traeCnState.sessions)) {
|
|
17114
|
+
validateTraeCnStoredContribution(traeCnState.sessions[sessionId]);
|
|
17115
|
+
}
|
|
17116
|
+
|
|
17117
|
+
// Monotonic prune (same watermark pattern as the kiroCli cap above): the
|
|
17118
|
+
// sync caller fetches a fixed trailing window, so a session whose entire
|
|
17119
|
+
// half-hour bucket sits before windowStartMs can never reappear in a later
|
|
17120
|
+
// payload — its stored contribution is dead reconciliation weight. Prune
|
|
17121
|
+
// only when the window start moved FORWARD past the persisted watermark:
|
|
17122
|
+
// an entry evicted under window W1 and re-fetched under an earlier window
|
|
17123
|
+
// would find previousEntry undefined and count twice. The +30min margin
|
|
17124
|
+
// matches bucketStart's half-hour floor, which can trail the row's
|
|
17125
|
+
// usage_time by up to 30 minutes.
|
|
17126
|
+
if (Number.isFinite(windowStartMs) && windowStartMs > traeCnState.prunedBeforeMs) {
|
|
17127
|
+
const pruneCutoffMs = windowStartMs - 30 * 60 * 1000;
|
|
17128
|
+
for (const sessionId of Object.keys(traeCnState.sessions)) {
|
|
17129
|
+
const bucketMs = Date.parse(traeCnState.sessions[sessionId].bucketStart);
|
|
17130
|
+
if (Number.isFinite(bucketMs) && bucketMs <= pruneCutoffMs) {
|
|
17131
|
+
delete traeCnState.sessions[sessionId];
|
|
17132
|
+
}
|
|
17133
|
+
}
|
|
17134
|
+
traeCnState.prunedBeforeMs = windowStartMs;
|
|
17135
|
+
}
|
|
17136
|
+
|
|
17137
|
+
// Deterministic: process unique session ids in sorted order. Sessions
|
|
17138
|
+
// whose canonical observation changed (or that were never seen) are also
|
|
17139
|
+
// collected for the account_session_state queue records.
|
|
17140
|
+
const sessionIds = [...bySession.keys()].sort();
|
|
17141
|
+
const changedObservations = [];
|
|
17142
|
+
for (const sessionId of sessionIds) {
|
|
17143
|
+
const current = bySession.get(sessionId);
|
|
17144
|
+
const previousEntry = traeCnState.sessions[sessionId];
|
|
17145
|
+
|
|
17146
|
+
const unchanged =
|
|
17147
|
+
previousEntry &&
|
|
17148
|
+
totalsKey(previousEntry.totals) === totalsKey(current.totals) &&
|
|
17149
|
+
previousEntry.bucketStart === current.bucketStart &&
|
|
17150
|
+
previousEntry.model === current.model;
|
|
17151
|
+
if (unchanged) {
|
|
17152
|
+
index += 1;
|
|
17153
|
+
if (cb) cb({ index, total, eventsAggregated, bucketsQueued: touchedBuckets.size });
|
|
17154
|
+
continue;
|
|
17155
|
+
}
|
|
17156
|
+
|
|
17157
|
+
if (previousEntry) {
|
|
17158
|
+
const oldBucket = getHourlyBucket(
|
|
17159
|
+
hourlyState,
|
|
17160
|
+
TRAE_CN_SOURCE,
|
|
17161
|
+
previousEntry.model,
|
|
17162
|
+
previousEntry.bucketStart,
|
|
17163
|
+
);
|
|
17164
|
+
assertTraeCnBucketCovers(oldBucket.totals, previousEntry.totals);
|
|
17165
|
+
subtractTotals(oldBucket.totals, previousEntry.totals);
|
|
17166
|
+
touchedBuckets.add(bucketKey(TRAE_CN_SOURCE, previousEntry.model, previousEntry.bucketStart));
|
|
17167
|
+
}
|
|
17168
|
+
|
|
17169
|
+
const bucket = getHourlyBucket(hourlyState, TRAE_CN_SOURCE, current.model, current.bucketStart);
|
|
17170
|
+
addTotals(bucket.totals, current.totals);
|
|
17171
|
+
touchedBuckets.add(bucketKey(TRAE_CN_SOURCE, current.model, current.bucketStart));
|
|
17172
|
+
|
|
17173
|
+
traeCnState.sessions[sessionId] = {
|
|
17174
|
+
model: current.model,
|
|
17175
|
+
bucketStart: current.bucketStart,
|
|
17176
|
+
totals: { ...current.totals },
|
|
17177
|
+
updatedAt: new Date().toISOString(),
|
|
17178
|
+
};
|
|
17179
|
+
changedObservations.push({
|
|
17180
|
+
sessionId,
|
|
17181
|
+
model: current.model,
|
|
17182
|
+
bucketStart: current.bucketStart,
|
|
17183
|
+
totals: current.totals,
|
|
17184
|
+
});
|
|
17185
|
+
eventsAggregated += 1;
|
|
17186
|
+
index += 1;
|
|
17187
|
+
if (cb) cb({ index, total, eventsAggregated, bucketsQueued: touchedBuckets.size });
|
|
17188
|
+
}
|
|
17189
|
+
|
|
17190
|
+
const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
|
|
17191
|
+
// Emit canonical session observations only after the reconciled bucket
|
|
17192
|
+
// rows are durably queued; a failure here aborts before cursor commit, so
|
|
17193
|
+
// the next sync replays the (idempotent) reconciliation and re-appends
|
|
17194
|
+
// both. snapshot_verified_at is stamped once per real fetch; the
|
|
17195
|
+
// append-only queue replays it verbatim, so transport retries never fake
|
|
17196
|
+
// freshness. Unchanged sessions emit nothing - a fixed-now no-change sync
|
|
17197
|
+
// stays byte-identical.
|
|
17198
|
+
const verifiedAtMs = Number.isFinite(snapshotVerifiedAtMs) ? snapshotVerifiedAtMs : Date.now();
|
|
17199
|
+
await appendTraeCnSessionStates({
|
|
17200
|
+
queuePath,
|
|
17201
|
+
observations: changedObservations,
|
|
17202
|
+
verifiedAtMs,
|
|
17203
|
+
});
|
|
17204
|
+
const updatedAt = new Date().toISOString();
|
|
17205
|
+
hourlyState.updatedAt = updatedAt;
|
|
17206
|
+
traeCnState.updatedAt = updatedAt;
|
|
17207
|
+
// Assign cursor state only after enqueue succeeds.
|
|
17208
|
+
cursors.hourly = hourlyState;
|
|
17209
|
+
cursors.traeCn = traeCnState;
|
|
17210
|
+
|
|
17211
|
+
return { recordsProcessed: total, eventsAggregated, bucketsQueued, skippedRows };
|
|
17212
|
+
}
|
|
17213
|
+
|
|
16381
17214
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
16382
17215
|
// DeepSeek Harness (dsh) — passive reader of the harness's session logs.
|
|
16383
17216
|
//
|
|
@@ -17182,6 +18015,11 @@ module.exports = {
|
|
|
17182
18015
|
resolvePiDefaultModel,
|
|
17183
18016
|
parsePiIncremental,
|
|
17184
18017
|
piAgentDirCollidesWithOmp,
|
|
18018
|
+
resolvePrimeAgentHome,
|
|
18019
|
+
resolvePrimeAgentDir,
|
|
18020
|
+
resolvePrimeAgentSessionFiles,
|
|
18021
|
+
resolvePrimeAgentDefaultModel,
|
|
18022
|
+
parsePrimeAgentIncremental,
|
|
17185
18023
|
resolveCraftConfigDir,
|
|
17186
18024
|
resolveCraftWorkspaceRoots,
|
|
17187
18025
|
resolveCraftSessionFiles,
|
|
@@ -17226,6 +18064,7 @@ module.exports = {
|
|
|
17226
18064
|
resolveTraePath,
|
|
17227
18065
|
resolveTraeStoragePath,
|
|
17228
18066
|
readTraeEntitlementFromStorage,
|
|
18067
|
+
parseTraeCnApiIncremental,
|
|
17229
18068
|
// DeepSeek Harness (dsh) — passive session-log reader
|
|
17230
18069
|
resolveDshHome,
|
|
17231
18070
|
resolveDshHomes,
|