UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

2,073 lines 86.2 kB
import { s as asFiniteNumber } from "./number-coercion-CLj0HTDM.js";
import { c as isRecord } from "./record-coerce-DItp3I4t.js";
import { c as normalizeOptionalLowercaseString, l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js";
import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.js";
import { n as normalizeAgentId } from "./agent-id-CeT3w4ap.js";
import { u as resolveAgentDir } from "./agent-scope-config-DcbEhP0R.js";
import { O as parseAgentSessionKey } from "./session-key-BnWWjqNc.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js";
import { ot as stripEnvelope, st as stripMessageIdHints } from "./openclaw-state-db-BRTnL-D8.js";
import { i as stripInboundMetadata } from "./strip-inbound-meta-D5F_RINt.js";
import { a as isPrimarySessionTranscriptFileName, d as parseSessionArchiveTimestamp, f as parseUsageCountedSessionIdFromFileName, g as materializeSessionArchiveForRead, s as isSessionArchiveArtifactName, u as isUsageCountedSessionTranscriptFileName } from "./artifacts-DMDO9PHy.js";
import { i as resolveSessionFilePathCore, l as resolveSessionTranscriptsDirForAgent } from "./paths-CXdaYWF_.js";
import { t as runTasksWithConcurrency } from "./run-with-concurrency-Dtu208ef.js";
import { i as trackAsyncWork, r as getAsyncWorkSignal } from "./async-work-scope-CMQS2uTf.js";
import { k as resolveOpenClawAgentSqlitePath } from "./openclaw-agent-db-lease-Djvd6LWN.js";
import "./openclaw-agent-db-CWtDoRbC.js";
import { t as calculateUsageCost } from "./usage-cost-Va5dwVFW.js";
import "./src-g7CpceXJ.js";
import { c as normalizeUsage } from "./usage-CtmV8Xxq.js";
import { Nt as selectVisibleTranscriptEvents } from "./session-accessor-YsytfDtG.js";
import { M as readTranscriptStatsSync, S as loadTranscriptEventsSync, T as readTranscriptEventAtSeqSync, y as loadTranscriptEventRowsAfterSeqSync } from "./session-accessor.sqlite-transcript-store-B0zw3fAU.js";
import { E as resolveSessionStorePathForScope, c as listSessionTranscriptInstances, k as listSessionTranscriptArchivesReadOnly, l as loadSessionEntry } from "./session-accessor.sqlite-entry-CWk3jL7s.js";
import { n as parseSqliteSessionFileMarker, t as formatSqliteSessionFileMarker } from "./legacy-sqlite-marker-COPKCuIN.js";
import { r as resolveSqliteTargetFromSessionStorePath } from "./session-sqlite-target-Dp-kgpcT.js";
import { c as scanSessionTranscriptTree, n as isSessionTranscriptLeafControl, t as isCanonicalSessionTranscriptEntry } from "./transcript-tree-BH69pMSi.js";
import { t as streamSessionTranscriptLines } from "./transcript-stream-DIGsiEca.js";
import { a as resolveModelCostConfig, o as resolveModelCostConfigFingerprint } from "./usage-format-DhUP1uYp.js";
import { t as createTimeZoneDayKeyFormatter } from "./format-datetime-xsAchuLA.js";
import { n as cloneCostUsageTotals, r as createEmptyCostUsageTotals, t as addCostUsageTotals } from "./session-cost-usage-totals-D4e-85ui.js";
import { a as writeSessionCostUsageRollup, i as readSessionCostUsageRollupRows, n as deleteSessionCostUsageRollupsExcept, r as isSessionCostUsageRefreshRunning, t as acquireSessionCostUsageRefreshLock } from "./session-cost-usage-cache.sqlite-CcTxDOOr.js";
import fs from "node:fs";
import path from "node:path";
import { createHash } from "node:crypto";
function resolveUsageCostSessionStorePath(params) {
	return params.storePath ?? (params.sessionsDir ? path.join(params.sessionsDir, "sessions.json") : resolveSessionStorePathForScope({ agentId: params.agentId }));
}
async function resolveUsageCostJsonlFile(sourcePath, sourceStats) {
	const filePath = materializeSessionArchiveForRead(sourcePath);
	const stats = filePath === sourcePath ? sourceStats : await fs.promises.stat(filePath);
	return {
		filePath,
		sourcePath,
		kind: "jsonl",
		sessionId: parseUsageCountedSessionIdFromFileName(path.basename(sourcePath)) ?? void 0,
		size: stats.size,
		mtimeMs: sourceStats.mtimeMs,
		device: stats.dev,
		inode: stats.ino
	};
}
async function listUsageCountedTranscriptFileStats(agentId, params) {
	const sessionsDir = params?.sessionsDir ?? resolveSessionTranscriptsDirForAgent(agentId);
	let entries;
	try {
		entries = await fs.promises.readdir(sessionsDir, { withFileTypes: true });
	} catch (error) {
		if (error.code === "ENOENT") return [];
		throw error;
	}
	const tasks = entries.filter((entry) => entry.isFile() && isUsageCountedSessionTranscriptFileName(entry.name)).map((entry) => async () => {
		const filePath = path.join(sessionsDir, entry.name);
		try {
			const stats = await fs.promises.stat(filePath);
			if (params?.minMtimeMs !== void 0 && stats.mtimeMs < params.minMtimeMs) return;
			return await resolveUsageCostJsonlFile(filePath, stats);
		} catch (error) {
			if (error.code === "ENOENT") return;
			throw error;
		}
	});
	const { firstError, hasError, results } = await runTasksWithConcurrency({
		tasks,
		limit: 32
	});
	if (hasError) throw firstError;
	return results.filter((file) => Boolean(file));
}
function listUsageCountedSqliteTranscriptStats(agentId, params) {
	const storePath = params.storePath;
	const files = [];
	const instances = listSessionTranscriptInstances({
		agentId,
		storePath,
		projection: "list"
	});
	for (const instance of instances) {
		const marker = {
			agentId,
			sessionId: instance.sessionId,
			storePath
		};
		const mtimeMs = instance.updatedAtMs;
		if (params.minMtimeMs !== void 0 && mtimeMs < params.minMtimeMs) continue;
		const stats = readTranscriptStatsSync({
			agentId: marker.agentId,
			sessionId: marker.sessionId,
			storePath: marker.storePath
		});
		const filePath = formatCanonicalUsageCostSqliteMarker(marker);
		files.push({
			filePath,
			sourcePath: filePath,
			kind: "sqlite",
			mtimeMs,
			sessionId: marker.sessionId,
			size: stats.sizeBytes,
			eventCount: stats.eventCount,
			maxSeq: stats.maxSeq
		});
	}
	return files;
}
function formatCanonicalUsageCostSqliteMarker(marker) {
	const storePath = resolveSqliteTargetFromSessionStorePath(marker.storePath, { agentId: marker.agentId }).path ?? resolveOpenClawAgentSqlitePath({ agentId: marker.agentId });
	return formatSqliteSessionFileMarker({
		...marker,
		storePath
	});
}
async function listUsageCountedTranscriptStats(agentId, params) {
	const storePath = resolveUsageCostSessionStorePath({
		agentId,
		...params?.sessionsDir ? { sessionsDir: params.sessionsDir } : {},
		...params?.storePath ? { storePath: params.storePath } : {}
	});
	const sessionsDir = params?.sessionsDir ?? path.dirname(storePath);
	const fileBacked = await listUsageCountedTranscriptFileStats(agentId, {
		...params?.minMtimeMs !== void 0 ? { minMtimeMs: params.minMtimeMs } : {},
		sessionsDir
	});
	const archiveSessionIds = new Map(listSessionTranscriptArchivesReadOnly({
		agentId,
		archiveNames: fileBacked.map((file) => path.basename(file.sourcePath)),
		storePath
	}).map((archive) => [archive.archiveName, archive.sessionId]));
	for (const file of fileBacked) {
		const sessionId = archiveSessionIds.get(path.basename(file.sourcePath));
		if (sessionId) file.sessionId = sessionId;
	}
	const sqliteBacked = listUsageCountedSqliteTranscriptStats(agentId, {
		...params?.minMtimeMs !== void 0 ? { minMtimeMs: params.minMtimeMs } : {},
		storePath
	});
	const sqliteSessionIds = new Set(sqliteBacked.map((file) => file.sessionId).filter(Boolean));
	return [...fileBacked.filter((file) => !file.sessionId || !sqliteSessionIds.has(file.sessionId)), ...sqliteBacked];
}
async function resolveUsageCostTranscriptFile(sessionFile) {
	const marker = parseSqliteSessionFileMarker(sessionFile);
	if (marker) {
		const stats = readTranscriptStatsSync({
			agentId: marker.agentId,
			sessionId: marker.sessionId,
			storePath: marker.storePath
		});
		const filePath = formatCanonicalUsageCostSqliteMarker(marker);
		return {
			filePath,
			sourcePath: filePath,
			kind: "sqlite",
			mtimeMs: stats.lastMutationAtMs ?? 0,
			sessionId: marker.sessionId,
			size: stats.sizeBytes,
			eventCount: stats.eventCount,
			maxSeq: stats.maxSeq
		};
	}
	try {
		return await resolveUsageCostJsonlFile(sessionFile, await fs.promises.stat(sessionFile));
	} catch {
		return;
	}
}
function loadSqliteUsageTranscriptEvents(marker) {
	return selectVisibleTranscriptEvents(loadTranscriptEventsSync({
		agentId: marker.agentId,
		sessionId: marker.sessionId,
		storePath: marker.storePath
	})).filter(isRecord);
}
async function* readTranscriptRecords(filePath) {
	const marker = parseSqliteSessionFileMarker(filePath);
	if (marker) {
		for (const event of loadSqliteUsageTranscriptEvents(marker)) yield event;
		return;
	}
	const transcriptPath = materializeSessionArchiveForRead(filePath);
	for await (const line of streamSessionTranscriptLines(transcriptPath)) try {
		const parsed = JSON.parse(line);
		if (isRecord(parsed)) yield parsed;
	} catch {}
}
async function* readTranscriptRecordsBestEffort(filePath) {
	try {
		yield* readTranscriptRecords(filePath);
	} catch {}
}
function resolveExistingUsageSessionFile(params) {
	const sessionId = normalizeOptionalString(params.sessionId);
	const target = params.sessionTarget ? {
		agentId: normalizeOptionalString(params.sessionTarget.agentId),
		sessionId: normalizeOptionalString(params.sessionTarget.sessionId),
		sessionKey: normalizeOptionalString(params.sessionTarget.sessionKey),
		storePath: normalizeOptionalString(params.sessionTarget.storePath)
	} : void 0;
	const completeTarget = Boolean(target?.agentId && target.sessionId && target.sessionKey && target.storePath);
	if (target && completeTarget) {
		const targetKeyAgentId = parseAgentSessionKey(target.sessionKey)?.agentId;
		const targetKeyEntry = loadSessionEntry({
			agentId: target.agentId,
			sessionKey: target.sessionKey,
			storePath: target.storePath
		});
		if (sessionId !== void 0 && target.sessionId !== sessionId || target.agentId !== params.agentId || targetKeyAgentId && targetKeyAgentId !== target.agentId || targetKeyEntry && targetKeyEntry.sessionId !== target.sessionId) return;
		return formatCanonicalUsageCostSqliteMarker({
			agentId: target.agentId,
			sessionId: target.sessionId,
			storePath: target.storePath
		});
	}
	const legacySessionFile = params.sessionEntry?.sessionFile;
	const entryMarker = parseSqliteSessionFileMarker(typeof legacySessionFile === "string" ? legacySessionFile : void 0);
	const explicitMarker = parseSqliteSessionFileMarker(params.sessionFile);
	const matchingEntryMarker = entryMarker && entryMarker.agentId === params.agentId && (!sessionId || entryMarker.sessionId === sessionId) ? entryMarker : void 0;
	const matchingExplicitMarker = explicitMarker && explicitMarker.agentId === params.agentId && (!sessionId || explicitMarker.sessionId === sessionId) ? explicitMarker : void 0;
	if (!matchingEntryMarker && explicitMarker && !matchingExplicitMarker) return;
	const sqliteMarker = matchingEntryMarker ?? matchingExplicitMarker;
	const targetKeyAgentId = parseAgentSessionKey(target?.sessionKey)?.agentId;
	const targetKeyEntry = target?.sessionKey && sqliteMarker && !completeTarget ? loadSessionEntry({
		agentId: sqliteMarker.agentId,
		sessionKey: target.sessionKey,
		storePath: sqliteMarker.storePath
	}) : void 0;
	if (target && !completeTarget && sqliteMarker && (target.agentId && target.agentId !== sqliteMarker.agentId || target.sessionId && target.sessionId !== sqliteMarker.sessionId || targetKeyAgentId && targetKeyAgentId !== sqliteMarker.agentId || target.sessionKey && targetKeyEntry?.sessionId !== sqliteMarker.sessionId || target.storePath && path.resolve(target.storePath) !== path.resolve(sqliteMarker.storePath))) return;
	if (sqliteMarker) return formatSqliteSessionFileMarker(sqliteMarker);
	if (entryMarker && !params.sessionFile) return;
	const candidate = params.sessionFile ?? (sessionId ? resolveSessionFilePathCore(sessionId, params.sessionEntry, { agentId: params.agentId }) : void 0);
	if (candidate && fs.existsSync(candidate)) return candidate;
	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;
	}
}
//#endregion
//#region src/utils/transcript-tools.ts
/**
* Transcript inspection helpers shared by session filesystem views and usage metrics.
* Keep provider-specific block aliases centralized so both surfaces classify tools consistently.
*/
const TOOL_CALL_TYPES = /* @__PURE__ */ new Set([
	"tool_use",
	"toolcall",
	"tool_call"
]);
const TOOL_RESULT_TYPES = /* @__PURE__ */ new Set(["tool_result", "tool_result_error"]);
const normalizeType = (value) => {
	return typeof value === "string" ? normalizeOptionalLowercaseString(value) ?? "" : "";
};
/** Extracts de-duplicated tool names from direct fields and structured content blocks. */
const extractToolCallNames = (message) => {
	const names = /* @__PURE__ */ new Set();
	const toolNameRaw = message.toolName ?? message.tool_name;
	const toolName = typeof toolNameRaw === "string" ? normalizeOptionalString(toolNameRaw) : void 0;
	if (toolName) names.add(toolName);
	const content = message.content;
	if (!Array.isArray(content)) return Array.from(names);
	for (const entry of content) {
		if (!entry || typeof entry !== "object") continue;
		const block = entry;
		const type = normalizeType(block.type);
		if (!TOOL_CALL_TYPES.has(type)) continue;
		const name = typeof block.name === "string" ? normalizeOptionalString(block.name) : void 0;
		if (name) names.add(name);
	}
	return Array.from(names);
};
/** Counts recognized tool-result blocks and the subset explicitly marked as errors. */
const countToolResults = (message) => {
	const content = message.content;
	if (!Array.isArray(content)) return {
		total: 0,
		errors: 0
	};
	let total = 0;
	let errors = 0;
	for (const entry of content) {
		if (!entry || typeof entry !== "object") continue;
		const block = entry;
		const type = normalizeType(block.type);
		if (!TOOL_RESULT_TYPES.has(type)) continue;
		total += 1;
		if (block.is_error === true) errors += 1;
	}
	return {
		total,
		errors
	};
};
//#endregion
//#region src/infra/session-cost-usage-pricing.ts
const normalizeUsageCostTotalOrigin = (value) => value === "provider-billed" ? value : void 0;
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),
		totalOrigin: normalizeUsageCostTotalOrigin(cost.totalOrigin)
	};
};
const parseTimestamp = (entry) => {
	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 raw = entry.timestamp;
	if (typeof raw === "string") {
		const parsed = new Date(raw);
		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;
	const isStandaloneToolResult = roleRaw === "tool" || roleRaw === "toolResult";
	if (!role && !isStandaloneToolResult) 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: isStandaloneToolResult ? [] : extractToolCallNames(message),
		toolResultCounts: isStandaloneToolResult ? {
			total: 1,
			errors: message.isError === true || message.is_error === true ? 1 : 0
		} : countToolResults(message)
	};
};
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, provider, model) => {
	if (costTotal === void 0) {
		totals.missingCostEntries += 1;
		const modelKey = `${normalizeOptionalString(provider) ?? "unknown"}/${normalizeOptionalString(model) ?? "unknown"}`;
		totals.missingCostByModel ??= {};
		totals.missingCostByModel[modelKey] = (totals.missingCostByModel[modelKey] ?? 0) + 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;
};
const shouldPreserveRecordedZeroCost = (costBreakdown) => costBreakdown?.total === 0 && (costBreakdown.totalOrigin === "provider-billed" || [
	costBreakdown.input,
	costBreakdown.output,
	costBreakdown.cacheRead,
	costBreakdown.cacheWrite
].some((value) => value !== void 0 && value !== 0));
function createUsageCostResolver(params) {
	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: params?.config,
			agentDir: params?.agentDir
		});
		cache.set(key, cost);
		return cost;
	};
}
function parseUsageCostTranscriptEntry(parsed, resolveCost) {
	const entry = parseTranscriptEntry(parsed);
	if (!entry?.usage) return entry;
	const cost = resolveCost({
		provider: entry.provider,
		model: entry.model
	});
	const usageTotals = computeUsageTokenTotals(entry.usage);
	const pricingKnown = isModelPricingKnown(cost);
	if (entry.costBreakdown?.totalOrigin === "provider-billed" || shouldPreserveRecordedZeroCost(entry.costBreakdown)) return entry;
	if (!pricingKnown && (entry.costTotal === void 0 || entry.costTotal === 0) && usageTotals.totalTokens > 0) {
		entry.costTotal = void 0;
		entry.costBreakdown = void 0;
	} else if (cost?.tieredPricing?.length || entry.costTotal === void 0 || entry.costTotal === 0 && pricingKnown && usageTotals.totalTokens > 0) {
		const estimated = cost ? calculateUsageCost(entry.usage, cost) : void 0;
		entry.costBreakdown = estimated && Number.isFinite(estimated.total) ? estimated : void 0;
		entry.costTotal = entry.costBreakdown?.total;
	}
	return entry;
}
//#endregion
//#region src/shared/usage-aggregates.ts
/** Builds a collision-free identity while preserving legacy missing-as-unknown grouping. */
function usageModelIdentity(provider, model) {
	return JSON.stringify([provider ?? "unknown", model ?? "unknown"]);
}
/** Extends the model identity with its calendar bucket without delimiter ambiguity. */
function usageDailyModelIdentity(date, provider, model) {
	return JSON.stringify([
		date,
		provider ?? "unknown",
		model ?? "unknown"
	]);
}
function createLatencyAccumulator() {
	return {
		count: 0,
		sum: 0,
		min: Number.POSITIVE_INFINITY,
		max: 0,
		p95Max: 0
	};
}
function mergeLatency(target, source) {
	target.count += source.count;
	target.sum += source.avgMs * source.count;
	target.min = Math.min(target.min, source.minMs);
	target.max = Math.max(target.max, source.maxMs);
	target.p95Max = Math.max(target.p95Max, source.p95Ms);
}
function summarizeLatency(value) {
	return {
		count: value.count,
		avgMs: value.count ? value.sum / value.count : 0,
		minMs: value.min === Number.POSITIVE_INFINITY ? 0 : value.min,
		maxMs: value.max,
		p95Ms: value.p95Max
	};
}
function mergeModelUsage(map, key, entry) {
	const existing = map.get(key) ?? {
		provider: entry.provider,
		model: entry.model,
		count: 0,
		totals: createEmptyCostUsageTotals()
	};
	existing.count += entry.count;
	addCostUsageTotals(existing.totals, entry.totals);
	map.set(key, existing);
}
function mergeGroupedTotals(map, key, usage) {
	if (!key) return;
	const totals = map.get(key) ?? createEmptyCostUsageTotals();
	addCostUsageTotals(totals, usage);
	map.set(key, totals);
}
function compareModelUsage(left, right) {
	return right.totals.totalCost - left.totals.totalCost || right.totals.totalTokens - left.totals.totalTokens;
}
/** Shared accounting for gateway-wide results and filtered Control UI session rows. */
function createUsageAggregateAccumulator() {
	const totals = createEmptyCostUsageTotals();
	const messages = {
		total: 0,
		user: 0,
		assistant: 0,
		toolCalls: 0,
		toolResults: 0,
		errors: 0
	};
	const tools = /* @__PURE__ */ new Map();
	const models = /* @__PURE__ */ new Map();
	const providers = /* @__PURE__ */ new Map();
	const agents = /* @__PURE__ */ new Map();
	const channels = /* @__PURE__ */ new Map();
	const days = /* @__PURE__ */ new Map();
	const dailyLatency = /* @__PURE__ */ new Map();
	const dailyModels = /* @__PURE__ */ new Map();
	const latency = createLatencyAccumulator();
	let sessionCount = 0;
	let longestSessionDurationMs = 0;
	function getDay(date) {
		let day = days.get(date);
		if (!day) {
			day = {
				date,
				tokens: 0,
				cost: 0,
				messages: 0,
				toolCalls: 0,
				errors: 0
			};
			days.set(date, day);
		}
		return day;
	}
	function add({ usage, agentId, channel }) {
		if (!usage) return;
		addCostUsageTotals(totals, usage);
		longestSessionDurationMs = Math.max(longestSessionDurationMs, usage.durationMs ?? 0);
		if (usage.firstActivity !== void 0 || (usage.messageCounts?.total ?? 0) > 0) sessionCount += 1;
		if (usage.messageCounts) {
			messages.total += usage.messageCounts.total;
			messages.user += usage.messageCounts.user;
			messages.assistant += usage.messageCounts.assistant;
			messages.toolCalls += usage.messageCounts.toolCalls;
			messages.toolResults += usage.messageCounts.toolResults;
			messages.errors += usage.messageCounts.errors;
		}
		for (const tool of usage.toolUsage?.tools ?? []) tools.set(tool.name, (tools.get(tool.name) ?? 0) + tool.count);
		for (const entry of usage.modelUsage ?? []) {
			mergeModelUsage(models, usageModelIdentity(entry.provider, entry.model), entry);
			mergeModelUsage(providers, entry.provider ?? "unknown", {
				...entry,
				model: void 0
			});
		}
		mergeGroupedTotals(agents, agentId, usage);
		mergeGroupedTotals(channels, channel, usage);
		if (usage.latency && usage.latency.count > 0) mergeLatency(latency, usage.latency);
		for (const day of usage.dailyLatency ?? []) {
			const existing = dailyLatency.get(day.date) ?? createLatencyAccumulator();
			mergeLatency(existing, day);
			dailyLatency.set(day.date, existing);
		}
		for (const day of usage.dailyBreakdown ?? []) {
			const existing = getDay(day.date);
			existing.tokens += day.tokens;
			existing.cost += day.cost;
		}
		for (const day of usage.dailyMessageCounts ?? []) {
			const existing = getDay(day.date);
			existing.messages += day.total;
			existing.toolCalls += day.toolCalls;
			existing.errors += day.errors;
		}
		for (const day of usage.dailyModelUsage ?? []) {
			const key = usageDailyModelIdentity(day.date, day.provider, day.model);
			const existing = dailyModels.get(key) ?? {
				date: day.date,
				provider: day.provider,
				model: day.model,
				tokens: 0,
				cost: 0,
				count: 0
			};
			existing.tokens += day.tokens;
			existing.cost += day.cost;
			existing.count += day.count;
			dailyModels.set(key, existing);
		}
	}
	function finish() {
		const toolEntries = Array.from(tools, ([name, count]) => ({
			name,
			count
		})).toSorted((a, b) => b.count - a.count);
		return {
			sessionCount,
			...longestSessionDurationMs > 0 ? { longestSessionDurationMs } : {},
			messages,
			tools: {
				totalCalls: toolEntries.reduce((sum, { count }) => sum + count, 0),
				uniqueTools: tools.size,
				tools: toolEntries
			},
			byModel: Array.from(models.values()).toSorted(compareModelUsage),
			byProvider: Array.from(providers.values()).toSorted(compareModelUsage),
			byAgent: Array.from(agents, ([agentId, groupTotals]) => ({
				agentId,
				totals: groupTotals
			})).toSorted((a, b) => b.totals.totalCost - a.totals.totalCost),
			byChannel: Array.from(channels, ([channel, groupTotals]) => ({
				channel,
				totals: groupTotals
			})).toSorted((a, b) => b.totals.totalCost - a.totals.totalCost),
			latency: latency.count > 0 ? summarizeLatency(latency) : void 0,
			dailyLatency: Array.from(dailyLatency, ([date, value]) => ({
				date,
				...summarizeLatency(value)
			})).toSorted((a, b) => a.date.localeCompare(b.date)),
			modelDaily: Array.from(dailyModels.values()).toSorted((a, b) => a.date.localeCompare(b.date) || b.cost - a.cost),
			daily: Array.from(days.values()).toSorted((a, b) => a.date.localeCompare(b.date))
		};
	}
	return {
		totals,
		add,
		finish
	};
}
//#endregion
//#region src/infra/session-cost-usage-rollup.ts
const MAX_LATENCY_MS = 432e5;
const MAX_LATENCY_CENTROIDS = 64;
const ERROR_STOP_REASONS = /* @__PURE__ */ new Set([
	"error",
	"aborted",
	"timeout"
]);
function emptyMessageCounts() {
	return {
		total: 0,
		user: 0,
		assistant: 0,
		toolCalls: 0,
		toolResults: 0,
		errors: 0
	};
}
function createLatencyAggregate() {
	return {
		centroids: [],
		count: 0,
		max: 0,
		sum: 0
	};
}
function compressLatencyCentroids(aggregate) {
	while (aggregate.centroids.length > MAX_LATENCY_CENTROIDS) {
		aggregate.centroids.sort((a, b) => a.value - b.value);
		let mergeIndex = 0;
		let smallestGap = Number.POSITIVE_INFINITY;
		for (let index = 1; index < aggregate.centroids.length; index += 1) {
			const gap = (aggregate.centroids[index]?.value ?? 0) - (aggregate.centroids[index - 1]?.value ?? 0);
			if (gap < smallestGap) {
				smallestGap = gap;
				mergeIndex = index - 1;
			}
		}
		const left = aggregate.centroids[mergeIndex];
		const right = aggregate.centroids[mergeIndex + 1];
		if (!left || !right) break;
		const count = left.count + right.count;
		aggregate.centroids.splice(mergeIndex, 2, {
			count,
			value: (left.value * left.count + right.value * right.count) / count
		});
	}
}
function addLatencyValue(aggregate, value) {
	const wasEmpty = aggregate.count === 0;
	aggregate.count += 1;
	aggregate.sum += value;
	aggregate.min = wasEmpty ? value : Math.min(aggregate.min ?? value, value);
	aggregate.max = Math.max(aggregate.max, value);
	aggregate.centroids.push({
		count: 1,
		value
	});
	compressLatencyCentroids(aggregate);
}
function mergeLatencyAggregate(target, source) {
	if (source.count === 0) return;
	const targetWasEmpty = target.count === 0;
	const sourceMin = source.min ?? source.max;
	target.count += source.count;
	target.sum += source.sum;
	target.min = targetWasEmpty ? sourceMin : Math.min(target.min ?? target.max, sourceMin);
	target.max = Math.max(target.max, source.max);
	target.centroids.push(...source.centroids.map((centroid) => ({
		count: centroid.count,
		value: centroid.value
	})));
	compressLatencyCentroids(target);
}
function createUntimestampedRollup() {
	return {
		totals: createEmptyCostUsageTotals(),
		messageCounts: emptyMessageCounts(),
		tools: [],
		models: []
	};
}
function createSessionUsageRollupData() {
	return {
		buckets: {},
		untimestamped: createUntimestampedRollup()
	};
}
function incrementTool(tools, name) {
	const existing = tools.find((entry) => entry.name === name);
	if (existing) existing.count += 1;
	else tools.push({
		name,
		count: 1
	});
}
function mergeTools(target, tools) {
	for (const tool of tools) target.set(tool.name, (target.get(tool.name) ?? 0) + tool.count);
}
function addModelUsage(models, provider, model, totals) {
	if (!provider && !model) return;
	const modelRef = usageModelIdentity(provider, model);
	let existing = models.find((entry) => usageModelIdentity(entry.provider, entry.model) === modelRef);
	if (!existing) {
		existing = {
			provider,
			model,
			count: 0,
			totals: createEmptyCostUsageTotals()
		};
		models.push(existing);
	}
	existing.count += 1;
	addCostUsageTotals(existing.totals, totals);
}
function mergeModels(target, models) {
	for (const model of models) {
		const modelRef = usageModelIdentity(model.provider, model.model);
		const existing = target.get(modelRef) ?? {
			provider: model.provider,
			model: model.model,
			count: 0,
			totals: createEmptyCostUsageTotals()
		};
		existing.count += model.count;
		addCostUsageTotals(existing.totals, model.totals);
		target.set(modelRef, existing);
	}
}
function addMessageContribution(target, contribution) {
	if (contribution.role === "user") {
		target.user += 1;
		target.total += 1;
	} else if (contribution.role === "assistant") {
		target.assistant += 1;
		target.total += 1;
	}
	target.toolCalls += contribution.toolNames.length;
	target.toolResults += contribution.toolResultCounts.total;
	target.errors += contribution.toolResultCounts.errors;
	if (contribution.stopReason && ERROR_STOP_REASONS.has(contribution.stopReason)) target.errors += 1;
}
function createBucket(timestampMs) {
	return {
		timestampMs,
		...createUntimestampedRollup(),
		latency: createLatencyAggregate()
	};
}
function appendSessionUsageRollupContribution(rollup, contribution) {
	const timestamp = contribution.timestamp;
	const timedBucket = timestamp === void 0 ? void 0 : rollup.buckets[String(timestamp)] ??= createBucket(timestamp);
	const bucket = timedBucket ?? rollup.untimestamped;
	addMessageContribution(bucket.messageCounts, contribution);
	for (const toolName of contribution.toolNames) incrementTool(bucket.tools, toolName);
	if (contribution.usageTotals) {
		addCostUsageTotals(bucket.totals, contribution.usageTotals);
		addModelUsage(bucket.models, contribution.provider, contribution.model, contribution.usageTotals);
	}
	if (!timedBucket) return;
	if (contribution.role === "assistant") {
		const sourceUserTimestamp = contribution.durationMs === void 0 ? rollup.lastUserTimestamp : void 0;
		const latencyMs = contribution.durationMs ?? (sourceUserTimestamp !== void 0 ? Math.max(0, timedBucket.timestampMs - sourceUserTimestamp) : void 0);
		if (latencyMs !== void 0 && Number.isFinite(latencyMs) && latencyMs <= MAX_LATENCY_MS) addLatencyValue(timedBucket.latency, latencyMs);
	}
	if (contribution.role === "user") rollup.lastUserTimestamp = timedBucket.timestampMs;
}
function computeLatencyStats(aggregate) {
	if (aggregate.count === 0) return;
	const targetCount = Math.ceil(aggregate.count * .95);
	let seen = 0;
	let p95Ms = aggregate.max;
	for (const centroid of aggregate.centroids.toSorted((a, b) => a.value - b.value)) {
		seen += centroid.count;
		if (seen >= targetCount) {
			p95Ms = centroid.value;
			break;
		}
	}
	return {
		count: aggregate.count,
		avgMs: aggregate.sum / aggregate.count,
		p95Ms,
		minMs: aggregate.min ?? aggregate.max,
		maxMs: aggregate.max
	};
}
function getUtcQuarterHourBucketKey(date) {
	const dateKey = `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}`;
	const quarterIndex = Math.floor((date.getUTCHours() * 60 + date.getUTCMinutes()) / 15);
	return {
		date: dateKey,
		quarterIndex,
		bucketId: `${dateKey}\0${quarterIndex}`
	};
}
function addMessageCounts(target, source) {
	target.total += source.total;
	target.user += source.user;
	target.assistant += source.assistant;
	target.toolCalls += source.toolCalls;
	target.toolResults += source.toolResults;
	target.errors += source.errors;
}
function sortedModelUsage(models, sortByUsage = true) {
	if (models.size === 0) return;
	const values = Array.from(models.values());
	return sortByUsage ? values.toSorted((a, b) => {
		return b.totals.totalCost - a.totals.totalCost || b.totals.totalTokens - a.totals.totalTokens;
	}) : values;
}
function buildToolUsage(tools, sortTiesByName = true) {
	if (tools.size === 0) return;
	const entries = Array.from(tools, ([name, count]) => ({
		name,
		count
	})).toSorted((a, b) => b.count - a.count || (sortTiesByName ? a.name.localeCompare(b.name) : 0));
	return {
		totalCalls: entries.reduce((sum, entry) => sum + entry.count, 0),
		uniqueTools: entries.length,
		tools: entries
	};
}
function mergeDatedRows(left, right, add, clone = (row) => ({ ...row })) {
	const rows = /* @__PURE__ */ new Map();
	for (const row of [...left ?? [], ...right ?? []]) {
		const key = row.quarterIndex === void 0 ? row.date : `${row.date}:${row.quarterIndex}`;
		const existing = rows.get(key);
		if (existing) add(existing, row);
		else rows.set(key, clone(row));
	}
	return rows.size ? Array.from(rows.values()).toSorted((a, b) => a.date.localeCompare(b.date) || (a.quarterIndex ?? 0) - (b.quarterIndex ?? 0)) : void 0;
}
function mergeMessageCountSummaries(left, right) {
	if (!left) return right ? { ...right } : void 0;
	const counts = { ...left };
	if (right) addMessageCounts(counts, right);
	return counts;
}
function mergeLatencyStats(left, right) {
	if (!left && !right) return;
	const leftCount = left?.count ?? 0;
	const rightCount = right?.count ?? 0;
	const count = leftCount + rightCount;
	return {
		count,
		avgMs: count > 0 ? ((left?.avgMs ?? 0) * leftCount + (right?.avgMs ?? 0) * rightCount) / count : 0,
		p95Ms: Math.max(left?.p95Ms ?? 0, right?.p95Ms ?? 0),
		minMs: Math.min(left?.minMs ?? Number.POSITIVE_INFINITY, right?.minMs ?? Number.POSITIVE_INFINITY),
		maxMs: Math.max(left?.maxMs ?? 0, right?.maxMs ?? 0)
	};
}
function mergeDailyLatencyStats(left, right) {
	const rows = /* @__PURE__ */ new Map();
	for (const row of [...left ?? [], ...right ?? []]) {
		const existing = rows.get(row.date);
		if (!existing) {
			rows.set(row.date, { ...row });
			continue;
		}
		const count = existing.count + row.count;
		existing.avgMs = count > 0 ? (existing.avgMs * existing.count + row.avgMs * row.count) / count : 0;
		existing.count = count;
		existing.p95Ms = Math.max(existing.p95Ms, row.p95Ms);
		existing.minMs = Math.min(existing.minMs, row.minMs);
		existing.maxMs = Math.max(existing.maxMs, row.maxMs);
	}
	return rows.size ? Array.from(rows.values()).toSorted((a, b) => a.date.localeCompare(b.date)) : void 0;
}
function mergeDailyModels(left, right) {
	const rows = /* @__PURE__ */ new Map();
	for (const row of [...left ?? [], ...right ?? []]) {
		const key = usageDailyModelIdentity(row.date, row.provider, row.model);
		const existing = rows.get(key);
		if (!existing) {
			rows.set(key, { ...row });
			continue;
		}
		existing.tokens += row.tokens;
		existing.cost += row.cost;
		existing.count += row.count;
	}
	return rows.size ? Array.from(rows.values()).toSorted((a, b) => a.date.localeCompare(b.date)) : void 0;
}
function mergeSessionCostSummaryInto(target, source) {
	addCostUsageTotals(target, source);
	target.firstActivity = target.firstActivity === void 0 ? source.firstActivity : source.firstActivity === void 0 ? target.firstActivity : Math.min(target.firstActivity, source.firstActivity);
	target.lastActivity = target.lastActivity === void 0 ? source.lastActivity : source.lastActivity === void 0 ? target.lastActivity : Math.max(target.lastActivity, source.lastActivity);
	if (target.firstActivity !== void 0 && target.lastActivity !== void 0) target.durationMs = Math.max(0, target.lastActivity - target.firstActivity);
	const activityDates = /* @__PURE__ */ new Set([...target.activityDates ?? [], ...source.activityDates ?? []]);
	if (activityDates.size) target.activityDates = Array.from(activityDates).toSorted();
	target.dailyBreakdown = mergeDatedRows(target.dailyBreakdown, source.dailyBreakdown, (current, row) => {
		addCostUsageTotals(current, row);
		current.tokens += row.tokens;
		current.cost += row.cost;
	}, (row) => ({
		...row,
		...cloneCostUsageTotals(row)
	}));
	target.dailyMessageCounts = mergeDatedRows(target.dailyMessageCounts, source.dailyMessageCounts, addMessageCounts);
	target.utcQuarterHourMessageCounts = mergeDatedRows(target.utcQuarterHourMessageCounts, source.utcQuarterHourMessageCounts, addMessageCounts);
	target.utcQuarterHourTokenUsage = mergeDatedRows(target.utcQuarterHourTokenUsage, source.utcQuarterHourTokenUsage, (current, row) => {
		current.input += row.input;
		current.output += row.output;
		current.cacheRead += row.cacheRead;
		current.cacheWrite += row.cacheWrite;
		current.totalTokens += row.totalTokens;
		current.totalCost += row.totalCost;
	});
	target.dailyLatency = mergeDailyLatencyStats(target.dailyLatency, source.dailyLatency);
	target.dailyModelUsage = mergeDailyModels(target.dailyModelUsage, source.dailyModelUsage);
	target.messageCounts = mergeMessageCountSummaries(target.messageCounts, source.messageCounts);
	const tools = /* @__PURE__ */ new Map();
	mergeTools(tools, target.toolUsage?.tools ?? []);
	mergeTools(tools, source.toolUsage?.tools ?? []);
	target.toolUsage = buildToolUsage(tools, false);
	const models = /* @__PURE__ */ new Map();
	mergeModels(models, target.modelUsage ?? []);
	mergeModels(models, source.modelUsage ?? []);
	target.modelUsage = sortedModelUsage(models, false);
	target.latency = mergeLatencyStats(target.latency, source.latency);
}
function usageBucketsInRange(rollup, startMs, endMs) {
	return Object.values(rollup.buckets).filter((bucket) => bucket.timestampMs >= startMs && bucket.timestampMs <= endMs).toSorted((a, b) => a.timestampMs - b.timestampMs);
}
function buildSessionCostSummaryFromRollup(params) {
	const totals = createEmptyCostUsageTotals();
	const messageCounts = emptyMessageCounts();
	const tools = /* @__PURE__ */ new Map();
	const models = /* @__PURE__ */ new Map();
	const activityDates = /* @__PURE__ */ new Set();
	const dailyUsage = /* @__PURE__ */ new Map();
	const dailyMessages = /* @__PURE__ */ new Map();
	const quarterMessages = /* @__PURE__ */ new Map();
	const quarterTokens = /* @__PURE__ */ new Map();
	const dailyLatencies = /* @__PURE__ */ new Map();
	const dailyModels = /* @__PURE__ */ new Map();
	const allLatencies = createLatencyAggregate();
	let firstActivity;
	let lastActivity;
	const mergeBucket = (bucket) => {
		const date = new Date(bucket.timestampMs);
		const dayKey = params.formatDay(date);
		const quarter = getUtcQuarterHourBucketKey(date);
		firstActivity = firstActivity === void 0 ? bucket.timestampMs : Math.min(firstActivity, bucket.timestampMs);
		lastActivity = lastActivity === void 0 ? bucket.timestampMs : Math.max(lastActivity, bucket.timestampMs);
		activityDates.add(dayKey);
		addCostUsageTotals(totals, bucket.totals);
		addMessageCounts(messageCounts, bucket.messageCounts);
		mergeTools(tools, bucket.tools);
		mergeModels(models, bucket.models);
		const daily = dailyUsage.get(dayKey) ?? createEmptyCostUsageTotals();
		addCostUsageTotals(daily, bucket.totals);
		dailyUsage.set(dayKey, daily);
		const dailyMessage = dailyMessages.get(dayKey) ?? {
			date: dayKey,
			...emptyMessageCounts()
		};
		addMessageCounts(dailyMessage, bucket.messageCounts);
		dailyMessages.set(dayKey, dailyMessage);
		const quarterMessage = quarterMessages.get(quarter.bucketId) ?? {
			date: quarter.date,
			quarterIndex: quarter.quarterIndex,
			...emptyMessageCounts()
		};
		addMessageCounts(quarterMessage, bucket.messageCounts);
		quarterMessages.set(quarter.bucketId, quarterMessage);
		const quarterUsage = quarterTokens.get(quarter.bucketId) ?? {
			date: quarter.date,
			quarterIndex: quarter.quarterIndex,
			input: 0,
			output: 0,
			cacheRead: 0,
			cacheWrite: 0,
			totalTokens: 0,
			totalCost: 0
		};
		quarterUsage.input += bucket.totals.input;
		quarterUsage.output += bucket.totals.output;
		quarterUsage.cacheRead += bucket.totals.cacheRead;
		quarterUsage.cacheWrite += bucket.totals.cacheWrite;
		quarterUsage.totalTokens += bucket.totals.totalTokens;
		quarterUsage.totalCost += bucket.totals.totalCost;
		quarterTokens.set(quarter.bucketId, quarterUsage);
		for (const model of bucket.models) {
			const modelBucketId = usageDailyModelIdentity(dayKey, model.provider, model.model);
			const existing = dailyModels.get(modelBucketId) ?? {
				date: dayKey,
				provider: model.provider,
				model: model.model,
				tokens: 0,
				cost: 0,
				count: 0
			};
			existing.tokens += model.totals.totalTokens;
			existing.cost += model.totals.totalCost;
			existing.count += model.count;
			dailyModels.set(modelBucketId, existing);
		}
		mergeLatencyAggregate(allLatencies, bucket.latency);
		const dailyLatency = dailyLatencies.get(dayKey) ?? createLatencyAggregate();
		mergeLatencyAggregate(dailyLatency, bucket.latency);
		dailyLatencies.set(dayKey, dailyLatency);
	};
	for (const bucket of usageBucketsInRange(params.rollup, params.startMs, params.endMs)) mergeBucket(bucket);
	if (params.includeUntimestamped) {
		addCostUsageTotals(totals, params.rollup.untimestamped.totals);
		addMessageCounts(messageCounts, params.rollup.untimestamped.messageCounts);
		mergeTools(tools, params.rollup.untimestamped.tools);
		mergeModels(models, params.rollup.untimestamped.models);
	}
	const dailyLatency = Array.from(dailyLatencies, ([date, aggregate]) => {
		const stats = computeLatencyStats(aggregate);
		return stats ? Object.assign({ date }, stats) : null;
	}).filter((entry) => entry !== null).toSorted((a, b) => a.date.localeCompare(b.date));
	const utcQuarterHourMessageCounts = Array.from(quarterMessages.values()).toSorted((a, b) => a.date.localeCompare(b.date) || a.quarterIndex - b.quarterIndex);
	const utcQuarterHourTokenUsage = Array.from(quarterTokens.values()).toSorted((a, b) => a.date.localeCompare(b.date) || a.quarterIndex - b.quarterIndex);
	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(activityDates).toSorted(),
		dailyBreakdown: Array.from(dailyUsage, ([date, usage]) => Object.assign({
			date,
			tokens: usage.totalTokens,
			cost: usage.totalCost
		}, usage)).toSorted((a, b) => a.date.localeCompare(b.date)),
		dailyMessageCounts: Array.from(dailyMessages.values()).toSorted((a, b) => a.date.localeCompare(b.date)),
		utcQuarterHourMessageCounts: utcQuarterHourMessageCounts.length ? utcQuarterHourMessageCounts : void 0,
		utcQuarterHourTokenUsage: utcQuarterHourTokenUsage.length ? utcQuarterHourTokenUsage : void 0,
		dailyLatency: dailyLatency.length ? dailyLatency : void 0,
		dailyModelUsage: dailyModels.size ? Array.from(dailyModels.values()).toSorted((a, b) => a.date.localeCompare(b.date) || b.cost - a.cost) : void 0,
		messageCounts,
		toolUsage: buildToolUsage(tools),
		modelUsage: sortedModelUsage(models),
		latency: computeLatencyStats(allLatencies),
		...totals
	};
}
function addRollupToCostUsageSummary(params) {
	for (const bucket of usageBucketsInRange(params.rollup, params.startMs, params.endMs)) {
		const dayKey = params.formatDay(new Date(bucket.timestampMs));
		const daily = params.daily.get(dayKey) ?? createEmptyCostUsageTotals();
		addCostUsageTotals(daily, bucket.totals);
		params.daily.set(dayKey, daily);
		addCostUsageTotals(params.totals, bucket.totals);
	}
}
function cloneSessionUsageRollupData(rollup) {
	return {
		buckets: Object.fromEntries(Object.entries(rollup.buckets).map(([bucketId, bucket]) => [bucketId, {
			...bucket,
			totals: cloneCostUsageTotals(bucket.totals),
			messageCounts: { ...bucket.messageCounts },
			tools: bucket.tools.map((tool) => ({ ...tool })),
			models: bucket.models.map((model) => ({
				...model,
				totals: cloneCostUsageTotals(model.totals)
			})),
			latency: {
				count: bucket.latency.count,
				max: bucket.latency.max,
				sum: bucket.latency.sum,
				...bucket.latency.min !== void 0 ? { min: bucket.latency.min } : {},
				centroids: bucket.latency.centroids.map((centroid) => ({
					count: centroid.count,
					value: centroid.value
				}))
			}
		}])),
		...rollup.lastUserTimestamp !== void 0 ? { lastUserTimestamp: rollup.lastUserTimestamp } : {},
		untimestamped: {
			totals: cloneCostUsageTotals(rollup.untimestamped.totals),
			messageCounts: { ...rollup.untimestamped.messageCounts },
			tools: rollup.untimestamped.tools.map((tool) => ({ ...tool })),
			models: rollup.untimestamped.models.map((model) => ({
				...model,
				totals: cloneCostUsageTotals(model.totals)
			}))
		}
	};
}
//#endregion
//#region src/infra/session-cost-usage-aggregation.ts
const USAGE_COST_ROLLUP_VERSION = 3;
const USAGE_COST_FILE_ANCHOR_BYTES = 4096;
function resolveUsageCostCacheDatabasePath(agentId) {
	return resolveOpenClawAgentSqlitePath({ agentId: normalizeAgentId(agentId) });
}
function resolveUsageCostAgentDir(config, agentId) {
	return resolveAgentDir(config ?? {}, agentId);
}
function resolveUsageCostPricingFingerprint(config, agentDir) {
	return resolveModelCostConfigFingerprint(config, agentDir);
}
function normalizeUsageCostRollup(raw, pricingFingerprint) {
	if (!raw || typeof raw !== "object") return;
	const record = raw;
	if (record.version !== USAGE_COST_ROLLUP_VERSION || record.pricingFingerprint !== pricingFingerprint || !record.checkpoint || !record.rollup || typeof record.scannedAt !== "number" || typeof record.parsedRecords !== "number" || typeof record.countedRecords !== "number") return;
	return record;
}
function readUsageCostRollups(agentId, pricingFingerprint, databasePath, params = {}) {
	const result = /* @__PURE__ */ new Map();
	const rows = params.rows ?? readSessionCostUsageRollupRows(agentId, databasePath, params.filePaths);
	for (const row of rows) try {
		const entry = normalizeUsageCostRollup(JSON.parse(row.valueJson), pricingFingerprint);
		if (entry) result.set(row.key, {
			entry,
			valueJson: row.valueJson
		});
	} catch {}
	return result;
}
function isUsageCostRollupFresh(params) {
	const checkpoint = params.stored?.entry.checkpoint;
	if (!checkpoint || checkpoint.kind !== params.file.kind) return false;
	if (checkpoint.kind === "jsonl") return checkpoint.observedSize === params.file.size && checkpoint.observedMtimeMs === params.file.mtimeMs && checkpoint.device === params.file.device && checkpoint.inode === params.file.inode;
	return checkpoint.size === params.file.size && checkpoint.mtimeMs === params.file.mtimeMs && checkpoint.eventCount === params.file.eventCount && checkpoint.maxSeq === params.file.maxSeq;
}
function canUseUsageCostRollupForPartial(params) {
	const checkpoint = params.stored?.entry.checkpoint;
	if (!checkpoint || checkpoint.kind !== params.file.kind) return false;
	if (checkpoint.kind === "jsonl") return checkpoint.parsedOffset <= params.file.size && checkpoint.device === params.file.device && checkpoint.inode === params.file.inode;
	return checkpoint.maxSeq <= (params.file.maxSeq ?? 0);
}
function getUsageCostStaleRollupFiles(params) {
	return params.files.filter((file) => !isUsageCostRollupFresh({
		stored: params.rollups.get(file.filePath),
		file
	}));
}
function countUsableUsageCostRollups(params) {
	return params.files.reduce((count, file) => count + (canUseUsageCostRollupForPartial({
		stored: params.rollups.get(file.filePath),
		file
	}) ? 1 : 0), 0);
}
function latestUsageCostRollupScan(rollups) {
	let latest = 0;
	for (const { entry } of rollups.values()) latest = Math.max(latest, entry.scannedAt);
	return latest || void 0;
}
function hashUsageCostCheckpoint(value) {
	return createHash("sha256").update(value).digest("base64url");
}
async function readJsonlAnchorHash(filePath, offset) {
	const start = Math.max(0, offset - USAGE_COST_FILE_ANCHOR_BYTES);
	const length = offset - start;
	if (length === 0) return hashUsageCostCheckpoint("");
	const handle = await fs.promises.open(filePath, "r").catch(() => null);
	if (!handle) return;
	try {
		const buffer = Buffer.alloc(length);
		const { bytesRead } = await handle.read(buffer, 0, length, start);
		return bytesRead === length ? hashUsageCostCheckpoint(buffer) : void 0;
	} finally {
		await handle.close().catch(() => void 0);
	}
}
function parseJsonlRecord(line) {
	const text = line.toString("utf8").trim();
	if (!text) return;
	try {
		const parsed = JSON.parse(text);
		return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
	} catch {
		return;
	}
}
async function scanJsonlRange(params) {
	if (params.endOffset <= params.startOffset) return params.startOffset;
	const stream = fs.createReadStream(params.filePath, {
		start: params.startOffset,
		end: params.endOffset - 1
	});
	let carry = Buffer.alloc(0);
	let carryStart = params.startOffset;
	let processedOffset = params.startOffset;
	try {
		for await (const chunk of stream) {
			const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
			const data = carry.length === 0 ? bytes : Buffer.concat([carry, bytes]);
			let lineStart = 0;
			for (let newline = data.indexOf(10); newline >= 0; newline = data.indexOf(10, lineStart)) {
				const record = parseJsonlRecord(data.subarray(lineStart, newline));
				if (record) params.onRecord(record);
				processedOffset = carryStart + newline + 1;
				lineStart = newline + 1;
			}
			carry = data.subarray(lineStart);
			carryStart = processedOffset;
		}
		if (carry.length > 0) {
			const record = parseJsonlRecord(carry);
			if (record) {
				params.onRecord(record);
				processedOffset = params.endOffset;
			}
		}
		return processedOffset;
	} finally {
		stream.destroy();
	}
}
function appendParsedEntryToRollup(rollup, entry) {
	let usageTotals;
	if (entry.usage) {
		usageTotals = createEmptyCostUsageTotals();
		applyUsageTotals(usageTotals, entry.usage);
		if (entry.costBreakdown?.total !== void 0) applyCostBreakdown(usageTotals, entry.costBreakdown);
		else applyCostTotal(usageTotals, entry.costTotal, entry.provider, entry.model);
	}
	const timestamp = entry.timestamp?.getTime();
	appendSessionUsageRollupContribution(rollup, {
		timestamp,
		role: entry.role,
		durationMs: entry.durationMs,
		provider: entry.provider,
		model: entry.model,
		stopReason: entry.stopReason,
		toolNames: entry.toolNames,
		toolResultCounts: entry.toolResultCounts,
		usageTotals
	});
	return {
		parsedRecord: Boolean(entry.usage),
		countedRecord: Boolean(entry.usage && timestamp)
	};
}
function scanRecordsIntoRollup(params) {
	let countedRecords = 0;
	let parsedRecords = 0;
	for (const record of params.records) {
		const entry = parseUsageCostTranscriptEntry(record, params.resolveCost);
		if (!entry) continue;
		const counted = appendParsedEntryToRollup(params.rollup, entry);
		countedRecords += counted.countedRecord ? 1 : 0;
		parsedRecords += counted.parsedRecord ? 1 : 0;
	}
	return {
		countedRecords,
		parsedRecords
	};
}
function createUsageRollupScan(params) {
	const previous = params.appendOnly ? params.previous?.entry : void 0;
	const rollup = previous ? cloneSessionUsageRollupData(previous.rollup) : createSessionUsageRollupData();
	let countedRecords = 0;
	let parsedRecords = 0;
	return {
		addRecords(records) {
			const counts = scanRecordsIntoRollup({
				records,
				rollup,
				resolveCost: params.resolveCost
			});
			countedRecords += counts.countedRecords;
			parsedRecords += counts.parsedRecords;
		},
		finish(checkpoint) {
			return {
				version: USAGE_COST_ROLLUP_VERSION,
				pricingFingerprint: params.pricingFingerprint,
				checkpoint,
				scannedAt: Date.now(),
				parsedRecords: (previous?.parsedRecords ?? 0) + parsedRecords,
				countedRecords: (previous?.countedRecords ?? 0) + countedRecords,
				rollup
			};
		}
	};
}
async function scanJsonlUsageRollup(params) {
	const previousCheckpoint = params.previous?.entry.checkpoint.kind === "jsonl" ? params.previous.entry.checkpoint : void 0;
	const identityMatches = previousCheckpoint && previousCheckpoint.device === params.file.device && previousCheckpoint.inode === params.file.inode && previousCheckpoint.parsedOffset <= params.file.size && params.file.size > previousCheckpoint.observedSize;
	const previousAnchor = identityMatches ? await readJsonlAnchorHash(params.file.filePath, previousCheckpoint.parsedOffset) : void 0;
	const appendOnly = Boolean(identityMatches && previousAnchor === previousCheckpoint?.anchorHash && params.previous);
	const startOffset = appendOnly ? previousCheckpoint?.parsedOffset ?? 0 : 0;
	const scan = createUsageRollupScan({
		...params,
		appendOnly
	});
	const processedOffset = await scanJsonlRange({
		filePath: params.file.filePath,
		startOffset,
		endOffset: params.file.size,
		onRecord: (record) => scan.addRecords([record])
	});
	const postStats = await fs.promises.stat(params.file.filePath);
	if (postStats.dev !== params.file.device || postStats.ino !== params.file.inode || postStats.size < params.file.size) throw new Error(`transcript changed identity while scanning: ${params.file.filePath}`);
	const anchorHash = await readJsonlAnchorHash(params.file.filePath, processedOffset);
	if (!anchorHash) throw new Error(`transcript checkpoint unavailable: ${params.file.filePath}`);
	return scan.finish({
		kind: "jsonl",
		parsedOffset: processedOffset,
		observedSize: params.file.size,
		observedMtimeMs: params.file.mtimeMs,
		device: params.file.device ?? 0,
		inode: params.file.inode ?? 0,
		anchorHash
	});
}
function selectIncrementalSqliteRecords(records, previousLeafId) {
	let visibleLeafId = previousLeafId;
	const visible = [];
	for (const record of records) {
		if (isSessionTranscriptLeafControl(record) || record.appendMode === "side") return;
		if (!isCanonicalSessionTranscriptEntry(record)) continue;
		const id = typeof record.id === "string" && record.id ? record.id : void 0;
		if (!id) return;
		if (Object.hasOwn(record, "parentId")) {
			if ((record.parentId === null ? void 0 : record.parentId) !== visibleLeafId) return;
		}
		visible.push(record);
		visibleLeafId = id;
	}
	return {
		records: visible,
		...visibleLeafId ? { visibleLeafId } : {}
	};
}
function sqliteCheckpointAnchorHash(event) {
	return hashUsageCostCheckpoint(JSON.stringify(event));
}
async function scanSqliteUsageRollup(params) {
	const marker = parseSqliteSessionFileMarker(params.file.filePath);
	if (!marker) throw new Error(`invalid SQLite transcript marker: ${params.file.filePath}`);
	const maxSeq = params.file.maxSeq ?? 0;
	const eventCount = params.file.eventCount ?? 0;
	const scope = {
		agentId: marker.agentId,
		sessionId: marker.sessionId,
		storePath: marker.storePath
	};
	const snapshotLastRow = maxSeq > 0 ? readTranscriptEventAtSeqSync(scope, maxSeq) : void 0;
	if (maxSeq > 0 && !snapshotLastRow) throw new Error(`SQLite transcript checkpoint unavailable: ${params.file.filePath}`);
	const snapshotAnchorHash = snapshotLastRow ? sqliteCheckpointAnchorHash(snapshotLastRow.event) : hashUsageCostCheckpoint("");
	const previousCheckpoint = params.previous?.entry.checkpoint.kind === "sqlite" ? params.previous.entry.checkpoint : void 0;
	const previousAnchor = previousCheckpoint?.maxSeq ? readTranscriptEventAtSeqSync(scope, previousCheckpoint.maxSeq) : void 0;
	const anchorMatches = previousCheckpoint?.maxSeq === 0 || previousAnchor && sqliteCheckpointAnchorHash(previousAnchor.event) === previousCheckpoint?.anchorHash;
	const appendCandidate = Boolean(params.previous && previousCheckpoint && previousCheckpoint.maxSeq < maxSeq && previousCheckpoint.eventCount < eventCount && anchorMatches);
	const afterSeq = appendCandidate ? previousCheckpoint?.maxSeq ?? 0 : 0;
	const rows = loadTranscriptEventRowsAfterSeqSync(scope, afterSeq, maxSeq);
	const rawRecords = rows.flatMap((row) => row.event && typeof row.event === "object" && !Array.isArray(row.event) ? [row.event] : []);
	const incremental = appendCandidate ? selectIncrementalSqliteRecords(rawRecords, previousCheckpoint?.visibleLeafId) : void 0;
	const appendOnly = Boolean(incremental && params.previous);
	const allRows = appendOnly ? rows : loadTranscriptEventRowsAfterSeqSync(scope, 0, maxSeq);
	const allRecords = appendOnly ? incremental?.records ?? [] : selectVisibleTranscriptEvents(allRows.map((row) => row.event)).flatMap((event) => event && typeof event === "object" && !Array.isArray(event) ? [event] : []);
	const scan = createUsageRollupScan({
		...params,
		appendOnly
	});
	scan.addRecords(allRecords);
	const postFile = await resolveUsageCostTranscriptFile(params.file.filePath);
	if (!postFile || (postFile.maxSeq ?? 0) < maxSeq || (postFile.eventCount ?? 0) < eventCount) throw new Error(`SQLite transcript changed while scanning: ${params.file.filePath}`);
	const currentLastRow = maxSeq > 0 ? readTranscriptEventAtSeqSync(scope, maxSeq) : void 0;
	if (maxSeq > 0 && !currentLastRow || currentLastRow && sqliteCheckpointAnchorHash(currentLastRow.event) !== snapshotAnchorHash) throw new Error(`SQLite transcript changed while scanning: ${params.file.filePath}`);
	const visibleLeafId = appendOnly ? incremental?.visibleLeafId : scanSessionTranscriptTree(allRows.map((row) => row.event)).leafId ?? void 0;
	return scan.finish({
		kind: "sqlite",
		maxSeq,
		eventCount,
		size: params.file.size,
		mtimeMs: params.file.mtimeMs,
		anchorHash: snapshotAnchorHash,
		...visibleLeafId ? { visibleLeafId } : {}
	});
}
async function scanUsageFileForRollup(params) {
	return params.file.kind === "sqlite" ? await scanSqliteUsageRollup(params) : await scanJsonlUsageRollup(params);
}
async function refreshCostUsageCacheForAgent(params) {
	const databasePath = params.databasePath ?? resolveUsageCostCacheDatabasePath(params.agentId);
	const lock = acquireSessionCostUsageRefreshLock(params.agentId, databasePath);
	if (!lock.acquired) return "busy";
	try {
		const agentDir = params.agentDir ?? resolveUsageCostAgentDir(params.config, params.agentId);
		const pricingFingerprint = resolveUsageCostPricingFingerprint(params.config, agentDir);
		const rows = readSessionCostUsageRollupRows(params.agentId, databasePath);
		const rawValues = new Map(rows.map((row) => [row.key, row.valueJson]));
		const rollups = readUsageCostRollups(params.agentId, pricingFingerprint, databasePath, { rows });
		const discoveredFiles = await listUsageCountedTranscriptStats(params.agentId, params.sessionsDir ? { sessionsDir: params.sessionsDir } : void 0);
		const requestedFiles = [];
		for (const requested of params.sessionFiles ?? []) {
			const resolved = await resolveUsageCostTranscriptFile(requested);
			if (resolved) requestedFiles.push(resolved);
		}
		const filesByPath = new Map(discoveredFiles.map((file) => [file.filePath, file]));
		for (const file of requestedFiles) filesByPath.set(file.filePath, file);
		const files = [...filesByPath.values()];
		deleteSessionCostUsageRollupsExcept({
			agentId: params.agentId,
			databasePath,
			liveKeys: new Set(files.map((file) => file.filePath)),
			rows
		});
		const requestedPaths = /* @__PURE__ */ new Set();
		for (const file of requestedFiles) requestedPaths.add(file.filePath);
		const refreshFiles = requestedPaths.size > 0 ? files.filter((file) => requestedPaths.has(file.filePath)) : params.startMs === void 0 ? files : files.filter((file) => file.mtimeMs >= params.startMs);
		const maxFiles = params.maxFiles !== void 0 && Number.isFinite(params.maxFiles) && params.maxFiles > 0 ? Math.floor(params.maxFiles) : void 0;
		const staleFiles = getUsageCostStaleRollupFiles({
			rollups,
			files: refreshFiles
		}).toSorted((a, b) => a.size - b.size || a.filePath.localeCompare(b.filePath)).slice(0, maxFiles);
		const resolveCost = createUsageCostResolver({
			config: params.config,
			agentDir
		});
		for (const file of staleFiles) {
			const entry = await scanUsageFileForRollup({
				file,
				previous: rollups.get(file.filePath),
				pricingFingerprint,
				resolveCost
			});
			const valueJson = JSON.stringify(entry);
			if (!writeSessionCostUsageRollup({
				agentId: params.agentId,
				databasePath,
				rollupId: file.filePath,
				previousValueJson: rawValues.get(file.filePath) ?? null,
				valueJson,
				updatedAt: entry.scannedAt
			})) throw new Error(`usage rollup changed while refreshing: ${file.filePath}`);
			rollups.set(file.filePath, {
				entry,
				valueJson
			});
			rawValues.set(file.filePath, valueJson);
		}
		return "refreshed";
	} finally {
		lock.release();
	}
}
//#endregion
//#region src/infra/session-cost-usage-projection.ts
const formatUtcDayKey = (date) => `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}`;
const createUsageDayKeyFormatter = (dayBucket) => {
	if (dayBucket?.mode === "utc-offset") return (date) => formatUtcDayKey(new Date(date.getTime() + dayBucket.utcOffsetMinutes * 60 * 1e3));
	const timeZone = dayBucket?.mode === "time-zone" ? dayBucket.timeZone : Intl.DateTimeFormat().resolvedOptions().timeZone;
	return createTimeZoneDayKeyFormatter(timeZone);
};
/**
* Maximum window (in days) for which we will zero-fill missing calendar
* days. Bounded ranges from the UI's range filter top out at 90 days for
* the explicit picker and "All" is the wildcard escape hatch — anything
* wider than this threshold is treated as an all-time / open-ended range
* and falls back to sparse behavior (only days with activity), since a
* dense series at that scale would produce tens of thousands of zero
* buckets (e.g. a 1970-based startMs → ~20k entries) without any user
* value. 366 days covers a full year + leap-day cushion.
*/
const MAX_ZERO_FILL_DAYS = 366;
/**
* Parse a `YYYY-MM-DD` day key into its UTC calendar-day timestamp. The
* timestamp is only used to enumerate calendar labels; usage timestamps stay
* in their requested timezone bucket.
*/
const parseDayKeyToUtcMs = (dayKey) => {
	const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dayKey);
	if (!match) return null;
	const year = Number(match[1]);
	const monthIdx = Number(match[2]) - 1;
	const day = Number(match[3]);
	const dayMs = Date.UTC(year, monthIdx, day);
	const date = new Date(dayMs);
	return date.getUTCFullYear() === year && date.getUTCMonth() === monthIdx && date.getUTCDate() === day ? dayMs : null;
};
/**
* Ensure the daily map has an entry for every calendar day in [startMs, endMs].
* Days without activity are inserted with a zero-valued totals bucket so the
* resulting `daily` series matches the requested range length (one bar per
* calendar day) instead of only covering days with recorded usage.
*
* Day keys must use the same calendar zone as the request range. Otherwise a
* remote Gateway can return local-date labels for UTC/browser-local ranges,
* which drops boundary usage when the UI compares calendar windows.
*/
const fillMissingDays = (dailyMap, startMs, endMs, formatDayKey) => {
	if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs < startMs) return;
	const dayMs = 864e5;
	const startKey = formatDayKey(new Date(startMs));
	const endKey = formatDayKey(new Date(endMs));
	const startDayMs = parseDayKeyToUtcMs(startKey);
	const endDayMs = parseDayKeyToUtcMs(endKey);
	if (startDayMs === null || endDayMs === null) {
		if (!dailyMap.has(startKey)) dailyMap.set(startKey, createEmptyCostUsageTotals());
		if (!dailyMap.has(endKey)) dailyMap.set(endKey, createEmptyCostUsageTotals());
		return;
	}
	if (Math.floor((endDayMs - startDayMs) / dayMs) + 1 > MAX_ZERO_FILL_DAYS) return;
	const maxIterations = 367;
	for (let cursorMs = startDayMs, i = 0; cursorMs <= endDayMs && i < maxIterations; i += 1) {
		const key = formatUtcDayKey(new Date(cursorMs));
		if (!dailyMap.has(key)) dailyMap.set(key, createEmptyCostUsageTotals());
		cursorMs += dayMs;
	}
	if (!dailyMap.has(endKey)) dailyMap.set(endKey, createEmptyCostUsageTotals());
};
const countCalendarDays = (startMs, endMs, formatDayKey) => {
	const startDayMs = parseDayKeyToUtcMs(formatDayKey(new Date(startMs)));
	const endDayMs = parseDayKeyToUtcMs(formatDayKey(new Date(endMs)));
	if (startDayMs === null || endDayMs === null || endDayMs < startDayMs) return Math.ceil((endMs - startMs) / 864e5) + 1;
	return Math.floor((endDayMs - startDayMs) / 864e5) + 1;
};
function buildCostUsageSummaryFromRollups(params) {
	const dailyMap = /* @__PURE__ */ new Map();
	const totals = createEmptyCostUsageTotals();
	const dayFormatter = createUsageDayKeyFormatter(params.dayBucket);
	const staleFiles = getUsageCostStaleRollupFiles(params);
	const cachedFiles = countUsableUsageCostRollups(params);
	for (const file of params.files) {
		const stored = params.rollups.get(file.filePath);
		if (!canUseUsageCostRollupForPartial({
			stored,
			file
		}) || !stored) continue;
		addRollupToCostUsageSummary({
			rollup: stored.entry.rollup,
			startMs: params.startMs,
			endMs: params.endMs,
			formatDay: dayFormatter,
			daily: dailyMap,
			totals
		});
	}
	fillMissingDays(dailyMap, params.startMs, params.endMs, dayFormatter);
	const status = params.refreshing ? "refreshing" : staleFiles.length > 0 ? cachedFiles > 0 ? "partial" : "stale" : "fresh";
	return {
		updatedAt: Date.now(),
		days: countCalendarDays(params.startMs, params.endMs, dayFormatter),
		daily: Array.from(dailyMap.entries()).map(([date, bucket]) => Object.assign({ date }, bucket)).toSorted((a, b) => a.date.localeCompare(b.date)),
		totals,
		cacheStatus: {
			status,
			cachedFiles,
			pendingFiles: staleFiles.length,
			staleFiles: staleFiles.length,
			refreshedAt: latestUsageCostRollupScan(params.rollups)
		}
	};
}
//#endregion
//#region src/infra/session-cost-usage-cache-runtime.ts
const USAGE_COST_REFRESH_RETRY_MIN_MS = 50;
const USAGE_COST_REFRESH_RETRY_MAX_MS = 5e3;
const logger = createSubsystemLogger("usage-cost-cache");
const usageCostRefreshes = /* @__PURE__ */ new Map();
function isUsageCostRefreshQueued(databasePath) {
	return usageCostRefreshes.get(getAsyncWorkSignal())?.has(databasePath) === true;
}
async function loadCostUsageSummary(params) {
	const now = Date.now();
	const defaultStart = new Date(now);
	defaultStart.setDate(defaultStart.getDate() - 29);
	const startMs = params.startMs ?? defaultStart.getTime();
	const endMs = params.endMs ?? now;
	const agentDir = resolveUsageCostAgentDir(params.config, params.agentId);
	const databasePath = resolveUsageCostCacheDatabasePath(params.agentId);
	const result = await refreshCostUsageCacheForAgent({
		config: params.config,
		agentId: params.agentId,
		agentDir,
		databasePath
	});
	const pricingFingerprint = resolveUsageCostPricingFingerprint(params.config, agentDir);
	return buildCostUsageSummaryFromRollups({
		rollups: readUsageCostRollups(params.agentId, pricingFingerprint, databasePath),
		files: await listUsageCountedTranscriptStats(params.agentId),
		startMs,
		endMs,
		dayBucket: params.dayBucket,
		refreshing: result === "busy" || isUsageCostRefreshQueued(databasePath) || isSessionCostUsageRefreshRunning(params.agentId, databasePath)
	});
}
async function loadCostUsageSummaryFromCache(params) {
	const agentDir = resolveUsageCostAgentDir(params.config, params.agentId);
	const databasePath = resolveUsageCostCacheDatabasePath(params.agentId);
	const pricingFingerprint = resolveUsageCostPricingFingerprint(params.config, agentDir);
	let rollups = readUsageCostRollups(params.agentId, pricingFingerprint, databasePath);
	let files = await listUsageCountedTranscriptStats(params.agentId);
	const staleFiles = getUsageCostStaleRollupFiles({
		rollups,
		files
	});
	if (params.requestRefresh !== false && staleFiles.length > 0) {
		const cachedFiles = countUsableUsageCostRollups({
			rollups,
			files
		});
		if (params.refreshMode === "sync-when-empty" && cachedFiles === 0) {
			const result = await refreshCostUsageCacheForAgent({
				config: params.config,
				agentId: params.agentId,
				agentDir,
				startMs: params.startMs
			});
			rollups = readUsageCostRollups(params.agentId, pricingFingerprint, databasePath);
			files = await listUsageCountedTranscriptStats(params.agentId);
			if (result === "refreshed" && getUsageCostStaleRollupFiles({
				rollups,
				files
			}).length > 0) requestCostUsageCacheRefresh({
				config: params.config,
				agentId: params.agentId
			});
		} else requestCostUsageCacheRefresh({
			config: params.config,
			agentId: params.agentId
		});
	}
	return buildCostUsageSummaryFromRollups({
		rollups,
		files,
		startMs: params.startMs,
		endMs: params.endMs,
		dayBucket: params.dayBucket,
		refreshing: isUsageCostRefreshQueued(databasePath) || isSessionCostUsageRefreshRunning(params.agentId, databasePath)
	});
}
async function loadSessionCostSummariesFromCache(params) {
	const agentDir = resolveUsageCostAgentDir(params.config, params.agentId);
	const databasePath = resolveUsageCostCacheDatabasePath(params.agentId);
	const pricingFingerprint = resolveUsageCostPricingFingerprint(params.config, agentDir);
	const fileTasks = params.sessions.map((session) => async () => await resolveUsageCostTranscriptFile(session.sessionFile));
	const { results: files } = await runTasksWithConcurrency({
		tasks: fileTasks,
		limit: 32
	});
	const rollups = readUsageCostRollups(params.agentId, pricingFingerprint, databasePath, { filePaths: files.flatMap((file) => file ? [file.filePath] : []) });
	const staleFiles = /* @__PURE__ */ new Set();
	let cachedFiles = 0;
	const hasExplicitRange = params.startMs !== void 0 || params.endMs !== void 0;
	const startMs = params.startMs ?? Number.NEGATIVE_INFINITY;
	const endMs = params.endMs ?? Number.POSITIVE_INFINITY;
	const dayFormatter = createUsageDayKeyFormatter(params.dayBucket);
	const summaries = params.sessions.map((session, index) => {
		const file = files[index];
		const stored = file ? rollups.get(file.filePath) : void 0;
		if (!file || !stored || !isUsageCostRollupFresh({
			stored,
			file
		})) {
			staleFiles.add(file?.sourcePath ?? session.sessionFile);
			return null;
		}
		cachedFiles += 1;
		return buildSessionCostSummaryFromRollup({
			rollup: stored.entry.rollup,
			sessionId: session.sessionId,
			sessionFile: session.sessionFile,
			startMs,
			endMs,
			includeUntimestamped: params.includeUntimestamped === true || !hasExplicitRange,
			formatDay: dayFormatter
		});
	});
	const refreshRequested = params.requestRefresh !== false && staleFiles.size > 0;
	if (refreshRequested) requestCostUsageCacheRefresh({
		config: params.config,
		agentId: params.agentId,
		sessionFiles: [...staleFiles]
	});
	const refreshRunning = isSessionCostUsageRefreshRunning(params.agentId, databasePath);
	return {
		summaries,
		cacheStatus: {
			status: staleFiles.size === 0 ? "fresh" : refreshRunning || refreshRequested ? "refreshing" : cachedFiles > 0 ? "partial" : "stale",
			cachedFiles,
			pendingFiles: staleFiles.size,
			staleFiles: staleFiles.size,
			refreshedAt: latestUsageCostRollupScan(rollups)
		}
	};
}
function requestCostUsageCacheRefresh(params) {
	const scopeSignal = getAsyncWorkSignal();
	if (scopeSignal?.aborted) return;
	const databasePath = resolveUsageCostCacheDatabasePath(params.agentId);
	const refreshes = usageCostRefreshes.get(scopeSignal) ?? /* @__PURE__ */ new Map();
	const existing = refreshes.get(databasePath);
	if (existing) {
		mergeUsageCostRefreshRequest(existing, params);
		return;
	}
	const state = {
		agentId: params.agentId,
		config: params.config,
		databasePath,
		fullRefreshRequested: false,
		pendingSessionFiles: /* @__PURE__ */ new Set(),
		sessionsDir: resolveSessionTranscriptsDirForAgent(params.agentId)
	};
	mergeUsageCostRefreshRequest(state, params);
	usageCostRefreshes.set(scopeSignal, refreshes);
	refreshes.set(databasePath, state);
	trackAsyncWork(() => runQueuedUsageCostRefresh(state, refreshes, scopeSignal));
}
function mergeUsageCostRefreshRequest(state, params) {
	if (params.config) state.config = params.config;
	state.agentId = params.agentId;
	if (!params.sessionFiles) {
		state.fullRefreshRequested = true;
		return;
	}
	for (const sessionFile of params.sessionFiles) state.pendingSessionFiles.add(sessionFile);
}
function waitForUsageCostRefresh(signal, delayMs) {
	return new Promise((resolve) => {
		const timer = setTimeout(finish, delayMs);
		timer.unref?.();
		function finish() {
			clearTimeout(timer);
			signal?.removeEventListener("abort", finish);
			resolve();
		}
		signal?.addEventListener("abort", finish, { once: true });
		if (signal?.aborted) finish();
	});
}
async function runQueuedUsageCostRefresh(state, refreshes, signal) {
	let busyRetryDelayMs = USAGE_COST_REFRESH_RETRY_MIN_MS;
	let retryDelayMs = 0;
	try {
		do {
			await waitForUsageCostRefresh(signal, retryDelayMs);
			if (signal?.aborted) return;
			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;
					const result = await refreshCostUsageCacheForAgent({
						config: state.config,
						agentId: state.agentId,
						databasePath: state.databasePath,
						sessionsDir: state.sessionsDir,
						sessionFiles: fullRefreshRequested ? void 0 : sessionFiles
					});
					if (signal?.aborted) return;
					if (result === "busy") {
						if (fullRefreshRequested) state.fullRefreshRequested = true;
						else for (const sessionFile of sessionFiles) state.pendingSessionFiles.add(sessionFile);
						retryDelayMs = busyRetryDelayMs;
						busyRetryDelayMs = Math.min(busyRetryDelayMs * 2, USAGE_COST_REFRESH_RETRY_MAX_MS);
						break;
					}
					busyRetryDelayMs = USAGE_COST_REFRESH_RETRY_MIN_MS;
				}
			} catch (error) {
				logger.warn(`background refresh failed: ${formatErrorMessage(error)}`, { error });
				if (signal?.aborted) return;
			}
		} while (state.fullRefreshRequested || state.pendingSessionFiles.size > 0);
	} finally {
		refreshes.delete(state.databasePath);
		if (refreshes.size === 0) usageCostRefreshes.delete(signal);
	}
}
//#endregion
//#region src/infra/session-cost-usage-reporting.ts
const USAGE_COST_DIRECT_REFRESH_RETRY_MS = 25;
/**
* 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 listUsageCountedTranscriptStats(params.agentId, { minMtimeMs: params.startMs });
	const discovered = /* @__PURE__ */ new Map();
	for (const file of files) {
		const { filePath, sourcePath: sessionFile, sessionId } = file;
		if (!sessionId) continue;
		const isPrimaryTranscript = file.kind === "sqlite" || isPrimarySessionTranscriptFileName(path.basename(sessionFile));
		let firstUserMessage;
		if (params.includeFirstUserMessage !== false) try {
			for await (const parsed of readTranscriptRecords(filePath)) try {
				const message = parsed.message;
				if (message?.role === "user") {
					const content = message.content;
					if (typeof content === "string") firstUserMessage = truncateUtf16Safe(content, 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 = truncateUtf16Safe(text, 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,
				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) return null;
	if (!await resolveUsageCostTranscriptFile(sessionFile)) return null;
	const agentDir = resolveUsageCostAgentDir(params.config, params.agentId);
	const databasePath = resolveUsageCostCacheDatabasePath(params.agentId);
	while (await refreshCostUsageCacheForAgent({
		config: params.config,
		agentId: params.agentId,
		agentDir,
		databasePath,
		sessionFiles: [sessionFile]
	}) === "busy") await new Promise((resolve) => {
		setTimeout(resolve, USAGE_COST_DIRECT_REFRESH_RETRY_MS);
	});
	const currentFile = await resolveUsageCostTranscriptFile(sessionFile);
	if (!currentFile) return null;
	const pricingFingerprint = resolveUsageCostPricingFingerprint(params.config, agentDir);
	const stored = readUsageCostRollups(params.agentId, pricingFingerprint, databasePath, { filePaths: [currentFile.filePath] }).get(currentFile.filePath);
	if (!stored || !isUsageCostRollupFresh({
		stored,
		file: currentFile
	})) return null;
	const hasExplicitRange = params.startMs !== void 0 || params.endMs !== void 0;
	return buildSessionCostSummaryFromRollup({
		rollup: stored.entry.rollup,
		sessionId: params.sessionId,
		sessionFile,
		startMs: params.startMs ?? Number.NEGATIVE_INFINITY,
		endMs: params.endMs ?? Number.POSITIVE_INFINITY,
		includeUntimestamped: params.includeUntimestamped === true || !hasExplicitRange,
		formatDay: createUsageDayKeyFormatter(params.dayBucket)
	});
}
async function loadSessionUsageTimeSeries(params) {
	const sessionFile = resolveExistingUsageSessionFile(params);
	if (!sessionFile) return null;
	if (!parseSqliteSessionFileMarker(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 = [];
	const agentDir = resolveUsageCostAgentDir(params.config, params.agentId);
	const resolveCost = createUsageCostResolver({
		config: params.config,
		agentDir
	});
	for await (const record of readTranscriptRecords(sessionFile)) {
		const entry = parseUsageCostTranscriptEntry(record, resolveCost);
		const timestamp = entry?.timestamp?.getTime();
		if (!entry?.usage || !timestamp) continue;
		const { input, output, cacheRead, cacheWrite, totalTokens } = computeUsageTokenTotals(entry.usage);
		points.push({
			timestamp,
			input,
			output,
			cacheRead,
			cacheWrite,
			totalTokens,
			cost: entry.costTotal ?? 0
		});
	}
	let cumulativeTokens = 0;
	let cumulativeCost = 0;
	const sortedPoints = points.toSorted((a, b) => a.timestamp - b.timestamp).map((point) => {
		cumulativeTokens += point.totalTokens;
		cumulativeCost += point.cost;
		return Object.assign(point, {
			cumulativeTokens,
			cumulativeCost
		});
	});
	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) return null;
	if (!parseSqliteSessionFileMarker(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 boundedLimit = Number.isInteger(limit);
	const retentionLimit = limit * 2;
	const agentDir = resolveUsageCostAgentDir(params.config, params.agentId);
	const resolveCost = createUsageCostResolver({
		config: params.config,
		agentDir
	});
	for await (const parsed of readTranscriptRecordsBestEffort(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 rawToolName = message.toolName ?? message.tool_name ?? message.name ?? message.tool;
		const toolName = normalizeOptionalString(rawToolName);
		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 = truncateUtf16Safe(content, maxLen) + "…";
		const entry = parseUsageCostTranscriptEntry(parsed, resolveCost);
		const usage = role === "assistant" ? entry?.usage : void 0;
		logs.push({
			timestamp: entry?.timestamp?.getTime() ?? 0,
			role,
			content,
			tokens: usage ? computeUsageTokenTotals(usage).totalTokens : void 0,
			cost: usage ? entry?.costTotal : void 0
		});
		if (boundedLimit && logs.length > retentionLimit) {
			logs.sort((a, b) => a.timestamp - b.timestamp);
			logs.splice(0, logs.length - limit);
		}
	} catch {}
	if (boundedLimit) {
		logs.sort((a, b) => a.timestamp - b.timestamp);
		return logs.length > limit ? logs.slice(-limit) : logs;
	}
	const sortedLogs = logs.toSorted((a, b) => a.timestamp - b.timestamp);
	if (sortedLogs.length > limit) return sortedLogs.slice(-limit);
	return sortedLogs;
}
//#endregion
export { loadCostUsageSummary as a, mergeSessionCostSummaryInto as c, loadSessionUsageTimeSeries as i, createUsageAggregateAccumulator as l, loadSessionCostSummary as n, loadCostUsageSummaryFromCache as o, loadSessionLogs as r, loadSessionCostSummariesFromCache as s, discoverAllSessions as t, resolveExistingUsageSessionFile as u };