@agentguard-run/burn 0.2.3 → 0.2.6

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +30 -2
  2. package/README.md +105 -20
  3. package/dist/src/adapters/codex.js +11 -1
  4. package/dist/src/adapters/cursor.js +2 -2
  5. package/dist/src/calibrate.js +2 -3
  6. package/dist/src/cli.js +53 -15
  7. package/dist/src/conformance.d.ts +5 -2
  8. package/dist/src/conformance.js +30 -17
  9. package/dist/src/defaults.d.ts +7 -4
  10. package/dist/src/defaults.js +9 -6
  11. package/dist/src/detectors/evaluate.d.ts +4 -5
  12. package/dist/src/detectors/evaluate.js +13 -11
  13. package/dist/src/eligibility.d.ts +17 -0
  14. package/dist/src/eligibility.js +29 -0
  15. package/dist/src/gateway.d.ts +2 -0
  16. package/dist/src/gateway.js +4 -8
  17. package/dist/src/history/claude-transcript.d.ts +20 -2
  18. package/dist/src/history/claude-transcript.js +56 -15
  19. package/dist/src/hook/pre-tool-use.d.ts +13 -9
  20. package/dist/src/hook/pre-tool-use.js +63 -31
  21. package/dist/src/insights/attribution.d.ts +4 -0
  22. package/dist/src/insights/attribution.js +151 -0
  23. package/dist/src/insights/blocks.d.ts +61 -0
  24. package/dist/src/insights/blocks.js +243 -0
  25. package/dist/src/insights/live.d.ts +53 -0
  26. package/dist/src/insights/live.js +211 -0
  27. package/dist/src/insights/pace.d.ts +34 -0
  28. package/dist/src/insights/pace.js +54 -0
  29. package/dist/src/insights/pricing.d.ts +48 -0
  30. package/dist/src/insights/pricing.js +139 -0
  31. package/dist/src/insights/render.d.ts +8 -0
  32. package/dist/src/insights/render.js +126 -0
  33. package/dist/src/insights/sessions.d.ts +12 -0
  34. package/dist/src/insights/sessions.js +51 -0
  35. package/dist/src/insights/transcript.d.ts +14 -0
  36. package/dist/src/insights/transcript.js +505 -0
  37. package/dist/src/insights/types.d.ts +164 -0
  38. package/dist/src/insights/types.js +4 -0
  39. package/dist/src/install.js +14 -5
  40. package/dist/src/policy.d.ts +4 -0
  41. package/dist/src/policy.js +57 -0
  42. package/dist/src/replay/render.js +4 -2
  43. package/dist/src/replay/simulate.d.ts +5 -0
  44. package/dist/src/replay/simulate.js +23 -8
  45. package/dist/src/state/reservations.d.ts +7 -5
  46. package/dist/src/state/reservations.js +60 -45
  47. package/dist/src/state/spawn-window.d.ts +10 -0
  48. package/dist/src/state/spawn-window.js +25 -0
  49. package/dist/src/types.d.ts +8 -1
  50. package/docs/USAGE_AND_PRICING.md +132 -0
  51. package/fixtures/usage-dedup-session/subagents/agent-synthetic-first.jsonl +5 -0
  52. package/fixtures/usage-dedup-session/subagents/agent-synthetic-second.jsonl +4 -0
  53. package/fixtures/usage-dedup-session.jsonl +4 -0
  54. package/package.json +4 -3
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.discoverInsightTranscripts = discoverInsightTranscripts;
4
+ exports.selectInsightTranscript = selectInsightTranscript;
5
+ const node_fs_1 = require("node:fs");
6
+ const node_path_1 = require("node:path");
7
+ const node_os_1 = require("node:os");
8
+ /** Local locators only. Never follows directory symlinks or reads message content. */
9
+ function discoverInsightTranscripts(roots = {
10
+ claude: (0, node_path_1.join)((0, node_os_1.homedir)(), '.claude', 'projects'),
11
+ codex: (0, node_path_1.join)((0, node_os_1.homedir)(), '.codex', 'sessions'),
12
+ }) {
13
+ const found = [];
14
+ function visit(directory, host, depth) {
15
+ if (depth > 8)
16
+ return;
17
+ let entries;
18
+ try {
19
+ entries = (0, node_fs_1.readdirSync)(directory, { withFileTypes: true });
20
+ }
21
+ catch {
22
+ return;
23
+ }
24
+ for (const entry of entries) {
25
+ const path = (0, node_path_1.join)(directory, entry.name);
26
+ if (entry.isDirectory())
27
+ visit(path, host, depth + 1);
28
+ else if (entry.isFile() && entry.name.endsWith('.jsonl')) {
29
+ try {
30
+ found.push({ path, host, sessionId: (0, node_path_1.basename)(path, '.jsonl'), modifiedAt: (0, node_fs_1.statSync)(path).mtimeMs });
31
+ }
32
+ catch { /* concurrently removed */ }
33
+ }
34
+ }
35
+ }
36
+ visit(roots.claude, 'claude', 0);
37
+ visit(roots.codex, 'codex', 0);
38
+ return found.sort((a, b) => b.modifiedAt - a.modifiedAt || a.path.localeCompare(b.path));
39
+ }
40
+ function selectInsightTranscript(session, locations) {
41
+ const requested = session || process.env.AGENTGUARD_SESSION_ID || process.env.CLAUDE_SESSION_ID || process.env.CODEX_THREAD_ID;
42
+ if (requested && (0, node_fs_1.existsSync)(requested) && (0, node_fs_1.statSync)(requested).isFile()) {
43
+ const path = (0, node_path_1.resolve)(requested);
44
+ return { path, host: /(?:rollout-|[\\/]\.codex[\\/])/.test(path) ? 'codex' : 'claude', sessionId: (0, node_path_1.basename)(path, '.jsonl'), modifiedAt: (0, node_fs_1.statSync)(path).mtimeMs };
45
+ }
46
+ const candidates = locations ?? discoverInsightTranscripts();
47
+ const selected = requested ? candidates.find(item => item.sessionId === requested || item.sessionId.endsWith(`-${requested}`)) : candidates[0];
48
+ if (!selected)
49
+ throw new Error(requested ? 'Session transcript not found. Pass its local JSONL path.' : 'No local Claude Code or Codex transcript found.');
50
+ return selected;
51
+ }
@@ -0,0 +1,14 @@
1
+ import type { InsightHost, InsightParseOptions, InsightTranscript, InsightTurn } from './types';
2
+ /** Provider response identity is shared by reporting and enforcement readers. */
3
+ export declare const providerResponseIdentity: (host: InsightHost, id: unknown) => string;
4
+ /**
5
+ * Parse complete JSONL records. Pass returned state when parsing appended lines.
6
+ * `updatedTurns` replaces rows with the same id; it must never be blindly added
7
+ * to previous totals because an assistant message can gain output usage later.
8
+ */
9
+ export declare function parseInsightTranscript(text: string, options?: InsightParseOptions): InsightTranscript;
10
+ export declare function readInsightTranscript(filename: string, options?: InsightParseOptions): InsightTranscript;
11
+ /** Read one Claude session plus its explicitly stored child transcripts. */
12
+ export declare function readInsightSession(filename: string, options?: InsightParseOptions): InsightTranscript;
13
+ /** Forked transcript copies can repeat provider response ids across files. */
14
+ export declare function deduplicateTurns(turns: InsightTurn[]): InsightTurn[];
@@ -0,0 +1,505 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.providerResponseIdentity = void 0;
4
+ exports.parseInsightTranscript = parseInsightTranscript;
5
+ exports.readInsightTranscript = readInsightTranscript;
6
+ exports.readInsightSession = readInsightSession;
7
+ exports.deduplicateTurns = deduplicateTurns;
8
+ const node_crypto_1 = require("node:crypto");
9
+ const node_fs_1 = require("node:fs");
10
+ const node_path_1 = require("node:path");
11
+ const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {};
12
+ const count = (value) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
13
+ const hasCount = (value) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
14
+ const digest = (value) => (0, node_crypto_1.createHash)('sha256').update(JSON.stringify(value)).digest('hex');
15
+ /** Provider response identity is shared by reporting and enforcement readers. */
16
+ const providerResponseIdentity = (host, id) => digest(['provider-response', host, id]);
17
+ exports.providerResponseIdentity = providerResponseIdentity;
18
+ const identifier = (value) => typeof value === 'string' && /^[A-Za-z0-9_.:/-]{1,128}$/.test(value) ? value : undefined;
19
+ const modelName = identifier;
20
+ function millis(value) {
21
+ if (typeof value === 'string') {
22
+ const parsed = Date.parse(value);
23
+ return Number.isFinite(parsed) ? parsed : undefined;
24
+ }
25
+ if (typeof value === 'number' && Number.isFinite(value) && value > 0)
26
+ return value < 100_000_000_000 ? value * 1000 : value;
27
+ return undefined;
28
+ }
29
+ const DETAIL_BUCKETS = {
30
+ instruction_tokens: 'instruction_stack', system_tokens: 'instruction_stack',
31
+ history_tokens: 'history_resent', history_resent_tokens: 'history_resent',
32
+ reread_file_tokens: 'reread_files', repeated_file_tokens: 'reread_files',
33
+ subagent_tokens: 'subagent_fanout', tool_output_tokens: 'tool_output',
34
+ };
35
+ /** Optional numeric host instrumentation is evidence; content length is not. */
36
+ function explicitAttribution(usage) {
37
+ const result = [];
38
+ const details = [
39
+ [usage.input_tokens_details ?? usage.prompt_tokens_details, 'input'],
40
+ [usage.cache_creation_input_tokens_details, 'cacheCreation'],
41
+ [usage.cache_read_input_tokens_details, 'cacheRead'],
42
+ ];
43
+ for (const [value, category] of details) {
44
+ for (const [key, tokens] of Object.entries(object(value))) {
45
+ const bucket = DETAIL_BUCKETS[key];
46
+ if (bucket && hasCount(tokens) && count(tokens) > 0)
47
+ result.push({ bucket, category, tokens: count(tokens) });
48
+ }
49
+ }
50
+ return result;
51
+ }
52
+ function emptyState(options) {
53
+ return { host: options.host ?? 'unknown', sessionId: identifier(options.sessionId) ?? 'unknown', compactionPending: false,
54
+ spawnedSession: Boolean(options.spawnedSession), seen: {}, recordsSeen: 0, codexEpoch: 0, intervalStateVersion: 1 };
55
+ }
56
+ function detectHost(raw, usage, fallback) {
57
+ if (raw.type === 'session_meta' || raw.type === 'turn_context' || raw.type === 'event_msg' || raw.type === 'response_item' || raw.type === 'compacted')
58
+ return 'codex';
59
+ if ('cache_creation_input_tokens' in usage || 'cache_read_input_tokens' in usage || raw.type === 'assistant' || raw.type === 'user' || raw.type === 'system')
60
+ return 'claude';
61
+ return fallback;
62
+ }
63
+ function usageOf(raw) {
64
+ const payload = object(raw.payload);
65
+ if (raw.type === 'event_msg' && payload.type === 'token_count') {
66
+ const info = object(payload.info);
67
+ return { usage: object(info.last_token_usage), total: Object.keys(object(info.total_token_usage)).length ? object(info.total_token_usage) : undefined, codex: true };
68
+ }
69
+ const usage = raw.usage ?? object(raw.message).usage ?? object(raw.response).usage ?? raw.token_usage;
70
+ return { usage: object(usage), codex: false };
71
+ }
72
+ function totalDigest(total) {
73
+ return digest([count(total.input_tokens), count(total.cached_input_tokens), count(total.cache_write_input_tokens), count(total.output_tokens), count(total.total_tokens)]);
74
+ }
75
+ function observeTools(raw, state) {
76
+ const message = object(raw.message), payload = object(raw.payload);
77
+ const blocks = Array.isArray(message.content) ? [...message.content] : [];
78
+ if (raw.type === 'response_item' && ['function_call', 'function_call_output', 'custom_tool_call', 'custom_tool_call_output'].includes(String(payload.type)))
79
+ blocks.push(payload);
80
+ state.observedCalls ??= {};
81
+ for (let index = 0; index < blocks.length; index++) {
82
+ const block = object(blocks[index]);
83
+ const toolId = block.id ?? block.call_id ?? block.tool_use_id ?? [message.id ?? raw.uuid ?? state.recordsSeen, index];
84
+ const isResult = block.type === 'tool_result' || block.type === 'function_call_output' || block.type === 'custom_tool_call_output';
85
+ if (isResult) {
86
+ state.observedCalls[digest(['result', toolId])] = { kind: 'tool_result' };
87
+ continue;
88
+ }
89
+ if (block.type !== 'tool_use' && block.type !== 'function_call' && block.type !== 'custom_tool_call')
90
+ continue;
91
+ const name = typeof block.name === 'string' ? block.name : '';
92
+ const isRead = /^(?:(?:functions\.)?Read|(?:functions\.)?read_file)$/.test(name);
93
+ const isSpawn = /^(?:(?:functions\.)?(?:Agent|Task|spawn_agent))$/.test(name);
94
+ if (!isRead && !isSpawn)
95
+ continue;
96
+ const id = digest(['tool', toolId]);
97
+ if (isSpawn) {
98
+ state.observedCalls[id] = { kind: 'spawn' };
99
+ continue;
100
+ }
101
+ let input = object(block.input ?? block.arguments);
102
+ if (typeof block.arguments === 'string') {
103
+ try {
104
+ input = object(JSON.parse(block.arguments));
105
+ }
106
+ catch {
107
+ continue;
108
+ }
109
+ }
110
+ const filename = input.file_path ?? input.path;
111
+ if (typeof filename === 'string' && filename.length) {
112
+ const fileKey = digest(filename);
113
+ state.observedCalls[id] = { kind: 'read', fileKey };
114
+ state.readPathsSeen ??= {};
115
+ state.readToolCalls ??= {};
116
+ if (!state.readToolCalls[id]) {
117
+ state.readToolCalls[id] = { fileKey, repeated: Boolean(state.readPathsSeen[fileKey]) };
118
+ state.readPathsSeen[fileKey] = true;
119
+ }
120
+ }
121
+ }
122
+ }
123
+ function emptyInterval() {
124
+ return { toolOutputBytes: 0, reReadBytes: 0, conversationBytes: 0, toolResults: 0, userMessages: 0, shared: false };
125
+ }
126
+ /** UTF-8 payload bytes are weights only. No content survives this call. */
127
+ function contentBytes(value) {
128
+ if (typeof value === 'string')
129
+ return Buffer.byteLength(value, 'utf8');
130
+ if (Array.isArray(value))
131
+ return value.reduce((sum, item) => sum + contentBytes(item), 0);
132
+ if (value === undefined || value === null)
133
+ return 0;
134
+ const record = object(value);
135
+ if (typeof record.text === 'string')
136
+ return Buffer.byteLength(record.text, 'utf8');
137
+ return Buffer.byteLength(JSON.stringify(value), 'utf8');
138
+ }
139
+ function observeInputEvents(raw, state) {
140
+ state.pendingInterval ??= emptyInterval();
141
+ state.inputEventsSeen ??= {};
142
+ const interval = state.pendingInterval;
143
+ const message = object(raw.message), payload = object(raw.payload);
144
+ const seen = (key) => {
145
+ const hash = digest(key);
146
+ if (state.inputEventsSeen[hash])
147
+ return true;
148
+ state.inputEventsSeen[hash] = true;
149
+ return false;
150
+ };
151
+ const toolResult = (block, fallback) => {
152
+ const toolId = block.tool_use_id ?? block.call_id ?? block.id;
153
+ if (seen(['tool-result', toolId ?? fallback]))
154
+ return;
155
+ const bytes = contentBytes(block.content ?? block.output);
156
+ const repeated = toolId !== undefined && state.readToolCalls?.[digest(['tool', toolId])]?.repeated;
157
+ if (repeated) {
158
+ interval.reReadBytes += bytes;
159
+ interval.reReadResults = (interval.reReadResults ?? 0) + 1;
160
+ }
161
+ else
162
+ interval.toolOutputBytes += bytes;
163
+ interval.toolResults++;
164
+ };
165
+ const userMessage = (content, key, source) => {
166
+ if (content === undefined || content === null)
167
+ return;
168
+ if (seen(['user-message', key]))
169
+ return;
170
+ const bytes = contentBytes(content);
171
+ // Codex records the same submitted message as both a response item and a
172
+ // user_message event. Equal text in separate same-source messages is real.
173
+ if (source) {
174
+ const fingerprint = digest(content);
175
+ if (state.conversationEcho?.digest === fingerprint && state.conversationEcho.source !== source) {
176
+ state.conversationEcho = undefined;
177
+ return;
178
+ }
179
+ state.conversationEcho = { digest: fingerprint, source };
180
+ }
181
+ interval.conversationBytes += bytes;
182
+ interval.userMessages++;
183
+ };
184
+ if (raw.type === 'user') {
185
+ const content = message.content;
186
+ const identity = raw.uuid ?? message.id ?? raw.id ?? ['line', state.recordsSeen];
187
+ if (Array.isArray(content)) {
188
+ const conversational = [];
189
+ content.forEach((value, index) => {
190
+ const block = object(value);
191
+ if (block.type === 'tool_result')
192
+ toolResult(block, [identity, index]);
193
+ else
194
+ conversational.push(value);
195
+ });
196
+ if (conversational.length)
197
+ userMessage(conversational, identity);
198
+ }
199
+ else if (content !== undefined)
200
+ userMessage(content, identity);
201
+ }
202
+ else if (raw.type === 'response_item') {
203
+ if (payload.type === 'function_call_output' || payload.type === 'custom_tool_call_output')
204
+ toolResult(payload, raw.id ?? ['line', state.recordsSeen]);
205
+ else if (payload.type === 'message' && payload.role === 'user') {
206
+ const content = Array.isArray(payload.content) ? payload.content.map(value => {
207
+ const block = object(value);
208
+ return typeof block.text === 'string' ? block.text : value;
209
+ }) : payload.content;
210
+ // Normalize text blocks for pairing with Codex's plain-string event echo.
211
+ const normalized = Array.isArray(content) && content.every(value => typeof value === 'string') ? content.join('') : content;
212
+ userMessage(normalized, payload.id ?? raw.id ?? ['line', state.recordsSeen], 'response');
213
+ }
214
+ }
215
+ else if (raw.type === 'event_msg' && payload.type === 'user_message') {
216
+ userMessage(payload.message, payload.id ?? raw.id ?? ['line', state.recordsSeen], 'event');
217
+ }
218
+ interval.shared = interval.toolResults > 0 && interval.userMessages > 0;
219
+ }
220
+ function observations(calls) {
221
+ const files = new Map();
222
+ const result = { readCalls: 0, repeatedReadCalls: 0, uniqueReadFiles: 0, spawnCalls: 0, toolResultRecords: 0 };
223
+ for (const call of Object.values(calls)) {
224
+ if (call.kind === 'spawn')
225
+ result.spawnCalls++;
226
+ else if (call.kind === 'tool_result')
227
+ result.toolResultRecords++;
228
+ else if (call.fileKey) {
229
+ result.readCalls++;
230
+ files.set(call.fileKey, (files.get(call.fileKey) ?? 0) + 1);
231
+ }
232
+ }
233
+ result.uniqueReadFiles = files.size;
234
+ result.repeatedReadCalls = [...files.values()].reduce((sum, total) => sum + Math.max(0, total - 1), 0);
235
+ return result;
236
+ }
237
+ /**
238
+ * Parse complete JSONL records. Pass returned state when parsing appended lines.
239
+ * `updatedTurns` replaces rows with the same id; it must never be blindly added
240
+ * to previous totals because an assistant message can gain output usage later.
241
+ */
242
+ function parseInsightTranscript(text, options = {}) {
243
+ const state = options.state ?? emptyState(options);
244
+ state.intervalStateVersion = 1;
245
+ const diagnostics = { lines: 0, malformedLines: 0, duplicateUsageRecords: 0, unsupportedUsageRecords: 0, inheritedUsageRecords: 0 };
246
+ const updated = new Map();
247
+ let seenCount = Object.keys(state.seen).length;
248
+ const acceptedQuotaLineNumbers = [];
249
+ let lineNumber = -1;
250
+ for (const line of text.split('\n')) {
251
+ lineNumber++;
252
+ if (!line.trim())
253
+ continue;
254
+ diagnostics.lines++;
255
+ state.recordsSeen = (state.recordsSeen ?? 0) + 1;
256
+ let raw;
257
+ try {
258
+ raw = object(JSON.parse(line));
259
+ }
260
+ catch {
261
+ diagnostics.malformedLines++;
262
+ continue;
263
+ }
264
+ const message = object(raw.message), payload = object(raw.payload), response = object(raw.response);
265
+ const extracted = usageOf(raw), usage = extracted.usage;
266
+ const at = millis(raw.timestamp ?? raw.created_at ?? payload.timestamp);
267
+ if (raw.type === 'fork-context-ref') {
268
+ state.lineage = { ...state.lineage, forkContextRef: true, forked: true,
269
+ ...(identifier(raw.parentSessionId) ? { parentSessionId: identifier(raw.parentSessionId) } : {}) };
270
+ }
271
+ if (raw.type === 'session_meta' && !state.sessionMetadataSeen) {
272
+ state.sessionMetadataSeen = true;
273
+ state.host = 'codex';
274
+ const spawned = object(object(object(payload.source).subagent).thread_spawn);
275
+ state.lineage = { ...state.lineage,
276
+ ...(identifier(payload.id ?? payload.session_id) ? { sessionId: identifier(payload.id ?? payload.session_id) } : {}),
277
+ ...(identifier(payload.parent_thread_id ?? spawned.parent_thread_id) ? { parentSessionId: identifier(payload.parent_thread_id ?? spawned.parent_thread_id) } : {}),
278
+ ...(identifier(payload.forked_from_id) ? { forked: true } : {}) };
279
+ if ('subagent' in object(payload.source) || identifier(payload.parent_thread_id) || identifier(payload.agent_path))
280
+ state.spawnedSession = true;
281
+ if (hasCount(payload.subagent_history_start_ordinal))
282
+ state.inheritedBeforeOrdinal = count(payload.subagent_history_start_ordinal);
283
+ if (identifier(payload.forked_from_id)) {
284
+ state.forkHistoryBoundaryUnknown = true;
285
+ state.awaitingForkBoundary = true;
286
+ if (identifier(payload.id))
287
+ state.forkThreadIdDigest = digest(payload.id);
288
+ }
289
+ }
290
+ // Modern Codex marks inherited history with ordinals. Copied legacy items
291
+ // can have rewritten timestamps, so wall-clock time is not a safe boundary.
292
+ const ordinalKnown = state.inheritedBeforeOrdinal !== undefined && hasCount(raw.ordinal);
293
+ const beforeOrdinal = ordinalKnown && count(raw.ordinal) < state.inheritedBeforeOrdinal;
294
+ const ownedSettings = raw.type === 'event_msg' && payload.type === 'thread_settings_applied' &&
295
+ state.forkThreadIdDigest !== undefined && identifier(payload.thread_id) !== undefined && digest(payload.thread_id) === state.forkThreadIdDigest;
296
+ if ((ordinalKnown && !beforeOrdinal) || (!ordinalKnown && ownedSettings)) {
297
+ state.awaitingForkBoundary = false;
298
+ state.forkHistoryBoundaryUnknown = false;
299
+ }
300
+ if (beforeOrdinal || state.awaitingForkBoundary) {
301
+ if (extracted.codex && extracted.total) {
302
+ state.inheritedTotalDigest = totalDigest(extracted.total);
303
+ diagnostics.inheritedUsageRecords++;
304
+ }
305
+ continue;
306
+ }
307
+ if (at !== undefined && raw.type === 'event_msg' && payload.type === 'token_count' && Object.keys(object(payload.rate_limits)).length) {
308
+ acceptedQuotaLineNumbers.push(lineNumber);
309
+ }
310
+ if (extracted.codex && extracted.total && state.inheritedTotalDigest === totalDigest(extracted.total)) {
311
+ diagnostics.inheritedUsageRecords++;
312
+ continue;
313
+ }
314
+ observeTools(raw, state);
315
+ observeInputEvents(raw, state);
316
+ const host = detectHost(raw, usage, state.host);
317
+ if (state.host === 'unknown')
318
+ state.host = host;
319
+ if (state.sessionId === 'unknown')
320
+ state.sessionId = identifier(raw.sessionId ?? raw.session_id ?? (raw.type === 'session_meta' ? payload.session_id ?? payload.id : undefined)) ?? 'unknown';
321
+ const rawModel = message.model ?? response.model ?? raw.model ?? payload.model;
322
+ const model = modelName(rawModel);
323
+ if (rawModel !== undefined)
324
+ state.model = model;
325
+ if ((raw.type === 'turn_context' || (raw.type === 'event_msg' && payload.type === 'task_started')) && identifier(payload.turn_id)) {
326
+ state.codexTurnIdDigest = digest(payload.turn_id);
327
+ }
328
+ if (raw.isSidechain === true || raw.is_sidechain === true ||
329
+ (raw.type === 'session_meta' && ('subagent' in object(payload.source) || identifier(payload.parent_thread_id) || identifier(payload.agent_path))))
330
+ state.spawnedSession = true;
331
+ if ((raw.type === 'system' && raw.subtype === 'compact_boundary') || raw.type === 'compacted' ||
332
+ payload.type === 'context_compacted' || payload.type === 'compaction')
333
+ state.compactionPending = true;
334
+ const prefix = raw.prompt_prefix_hash ?? raw.prefix_hash ?? message.prompt_prefix_hash ?? payload.prompt_prefix_hash ?? payload.prefix_hash;
335
+ const prefixDigest = typeof prefix === 'string' && prefix.length <= 256 ? digest(prefix) : undefined;
336
+ const changedPrefix = prefixDigest && state.prefixDigest ? prefixDigest !== state.prefixDigest : undefined;
337
+ if (changedPrefix !== undefined)
338
+ state.prefixChangedPending = Boolean(state.prefixChangedPending || changedPrefix);
339
+ if (prefixDigest)
340
+ state.prefixDigest = prefixDigest;
341
+ const inputRaw = usage.input_tokens ?? usage.prompt_tokens;
342
+ const outputRaw = usage.output_tokens ?? usage.completion_tokens;
343
+ if (!hasCount(inputRaw) && !hasCount(outputRaw) && !hasCount(usage.cache_creation_input_tokens) && !hasCount(usage.cache_read_input_tokens)) {
344
+ if (Object.keys(usage).length)
345
+ diagnostics.unsupportedUsageRecords++;
346
+ continue;
347
+ }
348
+ const details = object(usage.input_tokens_details ?? usage.prompt_tokens_details);
349
+ const cacheReadTokens = count(usage.cache_read_input_tokens ?? usage.cached_input_tokens ?? details.cached_tokens);
350
+ const cacheWriteTokens = count(usage.cache_creation_input_tokens ?? usage.cache_write_input_tokens ?? details.cache_write_tokens);
351
+ const includesCache = host === 'codex' || extracted.codex || (!('cache_creation_input_tokens' in usage) && !('cache_read_input_tokens' in usage) && ('cached_tokens' in details || 'cached_input_tokens' in usage));
352
+ if (includesCache && cacheReadTokens + cacheWriteTokens > count(inputRaw)) {
353
+ diagnostics.unsupportedUsageRecords++;
354
+ continue;
355
+ }
356
+ const inputTokens = includesCache ? Math.max(0, count(inputRaw) - cacheReadTokens - cacheWriteTokens) : count(inputRaw);
357
+ const outputTokens = count(outputRaw); // Reasoning output is already included.
358
+ if (inputTokens + cacheWriteTokens + cacheReadTokens + outputTokens === 0)
359
+ continue;
360
+ if (extracted.codex)
361
+ state.inheritedTotalDigest = undefined;
362
+ const uncertainties = [];
363
+ if (!model && !state.model)
364
+ uncertainties.push('model_unknown');
365
+ if (state.forkHistoryBoundaryUnknown)
366
+ uncertainties.push('fork_history_boundary_unknown');
367
+ let nativeId = message.id ?? response.id ?? raw.request_id ?? raw.requestId ?? raw.uuid ?? raw.id;
368
+ if (extracted.codex && extracted.total) {
369
+ const total = extracted.total;
370
+ const next = { input: count(total.input_tokens), cached: count(total.cached_input_tokens ?? object(total.input_tokens_details).cached_tokens), output: count(total.output_tokens) };
371
+ if (state.codexTotals && (next.input < state.codexTotals.input || next.output < state.codexTotals.output)) {
372
+ state.codexEpoch = (state.codexEpoch ?? 0) + 1;
373
+ uncertainties.push('codex_cumulative_counter_reset');
374
+ }
375
+ nativeId = ['codex-total', state.codexTurnIdDigest ?? state.codexEpoch ?? 0, next.input, next.cached, count(total.cache_write_input_tokens), next.output, count(total.total_tokens)];
376
+ if (!state.codexTurnIdDigest)
377
+ uncertainties.push('codex_turn_identity_missing');
378
+ state.codexTotals = next;
379
+ }
380
+ if (nativeId === undefined) {
381
+ nativeId = ['line', state.recordsSeen, at ?? null];
382
+ uncertainties.push('usage_id_missing');
383
+ }
384
+ // Provider response ids remain the same when a host copies history into a
385
+ // forked session. Preserve global identity instead of charging it twice.
386
+ const providerId = message.id !== undefined || response.id !== undefined;
387
+ const id = providerId ? (0, exports.providerResponseIdentity)(host, nativeId) : digest([extracted.codex && state.codexTurnIdDigest ? 'codex-turn' : state.sessionId, host, nativeId]);
388
+ const existing = state.seen[id];
389
+ const creation = object(usage.cache_creation);
390
+ const split5m = creation.ephemeral_5m_input_tokens, split1h = creation.ephemeral_1h_input_tokens;
391
+ const knownSplits = hasCount(split5m) && hasCount(split1h) && count(split5m) + count(split1h) === cacheWriteTokens;
392
+ const ttl = raw.cache_ttl_seconds ?? usage.cache_ttl_seconds ?? message.cache_ttl_seconds;
393
+ let cacheTtlSeconds = ttl === 300 || ttl === 3600 ? ttl : undefined;
394
+ let cacheTtlSource = cacheTtlSeconds ? 'explicit_metadata' : undefined;
395
+ if (!cacheTtlSeconds && knownSplits && cacheWriteTokens > 0 && (!count(split5m) || !count(split1h))) {
396
+ cacheTtlSeconds = count(split1h) ? 3600 : 300;
397
+ cacheTtlSource = 'usage_split';
398
+ }
399
+ if (cacheWriteTokens && !cacheTtlSeconds)
400
+ uncertainties.push(knownSplits ? 'cache_ttl_mixed' : 'cache_ttl_unknown');
401
+ if ((hasCount(split5m) || hasCount(split1h)) && !knownSplits)
402
+ uncertainties.push('cache_write_ttl_split_inconsistent');
403
+ const turn = { id, sessionId: state.sessionId, host, ...(at !== undefined ? { at } : {}),
404
+ ...(state.spawnedSession ? { subagent: true } : {}),
405
+ ...(state.model ? { model: state.model } : {}), inputTokens, cacheWriteTokens, cacheReadTokens, outputTokens,
406
+ ...(knownSplits ? { cacheWrite5mTokens: count(split5m), cacheWrite1hTokens: count(split1h) } : {}),
407
+ ...(cacheTtlSeconds ? { cacheTtlSeconds, cacheTtlSource } : {}), contextTokens: inputTokens + cacheWriteTokens + cacheReadTokens,
408
+ signals: existing?.signals ?? { afterCompaction: state.compactionPending, firstSpawnedTurn: state.spawnedSession && !state.forkHistoryBoundaryUnknown && seenCount === 0,
409
+ ...(state.lastAt !== undefined && at !== undefined && at >= state.lastAt ? { idleMs: at - state.lastAt } : {}),
410
+ ...(state.prefixChangedPending !== undefined ? { prefixChanged: state.prefixChangedPending } : {}) },
411
+ explicitAttribution: explicitAttribution(usage), uncertainties,
412
+ interval: existing?.interval ?? { ...state.pendingInterval } };
413
+ if (existing) {
414
+ diagnostics.duplicateUsageRecords++;
415
+ // Claude repeats message usage per content block and updates output counts.
416
+ // Take maxima for the same response, not a sum of repeated snapshots.
417
+ for (const key of ['inputTokens', 'cacheWriteTokens', 'cacheReadTokens', 'outputTokens'])
418
+ turn[key] = Math.max(existing[key], turn[key]);
419
+ turn.contextTokens = turn.inputTokens + turn.cacheWriteTokens + turn.cacheReadTokens;
420
+ turn.at = existing.at ?? turn.at;
421
+ turn.uncertainties = [...new Set([...existing.uncertainties, ...turn.uncertainties])];
422
+ if (existing.cacheWriteTokens > cacheWriteTokens) {
423
+ turn.cacheWrite5mTokens = existing.cacheWrite5mTokens;
424
+ turn.cacheWrite1hTokens = existing.cacheWrite1hTokens;
425
+ turn.cacheTtlSeconds = existing.cacheTtlSeconds;
426
+ turn.cacheTtlSource = existing.cacheTtlSource;
427
+ }
428
+ if (!turn.explicitAttribution.length)
429
+ turn.explicitAttribution = existing.explicitAttribution;
430
+ if (JSON.stringify(existing) === JSON.stringify(turn))
431
+ continue;
432
+ }
433
+ else {
434
+ state.pendingInterval = emptyInterval();
435
+ state.conversationEcho = undefined;
436
+ state.compactionPending = false;
437
+ state.prefixChangedPending = undefined;
438
+ seenCount++;
439
+ if (at !== undefined)
440
+ state.lastAt = at;
441
+ }
442
+ state.seen[id] = turn;
443
+ updated.set(id, turn);
444
+ }
445
+ return { host: state.host, sessionId: state.sessionId, turns: Object.values(state.seen), updatedTurns: [...updated.values()], diagnostics, state,
446
+ observations: observations(state.observedCalls ?? {}), acceptedQuotaLineNumbers };
447
+ }
448
+ function readInsightTranscript(filename, options = {}) {
449
+ return parseInsightTranscript((0, node_fs_1.readFileSync)(filename, 'utf8'), { sessionId: (0, node_path_1.basename)(filename, '.jsonl'), spawnedSession: /(?:^|[\\/])subagents[\\/]/.test(filename), ...options });
450
+ }
451
+ /** Read one Claude session plus its explicitly stored child transcripts. */
452
+ function readInsightSession(filename, options = {}) {
453
+ const parent = readInsightTranscript(filename, options);
454
+ const childDirectory = (0, node_path_1.join)(filename.replace(/\.jsonl$/, ''), 'subagents');
455
+ let files;
456
+ try {
457
+ files = (0, node_fs_1.readdirSync)(childDirectory).filter(name => name.endsWith('.jsonl')).sort();
458
+ }
459
+ catch (error) {
460
+ if (error.code === 'ENOENT')
461
+ return parent;
462
+ throw error;
463
+ }
464
+ const calls = { ...parent.state.observedCalls };
465
+ const turns = [...parent.turns];
466
+ for (const name of files) {
467
+ const child = readInsightTranscript((0, node_path_1.join)(childDirectory, name), { host: options.host, spawnedSession: true });
468
+ const known = new Set(turns.map(turn => turn.id));
469
+ const firstOwn = child.turns.findIndex(turn => !known.has(turn.id));
470
+ if (firstOwn > 0 && child.turns.slice(0, firstOwn).some(turn => turn.signals.firstSpawnedTurn)) {
471
+ const turn = child.turns[firstOwn];
472
+ child.turns[firstOwn] = { ...turn, signals: { ...turn.signals, firstSpawnedTurn: true } };
473
+ }
474
+ turns.push(...child.turns);
475
+ Object.assign(calls, child.state.observedCalls);
476
+ for (const key of ['lines', 'malformedLines', 'duplicateUsageRecords', 'unsupportedUsageRecords'])
477
+ parent.diagnostics[key] += child.diagnostics[key];
478
+ }
479
+ // State remains the parent's cursor state; recursive session reads are for
480
+ // reporting. Incremental hooks call parseInsightTranscript on one file.
481
+ parent.turns = deduplicateTurns(turns);
482
+ parent.updatedTurns = parent.turns;
483
+ parent.observations = observations(calls);
484
+ return parent;
485
+ }
486
+ /** Forked transcript copies can repeat provider response ids across files. */
487
+ function deduplicateTurns(turns) {
488
+ const unique = new Map();
489
+ for (const turn of turns) {
490
+ const previous = unique.get(turn.id);
491
+ if (!previous) {
492
+ unique.set(turn.id, turn);
493
+ continue;
494
+ }
495
+ // Fork copies preserve response/turn identity but may rewrite timestamps.
496
+ // Prefer the earlier original observation for ownership and date filtering.
497
+ const preferred = turn.at !== undefined && (previous.at === undefined || turn.at < previous.at) ? turn : previous;
498
+ const merged = { ...preferred, uncertainties: [...new Set([...previous.uncertainties, ...turn.uncertainties])] };
499
+ for (const key of ['inputTokens', 'cacheWriteTokens', 'cacheReadTokens', 'outputTokens'])
500
+ merged[key] = Math.max(turn[key], previous[key]);
501
+ merged.contextTokens = merged.inputTokens + merged.cacheWriteTokens + merged.cacheReadTokens;
502
+ unique.set(turn.id, merged);
503
+ }
504
+ return [...unique.values()];
505
+ }