tokentracker-cli 0.87.8 → 0.87.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/dist/assets/{AchievementBadge-CpxX02OK.js → AchievementBadge-BG9bVXuQ.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-CSms8IjI.js → AchievementsPage-DwYIajgz.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-Sm43tnkh.js → AchievementsSection-BPuhWCWC.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-cSNqMLsK.js → BadgeDetailModal-BU81AHIH.js} +1 -1
- package/dashboard/dist/assets/{Card-wNwNRVSb.js → Card-CxiGV6bE.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-S8sIS2Ca.js → ClawdAnimated-NHcv2_KK.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-DVeiwbPM.js → CommandPalette-BKmTa5b8.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-g5P4j3um.js → CommunityStatsModal-nhHtSw7U.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-2P2D5iIp.js → ConfirmModal-DCicMVqg.js} +1 -1
- package/dashboard/dist/assets/{Controls-CCaotoQP.js → Controls-CtjjJ8gt.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-DxPGTJpe.js → DashboardPage-BL-NfLIM.js} +1 -1
- package/dashboard/dist/assets/DevicePage-DuIiuci7.js +1 -0
- package/dashboard/dist/assets/{DialogClose-D0giFwO3.js → DialogClose-8GK6rJCf.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-C9StmaUp.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-JS_04eUC.js → DialogTitle-EBphaJIt.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-tFzTWDkC.js → FadeIn-B8099njp.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-B2EUJUTH.js → HeaderGithubStar-Cyxc4vKa.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-BJzHksPT.js → HoverTooltip-BcXNDc20.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-B8mA0mut.js → IpCheckPage-Bn5aOiJK.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-DmJZDKr9.js → LandingPage-BMbP2M_2.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-CDeRQUCg.js → LeaderboardAvatar-sRNGsVJE.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-Dkgm89kY.js → LeaderboardPage-DUv9hCNQ.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-B6_kuR1z.js → LeaderboardProfileModal-D66DzEVL.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-BRs8sJBV.js → LeaderboardProfilePage-CWzNEHjJ.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-DZ1s5NWV.js → LimitsPage-B7dTsxXt.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-Cu_hSnux.js → LocalOnlyNotice-BCR-4Ipk.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-yDZbGsHR.js → LoginCard-Dt7v1Pgi.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-DEvzCUfQ.js → LoginModal-BaKS0AvW.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-DBySF9do.js → LoginPage-J7ir_N3K.js} +1 -1
- package/dashboard/dist/assets/{PetPage-DnzH9t-r.js → PetPage-uAxjE9Vs.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-DgFsEMax.js → PopoverPopup-WRGNwhEe.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-BEixe-xP.js → ProviderIcon-BTZXNlML.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-excKqsCL.js → ResetPasswordPage-B_TvqHvF.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-DDFkH64o.js → SegmentedControl-C1NPnzuC.js} +1 -1
- package/dashboard/dist/assets/{Select-ChEPIuJf.js → Select-DetlANRp.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText--b7ISnJm.js → SelectItemText-CdhCuD7u.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-DtAY-Qnn.js → ServiceStatusPage-yMTV03js.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-CKFglvuj.js → SessionsPage-rJw-lY6d.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-s0605qWg.js → SettingsPage-BHIGCXa8.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-DTsbCucq.js → SkillsPage-BZTTHrKR.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-C_M7q8S4.js → TokenGalaxy-D2tFBs42.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-CXQwpea-.js → ToolbarRootContext-CD4Dh9IV.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-1Yhlplf0.js → TrendMonitor-mL7Z3Pu9.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-DC6pmtld.js → WidgetsPage-Dech66hD.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-DixoqR13.js → WrappedPage-CYaLijy2.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-5aLUeBkM.js → __vite-browser-external-CcMKDoRA.js} +2 -2
- package/dashboard/dist/assets/{agent-logos-DILTbPLg.js → agent-logos-BzFhwsm5.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-DgIDgjVW.js → arrow-up-right-TrcWBi5O.js} +1 -1
- package/dashboard/dist/assets/{check-Bz6l09VH.js → check-CKWLtHpO.js} +1 -1
- package/dashboard/dist/assets/{chevron-right-EvGQXbI3.js → chevron-right-BjL_B3tf.js} +1 -1
- package/dashboard/dist/assets/{copy-DbsB0OVy.js → copy-B5XqmEDV.js} +1 -1
- package/dashboard/dist/assets/{download-R8nUvXYh.js → download-BtVO_3_3.js} +1 -1
- package/dashboard/dist/assets/{external-link-x0oNzGeq.js → external-link-CTpQdeXU.js} +1 -1
- package/dashboard/dist/assets/{flame-Bo4IdG6k.js → flame-CMixyq1C.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-BSGC1-Gz.js → format-tokens-DOgiRUv2.js} +1 -1
- package/dashboard/dist/assets/{icons-D0Okncmo.js → icons-DNpysD5S.js} +1 -1
- package/dashboard/dist/assets/{index-D6F8eNr_.js → index-B7Zuq72U.js} +1 -1
- package/dashboard/dist/assets/{index-Bvvmei-I.js → index-Dadn8f0V.js} +1 -1
- package/dashboard/dist/assets/{index-BbOkC6wf.js → index-k5haeuyB.js} +1 -1
- package/dashboard/dist/assets/{info-DXXKNpLG.js → info-CodBJXwL.js} +1 -1
- package/dashboard/dist/assets/{limits-providers-DYa4fdsE.js → limits-providers-DiSK8fij.js} +1 -1
- package/dashboard/dist/assets/{link-2-C9mxEnT6.js → link-2-BE84-D1K.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-Y8V8Mq9L.js → loader-circle-62hGDJej.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-QuP69Xcg.js → maximize-2-Dn0j7N4g.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-B3SleRKs.js → modifiers.esm-D48EfMXn.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-B2LslYi7.js → monitor-smartphone-B4O-1qqp.js} +1 -1
- package/dashboard/dist/assets/{provider-display-DzBwrDZp.js → provider-display-BBlUUXou.js} +1 -1
- package/dashboard/dist/assets/{react-BooKG84W.js → react-B7hdp-KU.js} +1 -1
- package/dashboard/dist/assets/{search-zdYuHmL-.js → search-DYcfh3D9.js} +1 -1
- package/dashboard/dist/assets/{skills-api-C2IkKfwE.js → skills-api-BL0Jj4TS.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-BHvl5vG5.js → sortable.esm-UJyxEfLh.js} +1 -1
- package/dashboard/dist/assets/{store-BBP4jx0R.js → store-koEJUHLx.js} +1 -1
- package/dashboard/dist/assets/{terminal-C0nhFNQ0.js → terminal-CA8uz-Rp.js} +1 -1
- package/dashboard/dist/assets/{trash-2-DNVSqbxA.js → trash-2-Cwdcw5RS.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-BK070d1s.js → use-community-stats-BIBrzbia.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-BXOUxblO.js → use-limits-display-prefs-BCdQk1ck.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-0SVHZHaW.js → use-ordered-list-Bf9Ozum9.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-DGTFk58t.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-eaUpwuLK.js → use-session-efficiency-pref-C83fbwfz.js} +1 -1
- package/dashboard/dist/assets/{use-transform-Bhar2U4w.js → use-transform-DnR7Iz0p.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-BGGCMVzi.js → use-usage-limits-D_MtlDKv.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-gIJVmq3w.js → useCurrency-DVO1i6vp.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-BhFzeRtR.js → useScrollLock-CW2lrPuY.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-BZ8WT-A6.js → useTokenFormat-BFhS3XRy.js} +1 -1
- package/dashboard/dist/assets/{zap-Dvc_Jt1t.js → zap-DCY5z8oI.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/ip-check.html +1 -1
- package/dashboard/dist/leaderboard.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +1 -1
- package/src/commands/sync.js +430 -1
- package/src/lib/install-resolver.js +22 -6
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +488 -60
- package/dashboard/dist/assets/DevicePage-_nLwNash.js +0 -1
- package/dashboard/dist/assets/DialogDescription-8veCvaUb.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-abZbtoVS.js +0 -1
package/src/lib/rollout.js
CHANGED
|
@@ -7840,9 +7840,98 @@ function resolveCodebuddyDefaultModel(env = process.env) {
|
|
|
7840
7840
|
return fallback;
|
|
7841
7841
|
}
|
|
7842
7842
|
|
|
7843
|
+
// Modern CodeBuddy writes token usage into providerData.rawUsage on JSONL
|
|
7844
|
+
// records. Older IDE installs may have no usable JSONL usage and rely on the
|
|
7845
|
+
// extension log fallback below. Keep the sniff bounded: it is used by the
|
|
7846
|
+
// guarded historical migration to decide whether a JSONL rebuild is safe.
|
|
7847
|
+
function codebuddyJsonlHasUsage(filePath) {
|
|
7848
|
+
let fd;
|
|
7849
|
+
try {
|
|
7850
|
+
fd = fssync.openSync(filePath, "r");
|
|
7851
|
+
const buffer = Buffer.alloc(64 * 1024);
|
|
7852
|
+
const bytesRead = fssync.readSync(fd, buffer, 0, buffer.length, 0);
|
|
7853
|
+
const lines = buffer.toString("utf8", 0, bytesRead).split("\n");
|
|
7854
|
+
const limit = Math.min(lines.length, 100);
|
|
7855
|
+
for (let i = 0; i < limit; i += 1) {
|
|
7856
|
+
const line = lines[i];
|
|
7857
|
+
if (!line || (!line.includes("rawUsage") && !line.includes("\"usage\""))) continue;
|
|
7858
|
+
let entry;
|
|
7859
|
+
try { entry = JSON.parse(line); } catch { continue; }
|
|
7860
|
+
if (!entry || typeof entry !== "object") continue;
|
|
7861
|
+
const provider = entry.providerData;
|
|
7862
|
+
if (provider && typeof provider === "object" && provider.rawUsage && typeof provider.rawUsage === "object") {
|
|
7863
|
+
return true;
|
|
7864
|
+
}
|
|
7865
|
+
if (entry.usage && typeof entry.usage === "object") return true;
|
|
7866
|
+
}
|
|
7867
|
+
} catch {
|
|
7868
|
+
return false;
|
|
7869
|
+
} finally {
|
|
7870
|
+
if (fd !== undefined) {
|
|
7871
|
+
try { fssync.closeSync(fd); } catch {}
|
|
7872
|
+
}
|
|
7873
|
+
}
|
|
7874
|
+
return false;
|
|
7875
|
+
}
|
|
7876
|
+
|
|
7877
|
+
// Legacy IDE extension logs and modern JSONL records do not share an id. Keep
|
|
7878
|
+
// a bounded, source-neutral fingerprint ledger so a log line can be matched to
|
|
7879
|
+
// the JSONL round-trip it mirrors without collapsing legitimate same-second
|
|
7880
|
+
// requests. The ledger is persisted in the CodeBuddy cursor because the two
|
|
7881
|
+
// sources can arrive on different syncs.
|
|
7882
|
+
function codebuddyUsageFingerprint({ model, timestampMs, inputTokens, cacheRead, cacheCreation, outputTokens, reasoningTokens }) {
|
|
7883
|
+
if (!Number.isFinite(Number(timestampMs)) || Number(timestampMs) <= 0) return null;
|
|
7884
|
+
const second = Math.floor(Number(timestampMs) / 1000);
|
|
7885
|
+
return [
|
|
7886
|
+
second,
|
|
7887
|
+
normalizeModelInput(model) || "unknown",
|
|
7888
|
+
toNonNegativeInt(inputTokens),
|
|
7889
|
+
toNonNegativeInt(cacheRead),
|
|
7890
|
+
toNonNegativeInt(cacheCreation),
|
|
7891
|
+
toNonNegativeInt(outputTokens),
|
|
7892
|
+
toNonNegativeInt(reasoningTokens),
|
|
7893
|
+
].join(":");
|
|
7894
|
+
}
|
|
7895
|
+
|
|
7896
|
+
function restoreCodebuddyUsageFingerprints(raw) {
|
|
7897
|
+
const state = new Map();
|
|
7898
|
+
if (!raw || typeof raw !== "object") return state;
|
|
7899
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
7900
|
+
const jsonl = toNonNegativeInt(value?.jsonl);
|
|
7901
|
+
const log = toNonNegativeInt(value?.log);
|
|
7902
|
+
if (jsonl > 0 || log > 0) state.set(key, { jsonl, log });
|
|
7903
|
+
}
|
|
7904
|
+
return state;
|
|
7905
|
+
}
|
|
7906
|
+
|
|
7907
|
+
function addCodebuddyJsonlFingerprint(state, fingerprint) {
|
|
7908
|
+
if (!fingerprint) return false;
|
|
7909
|
+
const current = state.get(fingerprint) || { jsonl: 0, log: 0 };
|
|
7910
|
+
const mirrored = current.log > current.jsonl;
|
|
7911
|
+
current.jsonl += 1;
|
|
7912
|
+
state.set(fingerprint, current);
|
|
7913
|
+
return mirrored;
|
|
7914
|
+
}
|
|
7915
|
+
|
|
7916
|
+
function consumeCodebuddyLogFingerprint(state, fingerprint) {
|
|
7917
|
+
if (!fingerprint) return false;
|
|
7918
|
+
const current = state.get(fingerprint) || { jsonl: 0, log: 0 };
|
|
7919
|
+
const mirrored = current.jsonl > current.log;
|
|
7920
|
+
current.log += 1;
|
|
7921
|
+
state.set(fingerprint, current);
|
|
7922
|
+
return mirrored;
|
|
7923
|
+
}
|
|
7924
|
+
|
|
7925
|
+
function capCodebuddyUsageFingerprints(state, maxEntries = 10_000) {
|
|
7926
|
+
const entries = Array.from(state.entries());
|
|
7927
|
+
if (entries.length <= maxEntries) return Object.fromEntries(entries);
|
|
7928
|
+
return Object.fromEntries(entries.slice(entries.length - maxEntries));
|
|
7929
|
+
}
|
|
7930
|
+
|
|
7843
7931
|
function resolveCodebuddyProjectFiles(env = process.env) {
|
|
7844
7932
|
const codebuddyHome = resolveCodebuddyHome(env);
|
|
7845
|
-
const
|
|
7933
|
+
const jsonlFiles = [];
|
|
7934
|
+
const logFiles = [];
|
|
7846
7935
|
|
|
7847
7936
|
// 1. Recursive JSONL scan in codebuddyHome/projects
|
|
7848
7937
|
if (codebuddyHome) {
|
|
@@ -7863,7 +7952,7 @@ function resolveCodebuddyProjectFiles(env = process.env) {
|
|
|
7863
7952
|
} catch { continue; }
|
|
7864
7953
|
}
|
|
7865
7954
|
if (isDir) walkJsonl(full);
|
|
7866
|
-
else if (isFile && entry.name.endsWith(".jsonl"))
|
|
7955
|
+
else if (isFile && entry.name.endsWith(".jsonl")) jsonlFiles.push(full);
|
|
7867
7956
|
}
|
|
7868
7957
|
};
|
|
7869
7958
|
walkJsonl(projectsDir);
|
|
@@ -7916,10 +8005,10 @@ function resolveCodebuddyProjectFiles(env = process.env) {
|
|
|
7916
8005
|
walkLogs(full);
|
|
7917
8006
|
} else if (isFile && entry.name.endsWith(".log")) {
|
|
7918
8007
|
if (root.pattern === "*.log") {
|
|
7919
|
-
|
|
8008
|
+
logFiles.push(full);
|
|
7920
8009
|
} else if (root.pattern === "codebuddy-extension-log") {
|
|
7921
8010
|
if (full.toLowerCase().includes("tencent-cloud.coding-copilot")) {
|
|
7922
|
-
|
|
8011
|
+
logFiles.push(full);
|
|
7923
8012
|
}
|
|
7924
8013
|
}
|
|
7925
8014
|
}
|
|
@@ -7928,6 +8017,14 @@ function resolveCodebuddyProjectFiles(env = process.env) {
|
|
|
7928
8017
|
walkLogs(root.dir);
|
|
7929
8018
|
}
|
|
7930
8019
|
|
|
8020
|
+
const fallbackMode = String(env.TOKENTRACKER_CODEBUDDY_LOG_FALLBACK || "").trim();
|
|
8021
|
+
// Auto mode keeps both sources available. The parser performs bounded,
|
|
8022
|
+
// count-aware cross-source fingerprint matching, so mixed histories retain
|
|
8023
|
+
// legacy-only log sessions while mirrored rounds are counted once. Env=0 is
|
|
8024
|
+
// still a supported escape hatch for users who want to suppress extension
|
|
8025
|
+
// logs entirely; env=1 remains an explicit force-on value.
|
|
8026
|
+
const includeLogs = fallbackMode !== "0";
|
|
8027
|
+
const files = [...jsonlFiles, ...(includeLogs ? logFiles : [])];
|
|
7931
8028
|
files.sort((a, b) => a.localeCompare(b));
|
|
7932
8029
|
return files;
|
|
7933
8030
|
}
|
|
@@ -7989,10 +8086,21 @@ async function parseCodebuddyIncremental({
|
|
|
7989
8086
|
codebuddyState.logModelsByAgent && typeof codebuddyState.logModelsByAgent === "object"
|
|
7990
8087
|
? { ...codebuddyState.logModelsByAgent }
|
|
7991
8088
|
: {};
|
|
8089
|
+
const usageFingerprints = restoreCodebuddyUsageFingerprints(codebuddyState.usageFingerprints);
|
|
7992
8090
|
|
|
7993
|
-
const
|
|
8091
|
+
const discoveredFiles = Array.isArray(projectFiles)
|
|
7994
8092
|
? projectFiles
|
|
7995
8093
|
: resolveCodebuddyProjectFiles(env || process.env);
|
|
8094
|
+
// JSONL must be parsed before extension logs so the fingerprint ledger can
|
|
8095
|
+
// match mirrored rounds in the same sync. Keep the legacy bare-string API
|
|
8096
|
+
// accepted for callers and tests.
|
|
8097
|
+
const files = [...discoveredFiles].sort((a, b) => {
|
|
8098
|
+
const aPath = typeof a === "string" ? a : a?.path || "";
|
|
8099
|
+
const bPath = typeof b === "string" ? b : b?.path || "";
|
|
8100
|
+
const aLog = aPath.endsWith(".log") ? 1 : 0;
|
|
8101
|
+
const bLog = bPath.endsWith(".log") ? 1 : 0;
|
|
8102
|
+
return aLog - bLog || aPath.localeCompare(bPath);
|
|
8103
|
+
});
|
|
7996
8104
|
const fallbackModel = defaultModel || resolveCodebuddyDefaultModel(env || process.env);
|
|
7997
8105
|
|
|
7998
8106
|
if (files.length === 0) {
|
|
@@ -8000,6 +8108,7 @@ async function parseCodebuddyIncremental({
|
|
|
8000
8108
|
...codebuddyState,
|
|
8001
8109
|
seenIds: Array.from(seenIds),
|
|
8002
8110
|
fileOffsets,
|
|
8111
|
+
usageFingerprints: capCodebuddyUsageFingerprints(usageFingerprints),
|
|
8003
8112
|
updatedAt: new Date().toISOString(),
|
|
8004
8113
|
};
|
|
8005
8114
|
return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
@@ -8151,6 +8260,20 @@ async function parseCodebuddyIncremental({
|
|
|
8151
8260
|
fallbackModel;
|
|
8152
8261
|
const model = normalizeModelInput(rawModel);
|
|
8153
8262
|
|
|
8263
|
+
const fingerprint = codebuddyUsageFingerprint({
|
|
8264
|
+
model,
|
|
8265
|
+
timestampMs: tsMs,
|
|
8266
|
+
inputTokens,
|
|
8267
|
+
cacheRead,
|
|
8268
|
+
cacheCreation,
|
|
8269
|
+
outputTokens: completionTokens,
|
|
8270
|
+
reasoningTokens,
|
|
8271
|
+
});
|
|
8272
|
+
if (consumeCodebuddyLogFingerprint(usageFingerprints, fingerprint)) {
|
|
8273
|
+
seenIds.add(messageId);
|
|
8274
|
+
continue;
|
|
8275
|
+
}
|
|
8276
|
+
|
|
8154
8277
|
const delta = {
|
|
8155
8278
|
input_tokens: inputTokens,
|
|
8156
8279
|
cached_input_tokens: cacheRead,
|
|
@@ -8179,9 +8302,15 @@ async function parseCodebuddyIncremental({
|
|
|
8179
8302
|
} else {
|
|
8180
8303
|
let entry;
|
|
8181
8304
|
try { entry = JSON.parse(line); } catch { continue; }
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8305
|
+
if (!entry || typeof entry !== "object") continue;
|
|
8306
|
+
|
|
8307
|
+
// Usage is carried on ANY record with providerData.rawUsage — assistant
|
|
8308
|
+
// messages AND function_call records. Each LLM round-trip (whether it
|
|
8309
|
+
// ends in a text reply or a tool call) carries its own usage; on real
|
|
8310
|
+
// installs function_call records are ~93% of round-trips and ~14x the
|
|
8311
|
+
// assistant-message token volume, so filtering by record type drops
|
|
8312
|
+
// the majority of usage. Aggregate them all; dedup per round-trip.
|
|
8313
|
+
// (Same convention as the WorkBuddy reader — same transcript format.)
|
|
8185
8314
|
const provider = entry.providerData;
|
|
8186
8315
|
const rawUsage = provider && typeof provider === "object" ? provider.rawUsage : null;
|
|
8187
8316
|
if (!rawUsage || typeof rawUsage !== "object") continue;
|
|
@@ -8194,38 +8323,65 @@ async function parseCodebuddyIncremental({
|
|
|
8194
8323
|
Number.isFinite(Number(entry.timestamp)) && Number(entry.timestamp) > 0
|
|
8195
8324
|
? Number(entry.timestamp)
|
|
8196
8325
|
: null;
|
|
8326
|
+
// One usage record per LLM round-trip; providerData.messageId is the
|
|
8327
|
+
// response-level id shared by the function_call/message pair of the
|
|
8328
|
+
// same round-trip, so it is the most stable dedup key. entry.id is the
|
|
8329
|
+
// per-record append id (unique per line, NOT per round-trip) and must
|
|
8330
|
+
// NOT be preferred — preferring it would count a round-trip once per
|
|
8331
|
+
// record type instead of once total.
|
|
8197
8332
|
const messageId =
|
|
8198
|
-
typeof
|
|
8199
|
-
?
|
|
8200
|
-
: typeof entry.
|
|
8201
|
-
? entry.
|
|
8202
|
-
:
|
|
8203
|
-
?
|
|
8204
|
-
: null
|
|
8333
|
+
typeof provider?.messageId === "string" && provider?.messageId
|
|
8334
|
+
? provider.messageId
|
|
8335
|
+
: typeof entry.uuid === "string" && entry.uuid
|
|
8336
|
+
? entry.uuid
|
|
8337
|
+
: typeof entry.id === "string" && entry.id
|
|
8338
|
+
? entry.id
|
|
8339
|
+
: tsMs != null
|
|
8340
|
+
? `${sessionId}:${tsMs}`
|
|
8341
|
+
: null;
|
|
8205
8342
|
if (!messageId) continue;
|
|
8206
8343
|
if (seenIds.has(messageId)) continue;
|
|
8207
8344
|
|
|
8208
8345
|
recordsProcessed++;
|
|
8209
8346
|
|
|
8210
8347
|
const promptTokens = toNonNegativeInt(rawUsage.prompt_tokens);
|
|
8211
|
-
const
|
|
8348
|
+
const completionTokensRaw = toNonNegativeInt(rawUsage.completion_tokens);
|
|
8212
8349
|
const details =
|
|
8213
8350
|
rawUsage.prompt_tokens_details && typeof rawUsage.prompt_tokens_details === "object"
|
|
8214
8351
|
? rawUsage.prompt_tokens_details
|
|
8215
8352
|
: {};
|
|
8216
|
-
const
|
|
8353
|
+
const completionDetails =
|
|
8354
|
+
rawUsage.completion_tokens_details && typeof rawUsage.completion_tokens_details === "object"
|
|
8355
|
+
? rawUsage.completion_tokens_details
|
|
8356
|
+
: {};
|
|
8357
|
+
// Cache-read mirrors three ways depending on upstream: Anthropic-style
|
|
8358
|
+
// cache_read_input_tokens, OpenAI-style prompt_tokens_details.cached_tokens,
|
|
8359
|
+
// DeepSeek-style prompt_cache_hit_tokens. Take the max (they mirror the
|
|
8360
|
+
// same quantity; on real data exactly one is non-zero).
|
|
8361
|
+
const cachedTokens = Math.max(
|
|
8362
|
+
toNonNegativeInt(details.cached_tokens),
|
|
8363
|
+
toNonNegativeInt(rawUsage.prompt_cache_hit_tokens),
|
|
8364
|
+
);
|
|
8217
8365
|
const cacheReadAlt = toNonNegativeInt(rawUsage.cache_read_input_tokens);
|
|
8218
|
-
const cacheCreation =
|
|
8219
|
-
|
|
8366
|
+
const cacheCreation = Math.max(
|
|
8367
|
+
toNonNegativeInt(rawUsage.cache_creation_input_tokens),
|
|
8368
|
+
toNonNegativeInt(rawUsage.prompt_cache_write_tokens),
|
|
8369
|
+
);
|
|
8370
|
+
// reasoning_tokens lives in completion_tokens_details (verified on real
|
|
8371
|
+
// CodeBuddy data: prompt_tokens_details.reasoning_tokens is always 0).
|
|
8372
|
+
// completion_tokens INCLUDES reasoning, so subtract to avoid double-count.
|
|
8373
|
+
const reasoningTokens = Math.min(completionTokensRaw, toNonNegativeInt(completionDetails.reasoning_tokens));
|
|
8374
|
+
const completionTokens = Math.max(0, completionTokensRaw - reasoningTokens);
|
|
8220
8375
|
|
|
8221
8376
|
const cacheRead = Math.max(cachedTokens, cacheReadAlt);
|
|
8222
|
-
const inputTokens = Math.max(0, promptTokens - cacheRead);
|
|
8377
|
+
const inputTokens = Math.max(0, promptTokens - cacheRead - cacheCreation);
|
|
8223
8378
|
|
|
8224
8379
|
if (
|
|
8225
8380
|
inputTokens === 0 &&
|
|
8226
8381
|
completionTokens === 0 &&
|
|
8227
8382
|
cacheRead === 0 &&
|
|
8228
|
-
cacheCreation === 0
|
|
8383
|
+
cacheCreation === 0 &&
|
|
8384
|
+
reasoningTokens === 0
|
|
8229
8385
|
) {
|
|
8230
8386
|
seenIds.add(messageId);
|
|
8231
8387
|
continue;
|
|
@@ -8244,6 +8400,20 @@ async function parseCodebuddyIncremental({
|
|
|
8244
8400
|
normalizeModelInput(entry.model) ||
|
|
8245
8401
|
fallbackModel;
|
|
8246
8402
|
|
|
8403
|
+
const fingerprint = codebuddyUsageFingerprint({
|
|
8404
|
+
model,
|
|
8405
|
+
timestampMs: tsMs,
|
|
8406
|
+
inputTokens,
|
|
8407
|
+
cacheRead,
|
|
8408
|
+
cacheCreation,
|
|
8409
|
+
outputTokens: completionTokens,
|
|
8410
|
+
reasoningTokens,
|
|
8411
|
+
});
|
|
8412
|
+
if (addCodebuddyJsonlFingerprint(usageFingerprints, fingerprint)) {
|
|
8413
|
+
seenIds.add(messageId);
|
|
8414
|
+
continue;
|
|
8415
|
+
}
|
|
8416
|
+
|
|
8247
8417
|
const delta = {
|
|
8248
8418
|
input_tokens: inputTokens,
|
|
8249
8419
|
cached_input_tokens: cacheRead,
|
|
@@ -8259,6 +8429,13 @@ async function parseCodebuddyIncremental({
|
|
|
8259
8429
|
addTotals(bucket.totals, delta);
|
|
8260
8430
|
touchedBuckets.add(bucketKey("codebuddy", model, bucketStart));
|
|
8261
8431
|
seenIds.add(messageId);
|
|
8432
|
+
// Preserve the pre-fingerprint cursor key as well. Older releases
|
|
8433
|
+
// keyed JSONL rows by entry.id; retaining it prevents the first
|
|
8434
|
+
// post-upgrade sync from replaying an already-counted row when
|
|
8435
|
+
// providerData.messageId is now preferred.
|
|
8436
|
+
if (typeof entry.id === "string" && entry.id && entry.id !== messageId) {
|
|
8437
|
+
seenIds.add(entry.id);
|
|
8438
|
+
}
|
|
8262
8439
|
eventsAggregated++;
|
|
8263
8440
|
|
|
8264
8441
|
if (cb) {
|
|
@@ -8306,6 +8483,7 @@ async function parseCodebuddyIncremental({
|
|
|
8306
8483
|
seenIds: cappedSeen,
|
|
8307
8484
|
fileOffsets,
|
|
8308
8485
|
logModelsByAgent: cappedLogModelsByAgent,
|
|
8486
|
+
usageFingerprints: capCodebuddyUsageFingerprints(usageFingerprints),
|
|
8309
8487
|
updatedAt,
|
|
8310
8488
|
};
|
|
8311
8489
|
|
|
@@ -8356,6 +8534,14 @@ async function parseCodebuddyIncremental({
|
|
|
8356
8534
|
//
|
|
8357
8535
|
// model is the auto-router placeholder ("auto") — WorkBuddy does not expose
|
|
8358
8536
|
// the underlying model in the log, so we emit it verbatim.
|
|
8537
|
+
//
|
|
8538
|
+
// 4. Completed ~/.workbuddy/traces/**/trace_*.json summaries expose aggregate
|
|
8539
|
+
// modelInfo input/output/cache totals. They are a fallback only when the
|
|
8540
|
+
// session has no detailed JSONL usage. The current SQLite session_usage
|
|
8541
|
+
// table exposes bounded context-window state (`used`/`size`) plus credits,
|
|
8542
|
+
// not billable token columns, so SQLite-only rows are ignored rather than
|
|
8543
|
+
// mislabelled as input usage. A future schema with explicit token columns
|
|
8544
|
+
// is accepted by workbuddySqliteUsageSnapshot.
|
|
8359
8545
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
8360
8546
|
|
|
8361
8547
|
function resolveWorkbuddyHome(env = process.env) {
|
|
@@ -8390,14 +8576,16 @@ function resolveWorkbuddyDefaultModel(env = process.env) {
|
|
|
8390
8576
|
|
|
8391
8577
|
// Recursively collect every *.jsonl under ~/.workbuddy/projects so that
|
|
8392
8578
|
// per-session conversation logs AND their nested subagents/agent-*.jsonl files
|
|
8393
|
-
// are both discovered.
|
|
8579
|
+
// are both discovered. WorkBuddy also writes completed trace summaries under
|
|
8580
|
+
// ~/.workbuddy/traces/<pid>/trace_*.json. Those summaries are useful only when
|
|
8581
|
+
// the detailed JSONL for a session is absent, so they are returned as typed
|
|
8582
|
+
// entries and parsed after JSONL (JSONL remains authoritative when both exist).
|
|
8394
8583
|
function resolveWorkbuddyProjectFiles(env = process.env) {
|
|
8395
8584
|
const workbuddyHome = resolveWorkbuddyHome(env);
|
|
8396
8585
|
if (!workbuddyHome) return [];
|
|
8397
8586
|
const projectsDir = path.join(workbuddyHome, "projects");
|
|
8398
|
-
if (!fssync.existsSync(projectsDir)) return [];
|
|
8399
8587
|
const files = [];
|
|
8400
|
-
const
|
|
8588
|
+
const walkJsonl = (dir) => {
|
|
8401
8589
|
let entries;
|
|
8402
8590
|
try { entries = fssync.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
8403
8591
|
for (const entry of entries) {
|
|
@@ -8412,15 +8600,89 @@ function resolveWorkbuddyProjectFiles(env = process.env) {
|
|
|
8412
8600
|
isFile = st.isFile();
|
|
8413
8601
|
} catch { continue; }
|
|
8414
8602
|
}
|
|
8415
|
-
if (isDir)
|
|
8603
|
+
if (isDir) walkJsonl(full);
|
|
8416
8604
|
else if (isFile && entry.name.endsWith(".jsonl")) files.push(full);
|
|
8417
8605
|
}
|
|
8418
8606
|
};
|
|
8419
|
-
|
|
8420
|
-
|
|
8607
|
+
if (fssync.existsSync(projectsDir)) walkJsonl(projectsDir);
|
|
8608
|
+
|
|
8609
|
+
const tracesDir = path.join(workbuddyHome, "traces");
|
|
8610
|
+
const walkTraces = (dir) => {
|
|
8611
|
+
let entries;
|
|
8612
|
+
try { entries = fssync.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
8613
|
+
for (const entry of entries) {
|
|
8614
|
+
const full = path.join(dir, entry.name);
|
|
8615
|
+
let isDir = entry.isDirectory();
|
|
8616
|
+
let isFile = entry.isFile();
|
|
8617
|
+
if (!isDir && !isFile) {
|
|
8618
|
+
try {
|
|
8619
|
+
const st = fssync.statSync(full);
|
|
8620
|
+
isDir = st.isDirectory();
|
|
8621
|
+
isFile = st.isFile();
|
|
8622
|
+
} catch { continue; }
|
|
8623
|
+
}
|
|
8624
|
+
if (isDir) walkTraces(full);
|
|
8625
|
+
else if (isFile && /^trace_[^/]+\.json$/i.test(entry.name)) {
|
|
8626
|
+
files.push({ path: full, kind: "trace" });
|
|
8627
|
+
}
|
|
8628
|
+
}
|
|
8629
|
+
};
|
|
8630
|
+
if (fssync.existsSync(tracesDir)) walkTraces(tracesDir);
|
|
8631
|
+
files.sort((a, b) => {
|
|
8632
|
+
const aPath = typeof a === "string" ? a : a?.path || "";
|
|
8633
|
+
const bPath = typeof b === "string" ? b : b?.path || "";
|
|
8634
|
+
// JSONL first is intentional: trace summaries are a lossy fallback and
|
|
8635
|
+
// must not win over a detailed rawUsage record on the same session.
|
|
8636
|
+
const aKind = typeof a === "string" ? 0 : 1;
|
|
8637
|
+
const bKind = typeof b === "string" ? 0 : 1;
|
|
8638
|
+
return aKind - bKind || aPath.localeCompare(bPath);
|
|
8639
|
+
});
|
|
8421
8640
|
return files;
|
|
8422
8641
|
}
|
|
8423
8642
|
|
|
8643
|
+
// WorkBuddy's current session_usage table stores context-window state in
|
|
8644
|
+
// `used`/`size` and model credit dollars in `credit_json`; neither is a token
|
|
8645
|
+
// accounting breakdown. Only consume a SQLite row when a future schema (or a
|
|
8646
|
+
// newer installation) exposes explicit cumulative token columns. This keeps
|
|
8647
|
+
// the fallback forward-compatible without treating a context snapshot as
|
|
8648
|
+
// billable usage.
|
|
8649
|
+
function workbuddySqliteUsageSnapshot(row) {
|
|
8650
|
+
if (!row || typeof row !== "object") return null;
|
|
8651
|
+
const hasAny = (keys) => keys.some((key) => Object.prototype.hasOwnProperty.call(row, key));
|
|
8652
|
+
const inputKeys = ["input_tokens", "inputTokens", "prompt_tokens", "promptTokens"];
|
|
8653
|
+
const cacheReadKeys = [
|
|
8654
|
+
"cached_input_tokens",
|
|
8655
|
+
"cachedInputTokens",
|
|
8656
|
+
"cache_read_input_tokens",
|
|
8657
|
+
"cacheReadInputTokens",
|
|
8658
|
+
"cache_read_tokens",
|
|
8659
|
+
"cacheReadTokens",
|
|
8660
|
+
];
|
|
8661
|
+
const cacheCreationKeys = [
|
|
8662
|
+
"cache_creation_input_tokens",
|
|
8663
|
+
"cacheCreationInputTokens",
|
|
8664
|
+
"prompt_cache_write_tokens",
|
|
8665
|
+
"promptCacheWriteTokens",
|
|
8666
|
+
];
|
|
8667
|
+
const outputKeys = ["output_tokens", "outputTokens", "completion_tokens", "completionTokens"];
|
|
8668
|
+
const reasoningKeys = [
|
|
8669
|
+
"reasoning_output_tokens",
|
|
8670
|
+
"reasoningOutputTokens",
|
|
8671
|
+
"reasoning_tokens",
|
|
8672
|
+
"reasoningTokens",
|
|
8673
|
+
];
|
|
8674
|
+
if (!hasAny([...inputKeys, ...cacheReadKeys, ...cacheCreationKeys, ...outputKeys, ...reasoningKeys])) {
|
|
8675
|
+
return null;
|
|
8676
|
+
}
|
|
8677
|
+
return {
|
|
8678
|
+
input: firstPresentNonNegativeInt(inputKeys.map((key) => row[key])),
|
|
8679
|
+
cacheRead: firstPresentNonNegativeInt(cacheReadKeys.map((key) => row[key])),
|
|
8680
|
+
cacheCreation: firstPresentNonNegativeInt(cacheCreationKeys.map((key) => row[key])),
|
|
8681
|
+
output: firstPresentNonNegativeInt(outputKeys.map((key) => row[key])),
|
|
8682
|
+
reasoning: firstPresentNonNegativeInt(reasoningKeys.map((key) => row[key])),
|
|
8683
|
+
};
|
|
8684
|
+
}
|
|
8685
|
+
|
|
8424
8686
|
async function parseWorkbuddyIncremental({
|
|
8425
8687
|
projectFiles,
|
|
8426
8688
|
cursors,
|
|
@@ -8447,11 +8709,22 @@ async function parseWorkbuddyIncremental({
|
|
|
8447
8709
|
workbuddyState.detailedSessions && typeof workbuddyState.detailedSessions === "object"
|
|
8448
8710
|
? { ...workbuddyState.detailedSessions }
|
|
8449
8711
|
: {};
|
|
8712
|
+
const seenTraceIds = new Set(
|
|
8713
|
+
Array.isArray(workbuddyState.seenTraceIds) ? workbuddyState.seenTraceIds : [],
|
|
8714
|
+
);
|
|
8715
|
+
const tracedSessionIds = new Set(
|
|
8716
|
+
Array.isArray(workbuddyState.tracedSessionIds) ? workbuddyState.tracedSessionIds : [],
|
|
8717
|
+
);
|
|
8450
8718
|
const detailedSessionsWithUsage = new Set();
|
|
8451
8719
|
|
|
8452
|
-
const
|
|
8720
|
+
const allFiles = Array.isArray(projectFiles)
|
|
8453
8721
|
? projectFiles
|
|
8454
8722
|
: resolveWorkbuddyProjectFiles(env || process.env);
|
|
8723
|
+
const jsonlFiles = allFiles.filter((entry) => typeof entry === "string");
|
|
8724
|
+
const traceFiles = allFiles.filter(
|
|
8725
|
+
(entry) => entry && typeof entry === "object" && typeof entry.path === "string" && entry.kind === "trace",
|
|
8726
|
+
);
|
|
8727
|
+
const files = [...jsonlFiles, ...traceFiles];
|
|
8455
8728
|
const fallbackModel = defaultModel || resolveWorkbuddyDefaultModel(env || process.env);
|
|
8456
8729
|
|
|
8457
8730
|
const workbuddyHome = resolveWorkbuddyHome(env || process.env);
|
|
@@ -8462,6 +8735,8 @@ async function parseWorkbuddyIncremental({
|
|
|
8462
8735
|
cursors.workbuddy = {
|
|
8463
8736
|
...workbuddyState,
|
|
8464
8737
|
seenIds: Array.from(seenIds),
|
|
8738
|
+
seenTraceIds: Array.from(seenTraceIds),
|
|
8739
|
+
tracedSessionIds: Array.from(tracedSessionIds),
|
|
8465
8740
|
fileOffsets,
|
|
8466
8741
|
sqliteSessions,
|
|
8467
8742
|
detailedSessions,
|
|
@@ -8476,8 +8751,8 @@ async function parseWorkbuddyIncremental({
|
|
|
8476
8751
|
let recordsProcessed = 0;
|
|
8477
8752
|
let eventsAggregated = 0;
|
|
8478
8753
|
|
|
8479
|
-
for (let fileIdx = 0; fileIdx <
|
|
8480
|
-
const filePath =
|
|
8754
|
+
for (let fileIdx = 0; fileIdx < jsonlFiles.length; fileIdx++) {
|
|
8755
|
+
const filePath = jsonlFiles[fileIdx];
|
|
8481
8756
|
let stat;
|
|
8482
8757
|
try { stat = fssync.statSync(filePath); } catch { continue; }
|
|
8483
8758
|
|
|
@@ -8515,23 +8790,28 @@ async function parseWorkbuddyIncremental({
|
|
|
8515
8790
|
typeof entry.sessionId === "string" && entry.sessionId
|
|
8516
8791
|
? entry.sessionId
|
|
8517
8792
|
: path.basename(filePath, ".jsonl");
|
|
8518
|
-
|
|
8793
|
+
const sqliteSession = sqliteSessions[sessionId];
|
|
8794
|
+
if (sqliteSession?.detailed || tracedSessionIds.has(sessionId)) {
|
|
8519
8795
|
continue;
|
|
8520
8796
|
}
|
|
8521
8797
|
const tsMs =
|
|
8522
8798
|
Number.isFinite(Number(entry.timestamp)) && Number(entry.timestamp) > 0
|
|
8523
8799
|
? Number(entry.timestamp)
|
|
8524
8800
|
: null;
|
|
8525
|
-
// One usage record per LLM round-trip;
|
|
8526
|
-
//
|
|
8801
|
+
// One usage record per LLM round-trip; providerData.messageId is the
|
|
8802
|
+
// response-level id shared by function_call/message records and must win
|
|
8803
|
+
// over the per-record append id. Fall back to entry.uuid/id only when the
|
|
8804
|
+
// response-level id is absent.
|
|
8527
8805
|
const messageId =
|
|
8528
|
-
typeof
|
|
8529
|
-
? entry.id
|
|
8530
|
-
: typeof provider.messageId === "string" && provider.messageId
|
|
8806
|
+
typeof provider?.messageId === "string" && provider.messageId
|
|
8531
8807
|
? provider.messageId
|
|
8532
|
-
:
|
|
8533
|
-
?
|
|
8534
|
-
:
|
|
8808
|
+
: typeof entry.uuid === "string" && entry.uuid
|
|
8809
|
+
? entry.uuid
|
|
8810
|
+
: typeof entry.id === "string" && entry.id
|
|
8811
|
+
? entry.id
|
|
8812
|
+
: tsMs != null
|
|
8813
|
+
? `${sessionId}:${tsMs}`
|
|
8814
|
+
: null;
|
|
8535
8815
|
if (!messageId) continue;
|
|
8536
8816
|
if (seenIds.has(messageId)) continue;
|
|
8537
8817
|
|
|
@@ -8603,6 +8883,11 @@ async function parseWorkbuddyIncremental({
|
|
|
8603
8883
|
addTotals(bucket.totals, delta);
|
|
8604
8884
|
touchedBuckets.add(bucketKey("workbuddy", model, bucketStart));
|
|
8605
8885
|
seenIds.add(messageId);
|
|
8886
|
+
// Keep legacy entry-id keys alongside the response id so upgrading a
|
|
8887
|
+
// cursor cannot replay a row that an older build already counted.
|
|
8888
|
+
if (typeof entry.id === "string" && entry.id && entry.id !== messageId) {
|
|
8889
|
+
seenIds.add(entry.id);
|
|
8890
|
+
}
|
|
8606
8891
|
detailedSessions[sessionId] = true;
|
|
8607
8892
|
detailedSessionsWithUsage.add(sessionId);
|
|
8608
8893
|
eventsAggregated++;
|
|
@@ -8627,14 +8912,105 @@ async function parseWorkbuddyIncremental({
|
|
|
8627
8912
|
};
|
|
8628
8913
|
}
|
|
8629
8914
|
|
|
8915
|
+
// Trace summaries are a fallback for sessions whose JSONL has no detailed
|
|
8916
|
+
// providerData.rawUsage. A trace's modelInfo totals include the cache read
|
|
8917
|
+
// portion of input, so split input into uncached input + cached input. Do
|
|
8918
|
+
// not merge a trace with the same session's JSONL (or SQLite) data: the
|
|
8919
|
+
// JSONL path has per-round detail and the trace summary can be partial on
|
|
8920
|
+
// older WorkBuddy versions.
|
|
8921
|
+
for (let fileIdx = 0; fileIdx < traceFiles.length; fileIdx++) {
|
|
8922
|
+
const entry = traceFiles[fileIdx];
|
|
8923
|
+
const filePath = entry.path;
|
|
8924
|
+
let stat;
|
|
8925
|
+
try { stat = fssync.statSync(filePath); } catch { continue; }
|
|
8926
|
+
const prevEntry = fileOffsets[filePath] || {};
|
|
8927
|
+
const prevSize = Number(prevEntry.size) || 0;
|
|
8928
|
+
const prevIno = prevEntry.ino;
|
|
8929
|
+
const inodeChanged = typeof prevIno === "number" && prevIno !== stat.ino;
|
|
8930
|
+
const startOffset = stat.size < prevSize || inodeChanged ? 0 : prevSize;
|
|
8931
|
+
if (stat.size <= startOffset) continue;
|
|
8932
|
+
|
|
8933
|
+
let traceDoc;
|
|
8934
|
+
try {
|
|
8935
|
+
traceDoc = JSON.parse(fssync.readFileSync(filePath, "utf8"));
|
|
8936
|
+
} catch {
|
|
8937
|
+
continue;
|
|
8938
|
+
}
|
|
8939
|
+
const trace = traceDoc && typeof traceDoc === "object" && traceDoc.trace && typeof traceDoc.trace === "object"
|
|
8940
|
+
? traceDoc.trace
|
|
8941
|
+
: null;
|
|
8942
|
+
const traceId = typeof trace?.traceId === "string" && trace.traceId
|
|
8943
|
+
? trace.traceId
|
|
8944
|
+
: path.basename(filePath, ".json");
|
|
8945
|
+
const metadata = trace?.metadata && typeof trace.metadata === "object" ? trace.metadata : {};
|
|
8946
|
+
const modelInfo = trace?.modelInfo && typeof trace.modelInfo === "object"
|
|
8947
|
+
? trace.modelInfo
|
|
8948
|
+
: metadata.modelInfo && typeof metadata.modelInfo === "object"
|
|
8949
|
+
? metadata.modelInfo
|
|
8950
|
+
: {};
|
|
8951
|
+
const sessionId =
|
|
8952
|
+
(typeof trace?.sessionId === "string" && trace.sessionId) ||
|
|
8953
|
+
(typeof metadata.sessionId === "string" && metadata.sessionId) ||
|
|
8954
|
+
traceId;
|
|
8955
|
+
const startedAtRaw = trace?.startedAt || metadata.startedAt;
|
|
8956
|
+
const tsMs = typeof startedAtRaw === "number" && Number.isFinite(startedAtRaw)
|
|
8957
|
+
? (startedAtRaw > 10000000000 ? startedAtRaw : startedAtRaw * 1000)
|
|
8958
|
+
: Date.parse(String(startedAtRaw || ""));
|
|
8959
|
+
const totalInput = toNonNegativeInt(modelInfo.totalInputTokens);
|
|
8960
|
+
const totalOutput = toNonNegativeInt(modelInfo.totalOutputTokens);
|
|
8961
|
+
const totalCached = Math.min(totalInput, toNonNegativeInt(modelInfo.totalCachedTokens));
|
|
8962
|
+
if (seenTraceIds.has(traceId) || detailedSessions[sessionId] || detailedSessionsWithUsage.has(sessionId) || tracedSessionIds.has(sessionId)) {
|
|
8963
|
+
seenTraceIds.add(traceId);
|
|
8964
|
+
fileOffsets[filePath] = { size: stat.size, mtimeMs: stat.mtimeMs, ino: stat.ino };
|
|
8965
|
+
continue;
|
|
8966
|
+
}
|
|
8967
|
+
if (!Number.isFinite(tsMs) || tsMs <= 0 || (totalInput <= 0 && totalOutput <= 0)) {
|
|
8968
|
+
// Keep the file offset but do not permanently suppress a trace that may
|
|
8969
|
+
// still be finalized in place by WorkBuddy.
|
|
8970
|
+
fileOffsets[filePath] = { size: stat.size, mtimeMs: stat.mtimeMs, ino: stat.ino };
|
|
8971
|
+
continue;
|
|
8972
|
+
}
|
|
8973
|
+
|
|
8974
|
+
const bucketStart = toUtcHalfHourStart(new Date(tsMs).toISOString());
|
|
8975
|
+
if (!bucketStart) continue;
|
|
8976
|
+
const model =
|
|
8977
|
+
normalizeModelInput(Array.isArray(modelInfo.models) ? modelInfo.models[0] : modelInfo.model) ||
|
|
8978
|
+
fallbackModel;
|
|
8979
|
+
const inputTokens = Math.max(0, totalInput - totalCached);
|
|
8980
|
+
const delta = {
|
|
8981
|
+
input_tokens: inputTokens,
|
|
8982
|
+
cached_input_tokens: totalCached,
|
|
8983
|
+
cache_creation_input_tokens: 0,
|
|
8984
|
+
output_tokens: totalOutput,
|
|
8985
|
+
reasoning_output_tokens: 0,
|
|
8986
|
+
total_tokens: inputTokens + totalCached + totalOutput,
|
|
8987
|
+
conversation_count: 1,
|
|
8988
|
+
};
|
|
8989
|
+
const bucket = getHourlyBucket(hourlyState, "workbuddy", model, bucketStart);
|
|
8990
|
+
addTotals(bucket.totals, delta);
|
|
8991
|
+
touchedBuckets.add(bucketKey("workbuddy", model, bucketStart));
|
|
8992
|
+
seenTraceIds.add(traceId);
|
|
8993
|
+
tracedSessionIds.add(sessionId);
|
|
8994
|
+
recordsProcessed++;
|
|
8995
|
+
eventsAggregated++;
|
|
8996
|
+
fileOffsets[filePath] = { size: stat.size, mtimeMs: stat.mtimeMs, ino: stat.ino };
|
|
8997
|
+
if (cb) {
|
|
8998
|
+
cb({
|
|
8999
|
+
index: jsonlFiles.length + fileIdx + 1,
|
|
9000
|
+
total: files.length,
|
|
9001
|
+
recordsProcessed,
|
|
9002
|
+
eventsAggregated,
|
|
9003
|
+
bucketsQueued: touchedBuckets.size,
|
|
9004
|
+
});
|
|
9005
|
+
}
|
|
9006
|
+
}
|
|
9007
|
+
|
|
8630
9008
|
if (dbExists) {
|
|
8631
9009
|
const query = `
|
|
8632
9010
|
SELECT
|
|
8633
|
-
su
|
|
8634
|
-
|
|
8635
|
-
|
|
8636
|
-
s.model,
|
|
8637
|
-
s.cwd
|
|
9011
|
+
su.*,
|
|
9012
|
+
s.model AS session_model,
|
|
9013
|
+
s.cwd AS session_cwd
|
|
8638
9014
|
FROM session_usage su
|
|
8639
9015
|
LEFT JOIN sessions s ON s.id = su.session_id
|
|
8640
9016
|
WHERE su.used IS NOT NULL
|
|
@@ -8659,35 +9035,74 @@ async function parseWorkbuddyIncremental({
|
|
|
8659
9035
|
if (!row || typeof row !== "object") continue;
|
|
8660
9036
|
const sessionId = typeof row.session_id === "string" ? row.session_id.trim() : "";
|
|
8661
9037
|
if (!sessionId) continue;
|
|
8662
|
-
if (
|
|
9038
|
+
if (
|
|
9039
|
+
detailedSessions[sessionId] ||
|
|
9040
|
+
detailedSessionsWithUsage.has(sessionId) ||
|
|
9041
|
+
tracedSessionIds.has(sessionId)
|
|
9042
|
+
) continue;
|
|
8663
9043
|
|
|
8664
|
-
const usedNow = toNonNegativeInt(row.used);
|
|
8665
9044
|
const updatedAtRaw = toNonNegativeInt(row.updated_at);
|
|
8666
|
-
const rawModel = typeof row.
|
|
9045
|
+
const rawModel = typeof row.session_model === "string"
|
|
9046
|
+
? row.session_model.trim()
|
|
9047
|
+
: typeof row.model === "string" ? row.model.trim() : "";
|
|
8667
9048
|
|
|
8668
|
-
if (
|
|
9049
|
+
if (updatedAtRaw <= 0) continue;
|
|
8669
9050
|
|
|
8670
|
-
const
|
|
8671
|
-
const
|
|
8672
|
-
const
|
|
8673
|
-
|
|
8674
|
-
|
|
9051
|
+
const snapshot = workbuddySqliteUsageSnapshot(row);
|
|
9052
|
+
const usedNow = toNonNegativeInt(row.used);
|
|
9053
|
+
const prev = sqliteSessions[sessionId] || {};
|
|
9054
|
+
|
|
9055
|
+
// Current WorkBuddy releases expose only context state (`used` versus
|
|
9056
|
+
// `size`) here. Do not turn that bounded snapshot into a fake usage
|
|
9057
|
+
// delta; detailed JSONL/trace sources remain the authoritative path.
|
|
9058
|
+
if (!snapshot) {
|
|
9059
|
+
sqliteSessions[sessionId] = {
|
|
9060
|
+
...prev,
|
|
9061
|
+
used: usedNow,
|
|
9062
|
+
updatedAt: updatedAtRaw,
|
|
9063
|
+
model: rawModel || prev.model || fallbackModel,
|
|
9064
|
+
detailed: false,
|
|
9065
|
+
};
|
|
9066
|
+
continue;
|
|
9067
|
+
}
|
|
9068
|
+
|
|
9069
|
+
const previousTokens = prev.tokens && typeof prev.tokens === "object"
|
|
9070
|
+
? prev.tokens
|
|
9071
|
+
: { input: 0, cacheRead: 0, cacheCreation: 0, output: 0, reasoning: 0 };
|
|
9072
|
+
const isReset = ["input", "cacheRead", "cacheCreation", "output", "reasoning"]
|
|
9073
|
+
.some((key) => snapshot[key] < toNonNegativeInt(previousTokens[key]));
|
|
9074
|
+
const deltaTokens = {
|
|
9075
|
+
input: isReset ? snapshot.input : Math.max(0, snapshot.input - toNonNegativeInt(previousTokens.input)),
|
|
9076
|
+
cacheRead: isReset ? snapshot.cacheRead : Math.max(0, snapshot.cacheRead - toNonNegativeInt(previousTokens.cacheRead)),
|
|
9077
|
+
cacheCreation: isReset ? snapshot.cacheCreation : Math.max(0, snapshot.cacheCreation - toNonNegativeInt(previousTokens.cacheCreation)),
|
|
9078
|
+
output: isReset ? snapshot.output : Math.max(0, snapshot.output - toNonNegativeInt(previousTokens.output)),
|
|
9079
|
+
reasoning: isReset ? snapshot.reasoning : Math.max(0, snapshot.reasoning - toNonNegativeInt(previousTokens.reasoning)),
|
|
9080
|
+
};
|
|
9081
|
+
if (
|
|
9082
|
+
deltaTokens.input === 0 &&
|
|
9083
|
+
deltaTokens.cacheRead === 0 &&
|
|
9084
|
+
deltaTokens.cacheCreation === 0 &&
|
|
9085
|
+
deltaTokens.output === 0 &&
|
|
9086
|
+
deltaTokens.reasoning === 0
|
|
9087
|
+
) {
|
|
8675
9088
|
sqliteSessions[sessionId] = {
|
|
8676
9089
|
...prev,
|
|
8677
9090
|
used: usedNow,
|
|
8678
9091
|
updatedAt: updatedAtRaw,
|
|
8679
9092
|
model: rawModel || prev.model || fallbackModel,
|
|
9093
|
+
tokens: snapshot,
|
|
9094
|
+
detailed: true,
|
|
8680
9095
|
};
|
|
8681
9096
|
continue;
|
|
8682
9097
|
}
|
|
8683
9098
|
|
|
8684
9099
|
recordsProcessed++;
|
|
8685
9100
|
|
|
8686
|
-
const inputTokens =
|
|
8687
|
-
const completionTokens =
|
|
8688
|
-
const cacheRead =
|
|
8689
|
-
const cacheCreation =
|
|
8690
|
-
const reasoningTokens =
|
|
9101
|
+
const inputTokens = deltaTokens.input;
|
|
9102
|
+
const completionTokens = deltaTokens.output;
|
|
9103
|
+
const cacheRead = deltaTokens.cacheRead;
|
|
9104
|
+
const cacheCreation = deltaTokens.cacheCreation;
|
|
9105
|
+
const reasoningTokens = deltaTokens.reasoning;
|
|
8691
9106
|
|
|
8692
9107
|
const tsMs = updatedAtRaw > 10000000000 ? updatedAtRaw : updatedAtRaw * 1000;
|
|
8693
9108
|
const tsIso = new Date(tsMs).toISOString();
|
|
@@ -8702,8 +9117,8 @@ async function parseWorkbuddyIncremental({
|
|
|
8702
9117
|
cache_creation_input_tokens: cacheCreation,
|
|
8703
9118
|
output_tokens: completionTokens,
|
|
8704
9119
|
reasoning_output_tokens: reasoningTokens,
|
|
8705
|
-
|
|
8706
|
-
conversation_count:
|
|
9120
|
+
total_tokens: inputTokens + completionTokens + cacheRead + cacheCreation + reasoningTokens,
|
|
9121
|
+
conversation_count: Object.keys(previousTokens).every((key) => toNonNegativeInt(previousTokens[key]) === 0) || isReset ? 1 : 0,
|
|
8707
9122
|
};
|
|
8708
9123
|
|
|
8709
9124
|
const bucket = getHourlyBucket(hourlyState, "workbuddy", model, bucketStart);
|
|
@@ -8713,6 +9128,8 @@ async function parseWorkbuddyIncremental({
|
|
|
8713
9128
|
used: usedNow,
|
|
8714
9129
|
updatedAt: updatedAtRaw,
|
|
8715
9130
|
model,
|
|
9131
|
+
tokens: snapshot,
|
|
9132
|
+
detailed: true,
|
|
8716
9133
|
};
|
|
8717
9134
|
eventsAggregated++;
|
|
8718
9135
|
}
|
|
@@ -8740,6 +9157,14 @@ async function parseWorkbuddyIncremental({
|
|
|
8740
9157
|
detailedSessionEntries.length > 10_000
|
|
8741
9158
|
? Object.fromEntries(detailedSessionEntries.slice(detailedSessionEntries.length - 10_000))
|
|
8742
9159
|
: detailedSessions;
|
|
9160
|
+
const traceArr = Array.from(seenTraceIds);
|
|
9161
|
+
const cappedSeenTraceIds =
|
|
9162
|
+
traceArr.length > 10_000 ? traceArr.slice(traceArr.length - 10_000) : traceArr;
|
|
9163
|
+
const tracedSessionArr = Array.from(tracedSessionIds);
|
|
9164
|
+
const cappedTracedSessionIds =
|
|
9165
|
+
tracedSessionArr.length > 10_000
|
|
9166
|
+
? tracedSessionArr.slice(tracedSessionArr.length - 10_000)
|
|
9167
|
+
: tracedSessionArr;
|
|
8743
9168
|
|
|
8744
9169
|
const bucketsQueued = await enqueueTouchedBuckets({
|
|
8745
9170
|
queuePath,
|
|
@@ -8752,6 +9177,8 @@ async function parseWorkbuddyIncremental({
|
|
|
8752
9177
|
cursors.workbuddy = {
|
|
8753
9178
|
...workbuddyState,
|
|
8754
9179
|
seenIds: cappedSeen,
|
|
9180
|
+
seenTraceIds: cappedSeenTraceIds,
|
|
9181
|
+
tracedSessionIds: cappedTracedSessionIds,
|
|
8755
9182
|
fileOffsets,
|
|
8756
9183
|
sqliteSessions: cappedSqliteSessions,
|
|
8757
9184
|
detailedSessions: cappedDetailedSessions,
|
|
@@ -15448,6 +15875,7 @@ module.exports = {
|
|
|
15448
15875
|
resolveKimiCodeDefaultModel,
|
|
15449
15876
|
parseKimiCodeIncremental,
|
|
15450
15877
|
resolveCodebuddyHome,
|
|
15878
|
+
codebuddyJsonlHasUsage,
|
|
15451
15879
|
resolveCodebuddyProjectFiles,
|
|
15452
15880
|
resolveCodebuddyDefaultModel,
|
|
15453
15881
|
parseCodebuddyIncremental,
|