tokentracker-cli 0.91.0 → 0.92.0
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-iyRg0rYF.js → AchievementBadge-o8hl3my2.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-CkBm3bN3.js → AchievementsPage-CjDEyZfD.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-a4Z7VtlP.js → AchievementsSection-Cwv1Zmyf.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-DHTN0Ky6.js → BadgeDetailModal-BDxTgOvT.js} +1 -1
- package/dashboard/dist/assets/{Card-CdygGEqz.js → Card-Cp5v7Y19.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-D-Z823Nc.js → ClawdAnimated-6Ibb4qCZ.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-VGkj8w2X.js → CommandPalette-CIf9oAqq.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-BDT4xqfF.js → CommunityStatsModal-UwiOPFu0.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-BIKv4AlL.js → ConfirmModal-xkYsrh0v.js} +1 -1
- package/dashboard/dist/assets/{Controls-BkZVm9t1.js → Controls-C2Z2X0ZQ.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-BFkdjUdd.js → DashboardPage-Do_NMUFK.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-BGw861bu.js → DevicePage-DL5OjugJ.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-CSEl6ru8.js → DialogClose-D0w08oR2.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-rhGVm7N2.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-CWPdu5V-.js → DialogTitle-IKAPm5Pk.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-CdCANtvs.js → FadeIn-Dve77aEj.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-dQQnH85f.js → HeaderGithubStar-BVYbLWDi.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-CqJUI-WG.js → HoverTooltip-BrF9-z7f.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-BRqTXlSu.js → IpCheckPage-BZ1VyVsm.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-DhMTgDng.js → LandingPage-jkjtQl58.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-BrGcByG1.js → LeaderboardAvatar-D9kUudjQ.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-BmdopPjo.js → LeaderboardPage-DXg7Qv8T.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-DRZqPo-A.js → LeaderboardProfileModal-jTlaOi-j.js} +4 -4
- package/dashboard/dist/assets/{LeaderboardProfilePage-CTbC39KX.js → LeaderboardProfilePage-DsKQ_Of8.js} +3 -3
- package/dashboard/dist/assets/{LimitsPage-B7AuLq1S.js → LimitsPage-CMqZAQCa.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-DyIAspoq.js → LocalOnlyNotice-CkLs3CF_.js} +1 -1
- package/dashboard/dist/assets/LoginCard-SmOXM_PX.js +1 -0
- package/dashboard/dist/assets/{LoginModal-D3bmr_jI.js → LoginModal-CvtEBkrO.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-D7SgmEhZ.js → LoginPage-Zm-dRt_Y.js} +1 -1
- package/dashboard/dist/assets/{PetPage-5A3Q0zlw.js → PetPage-Bcj7hzNz.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-ajEhdI8m.js → PopoverPopup-CnteSROW.js} +1 -1
- package/dashboard/dist/assets/ProviderIcon-DcQmGbfz.js +1 -0
- package/dashboard/dist/assets/{ResetPasswordPage-Bwbt8tH2.js → ResetPasswordPage-zMb_m3Cx.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-BSa5hMnN.js → SegmentedControl-D3KkbQ7T.js} +1 -1
- package/dashboard/dist/assets/{Select-kPKDGb7M.js → Select-0RiFEISo.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-DNgLclTV.js → SelectItemText-BRV3DFEB.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-C1j7rPDp.js → ServiceStatusPage-CWF_vKOC.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-BgoKs_Yq.js → SessionsPage-DIqoT7MU.js} +2 -2
- package/dashboard/dist/assets/{SettingsPage-BUaPftV0.js → SettingsPage-DQZIqiE5.js} +2 -2
- package/dashboard/dist/assets/{SkillsPage-C5GkzfWW.js → SkillsPage-BIIOUHnq.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-DZffAa_0.js → TokenGalaxy-Bulaqjbm.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-CuLP92P_.js → ToolbarRootContext-Dg-zL8fE.js} +1 -1
- package/dashboard/dist/assets/TrendMonitor-DfafDX6b.js +19 -0
- package/dashboard/dist/assets/{WidgetsPage-BUhiCpcq.js → WidgetsPage-F_2yLE7n.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-sNFISXVG.js → WrappedPage-BFxYKGQs.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-Cyw7ZNtj.js → __vite-browser-external-CZMdPW0g.js} +4 -3
- package/dashboard/dist/assets/agent-logos-DjyCpabA.js +1 -0
- package/dashboard/dist/assets/{arrow-up-right-BcRkt7hr.js → arrow-up-right-CIAd6sXA.js} +1 -1
- package/dashboard/dist/assets/{check-qhr4U2S5.js → check-Bt3Yu07t.js} +1 -1
- package/dashboard/dist/assets/{chevron-right-iLGEAP7_.js → chevron-right-BiXULxma.js} +1 -1
- package/dashboard/dist/assets/{copy-BhEPCq-q.js → copy-D3k1Xbfl.js} +1 -1
- package/dashboard/dist/assets/{download-D7O1ZzsX.js → download-co2WHJvo.js} +1 -1
- package/dashboard/dist/assets/{external-link-CY2kK2hC.js → external-link-D5az2QwL.js} +1 -1
- package/dashboard/dist/assets/{flame-D4FPuBNn.js → flame-CSCUyn3Z.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-CVHJ7Hdh.js → format-tokens-BebRLYzr.js} +1 -1
- package/dashboard/dist/assets/{icons-B_-51UXa.js → icons-C_y5qr6T.js} +1 -1
- package/dashboard/dist/assets/{index-ClVWat-1.js → index-CXOOOeSl.js} +1 -1
- package/dashboard/dist/assets/{index-cwdr25Zm.js → index-DP3XlgDm.js} +1 -1
- package/dashboard/dist/assets/{index-Djv-aJy0.js → index-R488h3mD.js} +1 -1
- package/dashboard/dist/assets/{info-DOab0eZz.js → info-CD_G_DZp.js} +1 -1
- package/dashboard/dist/assets/{limits-providers-CESm2d6B.js → limits-providers-BlAlATd7.js} +1 -1
- package/dashboard/dist/assets/{link-2-B4LMsC4m.js → link-2-DIn-EC5g.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-CPx6wpSn.js → loader-circle-B5Az_VQe.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-C9z8mbdn.js → maximize-2-ptnkCbEN.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-5xRgAl_R.js → modifiers.esm-DEN3VPKe.js} +1 -1
- package/dashboard/dist/assets/module-CV1mYFsv.js +3 -0
- package/dashboard/dist/assets/{monitor-smartphone-B9HAspdR.js → monitor-smartphone-Dqr_CnOQ.js} +1 -1
- package/dashboard/dist/assets/provider-display-BOAoggwg.js +42 -0
- package/dashboard/dist/assets/{react-C45OGvDd.js → react-B3eL33ha.js} +1 -1
- package/dashboard/dist/assets/{search-Bg1qyEdz.js → search-CNNSHW9P.js} +1 -1
- package/dashboard/dist/assets/{skills-api-a8pi7UTd.js → skills-api-BShLlV2m.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-CtPp8cFA.js → sortable.esm-C6kDS9DZ.js} +1 -1
- package/dashboard/dist/assets/{store-b8zXM1yr.js → store-B4atnDzu.js} +1 -1
- package/dashboard/dist/assets/{terminal-CPf0K0Fn.js → terminal-CXMttuiJ.js} +1 -1
- package/dashboard/dist/assets/{trash-2-uorzBdA0.js → trash-2-aXKBjf5M.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-DaVwywsM.js → use-community-stats-rGR2J-Vi.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-UIzz7PJ3.js → use-limits-display-prefs-CazeqdNI.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-C2XOfMH3.js → use-ordered-list-Bx-enbl8.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-2NcjtyzY.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-D5KHakYg.js → use-session-efficiency-pref-B7RHUtup.js} +1 -1
- package/dashboard/dist/assets/{use-transform-Cfo_2_id.js → use-transform-CWVOY76a.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-CANTpxrA.js → use-usage-limits-C9XRiYNi.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-CjvWekKH.js → useCurrency-D7ICju5j.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-ChQdbuJg.js → useScrollLock-DXWPQF_D.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-4MY04JPl.js → useTokenFormat-wDhHrsAL.js} +1 -1
- package/dashboard/dist/assets/{zap-BSXHFUBb.js → zap-BZDqgZR1.js} +1 -1
- package/dashboard/dist/brand-logos/dots.png +0 -0
- package/dashboard/dist/brand-logos/pi.svg +8 -15
- package/dashboard/dist/index.html +31 -26
- 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 +3 -1
- package/src/commands/status.js +46 -0
- package/src/commands/sync.js +188 -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 +32 -1
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +730 -1
- package/src/lib/source-metadata.js +1 -1
- package/src/lib/trae-cn-config.js +539 -0
- package/dashboard/dist/assets/DialogDescription-DfNXnqay.js +0 -1
- package/dashboard/dist/assets/LoginCard-LcOaKaCK.js +0 -1
- package/dashboard/dist/assets/ProviderIcon-C4yuu9FG.js +0 -1
- package/dashboard/dist/assets/TrendMonitor-BKLAKJmT.js +0 -19
- package/dashboard/dist/assets/agent-logos-Bwf2YFHS.js +0 -1
- package/dashboard/dist/assets/module-BRJxSJmz.js +0 -3
- package/dashboard/dist/assets/provider-display-C0NS0EvN.js +0 -42
- package/dashboard/dist/assets/use-reduced-motion-CNyMWBog.js +0 -1
package/src/lib/rollout.js
CHANGED
|
@@ -15673,10 +15673,61 @@ async function retractStaleGrokQueueRows(queuePath, keepKeys) {
|
|
|
15673
15673
|
return lines.length;
|
|
15674
15674
|
}
|
|
15675
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
|
+
|
|
15676
15725
|
async function parseGrokBuildIncremental({
|
|
15677
15726
|
sessions,
|
|
15678
15727
|
cursors = {},
|
|
15679
15728
|
queuePath,
|
|
15729
|
+
projectQueuePath,
|
|
15730
|
+
publicRepoResolver,
|
|
15680
15731
|
onProgress,
|
|
15681
15732
|
env = process.env
|
|
15682
15733
|
} = {}) {
|
|
@@ -15721,6 +15772,25 @@ async function parseGrokBuildIncremental({
|
|
|
15721
15772
|
? grokState.updateOffsets
|
|
15722
15773
|
: {};
|
|
15723
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
|
+
|
|
15724
15794
|
// Rebuilt from the sessions seen this scan, so entries for deleted session
|
|
15725
15795
|
// dirs are pruned and the cursor stays bounded by the on-disk session count.
|
|
15726
15796
|
const updateOffsets = {};
|
|
@@ -15893,11 +15963,167 @@ async function parseGrokBuildIncremental({
|
|
|
15893
15963
|
};
|
|
15894
15964
|
}
|
|
15895
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
|
+
|
|
15896
16120
|
if (onProgress) {
|
|
15897
16121
|
onProgress({ index: index + 1, total: sessionList.length, bucketsQueued: touchedBuckets.size });
|
|
15898
16122
|
}
|
|
15899
16123
|
}
|
|
15900
16124
|
|
|
16125
|
+
if (projectEnabled) pruneMissingGrokProjectUpdateOffsets(projectUpdateOffsets);
|
|
16126
|
+
|
|
15901
16127
|
let bucketsQueued = queuePath
|
|
15902
16128
|
? await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
|
|
15903
16129
|
: 0;
|
|
@@ -15915,8 +16141,20 @@ async function parseGrokBuildIncremental({
|
|
|
15915
16141
|
bucketsQueued += retracted;
|
|
15916
16142
|
}
|
|
15917
16143
|
|
|
16144
|
+
const projectBucketsQueued = projectEnabled
|
|
16145
|
+
? await enqueueTouchedProjectBuckets({
|
|
16146
|
+
projectQueuePath,
|
|
16147
|
+
projectState,
|
|
16148
|
+
projectTouchedBuckets,
|
|
16149
|
+
})
|
|
16150
|
+
: 0;
|
|
16151
|
+
|
|
15918
16152
|
hourlyState.updatedAt = new Date().toISOString();
|
|
15919
16153
|
cursors.hourly = hourlyState;
|
|
16154
|
+
if (projectState) {
|
|
16155
|
+
projectState.updatedAt = hourlyState.updatedAt;
|
|
16156
|
+
cursors.projectHourly = projectState;
|
|
16157
|
+
}
|
|
15920
16158
|
sessionSnapshots = capGrokSessionSnapshots(sessionSnapshots);
|
|
15921
16159
|
|
|
15922
16160
|
const migrations = grokState.migrations && typeof grokState.migrations === "object"
|
|
@@ -15936,6 +16174,12 @@ async function parseGrokBuildIncremental({
|
|
|
15936
16174
|
sessionSnapshots,
|
|
15937
16175
|
seenSessions: Object.keys(sessionSnapshots),
|
|
15938
16176
|
updateOffsets,
|
|
16177
|
+
...(projectEnabled
|
|
16178
|
+
? {
|
|
16179
|
+
projectUpdateOffsets,
|
|
16180
|
+
projectSessionSnapshots: capGrokSessionSnapshots(projectSessionSnapshots),
|
|
16181
|
+
}
|
|
16182
|
+
: {}),
|
|
15939
16183
|
migrations,
|
|
15940
16184
|
updatedAt: new Date().toISOString()
|
|
15941
16185
|
};
|
|
@@ -15943,7 +16187,8 @@ async function parseGrokBuildIncremental({
|
|
|
15943
16187
|
return {
|
|
15944
16188
|
recordsProcessed: eventsAggregated,
|
|
15945
16189
|
eventsAggregated,
|
|
15946
|
-
bucketsQueued
|
|
16190
|
+
bucketsQueued,
|
|
16191
|
+
projectBucketsQueued,
|
|
15947
16192
|
};
|
|
15948
16193
|
}
|
|
15949
16194
|
|
|
@@ -16483,6 +16728,489 @@ function readTraeEntitlementFromStorage(storagePath) {
|
|
|
16483
16728
|
};
|
|
16484
16729
|
}
|
|
16485
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
|
+
|
|
16486
17214
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
16487
17215
|
// DeepSeek Harness (dsh) — passive reader of the harness's session logs.
|
|
16488
17216
|
//
|
|
@@ -17336,6 +18064,7 @@ module.exports = {
|
|
|
17336
18064
|
resolveTraePath,
|
|
17337
18065
|
resolveTraeStoragePath,
|
|
17338
18066
|
readTraeEntitlementFromStorage,
|
|
18067
|
+
parseTraeCnApiIncremental,
|
|
17339
18068
|
// DeepSeek Harness (dsh) — passive session-log reader
|
|
17340
18069
|
resolveDshHome,
|
|
17341
18070
|
resolveDshHomes,
|