@agent-finops/core 0.6.0 → 0.6.1
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.
- package/dist/activitySnapshot.d.ts +676 -0
- package/dist/activitySnapshot.js +1220 -0
- package/dist/activitySnapshotCache.d.ts +54 -0
- package/dist/activitySnapshotCache.js +489 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/localAgentLogs.d.ts +30 -0
- package/dist/localAgentLogs.js +805 -56
- package/dist/providerConnectors.d.ts +10 -0
- package/dist/providerConnectors.js +43 -6
- package/package.json +1 -1
package/dist/localAgentLogs.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
-
|
|
264
|
+
...(parsedUsage.latestTurnUsage
|
|
265
|
+
? { latestTurnUsage: parsedUsage.latestTurnUsage }
|
|
266
|
+
: {}),
|
|
139
267
|
usageScope: "turn",
|
|
140
|
-
|
|
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
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const
|
|
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
|
-
|
|
477
|
+
...(usageSupport === "complete" && parsedTurn.usage
|
|
478
|
+
? { latestTurnUsage: parsedTurn.usage }
|
|
479
|
+
: {}),
|
|
361
480
|
usageScope: "session_cumulative",
|
|
362
481
|
usageSupport,
|
|
363
|
-
...(reportedTotalTokens !== undefined
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
}
|