@agent-finops/core 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,8 @@
1
- import { readdir, readFile, stat } from "node:fs/promises";
1
+ import { createReadStream } from "node:fs";
2
+ import { lstat, open, readdir, readFile, stat } from "node:fs/promises";
2
3
  import { basename, isAbsolute, join, resolve, sep } from "node:path";
3
4
  import { homedir } from "node:os";
5
+ import { createInterface } from "node:readline";
4
6
  import { estimateTokenCostUsd } from "./modelPricing.js";
5
7
  import { redactSecrets } from "./discovery.js";
6
8
  import { createCodexInvocationCollector } from "./toolInvocations.js";
@@ -52,6 +54,141 @@ function totalUsageTokens(usage) {
52
54
  (usage.cacheWrite5mTokens ?? 0) +
53
55
  (usage.cacheWrite1hTokens ?? 0);
54
56
  }
57
+ function parseClaudeFinancialUsage(value, onDiagnostic) {
58
+ const inputTokens = tokenComponentOf(value.input_tokens);
59
+ const outputTokens = tokenComponentOf(value.output_tokens);
60
+ const cacheReadField = optionalTokenComponent(value, "cache_read_input_tokens");
61
+ const writeTotalField = optionalTokenComponent(value, "cache_creation_input_tokens");
62
+ const reportedTotalField = optionalTokenComponent(value, "total_tokens");
63
+ const cacheCreationPresent = Object.prototype.hasOwnProperty.call(value, "cache_creation");
64
+ const cacheCreation = isRecord(value.cache_creation) ? value.cache_creation : undefined;
65
+ const write5mField = cacheCreation
66
+ ? optionalTokenComponent(cacheCreation, "ephemeral_5m_input_tokens")
67
+ : { present: false };
68
+ const write1hField = cacheCreation
69
+ ? optionalTokenComponent(cacheCreation, "ephemeral_1h_input_tokens")
70
+ : { present: false };
71
+ const componentsSupported = inputTokens !== undefined &&
72
+ outputTokens !== undefined &&
73
+ (!cacheReadField.present || cacheReadField.value !== undefined) &&
74
+ (!writeTotalField.present || writeTotalField.value !== undefined) &&
75
+ (!reportedTotalField.present || reportedTotalField.value !== undefined) &&
76
+ (!cacheCreationPresent || Boolean(cacheCreation)) &&
77
+ (!write5mField.present || write5mField.value !== undefined) &&
78
+ (!write1hField.present || write1hField.value !== undefined);
79
+ const usage = {
80
+ // Retain every valid component for partial evidence, but never let a
81
+ // missing/invalid required field become a priceable zero-dollar call.
82
+ inputTokens: inputTokens ?? 0,
83
+ outputTokens: outputTokens ?? 0,
84
+ cacheReadTokens: cacheReadField.value ?? 0,
85
+ // Prefer the 5m/1h breakdown; fall back to the total as 5m (cheaper bound).
86
+ cacheWrite5mTokens: write5mField.value ?? writeTotalField.value ?? 0,
87
+ cacheWrite1hTokens: write1hField.value ?? 0
88
+ };
89
+ if (componentsSupported) {
90
+ return {
91
+ usage,
92
+ latestTurnUsage: toTurnUsage(usage, "assistant_message_usage")
93
+ };
94
+ }
95
+ onDiagnostic?.({ code: "unsupported_token_shape", count: 1 });
96
+ const reportedTotalTokens = reportedTotalField.value;
97
+ return {
98
+ usage,
99
+ usageSupport: "unsupported_token_shape",
100
+ ...(reportedTotalTokens !== undefined ? { reportedTotalTokens } : {})
101
+ };
102
+ }
103
+ function optionalTokenComponent(record, key) {
104
+ if (!Object.prototype.hasOwnProperty.call(record, key))
105
+ return { present: false };
106
+ const value = tokenComponentOf(record[key]);
107
+ return value === undefined ? { present: true } : { present: true, value };
108
+ }
109
+ function parseCodexCumulativeUsage(current, baseline) {
110
+ const rawInput = tokenComponentOf(current.input_tokens);
111
+ const rawOutput = tokenComponentOf(current.output_tokens);
112
+ const currentCachedField = optionalTokenComponent(current, "cached_input_tokens");
113
+ const currentTotalField = optionalTokenComponent(current, "total_tokens");
114
+ const rawCached = currentCachedField.value ?? 0;
115
+ const rawReportedTotal = currentTotalField.value;
116
+ const baselineInput = baseline ? tokenComponentOf(baseline.input_tokens) : undefined;
117
+ const baselineOutput = baseline ? tokenComponentOf(baseline.output_tokens) : undefined;
118
+ const baselineCachedField = baseline
119
+ ? optionalTokenComponent(baseline, "cached_input_tokens")
120
+ : { present: false };
121
+ const baselineTotalField = baseline
122
+ ? optionalTokenComponent(baseline, "total_tokens")
123
+ : { present: false };
124
+ const baselineCached = baselineCachedField.value ?? 0;
125
+ const baselineReportedTotal = baselineTotalField.value;
126
+ const currentSupported = rawInput !== undefined &&
127
+ rawOutput !== undefined &&
128
+ (!currentCachedField.present || currentCachedField.value !== undefined) &&
129
+ (!currentTotalField.present || currentTotalField.value !== undefined) &&
130
+ rawCached <= rawInput &&
131
+ (rawReportedTotal === undefined || rawReportedTotal >= rawInput + rawOutput) &&
132
+ !((rawReportedTotal ?? 0) > 0 && rawInput === 0 && rawOutput === 0);
133
+ const baselineSupported = !baseline || (baselineInput !== undefined &&
134
+ baselineOutput !== undefined &&
135
+ (!baselineCachedField.present || baselineCachedField.value !== undefined) &&
136
+ (!baselineTotalField.present || baselineTotalField.value !== undefined) &&
137
+ baselineCached <= baselineInput &&
138
+ (baselineReportedTotal === undefined ||
139
+ baselineReportedTotal >= baselineInput + baselineOutput) &&
140
+ !((baselineReportedTotal ?? 0) > 0 && baselineInput === 0 && baselineOutput === 0));
141
+ const monotonic = !baseline || (rawInput !== undefined && baselineInput !== undefined && rawInput >= baselineInput &&
142
+ rawOutput !== undefined && baselineOutput !== undefined && rawOutput >= baselineOutput &&
143
+ rawCached >= baselineCached &&
144
+ rawInput - rawCached >= baselineInput - baselineCached &&
145
+ (rawReportedTotal === undefined ||
146
+ baselineReportedTotal === undefined ||
147
+ rawReportedTotal >= baselineReportedTotal));
148
+ const input = Math.max(0, (rawInput ?? 0) - (baselineInput ?? 0));
149
+ const cached = Math.max(0, rawCached - baselineCached);
150
+ const output = Math.max(0, (rawOutput ?? 0) - (baselineOutput ?? 0));
151
+ const reportedTotalTokens = rawReportedTotal === undefined
152
+ ? undefined
153
+ : Math.max(0, rawReportedTotal - (baselineReportedTotal ?? 0));
154
+ return {
155
+ usage: {
156
+ // Codex input_tokens includes cached input; expose the non-cached split.
157
+ inputTokens: Math.max(0, input - cached),
158
+ outputTokens: output,
159
+ cacheReadTokens: cached
160
+ },
161
+ supported: currentSupported && baselineSupported && monotonic,
162
+ ...(reportedTotalTokens !== undefined ? { reportedTotalTokens } : {})
163
+ };
164
+ }
165
+ function parseCodexTurnUsage(value) {
166
+ const rawInput = tokenComponentOf(value.input_tokens);
167
+ const rawOutput = tokenComponentOf(value.output_tokens);
168
+ const cachedField = optionalTokenComponent(value, "cached_input_tokens");
169
+ const totalField = optionalTokenComponent(value, "total_tokens");
170
+ const cached = cachedField.value ?? 0;
171
+ const total = totalField.value;
172
+ const supported = rawInput !== undefined &&
173
+ rawOutput !== undefined &&
174
+ (!cachedField.present || cachedField.value !== undefined) &&
175
+ (!totalField.present || totalField.value !== undefined) &&
176
+ cached <= rawInput &&
177
+ (total === undefined || total >= rawInput + rawOutput);
178
+ if (!supported)
179
+ return { supported: false };
180
+ return {
181
+ supported: true,
182
+ usage: {
183
+ inputTokens: rawInput - cached,
184
+ outputTokens: rawOutput,
185
+ cacheReadTokens: cached,
186
+ contextTokens: rawInput,
187
+ totalTokens: total ?? rawInput + rawOutput,
188
+ source: "transcript_last_token_usage"
189
+ }
190
+ };
191
+ }
55
192
  /** Parse one Claude Code transcript (JSONL). Exported for tests. */
56
193
  export function parseClaudeCodeTranscript(content, filePath = "", sinceMs, onDiagnostic) {
57
194
  const calls = [];
@@ -113,18 +250,7 @@ export function parseClaudeCodeTranscript(content, filePath = "", sinceMs, onDia
113
250
  pendingPrompts.length = 0;
114
251
  continue;
115
252
  }
116
- const cacheCreation = isRecord(usage.cache_creation) ? usage.cache_creation : undefined;
117
- const write5m = numberOf(cacheCreation?.ephemeral_5m_input_tokens);
118
- const write1h = numberOf(cacheCreation?.ephemeral_1h_input_tokens);
119
- const writeTotal = numberOf(usage.cache_creation_input_tokens) ?? 0;
120
- const parsedUsage = {
121
- inputTokens: numberOf(usage.input_tokens) ?? 0,
122
- outputTokens: numberOf(usage.output_tokens) ?? 0,
123
- cacheReadTokens: numberOf(usage.cache_read_input_tokens) ?? 0,
124
- // Prefer the 5m/1h breakdown; fall back to the total as 5m (cheaper bound).
125
- cacheWrite5mTokens: write5m ?? writeTotal,
126
- cacheWrite1hTokens: write1h ?? 0
127
- };
253
+ const parsedUsage = parseClaudeFinancialUsage(usage, onDiagnostic);
128
254
  const workingDirectory = absoluteWorkingDirectory(stringOf(entry.cwd));
129
255
  const project = projectFromCwd(workingDirectory) ?? projectFromTranscriptPath(filePath);
130
256
  const sessionId = stringOf(entry.sessionId);
@@ -135,9 +261,15 @@ export function parseClaudeCodeTranscript(content, filePath = "", sinceMs, onDia
135
261
  project,
136
262
  workingDirectory,
137
263
  sessionId,
138
- latestTurnUsage: toTurnUsage(parsedUsage, "assistant_message_usage"),
264
+ ...(parsedUsage.latestTurnUsage
265
+ ? { latestTurnUsage: parsedUsage.latestTurnUsage }
266
+ : {}),
139
267
  usageScope: "turn",
140
- usage: parsedUsage
268
+ ...(parsedUsage.usageSupport ? { usageSupport: parsedUsage.usageSupport } : {}),
269
+ ...(parsedUsage.reportedTotalTokens !== undefined
270
+ ? { reportedTotalTokens: parsedUsage.reportedTotalTokens }
271
+ : {}),
272
+ usage: parsedUsage.usage
141
273
  };
142
274
  calls.push(call);
143
275
  const activityKey = localActivityScopeKey(sessionId, workingDirectory, project);
@@ -312,31 +444,16 @@ export function parseCodexRollout(content, onEntry, onDiagnostic) {
312
444
  }
313
445
  if (!lastTotal || isSubagent && !rootTaskStarted)
314
446
  return [];
315
- const rawInput = numberOf(lastTotal.input_tokens);
316
- const rawOutput = numberOf(lastTotal.output_tokens);
317
- const rawCached = numberOf(lastTotal.cached_input_tokens);
318
- const rawReportedTotal = numberOf(lastTotal.total_tokens);
319
- const baselineInput = numberOf(inheritedUsageBaseline?.input_tokens);
320
- const baselineOutput = numberOf(inheritedUsageBaseline?.output_tokens);
321
- const baselineReportedTotal = numberOf(inheritedUsageBaseline?.total_tokens);
322
- const currentComponentsComplete = rawInput !== undefined && rawOutput !== undefined && !((rawReportedTotal ?? 0) > 0 && rawInput === 0 && rawOutput === 0);
323
- const baselineComponentsComplete = !inheritedUsageBaseline || (baselineInput !== undefined && baselineOutput !== undefined && !((baselineReportedTotal ?? 0) > 0 && baselineInput === 0 && baselineOutput === 0));
324
- const usageSupport = currentComponentsComplete && baselineComponentsComplete
447
+ const parsedUsage = parseCodexCumulativeUsage(lastTotal, inheritedUsageBaseline);
448
+ const parsedTurn = lastTurn
449
+ ? parseCodexTurnUsage(lastTurn)
450
+ : { supported: true };
451
+ const usageSupport = parsedUsage.supported && parsedTurn.supported
325
452
  ? "complete"
326
453
  : "unsupported_token_shape";
327
454
  if (usageSupport === "unsupported_token_shape") {
328
455
  onDiagnostic?.({ code: "unsupported_token_shape", count: 1 });
329
456
  }
330
- const input = Math.max(0, (rawInput ?? 0) - (baselineInput ?? 0));
331
- const cached = Math.max(0, (rawCached ?? 0) -
332
- (numberOf(inheritedUsageBaseline?.cached_input_tokens) ?? 0));
333
- const output = Math.max(0, (rawOutput ?? 0) - (baselineOutput ?? 0));
334
- const reportedTotalTokens = rawReportedTotal === undefined
335
- ? undefined
336
- : Math.max(0, rawReportedTotal - (baselineReportedTotal ?? 0));
337
- const latestTurnUsage = lastTurn
338
- ? codexTurnUsage(lastTurn)
339
- : undefined;
340
457
  const workingDirectory = absoluteWorkingDirectory(dominantCodexCwd(rootCwd, toolWorkdirs));
341
458
  const project = projectFromCwd(workingDirectory);
342
459
  const activity = buildLocalAgentActivity({
@@ -357,16 +474,15 @@ export function parseCodexRollout(content, onEntry, onDiagnostic) {
357
474
  sessionId,
358
475
  rateLimits: lastRateLimits,
359
476
  activity,
360
- latestTurnUsage,
477
+ ...(usageSupport === "complete" && parsedTurn.usage
478
+ ? { latestTurnUsage: parsedTurn.usage }
479
+ : {}),
361
480
  usageScope: "session_cumulative",
362
481
  usageSupport,
363
- ...(reportedTotalTokens !== undefined ? { reportedTotalTokens } : {}),
364
- usage: {
365
- // Codex input_tokens INCLUDES cached tokens; split them out.
366
- inputTokens: Math.max(0, input - cached),
367
- outputTokens: output,
368
- cacheReadTokens: cached
369
- }
482
+ ...(parsedUsage.reportedTotalTokens !== undefined
483
+ ? { reportedTotalTokens: parsedUsage.reportedTotalTokens }
484
+ : {}),
485
+ usage: parsedUsage.usage
370
486
  }];
371
487
  }
372
488
  function collectEmbeddedToolWorkdirs(input, toolWorkdirs) {
@@ -507,6 +623,648 @@ export async function loadLocalAgentUsage(options = {}) {
507
623
  ...(codexInvocationFiles ? { codexInvocationFiles } : {})
508
624
  };
509
625
  }
626
+ /**
627
+ * Stream only the financial evidence required by init/status snapshots.
628
+ *
629
+ * Unlike `loadLocalAgentUsage`, this path never derives prompts, file focus,
630
+ * tool activity, or invocation evidence. It deliberately returns the same
631
+ * result contract so callers can preserve the existing evidence and
632
+ * diagnostics vocabulary without maintaining a second loader schema. Codex
633
+ * project attribution intentionally uses only root metadata; home-launched
634
+ * tool-workdir inference remains exclusive to the full qualitative loader.
635
+ */
636
+ export async function loadLocalAgentFinancialUsage(options = {}) {
637
+ const home = homedir();
638
+ const claudeDir = options.claudeProjectsDir ?? join(home, ".claude", "projects");
639
+ const codexDir = options.codexSessionsDir ?? join(home, ".codex", "sessions");
640
+ const claudeCalls = [];
641
+ const codexCalls = [];
642
+ const since = options.sinceIso ? Date.parse(options.sinceIso) : undefined;
643
+ const sinceMs = typeof since === "number" && Number.isFinite(since) ? since : undefined;
644
+ const claudeDiagnostics = [];
645
+ const codexDiagnostics = [];
646
+ const sourceScans = [
647
+ {
648
+ ...emptySourceScan("claude-code"),
649
+ filesSkippedBeforeWindow: 0,
650
+ jsonlValidationCoverage: "complete"
651
+ },
652
+ {
653
+ ...emptySourceScan("codex"),
654
+ filesSkippedBeforeWindow: 0,
655
+ filesReadFinancially: 0,
656
+ bytesSkippedAsNonFinancialHistory: 0,
657
+ nonFinancialLinesPrefiltered: 0,
658
+ nonFinancialBytesPrefiltered: 0,
659
+ jsonlValidationCoverage: "complete"
660
+ }
661
+ ];
662
+ const claudeScan = sourceScans[0];
663
+ const codexScan = sourceScans[1];
664
+ const scanClaude = async () => {
665
+ for (const file of await listJsonlFiles(claudeDir, claudeScan, claudeDiagnostics)) {
666
+ if (!await shouldStreamFile(file, sinceMs, "claude-code", claudeScan, claudeDiagnostics)) {
667
+ continue;
668
+ }
669
+ const fileCalls = [];
670
+ const fileDiagnostics = [];
671
+ const seen = new Set();
672
+ let streamed;
673
+ try {
674
+ streamed = await streamJsonlRecords(file, (entry) => {
675
+ const call = parseClaudeFinancialEntry(entry, file, sinceMs, seen, (diagnostic) => fileDiagnostics.push(diagnostic));
676
+ if (call)
677
+ fileCalls.push(call);
678
+ });
679
+ }
680
+ catch (error) {
681
+ recordUnreadableFile("claude-code", claudeScan, claudeDiagnostics, error);
682
+ continue;
683
+ }
684
+ if (!streamed.hadContent)
685
+ continue;
686
+ claudeScan.filesParsed += 1;
687
+ claudeCalls.push(...fileCalls);
688
+ for (const diagnostic of fileDiagnostics) {
689
+ recordParseDiagnostic("claude-code", claudeScan, claudeDiagnostics, diagnostic);
690
+ }
691
+ if (streamed.malformedLines > 0) {
692
+ recordParseDiagnostic("claude-code", claudeScan, claudeDiagnostics, {
693
+ code: "malformed_jsonl",
694
+ count: streamed.malformedLines
695
+ });
696
+ }
697
+ }
698
+ };
699
+ const scanCodex = async () => {
700
+ for (const file of await listJsonlFiles(codexDir, codexScan, codexDiagnostics)) {
701
+ if (!basename(file).startsWith("rollout-"))
702
+ continue;
703
+ if (!await shouldStreamFile(file, sinceMs, "codex", codexScan, codexDiagnostics)) {
704
+ continue;
705
+ }
706
+ let financialFile;
707
+ try {
708
+ financialFile = await readCodexFinancialFile(file);
709
+ }
710
+ catch (error) {
711
+ recordUnreadableFile("codex", codexScan, codexDiagnostics, error);
712
+ continue;
713
+ }
714
+ if (!financialFile.hadContent)
715
+ continue;
716
+ codexScan.filesParsed += 1;
717
+ codexScan.filesReadFinancially = (codexScan.filesReadFinancially ?? 0) + 1;
718
+ codexScan.bytesSkippedAsNonFinancialHistory =
719
+ (codexScan.bytesSkippedAsNonFinancialHistory ?? 0) + financialFile.bytesSkipped;
720
+ codexScan.nonFinancialLinesPrefiltered =
721
+ (codexScan.nonFinancialLinesPrefiltered ?? 0) + financialFile.prefilteredLines;
722
+ codexScan.nonFinancialBytesPrefiltered =
723
+ (codexScan.nonFinancialBytesPrefiltered ?? 0) + financialFile.prefilteredBytes;
724
+ if (financialFile.bytesSkipped > 0 || financialFile.prefilteredLines > 0) {
725
+ codexScan.jsonlValidationCoverage = "financial_events_only";
726
+ }
727
+ if (financialFile.malformedLines > 0) {
728
+ recordParseDiagnostic("codex", codexScan, codexDiagnostics, {
729
+ code: "malformed_jsonl",
730
+ count: financialFile.malformedLines
731
+ });
732
+ }
733
+ const state = createCodexFinancialStreamState();
734
+ for (const entry of financialFile.entries) {
735
+ consumeCodexFinancialEntry(state, entry);
736
+ }
737
+ const call = finishCodexFinancialStream(state, (diagnostic) => {
738
+ recordParseDiagnostic("codex", codexScan, codexDiagnostics, diagnostic);
739
+ });
740
+ if (call)
741
+ codexCalls.push(call);
742
+ }
743
+ };
744
+ // The sources are independent and can be traversed concurrently without
745
+ // changing per-file ordering or cumulative-session deduplication.
746
+ await Promise.all([scanClaude(), scanCodex()]);
747
+ const calls = [...claudeCalls, ...codexCalls];
748
+ const normalizedCalls = dedupeCumulativeSessionCalls(calls);
749
+ const filtered = typeof sinceMs === "number"
750
+ ? normalizedCalls.filter((call) => Date.parse(call.timestamp) >= sinceMs)
751
+ : normalizedCalls;
752
+ return {
753
+ records: aggregateCalls(filtered),
754
+ calls: filtered,
755
+ filesParsed: claudeScan.filesParsed + codexScan.filesParsed,
756
+ agentsDetected: [...new Set(filtered.map((call) => call.agent))],
757
+ sourceScans,
758
+ diagnostics: [...claudeDiagnostics, ...codexDiagnostics]
759
+ };
760
+ }
761
+ async function streamJsonlRecords(file, onRecord) {
762
+ const input = createReadStream(file, { encoding: "utf8" });
763
+ const lines = createInterface({ input, crlfDelay: Infinity });
764
+ let hadContent = false;
765
+ let malformedLines = 0;
766
+ try {
767
+ for await (const line of lines) {
768
+ hadContent = true;
769
+ if (!line.trim())
770
+ continue;
771
+ let entry;
772
+ try {
773
+ entry = JSON.parse(line);
774
+ }
775
+ catch {
776
+ malformedLines += 1;
777
+ continue;
778
+ }
779
+ // Parsed event type is checked before any deeper traversal. This keeps
780
+ // qualitative payloads out of the fast path while still validating
781
+ // every JSONL line and reporting malformed coverage honestly.
782
+ if (isRecord(entry))
783
+ onRecord(entry);
784
+ }
785
+ }
786
+ finally {
787
+ lines.close();
788
+ input.destroy();
789
+ }
790
+ return { hadContent, malformedLines };
791
+ }
792
+ const FINANCIAL_REVERSE_CHUNK_BYTES = 16 * 1024 * 1024;
793
+ /**
794
+ * Codex token counters are cumulative. Reading the root metadata plus the
795
+ * newest complete financial tail is therefore equivalent to replaying copied
796
+ * prompt/tool history, while avoiding multi-gigabyte inherited histories in
797
+ * compaction and subagent rollouts.
798
+ *
799
+ * The reverse scan has proof-based stopping conditions. A normal rollout must
800
+ * expose the newest total, last-turn usage, model, and rate-limit snapshot.
801
+ * Subagent rollouts always continue to byte zero because the full parser's
802
+ * first qualifying task boundary cannot be proved from timestamps alone. If
803
+ * any normal-session proof is missing, it also continues to byte zero; no
804
+ * byte/line cap silently truncates evidence.
805
+ */
806
+ async function readCodexFinancialFile(file) {
807
+ const handle = await open(file, "r");
808
+ try {
809
+ const fileStat = await handle.stat();
810
+ if (fileStat.size === 0) {
811
+ return {
812
+ hadContent: false,
813
+ malformedLines: 0,
814
+ entries: [],
815
+ bytesSkipped: 0,
816
+ prefilteredLines: 0,
817
+ prefilteredBytes: 0
818
+ };
819
+ }
820
+ const rootEntry = await readFirstJsonlRecord(handle, fileStat.size);
821
+ const rootPayload = rootEntry?.type === "session_meta" && isRecord(rootEntry.payload)
822
+ ? rootEntry.payload
823
+ : undefined;
824
+ const isSubagent = Boolean(rootPayload) && (stringOf(rootPayload?.thread_source) === "subagent" ||
825
+ isRecord(rootPayload?.source) && "subagent" in rootPayload.source);
826
+ const entriesReverse = [];
827
+ let malformedLines = 0;
828
+ let prefilteredLines = 0;
829
+ let prefilteredBytes = 0;
830
+ let position = fileStat.size;
831
+ // Chunks for one cross-boundary line are retained newest-first. Appending
832
+ // is O(1); they are ordered only once when the line's start is found.
833
+ let suffixPartsReverse = [];
834
+ let stoppedEarly = false;
835
+ let bytesSkipped = 0;
836
+ const proof = createCodexReverseProof(isSubagent, !rootPayload);
837
+ while (position > 0 && !stoppedEarly) {
838
+ const chunkStart = Math.max(0, position - FINANCIAL_REVERSE_CHUNK_BYTES);
839
+ const length = position - chunkStart;
840
+ const chunk = Buffer.allocUnsafe(length);
841
+ const { bytesRead } = await handle.read(chunk, 0, length, chunkStart);
842
+ if (bytesRead !== length) {
843
+ throw newErrorWithCode("EIO");
844
+ }
845
+ const current = chunk.subarray(0, bytesRead);
846
+ let lineEnd = current.length;
847
+ let newline = current.lastIndexOf(0x0a, lineEnd - 1);
848
+ while (newline >= 0) {
849
+ const leadingPart = current.subarray(newline + 1, lineEnd);
850
+ const parts = suffixPartsReverse.length > 0
851
+ ? [leadingPart, ...suffixPartsReverse.slice().reverse()]
852
+ : [leadingPart];
853
+ const observed = parseCodexFinancialLine(parts);
854
+ if (observed.malformed)
855
+ malformedLines += 1;
856
+ if (observed.prefilteredBytes > 0) {
857
+ prefilteredLines += 1;
858
+ prefilteredBytes += observed.prefilteredBytes;
859
+ }
860
+ if (observed.entry) {
861
+ entriesReverse.push(observed.entry);
862
+ if (observeCodexReverseProof(proof, observed.entry)) {
863
+ stoppedEarly = true;
864
+ bytesSkipped = chunkStart + newline;
865
+ break;
866
+ }
867
+ }
868
+ suffixPartsReverse = [];
869
+ lineEnd = newline;
870
+ newline = current.lastIndexOf(0x0a, lineEnd - 1);
871
+ }
872
+ if (!stoppedEarly) {
873
+ const prefix = current.subarray(0, lineEnd);
874
+ if (prefix.length > 0)
875
+ suffixPartsReverse.push(prefix);
876
+ position = chunkStart;
877
+ }
878
+ }
879
+ if (!stoppedEarly && suffixPartsReverse.length > 0) {
880
+ const observed = parseCodexFinancialLine(suffixPartsReverse.slice().reverse());
881
+ if (observed.malformed)
882
+ malformedLines += 1;
883
+ if (observed.prefilteredBytes > 0) {
884
+ prefilteredLines += 1;
885
+ prefilteredBytes += observed.prefilteredBytes;
886
+ }
887
+ if (observed.entry)
888
+ entriesReverse.push(observed.entry);
889
+ }
890
+ const entries = entriesReverse.reverse();
891
+ // A proof-complete tail does not contain byte-zero root metadata. Inject
892
+ // the separately parsed first record so session identity/start/cwd remain
893
+ // identical to the full parser. A full scan already contains that record.
894
+ if (stoppedEarly && rootEntry)
895
+ entries.unshift(rootEntry);
896
+ return {
897
+ hadContent: true,
898
+ malformedLines,
899
+ entries,
900
+ bytesSkipped: Math.max(0, bytesSkipped),
901
+ prefilteredLines,
902
+ prefilteredBytes
903
+ };
904
+ }
905
+ finally {
906
+ await handle.close();
907
+ }
908
+ }
909
+ async function readFirstJsonlRecord(handle, fileSize) {
910
+ const chunks = [];
911
+ let position = 0;
912
+ while (position < fileSize) {
913
+ const length = Math.min(FINANCIAL_REVERSE_CHUNK_BYTES, fileSize - position);
914
+ const chunk = Buffer.allocUnsafe(length);
915
+ const { bytesRead } = await handle.read(chunk, 0, length, position);
916
+ if (bytesRead <= 0)
917
+ break;
918
+ const value = chunk.subarray(0, bytesRead);
919
+ const newline = value.indexOf(0x0a);
920
+ if (newline >= 0) {
921
+ chunks.push(value.subarray(0, newline));
922
+ break;
923
+ }
924
+ chunks.push(value);
925
+ position += bytesRead;
926
+ }
927
+ if (chunks.length === 0)
928
+ return undefined;
929
+ try {
930
+ const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
931
+ return isRecord(parsed) ? parsed : undefined;
932
+ }
933
+ catch {
934
+ return undefined;
935
+ }
936
+ }
937
+ const CODEX_ENVELOPE_PREFIX_BYTES = 16 * 1024;
938
+ function parseCodexFinancialLine(parts) {
939
+ const byteLength = parts.reduce((total, part) => total + part.length, 0);
940
+ if (byteLength === 0)
941
+ return { malformed: false, prefilteredBytes: 0 };
942
+ const prefix = bufferPartsPrefix(parts, CODEX_ENVELOPE_PREFIX_BYTES).toString("utf8");
943
+ const classification = classifyCodexFinancialEnvelope(prefix);
944
+ if (classification === "nonfinancial") {
945
+ return { malformed: false, prefilteredBytes: byteLength };
946
+ }
947
+ const text = Buffer.concat(parts, byteLength).toString("utf8").trim();
948
+ if (!text)
949
+ return { malformed: false, prefilteredBytes: 0 };
950
+ let parsed;
951
+ try {
952
+ parsed = JSON.parse(text);
953
+ }
954
+ catch {
955
+ return { malformed: true, prefilteredBytes: 0 };
956
+ }
957
+ if (!isRecord(parsed))
958
+ return { malformed: false, prefilteredBytes: 0 };
959
+ const payload = isRecord(parsed.payload) ? parsed.payload : undefined;
960
+ const financial = parsed.type === "session_meta" ||
961
+ parsed.type === "turn_context" ||
962
+ parsed.type === "event_msg" && (payload?.type === "token_count" || payload?.type === "task_started");
963
+ return financial
964
+ ? { entry: parsed, malformed: false, prefilteredBytes: 0 }
965
+ : { malformed: false, prefilteredBytes: byteLength };
966
+ }
967
+ function bufferPartsPrefix(parts, limit) {
968
+ const selected = [];
969
+ let remaining = limit;
970
+ for (const part of parts) {
971
+ if (remaining <= 0)
972
+ break;
973
+ const value = part.subarray(0, Math.min(part.length, remaining));
974
+ if (value.length > 0)
975
+ selected.push(value);
976
+ remaining -= value.length;
977
+ }
978
+ return selected.length === 1
979
+ ? selected[0]
980
+ : Buffer.concat(selected, limit - remaining);
981
+ }
982
+ function classifyCodexFinancialEnvelope(prefix) {
983
+ // JSON property order is not semantic. Scan string/object depth so a nested
984
+ // payload type can never be mistaken for the top-level event type. If the
985
+ // bounded prefix does not prove the envelope, return unknown and let the
986
+ // complete line go through JSON.parse rather than silently prefiltering it.
987
+ const { topLevelType, payloadType } = scanCodexEnvelopeTypes(prefix);
988
+ if (!topLevelType)
989
+ return "unknown";
990
+ if (topLevelType === "session_meta" || topLevelType === "turn_context") {
991
+ return "financial";
992
+ }
993
+ if (topLevelType !== "event_msg")
994
+ return "nonfinancial";
995
+ if (!payloadType)
996
+ return "unknown";
997
+ return payloadType === "token_count" || payloadType === "task_started"
998
+ ? "financial"
999
+ : "nonfinancial";
1000
+ }
1001
+ function scanCodexEnvelopeTypes(prefix) {
1002
+ let objectDepth = 0;
1003
+ let payloadDepth;
1004
+ let topLevelType;
1005
+ let payloadType;
1006
+ for (let index = 0; index < prefix.length; index += 1) {
1007
+ const character = prefix[index];
1008
+ if (character === "{") {
1009
+ objectDepth += 1;
1010
+ continue;
1011
+ }
1012
+ if (character === "}") {
1013
+ if (payloadDepth === objectDepth)
1014
+ payloadDepth = undefined;
1015
+ objectDepth = Math.max(0, objectDepth - 1);
1016
+ continue;
1017
+ }
1018
+ if (character !== '"')
1019
+ continue;
1020
+ const token = readJsonStringToken(prefix, index);
1021
+ if (!token)
1022
+ break;
1023
+ index = token.end;
1024
+ let cursor = skipJsonWhitespace(prefix, token.end + 1);
1025
+ if (prefix[cursor] !== ":")
1026
+ continue;
1027
+ cursor = skipJsonWhitespace(prefix, cursor + 1);
1028
+ if (token.value === "payload" && objectDepth === 1 && prefix[cursor] === "{") {
1029
+ payloadDepth = objectDepth + 1;
1030
+ continue;
1031
+ }
1032
+ if (token.value !== "type" || prefix[cursor] !== '"')
1033
+ continue;
1034
+ const value = readJsonStringToken(prefix, cursor);
1035
+ if (!value)
1036
+ break;
1037
+ if (objectDepth === 1)
1038
+ topLevelType = value.value;
1039
+ if (payloadDepth === objectDepth)
1040
+ payloadType = value.value;
1041
+ index = value.end;
1042
+ }
1043
+ return { topLevelType, payloadType };
1044
+ }
1045
+ function readJsonStringToken(input, start) {
1046
+ if (input[start] !== '"')
1047
+ return undefined;
1048
+ let escaped = false;
1049
+ for (let index = start + 1; index < input.length; index += 1) {
1050
+ const character = input[index];
1051
+ if (escaped) {
1052
+ escaped = false;
1053
+ continue;
1054
+ }
1055
+ if (character === "\\") {
1056
+ escaped = true;
1057
+ continue;
1058
+ }
1059
+ if (character !== '"')
1060
+ continue;
1061
+ try {
1062
+ const value = JSON.parse(input.slice(start, index + 1));
1063
+ return typeof value === "string" ? { value, end: index } : undefined;
1064
+ }
1065
+ catch {
1066
+ return undefined;
1067
+ }
1068
+ }
1069
+ return undefined;
1070
+ }
1071
+ function skipJsonWhitespace(input, start) {
1072
+ let index = start;
1073
+ while (index < input.length && /\s/.test(input[index]))
1074
+ index += 1;
1075
+ return index;
1076
+ }
1077
+ function createCodexReverseProof(isSubagent, forceFullScan) {
1078
+ return {
1079
+ isSubagent,
1080
+ forceFullScan,
1081
+ totalSeen: false,
1082
+ turnSeen: false,
1083
+ rateLimitsSeen: false,
1084
+ modelSeen: false
1085
+ };
1086
+ }
1087
+ function observeCodexReverseProof(proof, entry) {
1088
+ if (proof.forceFullScan || proof.isSubagent)
1089
+ return false;
1090
+ const payload = isRecord(entry.payload) ? entry.payload : undefined;
1091
+ const info = payload?.type === "token_count" && isRecord(payload.info)
1092
+ ? payload.info
1093
+ : undefined;
1094
+ const hasTotal = Boolean(info && isRecord(info.total_token_usage));
1095
+ if (hasTotal)
1096
+ proof.totalSeen = true;
1097
+ if (info && isRecord(info.last_token_usage))
1098
+ proof.turnSeen = true;
1099
+ if (parseCodexRateLimits(payload?.rate_limits, stringOf(entry.timestamp))) {
1100
+ proof.rateLimitsSeen = true;
1101
+ }
1102
+ if (entry.type === "turn_context" && stringOf(payload?.model)) {
1103
+ proof.modelSeen = true;
1104
+ }
1105
+ return !proof.isSubagent &&
1106
+ proof.totalSeen &&
1107
+ proof.turnSeen &&
1108
+ proof.rateLimitsSeen &&
1109
+ proof.modelSeen;
1110
+ }
1111
+ async function shouldStreamFile(file, sinceMs, agent, scan, diagnostics) {
1112
+ let fileStat;
1113
+ try {
1114
+ fileStat = await lstat(file);
1115
+ }
1116
+ catch (error) {
1117
+ recordUnreadableFile(agent, scan, diagnostics, error);
1118
+ return false;
1119
+ }
1120
+ // Refuse a path swapped to a symlink/non-file after directory discovery.
1121
+ if (!fileStat.isFile()) {
1122
+ recordUnreadableFile(agent, scan, diagnostics, newErrorWithCode("EINVAL"));
1123
+ return false;
1124
+ }
1125
+ if (typeof sinceMs !== "number")
1126
+ return true;
1127
+ // ctime catches a file whose mtime was restored after recent replacement;
1128
+ // birthtime catches a newly copied file with a preserved old mtime. Skip
1129
+ // only when all available metadata proves the file predates the window.
1130
+ const newestFileEvidence = Math.max(fileStat.mtimeMs, fileStat.ctimeMs, fileStat.birthtimeMs);
1131
+ if (!Number.isFinite(newestFileEvidence) || newestFileEvidence >= sinceMs) {
1132
+ return true;
1133
+ }
1134
+ scan.filesSkippedBeforeWindow = (scan.filesSkippedBeforeWindow ?? 0) + 1;
1135
+ return false;
1136
+ }
1137
+ function newErrorWithCode(code) {
1138
+ const error = new Error(code);
1139
+ error.code = code;
1140
+ return error;
1141
+ }
1142
+ function parseClaudeFinancialEntry(entry, filePath, sinceMs, seen, onDiagnostic) {
1143
+ if (entry.type !== "assistant")
1144
+ return undefined;
1145
+ const message = isRecord(entry.message) ? entry.message : undefined;
1146
+ const usage = message && isRecord(message.usage) ? message.usage : undefined;
1147
+ if (!message || !usage || stringOf(message.model) === "<synthetic>")
1148
+ return undefined;
1149
+ const dedupeKey = `${stringOf(message.id) ?? ""}:${stringOf(entry.requestId) ?? ""}`;
1150
+ if (dedupeKey !== ":" && seen.has(dedupeKey))
1151
+ return undefined;
1152
+ seen.add(dedupeKey);
1153
+ const timestamp = toIso(stringOf(entry.timestamp)) ?? new Date(0).toISOString();
1154
+ if (typeof sinceMs === "number" && Date.parse(timestamp) < sinceMs)
1155
+ return undefined;
1156
+ const parsedUsage = parseClaudeFinancialUsage(usage, onDiagnostic);
1157
+ const workingDirectory = absoluteWorkingDirectory(stringOf(entry.cwd));
1158
+ return {
1159
+ agent: "claude-code",
1160
+ model: stringOf(message.model) ?? "claude-code",
1161
+ timestamp,
1162
+ project: projectFromCwd(workingDirectory) ?? projectFromTranscriptPath(filePath),
1163
+ workingDirectory,
1164
+ sessionId: stringOf(entry.sessionId),
1165
+ ...(parsedUsage.latestTurnUsage
1166
+ ? { latestTurnUsage: parsedUsage.latestTurnUsage }
1167
+ : {}),
1168
+ usageScope: "turn",
1169
+ ...(parsedUsage.usageSupport ? { usageSupport: parsedUsage.usageSupport } : {}),
1170
+ ...(parsedUsage.reportedTotalTokens !== undefined
1171
+ ? { reportedTotalTokens: parsedUsage.reportedTotalTokens }
1172
+ : {}),
1173
+ usage: parsedUsage.usage
1174
+ };
1175
+ }
1176
+ function createCodexFinancialStreamState() {
1177
+ return {
1178
+ rootSessionMetaSeen: false,
1179
+ rootTaskStarted: false,
1180
+ isSubagent: false
1181
+ };
1182
+ }
1183
+ function consumeCodexFinancialEntry(state, entry) {
1184
+ const payload = isRecord(entry.payload) ? entry.payload : undefined;
1185
+ if (entry.type === "session_meta" && payload && !state.rootSessionMetaSeen) {
1186
+ state.rootSessionMetaSeen = true;
1187
+ state.sessionId = stringOf(payload.id);
1188
+ state.rootCwd = stringOf(payload.cwd);
1189
+ state.startedAt = toIso(stringOf(payload.timestamp) ?? stringOf(entry.timestamp));
1190
+ state.rootStartedAtMs = timestampMilliseconds(payload.timestamp ?? entry.timestamp);
1191
+ state.isSubagent = stringOf(payload.thread_source) === "subagent" ||
1192
+ isRecord(payload.source) && "subagent" in payload.source;
1193
+ }
1194
+ if (entry.type === "turn_context" && payload) {
1195
+ state.model = stringOf(payload.model) ?? state.model;
1196
+ state.rootCwd ??= stringOf(payload.cwd);
1197
+ }
1198
+ if (state.isSubagent &&
1199
+ !state.rootTaskStarted &&
1200
+ payload?.type === "task_started" &&
1201
+ isRootSpecificTaskStart(payload.started_at, state.rootStartedAtMs)) {
1202
+ state.inheritedUsageBaseline = state.lastTotal;
1203
+ state.lastTotal = undefined;
1204
+ state.rootTaskStarted = true;
1205
+ state.model = undefined;
1206
+ state.lastTurn = undefined;
1207
+ state.lastRateLimits = undefined;
1208
+ state.lastActivityAt = toIso(stringOf(entry.timestamp)) ?? state.startedAt;
1209
+ }
1210
+ if (entry.type !== "event_msg" || payload?.type !== "token_count")
1211
+ return;
1212
+ const eventTimestamp = toIso(stringOf(entry.timestamp)) ??
1213
+ state.lastActivityAt ??
1214
+ state.startedAt;
1215
+ const info = isRecord(payload.info) ? payload.info : undefined;
1216
+ const total = info && isRecord(info.total_token_usage)
1217
+ ? info.total_token_usage
1218
+ : undefined;
1219
+ const turn = info && isRecord(info.last_token_usage)
1220
+ ? info.last_token_usage
1221
+ : undefined;
1222
+ if (total) {
1223
+ state.lastTotal = total;
1224
+ state.lastActivityAt = eventTimestamp;
1225
+ }
1226
+ if (turn) {
1227
+ state.lastTurn = turn;
1228
+ state.lastActivityAt = eventTimestamp;
1229
+ }
1230
+ const rateLimits = parseCodexRateLimits(payload.rate_limits, eventTimestamp);
1231
+ if (rateLimits)
1232
+ state.lastRateLimits = rateLimits;
1233
+ }
1234
+ function finishCodexFinancialStream(state, onDiagnostic) {
1235
+ if (!state.lastTotal || state.isSubagent && !state.rootTaskStarted)
1236
+ return undefined;
1237
+ const parsedUsage = parseCodexCumulativeUsage(state.lastTotal, state.inheritedUsageBaseline);
1238
+ const parsedTurn = state.lastTurn
1239
+ ? parseCodexTurnUsage(state.lastTurn)
1240
+ : { supported: true };
1241
+ const usageSupport = parsedUsage.supported && parsedTurn.supported
1242
+ ? "complete"
1243
+ : "unsupported_token_shape";
1244
+ if (usageSupport === "unsupported_token_shape") {
1245
+ onDiagnostic?.({ code: "unsupported_token_shape", count: 1 });
1246
+ }
1247
+ const workingDirectory = absoluteWorkingDirectory(state.rootCwd);
1248
+ return {
1249
+ agent: "codex",
1250
+ model: state.model ?? "codex",
1251
+ timestamp: state.lastActivityAt ?? state.startedAt ?? new Date(0).toISOString(),
1252
+ startedAt: state.startedAt,
1253
+ project: projectFromCwd(workingDirectory),
1254
+ workingDirectory,
1255
+ sessionId: state.sessionId,
1256
+ rateLimits: state.lastRateLimits,
1257
+ ...(usageSupport === "complete" && parsedTurn.usage
1258
+ ? { latestTurnUsage: parsedTurn.usage }
1259
+ : {}),
1260
+ usageScope: "session_cumulative",
1261
+ usageSupport,
1262
+ ...(parsedUsage.reportedTotalTokens !== undefined
1263
+ ? { reportedTotalTokens: parsedUsage.reportedTotalTokens }
1264
+ : {}),
1265
+ usage: parsedUsage.usage
1266
+ };
1267
+ }
510
1268
  /** Aggregate per-call usage into one UsageRecord per day+agent+model+project. */
511
1269
  export function aggregateCalls(calls) {
512
1270
  const groups = new Map();
@@ -969,19 +1727,6 @@ function topicTokens(value) {
969
1727
  .replace(/\b(?:attached|attachment|clipboard|image|images|photo|picture|screenshot|screenshots)\b/gi, " ");
970
1728
  return promptTokens(sanitizeLocalActivityText(withoutAbsolutePaths));
971
1729
  }
972
- function codexTurnUsage(value) {
973
- const rawInput = numberOf(value.input_tokens) ?? 0;
974
- const cached = Math.min(rawInput, numberOf(value.cached_input_tokens) ?? 0);
975
- const output = numberOf(value.output_tokens) ?? 0;
976
- return {
977
- inputTokens: Math.max(0, rawInput - cached),
978
- outputTokens: output,
979
- cacheReadTokens: cached,
980
- contextTokens: rawInput,
981
- totalTokens: numberOf(value.total_tokens) ?? rawInput + output,
982
- source: "transcript_last_token_usage"
983
- };
984
- }
985
1730
  function toTurnUsage(usage, source) {
986
1731
  const contextTokens = usage.inputTokens +
987
1732
  (usage.cacheReadTokens ?? 0) +
@@ -1096,6 +1841,10 @@ function sum(calls, pick) {
1096
1841
  function numberOf(value) {
1097
1842
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
1098
1843
  }
1844
+ function tokenComponentOf(value) {
1845
+ const parsed = numberOf(value);
1846
+ return parsed !== undefined && parsed >= 0 ? parsed : undefined;
1847
+ }
1099
1848
  function stringOf(value) {
1100
1849
  return typeof value === "string" && value.length > 0 ? value : undefined;
1101
1850
  }