/** * @file subagent-usage — the token usage of a Claude session's sub-agents (the * `agent-*.jsonl` transcripts Claude Code writes beside the session's own, in * `//subagents/` or `/subagents//` — the * same places `findSessionSubagents` in scripts/import-history.js looks), * summed per pricing bucket and per UTC hour. The hook writer adds it to the * main transcript's usage, so a session's total and its hours count every * sub-agent, the same total the importer's `combineSessionTokens` writes * (both go through the high-water `replaceTokenUsage`, so neither can inflate * the other). Workflow-tool agents (`subagents/workflows/…`) are left out: * `workflow-ingest.js` books those under their own service tier. Parsing goes * through a TranscriptCache of its own (stat-cached, incremental), so the main * transcripts' cache is never crowded out. * @author Nguyễn Ngọc Trí Vĩ */ const fs = require("fs"); const path = require("path"); const TranscriptCache = require("./transcript-cache"); const { emptyBucket, accumulateBucket } = require("./token-usage"); const cache = new TranscriptCache(500); /** The session's sub-agent transcripts, flat ones only. */ function subagentFiles(transcriptPath) { const dir = path.dirname(transcriptPath); const sessionId = path.basename(transcriptPath, ".jsonl"); const out = []; for (const d of [ path.join(dir, sessionId, "subagents"), path.join(dir, "subagents", sessionId), ]) { let names; try { names = fs.readdirSync(d); } catch { continue; } for (const n of names) if (n.endsWith(".jsonl")) out.push(path.join(d, n)); } return out.sort(); } /** `a` plus `b`, bucket by bucket (an hour bucket keeps its `hour`); never mutates. */ function addTokens(a, b) { const out = {}; for (const src of [a, b]) { for (const [key, t] of Object.entries(src || {})) { if (!out[key]) { out[key] = emptyBucket(t.model, t.speed, t.geo, t.tier); if (t.hour) out[key].hour = t.hour; } accumulateBucket(out[key], t); } } return out; } /** * The sub-agents' usage of the session whose transcript is `transcriptPath`: * `{tokensByModel, tokensByHour, parts}` (`parts` = each file's cached parse * result, the same object while that file is unchanged), or null when it has * no sub-agent with usage. */ function subagentUsage(transcriptPath) { if (typeof transcriptPath !== "string" || !transcriptPath.endsWith(".jsonl")) return null; const parts = []; for (const f of subagentFiles(transcriptPath)) { let r = null; try { r = cache.extract(f); } catch { /* unreadable or vanished: skipped this time */ } if (r && r.tokensByModel) parts.push(r); } if (!parts.length) return null; let tokensByModel = {}; let tokensByHour = {}; for (const r of parts) { tokensByModel = addTokens(tokensByModel, r.tokensByModel); tokensByHour = addTokens(tokensByHour, r.tokensByHour); } return { tokensByModel, tokensByHour, parts }; } /** Whether two `parts` lists are the same parses (nothing grew in between). */ function sameParts(a, b) { const x = a || []; const y = b || []; return x.length === y.length && x.every((r, i) => r === y[i]); } module.exports = { subagentUsage, subagentFiles, addTokens, sameParts };