diff --git a/actions/setup/js/daily_aic_workflow_helpers.cjs b/actions/setup/js/daily_aic_workflow_helpers.cjs index 67ba5c036ea..d62eac25b80 100644 --- a/actions/setup/js/daily_aic_workflow_helpers.cjs +++ b/actions/setup/js/daily_aic_workflow_helpers.cjs @@ -214,6 +214,12 @@ function sumAICFromUsageJSONLFiles(filePaths) { total += explicitAIC; continue; } + // Prefer proxy-emitted per-request AIC over locally computed when present. + const explicitPerRequest = getNumericAliasField(usage, parsed, ["ai_credits_this_response"]); + if (explicitPerRequest > 0) { + total += explicitPerRequest; + continue; + } const computed = computeInferenceAIC({ provider: getStringField(usage, parsed, "provider", "provider"), diff --git a/actions/setup/js/parse_mcp_gateway_log.cjs b/actions/setup/js/parse_mcp_gateway_log.cjs index 1fc2f0c378c..726c69e7e94 100644 --- a/actions/setup/js/parse_mcp_gateway_log.cjs +++ b/actions/setup/js/parse_mcp_gateway_log.cjs @@ -18,9 +18,11 @@ const { parseUnknownModelAICreditsFromAuditLog } = require("./ai_credits_context * - /tmp/gh-aw/mcp-logs/gateway.log (main gateway log, fallback) * - /tmp/gh-aw/mcp-logs/stderr.log (stderr output, fallback) * - /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl (token usage from firewall proxy) + * - /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl (audit copy, checked as fallback) */ const TOKEN_USAGE_PATH = "/tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl"; +const TOKEN_USAGE_AUDIT_PATH = "/tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl"; const MAX_RPC_SUMMARY_DETAILS_LENGTH = 120; const MAX_RPC_SUMMARY_GENERIC_LENGTH = 160; const MAX_RPC_MESSAGE_LABEL_LENGTH = 80; @@ -66,7 +68,7 @@ function parseTokenUsageJsonl(jsonlContent) { totalAIC: 0, ambientContextTokens: undefined, byModel: {}, - /** @type {{ model: string, provider: string, inputTokens: number, outputTokens: number, cacheReadTokens: number, cacheWriteTokens: number, reasoningTokens: number, durationMs: number, deltaAIC: number }[]} */ + /** @type {{ model: string, provider: string, inputTokens: number, outputTokens: number, cacheReadTokens: number, cacheWriteTokens: number, reasoningTokens: number, durationMs: number, deltaAIC: number, explicitDeltaAIC: number | null }[]} */ entries: [], }; @@ -84,6 +86,9 @@ function parseTokenUsageJsonl(jsonlContent) { const cacheWriteTokens = entry.cache_write_tokens || 0; const reasoningTokens = entry.reasoning_tokens || 0; const durationMs = entry.duration_ms || 0; + // When the proxy emits an explicit per-request AIC value, prefer it over + // the locally-computed value so that proxy-side pricing updates take effect. + const explicitDeltaAIC = typeof entry.ai_credits_this_response === "number" && entry.ai_credits_this_response > 0 ? entry.ai_credits_this_response : null; summary.totalInputTokens += inputTokens; summary.totalOutputTokens += outputTokens; @@ -117,7 +122,7 @@ function parseTokenUsageJsonl(jsonlContent) { m.requests++; m.durationMs += durationMs; - summary.entries.push({ model, provider: m.provider, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, reasoningTokens, durationMs, deltaAIC: 0 }); + summary.entries.push({ model, provider: m.provider, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, reasoningTokens, durationMs, deltaAIC: 0, explicitDeltaAIC }); } catch { // skip malformed lines } @@ -125,25 +130,11 @@ function parseTokenUsageJsonl(jsonlContent) { if (summary.totalRequests === 0) return null; - let totalAIC = 0; - for (const [model, usage] of Object.entries(summary.byModel)) { - const aic = computeInferenceAIC({ - provider: usage.provider || "", - model, - inputTokens: usage.inputTokens, - outputTokens: usage.outputTokens, - cacheReadTokens: usage.cacheReadTokens, - cacheWriteTokens: usage.cacheWriteTokens, - reasoningTokens: usage.reasoningTokens || 0, - }); - usage.aic = aic; - totalAIC += aic; - } - summary.totalAIC = totalAIC; - // Compute per-request AI credits. + // Prefer the proxy-emitted explicit value when available; fall back to + // computing from token counts and the local pricing catalog. for (const entry of summary.entries) { - entry.deltaAIC = computeInferenceAIC({ + const computed = computeInferenceAIC({ provider: entry.provider || "", model: entry.model, inputTokens: entry.inputTokens, @@ -152,7 +143,18 @@ function parseTokenUsageJsonl(jsonlContent) { cacheWriteTokens: entry.cacheWriteTokens, reasoningTokens: entry.reasoningTokens || 0, }); + entry.deltaAIC = entry.explicitDeltaAIC ?? computed; + } + + // Aggregate per-model AIC and overall total by summing per-entry deltaAIC. + // This keeps model totals consistent with the per-entry view regardless of + // whether explicit or computed AIC is used. + let totalAIC = 0; + for (const entry of summary.entries) { + summary.byModel[entry.model].aic += entry.deltaAIC; + totalAIC += entry.deltaAIC; } + summary.totalAIC = totalAIC; return summary; } @@ -202,25 +204,49 @@ function generateTokenUsageSummary(summary) { * @param {typeof import('@actions/core')} coreObj - The GitHub Actions core object */ function writeStepSummaryWithTokenUsage(coreObj) { - if (!fs.existsSync(TOKEN_USAGE_PATH)) { - coreObj.debug(`No token-usage.jsonl found at: ${TOKEN_USAGE_PATH}`); - } else { - const content = fs.readFileSync(TOKEN_USAGE_PATH, "utf8"); - if (content?.trim()) { - coreObj.info(`Found token-usage.jsonl (${content.length} bytes)`); - const parsedSummary = parseTokenUsageJsonl(content); - if (parsedSummary && parsedSummary.totalAIC > 0) { - const roundedAIC = parsedSummary.totalAIC.toFixed(3); - coreObj.exportVariable("GH_AW_AIC", roundedAIC); - coreObj.setOutput("aic", roundedAIC); - coreObj.info(`AI Credits: ${roundedAIC}`); - } - if (parsedSummary && typeof parsedSummary.ambientContextTokens === "number" && parsedSummary.ambientContextTokens > 0) { - const roundedAmbientContext = String(Math.round(parsedSummary.ambientContextTokens)); - coreObj.exportVariable("GH_AW_AMBIENT_CONTEXT", roundedAmbientContext); - coreObj.setOutput("ambient_context", roundedAmbientContext); - coreObj.info(`Ambient context: ${roundedAmbientContext}`); - } + // Read from both the primary path and the audit path, deduplicating by request_id. + // The audit path may contain additional entries when the primary path is absent or + // partially written (e.g. the proxy was restarted mid-run). + const paths = [TOKEN_USAGE_AUDIT_PATH, TOKEN_USAGE_PATH]; + const seenRequestIds = new Set(); + const dedupedLines = []; + + for (const filePath of paths) { + if (!fs.existsSync(filePath)) { + coreObj.debug(`No token-usage.jsonl found at: ${filePath}`); + continue; + } + const raw = fs.readFileSync(filePath, "utf8"); + if (!raw?.trim()) continue; + coreObj.info(`Found token-usage.jsonl at ${filePath} (${raw.length} bytes)`); + for (const rawLine of raw.split("\n")) { + const line = rawLine.trim(); + if (!line) continue; + // Lightweight request_id extraction for deduplication — mirrors extractRequestId() in + // parse_token_usage.cjs. The pattern covers standard JSON escaping (\\, \") but not + // exotic cases; api-proxy request IDs are UUIDs so this is sufficient in practice. + const idMatch = line.match(/"request_id"\s*:\s*"((?:\\.|[^"\\])*)"/); + const dedupeKey = idMatch ? `request_id:${idMatch[1]}` : line; + if (seenRequestIds.has(dedupeKey)) continue; + seenRequestIds.add(dedupeKey); + dedupedLines.push(line); + } + } + + if (dedupedLines.length > 0) { + const content = dedupedLines.join("\n"); + const parsedSummary = parseTokenUsageJsonl(content); + if (parsedSummary && parsedSummary.totalAIC > 0) { + const roundedAIC = parsedSummary.totalAIC.toFixed(3); + coreObj.exportVariable("GH_AW_AIC", roundedAIC); + coreObj.setOutput("aic", roundedAIC); + coreObj.info(`AI Credits: ${roundedAIC}`); + } + if (parsedSummary && typeof parsedSummary.ambientContextTokens === "number" && parsedSummary.ambientContextTokens > 0) { + const roundedAmbientContext = String(Math.round(parsedSummary.ambientContextTokens)); + coreObj.exportVariable("GH_AW_AMBIENT_CONTEXT", roundedAmbientContext); + coreObj.setOutput("ambient_context", roundedAmbientContext); + coreObj.info(`Ambient context: ${roundedAmbientContext}`); } } @@ -1126,6 +1152,8 @@ if (typeof module !== "undefined" && module.exports) { hasAICreditsRateLimitError, hasUnknownModelAICreditsError, setUnknownModelAICreditsOutput, + TOKEN_USAGE_PATH, + TOKEN_USAGE_AUDIT_PATH, }; } diff --git a/actions/setup/js/parse_mcp_gateway_log.test.cjs b/actions/setup/js/parse_mcp_gateway_log.test.cjs index 385c7b13732..1feb207f7f9 100644 --- a/actions/setup/js/parse_mcp_gateway_log.test.cjs +++ b/actions/setup/js/parse_mcp_gateway_log.test.cjs @@ -1,6 +1,10 @@ // @ts-check /// +const fs = require("fs"); +const os = require("os"); +const path = require("path"); + const { generateGatewayLogSummary, generatePlainTextGatewaySummary, @@ -18,6 +22,8 @@ const { parseTokenUsageJsonl, generateTokenUsageSummary, formatDurationMs, + TOKEN_USAGE_PATH, + TOKEN_USAGE_AUDIT_PATH, } = require("./parse_mcp_gateway_log.cjs"); describe("parse_mcp_gateway_log", () => { @@ -476,6 +482,160 @@ Some content here.`; } }); + test("exports GH_AW_AIC from audit path when primary token-usage.jsonl is absent", async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mcp-test-")); + const gatewayMdPath = path.join(tmpDir, "gateway.md"); + const auditTokenUsagePath = path.join(tmpDir, "audit-token-usage.jsonl"); + const originalExistsSync = fs.existsSync; + const originalReadFileSync = fs.readFileSync; + + try { + fs.writeFileSync(gatewayMdPath, "# Gateway Summary\n\nSome markdown content"); + fs.writeFileSync( + auditTokenUsagePath, + JSON.stringify({ + model: "claude-sonnet-4.6", + provider: "copilot", + input_tokens: 27252, + output_tokens: 461, + cache_read_tokens: 0, + cache_write_tokens: 0, + duration_ms: 6230, + ai_credits_this_response: 8.8671, + ai_credits_total: 8.8671, + }) + ); + + const mockCore = { + info: vi.fn(), + debug: vi.fn(), + startGroup: vi.fn(), + endGroup: vi.fn(), + notice: vi.fn(), + warning: vi.fn(), + error: vi.fn(), + setFailed: vi.fn(), + exportVariable: vi.fn(), + setOutput: vi.fn(), + summary: { + addRaw: vi.fn().mockReturnThis(), + addDetails: vi.fn().mockReturnThis(), + write: vi.fn(), + }, + }; + + fs.existsSync = vi.fn(filepath => { + if (filepath === "/tmp/gh-aw/mcp-logs/gateway.md") return true; + // Primary token-usage.jsonl is absent — audit path is present. + if (filepath === TOKEN_USAGE_PATH) return false; + if (filepath === TOKEN_USAGE_AUDIT_PATH) return true; + return originalExistsSync(filepath); + }); + + fs.readFileSync = vi.fn((filepath, encoding) => { + if (filepath === "/tmp/gh-aw/mcp-logs/gateway.md") { + return originalReadFileSync(gatewayMdPath, encoding); + } + if (filepath === TOKEN_USAGE_AUDIT_PATH) { + return originalReadFileSync(auditTokenUsagePath, encoding); + } + return originalReadFileSync(filepath, encoding); + }); + + global.core = mockCore; + + const { main } = require("./parse_mcp_gateway_log.cjs"); + await main(); + + expect(mockCore.exportVariable).toHaveBeenCalledWith("GH_AW_AIC", "8.867"); + expect(mockCore.summary.write).toHaveBeenCalled(); + } finally { + fs.existsSync = originalExistsSync; + fs.readFileSync = originalReadFileSync; + delete global.core; + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + test("deduplicates entries shared between primary and audit token-usage.jsonl paths", async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mcp-test-")); + const gatewayMdPath = path.join(tmpDir, "gateway.md"); + const primaryTokenUsagePath = path.join(tmpDir, "primary-token-usage.jsonl"); + const auditTokenUsagePath = path.join(tmpDir, "audit-token-usage.jsonl"); + const originalExistsSync = fs.existsSync; + const originalReadFileSync = fs.readFileSync; + + try { + const entry = JSON.stringify({ + request_id: "req-dedup-1", + model: "claude-sonnet-4.6", + provider: "copilot", + input_tokens: 1000, + output_tokens: 100, + cache_read_tokens: 0, + cache_write_tokens: 0, + duration_ms: 1000, + ai_credits_this_response: 4.0, + }); + fs.writeFileSync(gatewayMdPath, "# Gateway Summary\n\nSome markdown content"); + // Both paths contain the same entry (same request_id). + fs.writeFileSync(primaryTokenUsagePath, entry); + fs.writeFileSync(auditTokenUsagePath, entry); + + const mockCore = { + info: vi.fn(), + debug: vi.fn(), + startGroup: vi.fn(), + endGroup: vi.fn(), + notice: vi.fn(), + warning: vi.fn(), + error: vi.fn(), + setFailed: vi.fn(), + exportVariable: vi.fn(), + setOutput: vi.fn(), + summary: { + addRaw: vi.fn().mockReturnThis(), + addDetails: vi.fn().mockReturnThis(), + write: vi.fn(), + }, + }; + + fs.existsSync = vi.fn(filepath => { + if (filepath === "/tmp/gh-aw/mcp-logs/gateway.md") return true; + if (filepath === TOKEN_USAGE_PATH) return true; + if (filepath === TOKEN_USAGE_AUDIT_PATH) return true; + return originalExistsSync(filepath); + }); + + fs.readFileSync = vi.fn((filepath, encoding) => { + if (filepath === "/tmp/gh-aw/mcp-logs/gateway.md") { + return originalReadFileSync(gatewayMdPath, encoding); + } + if (filepath === TOKEN_USAGE_PATH) { + return originalReadFileSync(primaryTokenUsagePath, encoding); + } + if (filepath === TOKEN_USAGE_AUDIT_PATH) { + return originalReadFileSync(auditTokenUsagePath, encoding); + } + return originalReadFileSync(filepath, encoding); + }); + + global.core = mockCore; + + const { main } = require("./parse_mcp_gateway_log.cjs"); + await main(); + + // With deduplication, entry should only be counted once → AIC = 4.0, not 8.0. + expect(mockCore.exportVariable).toHaveBeenCalledWith("GH_AW_AIC", "4.000"); + expect(mockCore.summary.write).toHaveBeenCalled(); + } finally { + fs.existsSync = originalExistsSync; + fs.readFileSync = originalReadFileSync; + delete global.core; + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + test("appends steering from rpc-messages.jsonl after gateway.md", async () => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mcp-test-")); const gatewayMdPath = path.join(tmpDir, "gateway.md"); @@ -1721,13 +1881,44 @@ not-json expect(md).not.toContain("effective token"); }); - test("does not include cache efficiency or effective token wording", () => { - const content = JSON.stringify({ model: "m", input_tokens: 100, output_tokens: 10, cache_read_tokens: 900, cache_write_tokens: 0, duration_ms: 100 }); - const summary = parseTokenUsageJsonl(content); - const md = generateTokenUsageSummary(summary); - expect(md).not.toContain("●"); - expect(md).not.toContain("Cache efficiency"); - expect(md).not.toContain("effective token"); + test("uses ai_credits_this_response field when present instead of computing from tokens", () => { + const explicitAIC = 8.8671; + const line = JSON.stringify({ + model: "claude-sonnet-4.6", + provider: "copilot", + input_tokens: 27252, + output_tokens: 461, + cache_read_tokens: 0, + cache_write_tokens: 0, + duration_ms: 6230, + ai_credits_this_response: explicitAIC, + ai_credits_total: explicitAIC, + }); + const summary = parseTokenUsageJsonl(line); + expect(summary).not.toBeNull(); + expect(summary.entries).toHaveLength(1); + expect(summary.entries[0].explicitDeltaAIC).toBe(explicitAIC); + expect(summary.entries[0].deltaAIC).toBe(explicitAIC); + expect(Math.abs(summary.totalAIC - explicitAIC)).toBeLessThan(0.0001); + }); + + test("sums explicit ai_credits_this_response across multiple entries", () => { + const lines = [ + JSON.stringify({ model: "claude-sonnet-4.6", provider: "copilot", input_tokens: 1000, output_tokens: 100, cache_read_tokens: 0, cache_write_tokens: 0, duration_ms: 1000, ai_credits_this_response: 3.5 }), + JSON.stringify({ model: "claude-sonnet-4.6", provider: "copilot", input_tokens: 2000, output_tokens: 200, cache_read_tokens: 0, cache_write_tokens: 0, duration_ms: 2000, ai_credits_this_response: 5.25 }), + ]; + const summary = parseTokenUsageJsonl(lines.join("\n")); + expect(summary).not.toBeNull(); + expect(Math.abs(summary.totalAIC - 8.75)).toBeLessThan(0.0001); + expect(Math.abs(summary.byModel["claude-sonnet-4.6"].aic - 8.75)).toBeLessThan(0.0001); + }); + + test("falls back to computed AIC when ai_credits_this_response is absent", () => { + const line = JSON.stringify({ model: "claude-sonnet-4-6", provider: "anthropic", input_tokens: 100, output_tokens: 50, cache_read_tokens: 0, cache_write_tokens: 0, duration_ms: 100 }); + const summary = parseTokenUsageJsonl(line); + expect(summary).not.toBeNull(); + expect(summary.entries[0].explicitDeltaAIC).toBeNull(); + expect(summary.entries[0].deltaAIC).toBeGreaterThan(0); }); }); }); diff --git a/actions/setup/js/parse_token_usage.cjs b/actions/setup/js/parse_token_usage.cjs index 5df4d3add1f..ddfeb419cbc 100644 --- a/actions/setup/js/parse_token_usage.cjs +++ b/actions/setup/js/parse_token_usage.cjs @@ -18,6 +18,8 @@ const TOKEN_USAGE_AUDIT_PATH = "/tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy const TOKEN_USAGE_PATH = "/tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl"; const TOKEN_USAGE_PATHS = [TOKEN_USAGE_AUDIT_PATH, TOKEN_USAGE_PATH]; const AGENT_USAGE_PATH = "/tmp/gh-aw/agent_usage.json"; +// agent_usage.jsonl is the path expected by the usage artifact collector; keep both in sync. +const AGENT_USAGE_JSONL_PATH = "/tmp/gh-aw/agent_usage.jsonl"; const DEFAULT_SUMMARY_TITLE = "Token Usage"; /** @@ -171,6 +173,8 @@ async function main() { ...(primaryModel ? { primary_model: primaryModel } : {}), }; fs.writeFileSync(AGENT_USAGE_PATH, JSON.stringify(agentUsage) + "\n"); + // Also write the .jsonl path so the usage artifact collector can find it. + fs.writeFileSync(AGENT_USAGE_JSONL_PATH, JSON.stringify(agentUsage) + "\n"); if (summary.totalAIC > 0) { const aic = summary.totalAIC.toFixed(3); @@ -203,6 +207,7 @@ if (typeof module !== "undefined" && module.exports) { TOKEN_USAGE_PATH, TOKEN_USAGE_PATHS, AGENT_USAGE_PATH, + AGENT_USAGE_JSONL_PATH, DEFAULT_SUMMARY_TITLE, }; } diff --git a/actions/setup/js/parse_token_usage.test.cjs b/actions/setup/js/parse_token_usage.test.cjs index 6cd34123cc4..69c0df16ed2 100644 --- a/actions/setup/js/parse_token_usage.test.cjs +++ b/actions/setup/js/parse_token_usage.test.cjs @@ -16,6 +16,7 @@ const { TOKEN_USAGE_PATH, TOKEN_USAGE_PATHS, AGENT_USAGE_PATH, + AGENT_USAGE_JSONL_PATH, DEFAULT_SUMMARY_TITLE, } = require("./parse_token_usage.cjs"); @@ -52,6 +53,10 @@ describe("parse_token_usage", () => { expect(AGENT_USAGE_PATH).toBe("/tmp/gh-aw/agent_usage.json"); }); + test("AGENT_USAGE_JSONL_PATH points to agent_usage.jsonl", () => { + expect(AGENT_USAGE_JSONL_PATH).toBe("/tmp/gh-aw/agent_usage.jsonl"); + }); + test("DEFAULT_SUMMARY_TITLE points to Token Usage", () => { expect(DEFAULT_SUMMARY_TITLE).toBe("Token Usage"); }); @@ -183,6 +188,8 @@ describe("parse_token_usage", () => { fs.writeFileSync = vi.fn((p, data) => { if (p === AGENT_USAGE_PATH) { originalWriteFileSync(agentUsageFile, data); + } else if (p === AGENT_USAGE_JSONL_PATH) { + // agent_usage.jsonl is written alongside agent_usage.json; no additional assertions needed } else { originalWriteFileSync(p, data); } @@ -254,6 +261,7 @@ describe("parse_token_usage", () => { test("writes agent_usage.json with aggregated token totals and primary_model", async () => { const agentUsageFile = path.join(tmpDir, "agent_usage.json"); + const agentUsageJsonlFile = path.join(tmpDir, "agent_usage.jsonl"); fs.existsSync = vi.fn(p => { if (p === TOKEN_USAGE_PATH) return true; @@ -273,6 +281,8 @@ describe("parse_token_usage", () => { fs.writeFileSync = vi.fn((p, data) => { if (p === AGENT_USAGE_PATH) { originalWriteFileSync(agentUsageFile, data); + } else if (p === AGENT_USAGE_JSONL_PATH) { + originalWriteFileSync(agentUsageJsonlFile, data); } else { originalWriteFileSync(p, data); } @@ -290,6 +300,10 @@ describe("parse_token_usage", () => { expect(typeof agentUsage.ai_credits).toBe("number"); // primary_model is the actual model from token-usage data (not a user alias) expect(agentUsage.primary_model).toBe("claude-sonnet-4-6"); + // agent_usage.jsonl is also written so the usage artifact collector can find it + expect(fs.existsSync(agentUsageJsonlFile)).toBe(true); + const agentUsageJsonl = JSON.parse(fs.readFileSync(agentUsageJsonlFile, "utf8")); + expect(agentUsageJsonl.ai_credits).toBe(agentUsage.ai_credits); }); test("handles multiple model entries", async () => { @@ -313,6 +327,8 @@ describe("parse_token_usage", () => { fs.writeFileSync = vi.fn((p, data) => { if (p === AGENT_USAGE_PATH) { originalWriteFileSync(agentUsageFile, data); + } else if (p === AGENT_USAGE_JSONL_PATH) { + // agent_usage.jsonl is written alongside agent_usage.json; no additional assertions needed } else { originalWriteFileSync(p, data); } @@ -354,6 +370,8 @@ describe("parse_token_usage", () => { fs.writeFileSync = vi.fn((p, data) => { if (p === AGENT_USAGE_PATH) { originalWriteFileSync(agentUsageFile, data); + } else if (p === AGENT_USAGE_JSONL_PATH) { + // agent_usage.jsonl is written alongside agent_usage.json; no additional assertions needed } else { originalWriteFileSync(p, data); } @@ -422,6 +440,8 @@ describe("parse_token_usage", () => { fs.writeFileSync = vi.fn((p, data) => { if (p === AGENT_USAGE_PATH) { originalWriteFileSync(agentUsageFile, data); + } else if (p === AGENT_USAGE_JSONL_PATH) { + // agent_usage.jsonl is written alongside agent_usage.json; no additional assertions needed } else { originalWriteFileSync(p, data); }