openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,618 lines • 62.3 kB
JavaScript
import { c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js";
import { s as asFiniteNumber } from "./number-coercion-CJQ8TR--.js";
import { i as formatErrorMessage } from "./errors-BXgSefBE.js";
import { t as createSubsystemLogger } from "./subsystem-BzXSmsuh.js";
import { n as replaceFileAtomic } from "./replace-file-BrS02dAb.js";
import { t as runTasksWithConcurrency } from "./run-with-concurrency-DivrDqxu.js";
import { C as parseUsageCountedSessionIdFromFileName, S as parseSessionArchiveTimestamp, b as isUsageCountedSessionTranscriptFileName, h as isSessionArchiveArtifactName, i as resolveSessionFilePath, l as resolveSessionTranscriptsDirForAgent, m as isPrimarySessionTranscriptFileName } from "./paths-NEwU8m3X.js";
import { r as stripInboundMetadata } from "./strip-inbound-meta-BHmOsIBM.js";
import { C as stripMessageIdHints, S as stripEnvelope, b as extractToolCallNames, y as countToolResults } from "./session-utils.fs-D_8-X4jl.js";
import { o as normalizeUsage } from "./usage-C67Kbb7n.js";
import { a as resolveModelCostConfig, o as resolveModelCostConfigFingerprint, t as estimateUsageCost } from "./usage-format-DAT-61Hm.js";
import fs from "node:fs";
import path from "node:path";
import readline from "node:readline";
//#region src/infra/session-cost-usage.ts
const emptyTotals = () => ({
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
totalCost: 0,
inputCost: 0,
outputCost: 0,
cacheReadCost: 0,
cacheWriteCost: 0,
missingCostEntries: 0
});
const USAGE_COST_CACHE_VERSION = 4;
const USAGE_COST_CACHE_FILE = ".usage-cost-cache.json";
const USAGE_COST_CACHE_LOCK_WRITE_GRACE_MS = 1e4;
const USAGE_COST_TRANSCRIPT_STAT_CONCURRENCY = 32;
const USAGE_COST_CACHE_CHECKPOINT_FILES = 256;
const USAGE_COST_CACHE_CHECKPOINT_INTERVAL_MS = 5e3;
const logger = createSubsystemLogger("usage-cost-cache");
const usageCostRefreshes = /* @__PURE__ */ new Map();
const cloneTotals = (totals) => ({
input: totals.input,
output: totals.output,
cacheRead: totals.cacheRead,
cacheWrite: totals.cacheWrite,
totalTokens: totals.totalTokens,
totalCost: totals.totalCost,
inputCost: totals.inputCost,
outputCost: totals.outputCost,
cacheReadCost: totals.cacheReadCost,
cacheWriteCost: totals.cacheWriteCost,
missingCostEntries: totals.missingCostEntries
});
const addTotals = (target, source) => {
target.input += source.input;
target.output += source.output;
target.cacheRead += source.cacheRead;
target.cacheWrite += source.cacheWrite;
target.totalTokens += source.totalTokens;
target.totalCost += source.totalCost;
target.inputCost += source.inputCost;
target.outputCost += source.outputCost;
target.cacheReadCost += source.cacheReadCost;
target.cacheWriteCost += source.cacheWriteCost;
target.missingCostEntries += source.missingCostEntries;
};
function resolveUsageCostPricingFingerprint(config) {
return resolveModelCostConfigFingerprint(config);
}
function resolveUsageCostCachePath(agentId) {
return path.join(resolveSessionTranscriptsDirForAgent(agentId), USAGE_COST_CACHE_FILE);
}
function resolveUsageCostCacheLockPath(cachePath) {
return `${cachePath}.lock`;
}
function parseUsageCostCacheLock(raw) {
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== "object") return null;
const lock = parsed;
if (typeof lock.pid !== "number" || !Number.isInteger(lock.pid) || lock.pid <= 0 || typeof lock.startedAt !== "number" || !Number.isFinite(lock.startedAt) || lock.token !== void 0 && typeof lock.token !== "string") return null;
return {
pid: lock.pid,
startedAt: lock.startedAt,
token: lock.token
};
}
async function readUsageCostCacheLockState(lockPath) {
try {
const lock = parseUsageCostCacheLock(await fs.promises.readFile(lockPath, "utf-8"));
if (lock) return {
state: "valid",
lock
};
} catch (err) {
if (err.code === "ENOENT") return { state: "missing" };
}
const stats = await fs.promises.stat(lockPath).catch(() => null);
if (!stats) return { state: "missing" };
return {
state: "malformed",
mtimeMs: stats.mtimeMs
};
}
async function readUsageCostCacheLock(lockPath) {
const result = await readUsageCostCacheLockState(lockPath);
return result.state === "valid" ? result.lock : null;
}
function isMalformedUsageCostCacheLockRecent(mtimeMs) {
return Date.now() - mtimeMs < USAGE_COST_CACHE_LOCK_WRITE_GRACE_MS;
}
async function writeUsageCostCacheLockAtomically(lockPath, lock) {
const tempPath = `${lockPath}.${process.pid}.${process.hrtime.bigint()}.tmp`;
await fs.promises.writeFile(tempPath, `${JSON.stringify(lock)}\n`, { flag: "wx" });
try {
await fs.promises.link(tempPath, lockPath);
} finally {
await fs.promises.rm(tempPath, { force: true }).catch(() => void 0);
}
}
function isProcessRunning(pid) {
try {
process.kill(pid, 0);
return true;
} catch (err) {
return err.code === "EPERM";
}
}
async function isUsageCostCacheRefreshRunning(cachePath) {
const lockPath = resolveUsageCostCacheLockPath(cachePath);
const result = await readUsageCostCacheLockState(lockPath);
if (result.state === "missing") return false;
if (result.state === "malformed") {
if (isMalformedUsageCostCacheLockRecent(result.mtimeMs)) return true;
await fs.promises.rm(lockPath, { force: true }).catch(() => void 0);
return false;
}
const lock = result.lock;
if (isProcessRunning(lock.pid)) return true;
await fs.promises.rm(lockPath, { force: true }).catch(() => void 0);
return false;
}
async function acquireUsageCostCacheRefreshLock(cachePath) {
const lockPath = resolveUsageCostCacheLockPath(cachePath);
await fs.promises.mkdir(path.dirname(lockPath), { recursive: true });
const lock = {
pid: process.pid,
startedAt: Date.now(),
token: `${process.pid}:${Date.now()}:${process.hrtime.bigint()}`
};
try {
await writeUsageCostCacheLockAtomically(lockPath, lock);
return {
acquired: true,
release: async () => {
const current = await readUsageCostCacheLock(lockPath);
if (current?.pid === lock.pid && current.startedAt === lock.startedAt && current.token === lock.token) await fs.promises.rm(lockPath, { force: true }).catch(() => void 0);
}
};
} catch (err) {
if (err.code !== "EEXIST") throw err;
if (await isUsageCostCacheRefreshRunning(cachePath)) return {
acquired: false,
release: async () => void 0
};
await fs.promises.rm(lockPath, { force: true }).catch(() => void 0);
return acquireUsageCostCacheRefreshLock(cachePath);
}
}
function normalizeUsageCostCache(raw) {
if (!raw || typeof raw !== "object") return {
version: USAGE_COST_CACHE_VERSION,
updatedAt: 0,
files: {}
};
const record = raw;
if (record.version !== USAGE_COST_CACHE_VERSION || !record.files || typeof record.files !== "object") return {
version: USAGE_COST_CACHE_VERSION,
updatedAt: 0,
files: {}
};
return {
version: USAGE_COST_CACHE_VERSION,
updatedAt: asFiniteNumber(record.updatedAt) ?? 0,
files: record.files
};
}
async function readUsageCostCache(cachePath) {
try {
const raw = await fs.promises.readFile(cachePath, "utf-8");
return normalizeUsageCostCache(JSON.parse(raw));
} catch {
return {
version: USAGE_COST_CACHE_VERSION,
updatedAt: 0,
files: {}
};
}
}
async function writeUsageCostCache(cachePath, cache) {
await replaceFileAtomic({
filePath: cachePath,
content: `${JSON.stringify(cache)}\n`,
tempPrefix: ".usage-cost-cache"
});
}
async function listUsageCountedTranscriptFileStats(agentId, params) {
const sessionsDir = params?.sessionsDir ?? resolveSessionTranscriptsDirForAgent(agentId);
const { results } = await runTasksWithConcurrency({
tasks: (await fs.promises.readdir(sessionsDir, { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isFile() && isUsageCountedSessionTranscriptFileName(entry.name)).map((entry) => async () => {
const filePath = path.join(sessionsDir, entry.name);
const stats = await fs.promises.stat(filePath).catch(() => null);
if (!stats) return;
if (params?.minMtimeMs !== void 0 && stats.mtimeMs < params.minMtimeMs) return;
return {
filePath,
size: stats.size,
mtimeMs: stats.mtimeMs
};
}),
limit: USAGE_COST_TRANSCRIPT_STAT_CONCURRENCY
});
return results.filter((file) => Boolean(file));
}
async function listUsageCountedTranscriptFiles(agentId, params) {
return await listUsageCountedTranscriptFileStats(agentId, params);
}
function isUsageCostCacheEntryFresh(params) {
return Boolean(params.entry && params.entry.size === params.file.size && params.entry.mtimeMs === params.file.mtimeMs && params.entry.pricingFingerprint === params.pricingFingerprint && (!params.requireSessionSummary || params.entry.sessionSummary));
}
function canUseUsageCostCacheEntryForPartial(params) {
return Boolean(params.entry && params.entry.size <= params.file.size && params.entry.mtimeMs <= params.file.mtimeMs && params.entry.pricingFingerprint === params.pricingFingerprint);
}
function getUsageCostStaleFiles(params) {
const sessionSummaryFiles = params.sessionSummaryFiles ?? /* @__PURE__ */ new Set();
return params.files.filter((file) => !isUsageCostCacheEntryFresh({
entry: params.cache.files[file.filePath],
file,
pricingFingerprint: params.pricingFingerprint,
requireSessionSummary: sessionSummaryFiles.has(file.filePath)
}));
}
function countUsableUsageCostCacheFiles(params) {
const filesByPath = new Map(params.files.map((file) => [file.filePath, file]));
let cachedFiles = 0;
for (const [filePath, entry] of Object.entries(params.cache.files)) {
const file = filesByPath.get(filePath);
if (file && canUseUsageCostCacheEntryForPartial({
entry,
file,
pricingFingerprint: params.pricingFingerprint
})) cachedFiles += 1;
}
return cachedFiles;
}
function buildCostUsageSummaryFromCache(params) {
const dailyMap = /* @__PURE__ */ new Map();
const totals = emptyTotals();
const filesByPath = new Map(params.files.map((file) => [file.filePath, file]));
const staleFiles = getUsageCostStaleFiles({
cache: params.cache,
files: params.files,
pricingFingerprint: params.pricingFingerprint
});
const cachedFiles = countUsableUsageCostCacheFiles({
cache: params.cache,
files: params.files,
pricingFingerprint: params.pricingFingerprint
});
for (const [filePath, entry] of Object.entries(params.cache.files)) {
const file = filesByPath.get(filePath);
if (!file || !canUseUsageCostCacheEntryForPartial({
entry,
file,
pricingFingerprint: params.pricingFingerprint
})) continue;
for (const usageEntry of entry.usageEntries) {
if (usageEntry.timestamp < params.startMs || usageEntry.timestamp > params.endMs) continue;
const date = formatDayKey(new Date(usageEntry.timestamp));
const bucket = dailyMap.get(date) ?? emptyTotals();
addTotals(bucket, usageEntry);
dailyMap.set(date, bucket);
addTotals(totals, usageEntry);
}
}
const daily = Array.from(dailyMap.entries()).map(([date, bucket]) => Object.assign({ date }, bucket)).toSorted((a, b) => a.date.localeCompare(b.date));
const days = Math.ceil((params.endMs - params.startMs) / (1440 * 60 * 1e3)) + 1;
const status = params.refreshing ? "refreshing" : staleFiles.length > 0 ? cachedFiles > 0 ? "partial" : "stale" : "fresh";
return {
updatedAt: Date.now(),
days,
daily,
totals,
cacheStatus: {
status,
cachedFiles,
pendingFiles: staleFiles.length,
staleFiles: staleFiles.length,
refreshedAt: params.cache.updatedAt || void 0
}
};
}
function isSessionSummaryContainedInRange(summary, startMs, endMs) {
return (summary.firstActivity === void 0 || summary.firstActivity >= startMs) && (summary.lastActivity === void 0 || summary.lastActivity <= endMs);
}
function buildSessionCostSummaryFromCacheEntry(params) {
if (!params.entry.transcriptEntries) return null;
const totals = emptyTotals();
const activityDatesSet = /* @__PURE__ */ new Set();
const dailyMap = /* @__PURE__ */ new Map();
const dailyMessageMap = /* @__PURE__ */ new Map();
const utcQuarterHourMessageMap = /* @__PURE__ */ new Map();
const utcQuarterHourTokenMap = /* @__PURE__ */ new Map();
const dailyLatencyMap = /* @__PURE__ */ new Map();
const dailyModelUsageMap = /* @__PURE__ */ new Map();
const messageCounts = {
total: 0,
user: 0,
assistant: 0,
toolCalls: 0,
toolResults: 0,
errors: 0
};
const toolUsageMap = /* @__PURE__ */ new Map();
const modelUsageMap = /* @__PURE__ */ new Map();
const errorStopReasons = new Set([
"error",
"aborted",
"timeout"
]);
const latencyValues = [];
let firstActivity;
let lastActivity;
let lastUserTimestamp;
const maxLatencyMs = 720 * 60 * 1e3;
for (const entry of params.entry.transcriptEntries) {
const ts = entry.timestamp;
if (ts !== void 0 && ts < params.startMs) continue;
if (ts !== void 0 && ts > params.endMs) continue;
if (ts !== void 0) {
firstActivity = firstActivity === void 0 ? ts : Math.min(firstActivity, ts);
lastActivity = lastActivity === void 0 ? ts : Math.max(lastActivity, ts);
}
if (entry.role === "user") {
messageCounts.user += 1;
messageCounts.total += 1;
if (ts !== void 0) lastUserTimestamp = ts;
}
if (entry.role === "assistant") {
messageCounts.assistant += 1;
messageCounts.total += 1;
if (ts !== void 0) {
const latencyMs = entry.durationMs ?? (lastUserTimestamp !== void 0 ? Math.max(0, ts - lastUserTimestamp) : void 0);
if (latencyMs !== void 0 && Number.isFinite(latencyMs) && latencyMs <= maxLatencyMs) {
latencyValues.push(latencyMs);
const dayKey = formatDayKey(new Date(ts));
const dailyLatencies = dailyLatencyMap.get(dayKey) ?? [];
dailyLatencies.push(latencyMs);
dailyLatencyMap.set(dayKey, dailyLatencies);
}
}
}
if (entry.toolNames.length > 0) {
messageCounts.toolCalls += entry.toolNames.length;
for (const name of entry.toolNames) toolUsageMap.set(name, (toolUsageMap.get(name) ?? 0) + 1);
}
if (entry.toolResultCounts.total > 0) {
messageCounts.toolResults += entry.toolResultCounts.total;
messageCounts.errors += entry.toolResultCounts.errors;
}
if (entry.stopReason && errorStopReasons.has(entry.stopReason)) messageCounts.errors += 1;
if (ts !== void 0) {
const date = new Date(ts);
const dayKey = formatDayKey(date);
activityDatesSet.add(dayKey);
const daily = dailyMessageMap.get(dayKey) ?? {
date: dayKey,
total: 0,
user: 0,
assistant: 0,
toolCalls: 0,
toolResults: 0,
errors: 0
};
daily.total += entry.role === "user" || entry.role === "assistant" ? 1 : 0;
if (entry.role === "user") daily.user += 1;
else if (entry.role === "assistant") daily.assistant += 1;
daily.toolCalls += entry.toolNames.length;
daily.toolResults += entry.toolResultCounts.total;
daily.errors += entry.toolResultCounts.errors;
if (entry.stopReason && errorStopReasons.has(entry.stopReason)) daily.errors += 1;
dailyMessageMap.set(dayKey, daily);
const quarterBucket = getUtcQuarterHourBucketKey(date);
const utcQuarterHour = utcQuarterHourMessageMap.get(quarterBucket.key) ?? {
date: quarterBucket.date,
quarterIndex: quarterBucket.quarterIndex,
total: 0,
user: 0,
assistant: 0,
toolCalls: 0,
toolResults: 0,
errors: 0
};
utcQuarterHour.total += entry.role === "user" || entry.role === "assistant" ? 1 : 0;
if (entry.role === "user") utcQuarterHour.user += 1;
else if (entry.role === "assistant") utcQuarterHour.assistant += 1;
utcQuarterHour.toolCalls += entry.toolNames.length;
utcQuarterHour.toolResults += entry.toolResultCounts.total;
utcQuarterHour.errors += entry.toolResultCounts.errors;
if (entry.stopReason && errorStopReasons.has(entry.stopReason)) utcQuarterHour.errors += 1;
utcQuarterHourMessageMap.set(quarterBucket.key, utcQuarterHour);
}
const usageTotals = entry.usageTotals;
if (!usageTotals) continue;
addTotals(totals, usageTotals);
if (ts !== void 0) {
const date = new Date(ts);
const dayKey = formatDayKey(date);
const componentTokens = usageTotals.input + usageTotals.output + usageTotals.cacheRead + usageTotals.cacheWrite;
const existingDaily = dailyMap.get(dayKey) ?? {
tokens: 0,
cost: 0
};
existingDaily.tokens += componentTokens;
existingDaily.cost += usageTotals.totalCost;
dailyMap.set(dayKey, existingDaily);
const quarterBucket = getUtcQuarterHourBucketKey(date);
const utcQuarterHourToken = utcQuarterHourTokenMap.get(quarterBucket.key) ?? {
date: quarterBucket.date,
quarterIndex: quarterBucket.quarterIndex,
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
totalCost: 0
};
utcQuarterHourToken.input += usageTotals.input;
utcQuarterHourToken.output += usageTotals.output;
utcQuarterHourToken.cacheRead += usageTotals.cacheRead;
utcQuarterHourToken.cacheWrite += usageTotals.cacheWrite;
utcQuarterHourToken.totalTokens += usageTotals.totalTokens;
utcQuarterHourToken.totalCost += usageTotals.totalCost;
utcQuarterHourTokenMap.set(quarterBucket.key, utcQuarterHourToken);
if (entry.provider || entry.model) {
const dailyModelKey = `${dayKey}::${entry.provider ?? "unknown"}::${entry.model ?? "unknown"}`;
const dailyModel = dailyModelUsageMap.get(dailyModelKey) ?? {
date: dayKey,
provider: entry.provider,
model: entry.model,
tokens: 0,
cost: 0,
count: 0
};
dailyModel.tokens += componentTokens;
dailyModel.cost += usageTotals.totalCost;
dailyModel.count += 1;
dailyModelUsageMap.set(dailyModelKey, dailyModel);
}
}
if (entry.provider || entry.model) {
const modelKey = `${entry.provider ?? "unknown"}::${entry.model ?? "unknown"}`;
const modelUsage = modelUsageMap.get(modelKey) ?? {
provider: entry.provider,
model: entry.model,
count: 0,
totals: emptyTotals()
};
modelUsage.count += 1;
addTotals(modelUsage.totals, usageTotals);
modelUsageMap.set(modelKey, modelUsage);
}
}
const dailyBreakdown = Array.from(dailyMap.entries()).map(([date, data]) => ({
date,
tokens: data.tokens,
cost: data.cost
})).toSorted((a, b) => a.date.localeCompare(b.date));
const dailyMessageCounts = Array.from(dailyMessageMap.values()).toSorted((a, b) => a.date.localeCompare(b.date));
const utcQuarterHourMessageCounts = Array.from(utcQuarterHourMessageMap.values()).toSorted((a, b) => a.date.localeCompare(b.date) || a.quarterIndex - b.quarterIndex);
const utcQuarterHourTokenUsage = Array.from(utcQuarterHourTokenMap.values()).toSorted((a, b) => a.date.localeCompare(b.date) || a.quarterIndex - b.quarterIndex);
const dailyLatency = Array.from(dailyLatencyMap.entries()).map(([date, values]) => {
const stats = computeLatencyStats(values);
if (!stats) return null;
return Object.assign({ date }, stats);
}).filter((entry) => Boolean(entry)).toSorted((a, b) => a.date.localeCompare(b.date));
const dailyModelUsage = Array.from(dailyModelUsageMap.values()).toSorted((a, b) => a.date.localeCompare(b.date) || b.cost - a.cost);
const toolUsage = toolUsageMap.size ? {
totalCalls: Array.from(toolUsageMap.values()).reduce((sum, count) => sum + count, 0),
uniqueTools: toolUsageMap.size,
tools: Array.from(toolUsageMap.entries()).map(([name, count]) => ({
name,
count
})).toSorted((a, b) => b.count - a.count)
} : void 0;
const modelUsage = Array.from(modelUsageMap.values()).toSorted((a, b) => {
const costDiff = (b.totals?.totalCost ?? 0) - (a.totals?.totalCost ?? 0);
if (costDiff !== 0) return costDiff;
return (b.totals?.totalTokens ?? 0) - (a.totals?.totalTokens ?? 0);
});
return {
sessionId: params.sessionId,
sessionFile: params.sessionFile,
firstActivity,
lastActivity,
durationMs: firstActivity !== void 0 && lastActivity !== void 0 ? Math.max(0, lastActivity - firstActivity) : void 0,
activityDates: Array.from(activityDatesSet).toSorted(),
dailyBreakdown,
dailyMessageCounts,
utcQuarterHourMessageCounts: utcQuarterHourMessageCounts.length ? utcQuarterHourMessageCounts : void 0,
utcQuarterHourTokenUsage: utcQuarterHourTokenUsage.length ? utcQuarterHourTokenUsage : void 0,
dailyLatency: dailyLatency.length ? dailyLatency : void 0,
dailyModelUsage: dailyModelUsage.length ? dailyModelUsage : void 0,
messageCounts,
toolUsage,
modelUsage: modelUsage.length ? modelUsage : void 0,
latency: computeLatencyStats(latencyValues),
...totals
};
}
const extractCostBreakdown = (usageRaw) => {
if (!usageRaw || typeof usageRaw !== "object") return;
const cost = usageRaw.cost;
if (!cost) return;
const total = asFiniteNumber(cost.total);
if (total === void 0 || total < 0) return;
return {
total,
input: asFiniteNumber(cost.input),
output: asFiniteNumber(cost.output),
cacheRead: asFiniteNumber(cost.cacheRead),
cacheWrite: asFiniteNumber(cost.cacheWrite)
};
};
const parseTimestamp = (entry) => {
const raw = entry.timestamp;
if (typeof raw === "string") {
const parsed = new Date(raw);
if (!Number.isNaN(parsed.valueOf())) return parsed;
}
const message = entry.message;
const messageTimestamp = asFiniteNumber(message?.timestamp);
if (messageTimestamp !== void 0) {
const parsed = new Date(messageTimestamp);
if (!Number.isNaN(parsed.valueOf())) return parsed;
}
};
const parseTranscriptEntry = (entry) => {
const message = entry.message;
if (!message || typeof message !== "object") return null;
const roleRaw = message.role;
const role = roleRaw === "user" || roleRaw === "assistant" ? roleRaw : void 0;
if (!role) return null;
const usageRaw = message.usage ?? entry.usage;
const usage = usageRaw ? normalizeUsage(usageRaw) ?? void 0 : void 0;
const provider = (typeof message.provider === "string" ? message.provider : void 0) ?? (typeof entry.provider === "string" ? entry.provider : void 0);
const model = (typeof message.model === "string" ? message.model : void 0) ?? (typeof entry.model === "string" ? entry.model : void 0);
const costBreakdown = extractCostBreakdown(usageRaw);
const stopReason = typeof message.stopReason === "string" ? message.stopReason : void 0;
const durationMs = asFiniteNumber(message.durationMs ?? entry.durationMs);
return {
message,
role,
timestamp: parseTimestamp(entry),
durationMs,
usage,
costTotal: costBreakdown?.total,
costBreakdown,
provider,
model,
stopReason,
toolNames: extractToolCallNames(message),
toolResultCounts: countToolResults(message)
};
};
const formatDayKey = (date) => date.toLocaleDateString("en-CA", { timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone });
const formatUtcDayKey = (date) => `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}`;
const getUtcQuarterHourBucketKey = (date) => {
const quarterIndex = Math.floor((date.getUTCHours() * 60 + date.getUTCMinutes()) / 15);
const utcDayKey = formatUtcDayKey(date);
return {
date: utcDayKey,
quarterIndex,
key: `${utcDayKey}::${quarterIndex}`
};
};
/**
* Accumulate message-level counts into a bucket (daily or UTC quarter-hour).
* Avoids duplicating the same logic for both daily and quarter-hour message counts.
*/
const accumulateMessageCounts = (bucket, entry, errorStopReasons) => {
bucket.total += entry.role === "user" || entry.role === "assistant" ? 1 : 0;
if (entry.role === "user") bucket.user += 1;
else if (entry.role === "assistant") bucket.assistant += 1;
bucket.toolCalls += entry.toolNames.length;
bucket.toolResults += entry.toolResultCounts.total;
bucket.errors += entry.toolResultCounts.errors;
if (entry.stopReason && errorStopReasons.has(entry.stopReason)) bucket.errors += 1;
};
const computeLatencyStats = (values) => {
if (!values.length) return;
const sorted = values.toSorted((a, b) => a - b);
const total = sorted.reduce((sum, v) => sum + v, 0);
const count = sorted.length;
const p95Index = Math.max(0, Math.ceil(count * .95) - 1);
return {
count,
avgMs: total / count,
p95Ms: sorted[p95Index] ?? sorted[count - 1],
minMs: sorted[0],
maxMs: sorted[count - 1]
};
};
const computeUsageTokenTotals = (usage) => {
const input = usage.input ?? 0;
const output = usage.output ?? 0;
const cacheRead = usage.cacheRead ?? 0;
const cacheWrite = usage.cacheWrite ?? 0;
const componentTotal = input + output + cacheRead + cacheWrite;
return {
input,
output,
cacheRead,
cacheWrite,
componentTotal,
totalTokens: usage.total ?? componentTotal
};
};
const applyUsageTotals = (totals, usage) => {
const usageTotals = computeUsageTokenTotals(usage);
totals.input += usageTotals.input;
totals.output += usageTotals.output;
totals.cacheRead += usageTotals.cacheRead;
totals.cacheWrite += usageTotals.cacheWrite;
totals.totalTokens += usageTotals.totalTokens;
};
const applyCostBreakdown = (totals, costBreakdown) => {
if (costBreakdown === void 0 || costBreakdown.total === void 0) return;
totals.totalCost += costBreakdown.total;
totals.inputCost += costBreakdown.input ?? 0;
totals.outputCost += costBreakdown.output ?? 0;
totals.cacheReadCost += costBreakdown.cacheRead ?? 0;
totals.cacheWriteCost += costBreakdown.cacheWrite ?? 0;
};
const applyCostTotal = (totals, costTotal) => {
if (costTotal === void 0) {
totals.missingCostEntries += 1;
return;
}
totals.totalCost += costTotal;
};
const isModelPricingKnown = (cost) => {
if (!cost) return false;
if (cost.tieredPricing && cost.tieredPricing.length > 0) return true;
return cost.input > 0 || cost.output > 0 || cost.cacheRead > 0 || cost.cacheWrite > 0;
};
function createUsageCostResolver(config) {
const cache = /* @__PURE__ */ new Map();
return ({ provider, model }) => {
const key = `${provider ?? ""}\0${model ?? ""}`;
if (cache.has(key)) return cache.get(key);
const cost = resolveModelCostConfig({
provider,
model,
config
});
cache.set(key, cost);
return cost;
};
}
async function canReadJsonlFromOffset(filePath, startOffset) {
if (startOffset <= 0) return true;
const handle = await fs.promises.open(filePath, "r").catch(() => null);
if (!handle) return false;
try {
const buffer = Buffer.alloc(1);
return (await handle.read(buffer, 0, 1, startOffset - 1)).bytesRead === 1 && buffer[0] === 10;
} finally {
await handle.close().catch(() => void 0);
}
}
async function* readJsonlRecords(filePath, startOffset = 0, endOffset) {
if (endOffset !== void 0 && endOffset <= startOffset) return;
const streamOptions = {
encoding: "utf-8",
start: Math.max(0, startOffset)
};
if (endOffset !== void 0) streamOptions.end = endOffset - 1;
const fileStream = fs.createReadStream(filePath, streamOptions);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});
try {
for await (const line of rl) {
const trimmed = line.trim();
if (!trimmed) continue;
try {
const parsed = JSON.parse(trimmed);
if (!parsed || typeof parsed !== "object") continue;
yield parsed;
} catch {}
}
} finally {
rl.close();
fileStream.destroy();
}
}
async function scanTranscriptFile(params) {
const resolveCost = params.resolveCost ?? createUsageCostResolver(params.config);
for await (const parsed of readJsonlRecords(params.filePath, params.startOffset, params.endOffset)) {
const entry = parseTranscriptEntry(parsed);
if (!entry) continue;
if (entry.usage) {
const cost = resolveCost({
provider: entry.provider,
model: entry.model
});
if (cost?.tieredPricing && cost.tieredPricing.length > 0) {
entry.costTotal = estimateUsageCost({
usage: entry.usage,
cost
});
entry.costBreakdown = void 0;
} else if (!isModelPricingKnown(cost) && (entry.costTotal === void 0 || entry.costTotal === 0) && computeUsageTokenTotals(entry.usage).totalTokens > 0) {
entry.costTotal = void 0;
entry.costBreakdown = void 0;
} else if (entry.costTotal === void 0) entry.costTotal = estimateUsageCost({
usage: entry.usage,
cost
});
}
params.onEntry(entry);
}
}
async function scanUsageFile(params) {
await scanTranscriptFile({
filePath: params.filePath,
config: params.config,
resolveCost: params.resolveCost,
startOffset: params.startOffset,
endOffset: params.endOffset,
onEntry: (entry) => {
if (!entry.usage) return;
params.onEntry({
usage: entry.usage,
costTotal: entry.costTotal,
costBreakdown: entry.costBreakdown,
provider: entry.provider,
model: entry.model,
timestamp: entry.timestamp
});
}
});
}
function resolveExistingUsageSessionFile(params) {
const candidate = params.sessionFile ?? (params.sessionId ? resolveSessionFilePath(params.sessionId, params.sessionEntry, { agentId: params.agentId }) : void 0);
if (candidate && fs.existsSync(candidate)) return candidate;
const sessionId = params.sessionId?.trim();
if (!sessionId) return candidate;
try {
const sessionsDir = candidate ? path.dirname(candidate) : resolveSessionTranscriptsDirForAgent(params.agentId);
const baseFileName = `${sessionId}.jsonl`;
const entries = fs.readdirSync(sessionsDir, { withFileTypes: true }).filter((entry) => {
return entry.isFile() && (entry.name === baseFileName || entry.name.startsWith(`${baseFileName}.reset.`) || entry.name.startsWith(`${baseFileName}.deleted.`));
});
const primary = entries.find((entry) => entry.name === baseFileName);
if (primary) return path.join(sessionsDir, primary.name);
const latestArchive = entries.filter((entry) => isSessionArchiveArtifactName(entry.name)).map((entry) => entry.name).toSorted((a, b) => {
const tsA = parseSessionArchiveTimestamp(a, "deleted") ?? parseSessionArchiveTimestamp(a, "reset") ?? 0;
return (parseSessionArchiveTimestamp(b, "deleted") ?? parseSessionArchiveTimestamp(b, "reset") ?? 0) - tsA || b.localeCompare(a);
})[0];
return latestArchive ? path.join(sessionsDir, latestArchive) : candidate;
} catch {
return candidate;
}
}
async function loadCostUsageSummary(params) {
const now = /* @__PURE__ */ new Date();
let sinceTime;
let untilTime;
if (params?.startMs !== void 0 && params?.endMs !== void 0) {
sinceTime = params.startMs;
untilTime = params.endMs;
} else {
const days = Math.max(1, Math.floor(params?.days ?? 30));
const since = new Date(now);
since.setDate(since.getDate() - (days - 1));
sinceTime = since.getTime();
untilTime = now.getTime();
}
const dailyMap = /* @__PURE__ */ new Map();
const totals = emptyTotals();
const resolveCost = createUsageCostResolver(params?.config);
const files = await listUsageCountedTranscriptFileStats(params?.agentId, { minMtimeMs: sinceTime });
for (const file of files) await scanUsageFile({
filePath: file.filePath,
config: params?.config,
resolveCost,
onEntry: (entry) => {
const ts = entry.timestamp?.getTime();
if (!ts || ts < sinceTime || ts > untilTime) return;
const dayKey = formatDayKey(entry.timestamp ?? now);
const bucket = dailyMap.get(dayKey) ?? emptyTotals();
applyUsageTotals(bucket, entry.usage);
if (entry.costBreakdown?.total !== void 0) applyCostBreakdown(bucket, entry.costBreakdown);
else applyCostTotal(bucket, entry.costTotal);
dailyMap.set(dayKey, bucket);
applyUsageTotals(totals, entry.usage);
if (entry.costBreakdown?.total !== void 0) applyCostBreakdown(totals, entry.costBreakdown);
else applyCostTotal(totals, entry.costTotal);
}
});
const daily = Array.from(dailyMap.entries()).map(([date, bucket]) => Object.assign({ date }, bucket)).toSorted((a, b) => a.date.localeCompare(b.date));
const days = Math.ceil((untilTime - sinceTime) / (1440 * 60 * 1e3)) + 1;
return {
updatedAt: Date.now(),
days,
daily,
totals
};
}
async function scanUsageFileForCache(params) {
const pricingFingerprint = resolveUsageCostPricingFingerprint(params.config);
const appendOnlyPreviousCandidate = params.previous && params.previous.filePath === params.file.filePath && params.previous.size > 0 && params.previous.size < params.file.size && params.previous.pricingFingerprint === pricingFingerprint && params.previous.mtimeMs <= params.file.mtimeMs ? params.previous : void 0;
const appendOnlyPrevious = appendOnlyPreviousCandidate && (!params.includeSessionSummary || appendOnlyPreviousCandidate.transcriptEntries) ? appendOnlyPreviousCandidate : void 0;
const totals = emptyTotals();
const usageEntries = [];
const shouldTrackTranscriptEntries = params.includeSessionSummary || Boolean(appendOnlyPrevious?.transcriptEntries);
const transcriptEntries = shouldTrackTranscriptEntries ? [] : void 0;
let parsedRecords = 0;
let countedRecords = 0;
const startOffset = appendOnlyPrevious && await canReadJsonlFromOffset(params.file.filePath, appendOnlyPrevious.size) ? appendOnlyPrevious.size : void 0;
await scanTranscriptFile({
filePath: params.file.filePath,
config: params.config,
resolveCost: params.resolveCost,
startOffset,
endOffset: params.file.size,
onEntry: (entry) => {
const ts = entry.timestamp?.getTime();
let entryTotals;
if (entry.usage) {
parsedRecords += 1;
entryTotals = emptyTotals();
applyUsageTotals(entryTotals, entry.usage);
if (entry.costBreakdown?.total !== void 0) applyCostBreakdown(entryTotals, entry.costBreakdown);
else applyCostTotal(entryTotals, entry.costTotal);
addTotals(totals, entryTotals);
if (ts !== void 0) {
countedRecords += 1;
usageEntries.push({
timestamp: ts,
provider: entry.provider,
model: entry.model,
...entryTotals
});
}
}
transcriptEntries?.push({
timestamp: ts,
role: entry.role,
durationMs: entry.durationMs,
provider: entry.provider,
model: entry.model,
stopReason: entry.stopReason,
toolNames: entry.toolNames,
toolResultCounts: entry.toolResultCounts,
usageTotals: entryTotals ? cloneTotals(entryTotals) : void 0
});
}
});
const sessionId = parseUsageCountedSessionIdFromFileName(path.basename(params.file.filePath)) ?? void 0;
const combinedTranscriptEntries = shouldTrackTranscriptEntries ? [...(appendOnlyPrevious && startOffset !== void 0 ? appendOnlyPrevious.transcriptEntries : void 0) ?? [], ...transcriptEntries ?? []] : void 0;
const sessionSummary = combinedTranscriptEntries && (params.includeSessionSummary || appendOnlyPrevious?.sessionSummary) ? buildSessionCostSummaryFromCacheEntry({
entry: {
filePath: params.file.filePath,
size: params.file.size,
mtimeMs: params.file.mtimeMs,
pricingFingerprint,
scannedAt: Date.now(),
parsedRecords,
countedRecords,
usageEntries,
transcriptEntries: combinedTranscriptEntries,
totals,
sessionId
},
sessionId,
sessionFile: params.file.filePath,
startMs: Number.NEGATIVE_INFINITY,
endMs: Number.POSITIVE_INFINITY
}) ?? void 0 : void 0;
if (appendOnlyPrevious && startOffset !== void 0) {
const previousTotals = cloneTotals(appendOnlyPrevious.totals);
addTotals(previousTotals, totals);
return {
...appendOnlyPrevious,
size: params.file.size,
mtimeMs: params.file.mtimeMs,
pricingFingerprint,
scannedAt: Date.now(),
parsedRecords: appendOnlyPrevious.parsedRecords + parsedRecords,
countedRecords: appendOnlyPrevious.countedRecords + countedRecords,
usageEntries: [...appendOnlyPrevious.usageEntries, ...usageEntries],
transcriptEntries: combinedTranscriptEntries,
totals: previousTotals,
sessionSummary
};
}
return {
filePath: params.file.filePath,
size: params.file.size,
mtimeMs: params.file.mtimeMs,
pricingFingerprint,
scannedAt: Date.now(),
parsedRecords,
countedRecords,
usageEntries,
transcriptEntries: combinedTranscriptEntries,
totals,
sessionId,
sessionSummary
};
}
async function refreshCostUsageCacheForPath(params) {
const cachePath = params?.cachePath ?? resolveUsageCostCachePath(params?.agentId);
const lock = await acquireUsageCostCacheRefreshLock(cachePath);
if (!lock.acquired) return "busy";
try {
const pricingFingerprint = resolveUsageCostPricingFingerprint(params?.config);
const cache = await readUsageCostCache(cachePath);
const files = await listUsageCountedTranscriptFiles(params?.agentId, { sessionsDir: params?.sessionsDir });
const sessionSummaryFiles = new Set(params?.sessionFiles ?? []);
const refreshStartMs = params?.startMs;
const refreshFiles = sessionSummaryFiles.size > 0 ? files.filter((file) => sessionSummaryFiles.has(file.filePath)) : refreshStartMs === void 0 ? files : files.filter((file) => file.mtimeMs >= refreshStartMs);
const livePaths = new Set(files.map((file) => file.filePath));
let cacheMutated = false;
for (const filePath of Object.keys(cache.files)) if (!livePaths.has(filePath)) {
delete cache.files[filePath];
cacheMutated = true;
}
const maxFiles = params?.maxFiles !== void 0 && Number.isFinite(params.maxFiles) && params.maxFiles > 0 ? Math.floor(params.maxFiles) : void 0;
const staleFiles = getUsageCostStaleFiles({
cache,
files: refreshFiles,
pricingFingerprint,
sessionSummaryFiles
}).toSorted((a, b) => {
return (sessionSummaryFiles.has(a.filePath) ? 0 : 1) - (sessionSummaryFiles.has(b.filePath) ? 0 : 1) || a.size - b.size || a.filePath.localeCompare(b.filePath);
}).slice(0, maxFiles);
const resolveCost = createUsageCostResolver(params?.config);
let dirtyCount = 0;
let lastCheckpointMs = Date.now();
for (const file of staleFiles) {
cache.files[file.filePath] = await scanUsageFileForCache({
file,
config: params?.config,
resolveCost,
previous: cache.files[file.filePath],
includeSessionSummary: sessionSummaryFiles.has(file.filePath)
});
dirtyCount += 1;
cacheMutated = true;
const now = Date.now();
if (dirtyCount >= USAGE_COST_CACHE_CHECKPOINT_FILES || now - lastCheckpointMs >= USAGE_COST_CACHE_CHECKPOINT_INTERVAL_MS) {
cache.updatedAt = now;
await writeUsageCostCache(cachePath, cache);
dirtyCount = 0;
lastCheckpointMs = Date.now();
}
}
if (cacheMutated || dirtyCount > 0) {
cache.updatedAt = Date.now();
await writeUsageCostCache(cachePath, cache);
}
return "refreshed";
} finally {
await lock.release();
}
}
async function refreshCostUsageCache(params) {
return await refreshCostUsageCacheForPath(params);
}
async function loadCostUsageSummaryFromCache(params) {
const cachePath = resolveUsageCostCachePath(params.agentId);
const pricingFingerprint = resolveUsageCostPricingFingerprint(params.config);
let [cache, files] = await Promise.all([readUsageCostCache(cachePath), listUsageCountedTranscriptFiles(params.agentId)]);
const staleFiles = getUsageCostStaleFiles({
cache,
files,
pricingFingerprint
});
if (params.requestRefresh !== false && staleFiles.length > 0) {
const cachedFiles = countUsableUsageCostCacheFiles({
cache,
files,
pricingFingerprint
});
if (params.refreshMode === "sync-when-empty" && cachedFiles === 0) {
const result = await refreshCostUsageCache({
config: params.config,
agentId: params.agentId,
startMs: params.startMs
});
[cache, files] = await Promise.all([readUsageCostCache(cachePath), listUsageCountedTranscriptFiles(params.agentId)]);
if (result === "refreshed") {
if (getUsageCostStaleFiles({
cache,
files,
pricingFingerprint
}).length > 0) requestCostUsageCacheRefresh({
config: params.config,
agentId: params.agentId
});
}
} else requestCostUsageCacheRefresh({
config: params.config,
agentId: params.agentId
});
}
const refreshRunning = await isUsageCostCacheRefreshRunning(cachePath);
return buildCostUsageSummaryFromCache({
cache,
files,
startMs: params.startMs,
endMs: params.endMs,
pricingFingerprint,
refreshing: usageCostRefreshes.has(cachePath) || refreshRunning
});
}
async function loadSessionCostSummaryFromCache(params) {
const cachePath = resolveUsageCostCachePath(params.agentId);
const pricingFingerprint = resolveUsageCostPricingFingerprint(params.config);
let [cache, stats] = await Promise.all([readUsageCostCache(cachePath), fs.promises.stat(params.sessionFile).catch(() => null)]);
let file = stats ? {
filePath: params.sessionFile,
size: stats.size,
mtimeMs: stats.mtimeMs
} : void 0;
let entry = cache.files[params.sessionFile];
let stale = !file || !isUsageCostCacheEntryFresh({
entry,
file,
pricingFingerprint,
requireSessionSummary: true
});
let refreshRequested = false;
if (params.requestRefresh !== false && stale) if (params.refreshMode === "sync-when-empty") if (await refreshCostUsageCache({
config: params.config,
agentId: params.agentId,
sessionFiles: [params.sessionFile]
}) === "refreshed") {
[cache, stats] = await Promise.all([readUsageCostCache(cachePath), fs.promises.stat(params.sessionFile).catch(() => null)]);
file = stats ? {
filePath: params.sessionFile,
size: stats.size,
mtimeMs: stats.mtimeMs
} : void 0;
entry = cache.files[params.sessionFile];
stale = !file || !isUsageCostCacheEntryFresh({
entry,
file,
pricingFingerprint,
requireSessionSummary: true
});
} else {
requestCostUsageCacheRefresh({
config: params.config,
agentId: params.agentId,
sessionFiles: [params.sessionFile]
});
refreshRequested = true;
}
else {
requestCostUsageCacheRefresh({
config: params.config,
agentId: params.agentId,
sessionFiles: [params.sessionFile]
});
refreshRequested = true;
}
const refreshRunning = usageCostRefreshes.has(cachePath) || await isUsageCostCacheRefreshRunning(cachePath);
let summary = stale ? null : entry?.sessionSummary ?? null;
if (!summary && params.refreshMode === "sync-when-empty") summary = await loadSessionCostSummary({
sessionId: params.sessionId,
sessionEntry: params.sessionEntry,
sessionFile: params.sessionFile,
config: params.config,
agentId: params.agentId,
startMs: params.startMs,
endMs: params.endMs
});
if (summary && params.startMs !== void 0 && params.endMs !== void 0 && !isSessionSummaryContainedInRange(summary, params.startMs, params.endMs)) summary = entry ? buildSessionCostSummaryFromCacheEntry({
entry,
sessionId: params.sessionId,
sessionFile: params.sessionFile,
startMs: params.startMs,
endMs: params.endMs
}) : params.refreshMode === "sync-when-empty" ? await loadSessionCostSummary({
sessionId: params.sessionId,
sessionEntry: params.sessionEntry,
sessionFile: params.sessionFile,
config: params.config,
agentId: params.agentId,
startMs: params.startMs,
endMs: params.endMs
}) : null;
return {
summary,
cacheStatus: {
status: stale ? refreshRunning || refreshRequested ? "refreshing" : summary ? "partial" : "stale" : "fresh",
cachedFiles: stale ? 0 : 1,
pendingFiles: stale ? 1 : 0,
staleFiles: stale ? 1 : 0,
refreshedAt: cache.updatedAt || void 0
}
};
}
function requestCostUsageCacheRefresh(params) {
const cachePath = resolveUsageCostCachePath(params?.agentId);
const existing = usageCostRefreshes.get(cachePath);
if (existing) {
mergeUsageCostRefreshRequest(existing, params);
return;
}
const state = {
agentId: params?.agentId,
cachePath,
config: params?.config,
fullRefreshRequested: false,
pendingSessionFiles: /* @__PURE__ */ new Set(),
running: false,
sessionsDir: path.dirname(cachePath)
};
mergeUsageCostRefreshRequest(state, params);
usageCostRefreshes.set(cachePath, state);
scheduleUsageCostRefresh(cachePath, state);
}
function mergeUsageCostRefreshRequest(state, params) {
if (params?.config) state.config = params.config;
if (params?.agentId) state.agentId = params.agentId;
if (!params?.sessionFiles) {
state.fullRefreshRequested = true;
return;
}
for (const sessionFile of params.sessionFiles) state.pendingSessionFiles.add(sessionFile);
}
function scheduleUsageCostRefresh(refreshKey, state, delayMs = 0) {
if (state.running || state.timer) return;
const timer = setTimeout(() => {
state.timer = void 0;
runQueuedUsageCostRefresh(refreshKey, state);
}, delayMs);
timer.unref?.();
state.timer = timer;
}
async function runQueuedUsageCostRefresh(refreshKey, state) {
state.running = true;
let retryDelayMs = 0;
try {
while (state.fullRefreshRequested || state.pendingSessionFiles.size > 0) {
const fullRefreshRequested = state.fullRefreshRequested;
const sessionFiles = fullRefreshRequested ? [] : [...state.pendingSessionFiles];
if (!fullRefreshRequested) state.pendingSessionFiles.clear();
state.fullRefreshRequested = false;
if (await refreshCostUsageCacheForPath({
cachePath: state.cachePath,
config: state.config,
agentId: state.agentId,
sessionsDir: state.sessionsDir,
sessionFiles: fullRefreshRequested ? void 0 : sessionFiles
}) === "busy") {
if (fullRefreshRequested) state.fullRefreshRequested = true;
else for (const sessionFile of sessionFiles) state.pendingSessionFiles.add(sessionFile);
retryDelayMs = 50;
break;
}
}
} catch (error) {
logger.warn(`background refresh failed: ${formatErrorMessage(error)}`, { error });
} finally {
state.running = false;
if (state.fullRefreshRequested || state.pendingSessionFiles.size > 0) scheduleUsageCostRefresh(refreshKey, state, retryDelayMs);
else usageCostRefreshes.delete(refreshKey);
}
}
/**
* Scan all transcript files to discover sessions not in the session store.
* Returns basic metadata for each discovered session.
*/
async function discoverAllSessions(params) {
const files = await listUsageCountedTranscriptFileStats(params?.agentId, { minMtimeMs: params?.startMs });
const discovered = /* @__PURE__ */ new Map();
for (const file of files) {
const filePath = file.filePath;
const fileName = path.basename(filePath);
const sessionId = parseUsageCountedSessionIdFromFileName(fileName);
if (!sessionId) continue;
const isPrimaryTranscript = isPrimarySessionTranscriptFileName(fileName);
let firstUserMessage;
if (params?.includeFirstUserMessage !== false) try {
for await (const parsed of readJsonlRecords(filePath)) try {
const message = parsed.message;
if (message?.role === "user") {
const content = message.content;
if (typeof content === "string") firstUserMessage = content.slice(0, 100);
else if (Array.isArray(content)) {
for (const block of content) if (typeof block === "object" && block && block.type === "text") {
const text = block.text;
if (typeof text === "string") firstUserMessage = text.slice(0, 100);
break;
}
}
break;
}
} catch {}
} catch {}
const existing = discovered.get(sessionId);
const existingIsPrimary = existing ? isPrimarySessionTranscriptFileName(path.basename(existing.sessionFile)) : false;
if (!existing || isPrimaryTranscript && !existingIsPrimary || isPrimaryTranscript === existingIsPrimary && file.mtimeMs >= existing.mtime) {
discovered.set(sessionId, {
sessionId,
sessionFile: filePath,
mtime: file.mtimeMs,
firstUserMessage: firstUserMessage ?? existing?.firstUserMessage
});
continue;
}
if (!existing.firstUserMessage && firstUserMessage) {
existing.firstUserMessage = firstUserMessage;
discovered.set(sessionId, existing);
}
}
return Array.from(discovered.values()).toSorted((a, b) => b.mtime - a.mtime);
}
async function loadSessionCostSummary(params) {
const sessionFile = resolveExistingUsageSessionFile(params);
if (!sessionFile || !fs.existsSync(sessionFile)) return null;
const totals = emptyTotals();
let firstActivity;
let lastActivity;
const activityDatesSet = /* @__PURE__ */ new Set();
const dailyMap = /* @__PURE__ */ new Map();
const dailyMessageMap = /* @__PURE__ */ new Map();
const utcQuarterHourMessageMap = /* @__PURE__ */ new Map();
const utcQuarterHourTokenMap = /* @__PURE__ */ new Map();
const dailyLatencyMap = /* @__PURE__ */ new Map();
const dailyModelUsageMap = /* @__PURE__ */ new Map();
const messageCounts = {
total: 0,
user: 0,
assistant: 0,
toolCalls: 0,
toolResults: 0,
errors: 0
};
const toolUsageMap = /* @__PURE__ */ new Map();
const modelUsageMap = /* @__PURE__ */ new Map();
const errorStopReasons = new Set([
"error",
"aborted",
"timeout"
]);
const latencyValues = [];
let lastUserTimestamp;
const MAX_LATENCY_MS = 720 * 60 * 1e3;
const resolveCost = createUsageCostResolver(params.config);
await scanTranscriptFile({
filePath: sessionFile,
config: params.config,
resolveCost,
onEntry: (entry) => {
const ts = entry.timestamp?.getTime();
if (params.startMs !== void 0 && ts !== void 0 && ts < params.startMs) return;
if (params.endMs !== void 0 && ts !== void 0 && ts > params.endMs) return;
if (ts !== void 0) {
if (!firstActivity || ts < firstActivity) firstActivity = ts;
if (!lastActivity || ts > lastActivity) lastActivity = ts;
}
if (entry.role === "user") {
messageCounts.user += 1;
messageCounts.total += 1;
if (entry.timestamp) lastUserTimestamp = entry.timestamp.getTime();
}
if (entry.role === "assistant") {
messageCounts.assistant += 1;
messageCounts.total += 1;
const tsLocal = entry.timestamp?.getTime();
if (tsLocal !== void 0) {
const latencyMs = entry.durationMs ?? (lastUserTimestamp !== void 0 ? Math.max(0, tsLocal - lastUserTimestamp) : void 0);
if (latencyMs !== void 0 && Number.isFinite(latencyMs) && latencyMs <= MAX_LATENCY_MS) {
latencyValues.push(latencyMs);
const dayKey = formatDayKey(entry.timestamp ?? new Date(tsLocal));
const dailyLatencies = dailyLatencyMap.get(dayKey) ?? [];
dailyLatencies.push(latencyMs);
dailyLatencyMap.set(dayKey, dailyLatencies);
}
}
}
if (entry.toolNames.length > 0) {
messageCounts.toolCalls += entry.toolNames.length;
for (const name of entry.toolNames) toolUsageMap.set(name, (toolUsageMap.get(name) ?? 0) + 1);
}
if (entry.toolResultCounts.total > 0) {
messageCounts.toolResults += entry.toolResultCounts.total;
messageCounts.errors += entry.toolResultCounts.errors;
}
if (entry.stopReason && errorStopReasons.has(entry.stopReason)) messageCounts.errors += 1;
if (entry.timestamp) {
const dayKey = formatDayKey(entry.timestamp);
activityDatesSet.add(dayKey);
const daily = dailyMessageMap.get(dayKey) ?? {
date: dayKey,
total: 0,
user: 0,
assistant: 0,
toolCalls: 0,
toolResults: 0,
errors: 0
};
accumulateMessageCounts(daily, entry, errorStopReasons);
dailyMessageMap.set(dayKey, daily);
const quarterBucket = getUtcQuarterHourBucketKey(entry.timestamp);
const utcQuarterHour = utcQuarterHourMessageMap.get(quarterBucket.key) ?? {
date: quarterBucket.date,
quarterIndex: quarterBucket.quarterIndex,
total: 0,
user: 0,
assistant: 0,
toolCalls: 0,
toolResults: 0,
errors: 0
};
accumulateMessageCounts(utcQuarterHour, entry, errorStopReasons);
utcQuarterHourMessageMap.set(quarterBucket.key, utcQuarterHour);
}
if (!entry.usage) return;
applyUsageTotals(totals, entry.usage);
if (entry.costBreakdown?.total !== void 0) applyCostBreakdown(totals, entry.costBreakdown);
else applyCostTotal(totals, entry.costTotal);
if (entry.timestamp) {
const dayKey = formatDayKey(entry.timestamp);
const entryTokenTotals = computeUsageTokenTotals(entry.usage);
const entryTokens = entryTokenTotals.componentTotal;
const entryCost = entry.costBreakdown?.total ?? (entry.costBreakdown ? (entry.costBreakdown.input ?? 0) + (entry.costBreakdown.output ?? 0) + (entry.costBreakdown.cacheRead ?? 0) + (entry.costBreakdown.cacheWrite ?? 0) : entry.costTotal ?? 0);
const quarterBucket = getUtcQuarterHourBucketKey(entry.timestamp);
const utcQuarterHourToken = utcQuarterHourTokenMap.get(quarterBucket.key) ?? {
date: quarterBucket.date,
quarterIndex: quarterBucket.quarterIndex,
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
totalCost: 0
};
utcQuarterHourToken.input += entryTokenTotals.input;
utcQuarterHourToken.output += entryTokenTotals.output;
utcQuarterHourToken.cacheRead += entryTokenTotals.cacheRead;
utcQuarterHourToken.cacheWrite += entryTokenTotals.cacheWrite;
utcQuarterHourToken.totalTokens += entryTokenTotals.totalTokens;
utcQuarterHourToken.totalCost += entryCost;
utcQuarterHourTokenMap.set(quarterBucket.key, utcQuarterHourToken);
const existing = dailyMap.get(dayKey) ?? {
tokens: 0,
cost: 0
};
dailyMap.set(dayKey, {
tokens: existing.tokens + entryTokens,
cost: existing.cost + entryCost
});
if (entry.provider || entry.model) {
const modelKey = `${dayKey}::${entry.provider ?? "unknown"}::${entry.model ?? "unknown"}`;
const dailyModel = dailyModelUsageMap.get(modelKey) ?? {
date: dayKey,
provider: entry.provider,
model: entry.model,
tokens: 0,
cost: 0,
count: 0
};
dailyModel.tokens += entryTokens;
dailyModel.cost += entryCost;
dailyModel.count += 1;
dailyModelUsageMap.set(modelKey, dailyModel);
}
}
if (entry.provider || entry.model) {
const key = `${entry.provider ?? "unknown"}::${entry.model ?? "unknown"}`;
const existing = modelUsageMap.get(key) ?? {
provider: entry.provider,
model: entry.model,
count: 0,
totals: emptyTotals()
};
existing.count += 1;
applyUsageTotals(existing.totals, entry.usage);
if (entry.costBreakdown?.total !== void 0) applyCostBreakdown(existing.totals, entry.costBreakdown);
else applyCostTotal(existing.totals, entry.costTotal);
modelUsageMap.set(key, existing);
}
}
});
const dailyBreakdown = Array.from(dailyMap.entries()).map(([date, data]) => ({
date,
tokens: data.tokens,
cost: data.cost
})).toSorted((a, b) => a.date.localeCompare(b.date));
const dailyMessageCounts = Array.from(dailyMessageMap.values()).toSorted((a, b) => a.date.localeCompare(b.date));
const utcQuarterHourMessageCounts = Array.from(utcQuarterHourMessageMap.values()).toSorted((a, b) => a.date.localeCompare(b.date) || a.quarterIndex - b.quarterIndex);
const utcQuarterHourTokenUsage = Array.from(utcQuarterHourTokenMap.values()).toSorted((a, b) => a.date.localeCompare(b.date) || a.quarterIndex - b.quarterIndex);
const dailyLatency = Array.from(dailyLatencyMap.entries()).map(([date, values]) => {
const stats = computeLatencyStats(values);
if (!stats) return null;
return Object.assign({ date }, stats);
}).filter((entry) => Boolean(entry)).toSorted((a, b) => a.date.localeCompare(b.date));
const dailyModelUsage = Array.from(dailyModelUsageMap.values()).toSorted((a, b) => a.date.localeCompare(b.date) || b.cost - a.cost);
const toolUsage = toolUsageMap.size ? {
totalCalls: Array.from(toolUsageMap.values()).reduce((sum, count) => sum + count, 0),
uniqueTools: toolUsageMap.size,
tools: Array.from(toolUsageMap.entries()).map(([name, count]) => ({
name,
count
})).toSorted((a, b) => b.count - a.count)
} : void 0;
const modelUsage = modelUsageMap.size ? Array.from(modelUsageMap.values()).toSorted((a, b) => {
const costDiff = (b.totals?.totalCost ?? 0) - (a.totals?.totalCost ?? 0);
if (costDiff !== 0) return costDiff;
return (b.totals?.totalTokens ?? 0) - (a.totals?.totalTokens ?? 0);
}) : void 0;
return {
sessionId: params.sessionId,
sessionFile,
firstActivity,
lastActivity,
durationMs: firstActivity !== void 0 && lastActivity !== void 0 ? Math.max(0, lastActivity - firstActivity) : void 0,
activityDates: Array.from(activityDatesSet).toSorted(),
dailyBreakdown,
dailyMessageCounts,
utcQuarterHourMessageCounts: utcQuarterHourMessageCounts.length ? utcQuarterHourMessageCounts : void 0,
utcQuarterHourTokenUsage: utcQuarterHourTokenUsage.length ? utcQuarterHourTokenUsage : void 0,
dailyLatency: dailyLatency.length ? dailyLatency : void 0,
dailyModelUsage: dailyModelUsage.length ? dailyModelUsage : void 0,
messageCounts,
toolUsage,
modelUsage,
latency: computeLatencyStats(latencyValues),
...totals
};
}
async function loadSessionUsageTimeSeries(params) {
const sessionFile = resolveExistingUsageSessionFile(params);
if (!sessionFile || !fs.existsSync(sessionFile)) return null;
if (params.maxPoints !== void 0 && params.maxPoints !== null) {
if (!Number.isFinite(params.maxPoints) || params.maxPoints <= 0) return {
sessionId: params.sessionId,
points: []
};
}
const points = [];
let cumulativeTokens = 0;
let cumulativeCost = 0;
const resolveCost = createUsageCostResolver(params.config);
await scanUsageFile({
filePath: sessionFile,
config: params.config,
resolveCost,
onEntry: (entry) => {
const ts = entry.timestamp?.getTime();
if (!ts) return;
const { input, output, cacheRead, cacheWrite, totalTokens } = computeUsageTokenTotals(entry.usage);
const cost = entry.costTotal ?? 0;
cumulativeTokens += totalTokens;
cumulativeCost += cost;
points.push({
timestamp: ts,
input,
output,
cacheRead,
cacheWrite,
totalTokens,
cost,
cumulativeTokens,
cumulativeCost
});
}
});
const sortedPoints = points.toSorted((a, b) => a.timestamp - b.timestamp);
const maxPoints = params.maxPoints ?? 100;
if (sortedPoints.length > maxPoints) {
const step = Math.ceil(sortedPoints.length / maxPoints);
const downsampled = [];
let downsampledCumulativeTokens = 0;
let downsampledCumulativeCost = 0;
for (let i = 0; i < sortedPoints.length; i += step) {
const bucket = sortedPoints.slice(i, i + step);
const bucketLast = bucket[bucket.length - 1];
if (!bucketLast) continue;
let bucketInput = 0;
let bucketOutput = 0;
let bucketCacheRead = 0;
let bucketCacheWrite = 0;
let bucketTotalTokens = 0;
let bucketCost = 0;
for (const point of bucket) {
bucketInput += point.input;
bucketOutput += point.output;
bucketCacheRead += point.cacheRead;
bucketCacheWrite += point.cacheWrite;
bucketTotalTokens += point.totalTokens;
bucketCost += point.cost;
}
downsampledCumulativeTokens += bucketTotalTokens;
downsampledCumulativeCost += bucketCost;
downsampled.push({
timestamp: bucketLast.timestamp,
input: bucketInput,
output: bucketOutput,
cacheRead: bucketCacheRead,
cacheWrite: bucketCacheWrite,
totalTokens: bucketTotalTokens,
cost: bucketCost,
cumulativeTokens: downsampledCumulativeTokens,
cumulativeCost: downsampledCumulativeCost
});
}
return {
sessionId: params.sessionId,
points: downsampled
};
}
return {
sessionId: params.sessionId,
points: sortedPoints
};
}
async function loadSessionLogs(params) {
const sessionFile = resolveExistingUsageSessionFile(params);
if (!sessionFile || !fs.existsSync(sessionFile)) return null;
const logs = [];
if (params.limit !== void 0 && params.limit !== null) {
if (!Number.isFinite(params.limit) || params.limit <= 0) return [];
}
const limit = params.limit ?? 50;
const resolveCost = createUsageCostResolver(params.config);
for await (const parsed of readJsonlRecords(sessionFile)) try {
const message = parsed.message;
if (!message) continue;
const role = message.role;
if (role !== "user" && role !== "assistant" && role !== "tool" && role !== "toolResult") continue;
const contentParts = [];
const toolName = normalizeOptionalString(message.toolName ?? message.tool_name ?? message.name ?? message.tool);
if (role === "tool" || role === "toolResult") {
contentParts.push(`[Tool: ${toolName ?? "tool"}]`);
contentParts.push("[Tool Result]");
}
const rawContent = message.content;
if (typeof rawContent === "string") contentParts.push(rawContent);
else if (Array.isArray(rawContent)) {
const contentText = rawContent.map((block) => {
if (typeof block === "string") return block;
const b = block;
if (b.type === "text" && typeof b.text === "string") return b.text;
if (b.type === "tool_use") return `[Tool: ${typeof b.name === "string" ? b.name : "unknown"}]`;
if (b.type === "tool_result") return `[Tool Result]`;
return "";
}).filter(Boolean).join("\n");
if (contentText) contentParts.push(contentText);
}
const rawToolCalls = message.tool_calls ?? message.toolCalls ?? message.function_call ?? message.functionCall;
const toolCalls = Array.isArray(rawToolCalls) ? rawToolCalls : rawToolCalls ? [rawToolCalls] : [];
if (toolCalls.length > 0) for (const call of toolCalls) {
const callObj = call;
const directName = typeof callObj.name === "string" ? callObj.name : void 0;
const fn = callObj.function;
const fnName = typeof fn?.name === "string" ? fn.name : void 0;
const name = directName ?? fnName ?? "unknown";
contentParts.push(`[Tool: ${name}]`);
}
let content = contentParts.join("\n").trim();
if (!content) continue;
content = stripInboundMetadata(content);
if (role === "user") content = stripMessageIdHints(stripEnvelope(content)).trim();
if (!content) continue;
const maxLen = 2e3;
if (content.length > maxLen) content = content.slice(0, maxLen) + "…";
let timestamp = 0;
if (typeof parsed.timestamp === "string") timestamp = new Date(parsed.timestamp).getTime();
else if (typeof message.timestamp === "number") timestamp = message.timestamp;
let tokens;
let cost;
if (role === "assistant") {
const usageRaw = message.usage;
const usage = normalizeUsage(usageRaw);
if (usage) {
tokens = usage.total ?? (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
const breakdown = extractCostBreakdown(usageRaw);
if (breakdown?.total !== void 0) cost = breakdown.total;
else cost = estimateUsageCost({
usage,
cost: resolveCost({
provider: message.provider,
model: message.model
})
});
}
}
logs.push({
timestamp,
role,
content,
tokens,
cost
});
} catch {}
const sortedLogs = logs.toSorted((a, b) => a.timestamp - b.timestamp);
if (sortedLogs.length > limit) return sortedLogs.slice(-limit);
return sortedLogs;
}
//#endregion
export { loadSessionCostSummaryFromCache as a, resolveExistingUsageSessionFile as c, loadSessionCostSummary as i, loadCostUsageSummary as n, loadSessionLogs as o, loadCostUsageSummaryFromCache as r, loadSessionUsageTimeSeries as s, discoverAllSessions as t };