@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,151 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.classifyRewrite = classifyRewrite;
4
+ exports.attributeTurns = attributeTurns;
5
+ const types_1 = require("./types");
6
+ const transcript_1 = require("./transcript");
7
+ function classifyRewrite(turn) {
8
+ if (!(turn.cacheWriteTokens > 150_000 && turn.cacheWriteTokens > turn.contextTokens / 2))
9
+ return null;
10
+ const idleMs = turn.signals.idleMs;
11
+ const ttl = turn.cacheTtlSeconds;
12
+ const evidence = { turnId: turn.id, cacheWriteTokens: turn.cacheWriteTokens, contextTokens: turn.contextTokens,
13
+ ...(idleMs !== undefined ? { idleMs } : {}), idleOver60Minutes: idleMs !== undefined && idleMs > 3_600_000,
14
+ ...(ttl ? { ttlSeconds: ttl } : {}), ttlUnknown: !ttl };
15
+ if (ttl && idleMs !== undefined && idleMs > ttl * 1000)
16
+ return { ...evidence, cause: 'idle_ttl_expired', explanation: 'Idle gap exceeds the TTL reported for this cache-write class; timing is consistent with expiration.' };
17
+ if (turn.signals.afterCompaction)
18
+ return { ...evidence, cause: 'compaction', explanation: 'A compaction boundary immediately precedes this usage turn.' };
19
+ if (turn.signals.firstSpawnedTurn)
20
+ return { ...evidence, cause: 'first_spawned_turn', explanation: 'This is the first recorded usage turn in an explicitly identified subagent session.' };
21
+ if (!ttl)
22
+ return { ...evidence, cause: 'unknown_ttl', explanation: 'Cache TTL is absent or mixed; expiration cannot be distinguished from a prefix change.' };
23
+ return { ...evidence, cause: 'prefix_change', explanation: turn.signals.prefixChanged
24
+ ? 'The host reports a changed prefix identifier without an expired-TTL or compaction signal.'
25
+ : 'No expiration, compaction, or first-subagent-turn signal explains the rewrite; prefix change is a residual hypothesis, not an observed cause.' };
26
+ }
27
+ const tokenCategories = ['input', 'cacheCreation', 'cacheRead', 'output'];
28
+ const emptyBuckets = () => types_1.INSIGHT_BUCKETS.map(bucket => ({ bucket, tokens: 0,
29
+ categories: { input: 0, cacheCreation: 0, cacheRead: 0, output: 0 } }));
30
+ /** Largest remainders split an already measured token total without inventing tokens from bytes. */
31
+ function integerShares(total, weights) {
32
+ const weight = weights.reduce((sum, value) => sum + value, 0);
33
+ if (!weight || !total)
34
+ return weights.map(() => 0);
35
+ const exact = weights.map(value => total * (value / weight));
36
+ const shares = exact.map(Math.floor);
37
+ const order = exact.map((value, index) => ({ index, remainder: value - shares[index] }))
38
+ .sort((a, b) => b.remainder - a.remainder || a.index - b.index);
39
+ for (let left = total - shares.reduce((sum, value) => sum + value, 0), i = 0; left > 0; left--, i++)
40
+ shares[order[i % order.length].index]++;
41
+ return shares;
42
+ }
43
+ /** Partition measured usage; byte lengths only divide a measured increment among its recorded events. */
44
+ function attributeTurns(turns) {
45
+ const buckets = emptyBuckets();
46
+ const byBucket = new Map(buckets.map(bucket => [bucket.bucket, bucket]));
47
+ const result = { buckets, totalTokens: 0, rewrites: [],
48
+ rewriteCounts: { idle_ttl_expired: 0, compaction: 0, first_spawned_turn: 0, prefix_change: 0, unknown_ttl: 0 },
49
+ idleOver60MinuteRewrites: 0, uncertainties: [], turnAttributions: [], sharedTurns: 0,
50
+ prefixRebaselines: 0, previousOutputTokensReserved: 0 };
51
+ const uncertainties = new Set();
52
+ const sessions = new Map();
53
+ const ordered = (0, transcript_1.deduplicateTurns)(turns).sort((a, b) => (a.at ?? 0) - (b.at ?? 0));
54
+ for (const turn of ordered) {
55
+ const remaining = { input: turn.inputTokens, cacheCreation: turn.cacheWriteTokens, cacheRead: turn.cacheReadTokens, output: turn.outputTokens };
56
+ const turnBuckets = emptyBuckets();
57
+ const turnByBucket = new Map(turnBuckets.map(bucket => [bucket.bucket, bucket]));
58
+ const totalTokens = Object.values(remaining).reduce((sum, value) => sum + value, 0);
59
+ result.totalTokens += totalTokens;
60
+ const allocate = (bucket, category, tokens) => {
61
+ const amount = Math.min(remaining[category], Math.max(0, tokens));
62
+ if (amount < tokens)
63
+ uncertainties.add('attribution_evidence_exceeds_category_total');
64
+ for (const target of [byBucket.get(bucket), turnByBucket.get(bucket)]) {
65
+ target.tokens += amount;
66
+ target.categories[category] += amount;
67
+ }
68
+ remaining[category] -= amount;
69
+ };
70
+ const previous = sessions.get(turn.sessionId);
71
+ let prefix = previous?.prefix ?? turn.contextTokens;
72
+ allocate('output', 'output', remaining.output);
73
+ const rewrite = classifyRewrite(turn);
74
+ if (rewrite) {
75
+ result.rewrites.push(rewrite);
76
+ result.rewriteCounts[rewrite.cause]++;
77
+ if (rewrite.idleOver60Minutes)
78
+ result.idleOver60MinuteRewrites++;
79
+ allocate('full_prefix_rewrites', 'cacheCreation', remaining.cacheCreation);
80
+ if (rewrite.ttlUnknown)
81
+ uncertainties.add('rewrite_ttl_unknown');
82
+ if (rewrite.cause === 'prefix_change') {
83
+ prefix = turn.contextTokens;
84
+ if (previous)
85
+ result.prefixRebaselines++;
86
+ if (!turn.signals.prefixChanged)
87
+ uncertainties.add('prefix_change_is_residual_hypothesis');
88
+ }
89
+ }
90
+ for (const item of turn.explicitAttribution) {
91
+ if (item.bucket !== 'output' && item.bucket !== 'full_prefix_rewrites' && item.bucket !== 'unattributed' && Number.isSafeInteger(item.tokens) && item.tokens > 0)
92
+ allocate(item.bucket, item.category, item.tokens);
93
+ }
94
+ if (turn.subagent) {
95
+ for (const category of ['input', 'cacheCreation', 'cacheRead'])
96
+ allocate('subagent_fanout', category, remaining[category]);
97
+ }
98
+ else if (!turn.explicitAttribution.length) {
99
+ if (!previous || rewrite?.cause === 'prefix_change') {
100
+ // Rewrites retain their measured write tokens; the rest establishes the new fixed prefix.
101
+ for (const category of ['input', 'cacheCreation', 'cacheRead'])
102
+ allocate('instruction_stack', category, remaining[category]);
103
+ }
104
+ else {
105
+ const history = Math.max(0, turn.cacheReadTokens - prefix);
106
+ allocate('instruction_stack', 'cacheRead', Math.min(remaining.cacheRead, prefix));
107
+ allocate('history_resent', 'cacheRead', Math.min(remaining.cacheRead, history));
108
+ if (!rewrite) {
109
+ // Prior assistant output reappears in input usage but is not newly arrived content.
110
+ // Keep that measured input in the residual rather than count it as generated output twice.
111
+ let reserved = Math.min(previous.previousOutput, remaining.input + remaining.cacheCreation);
112
+ result.previousOutputTokensReserved += reserved;
113
+ for (const category of ['input', 'cacheCreation']) {
114
+ const amount = Math.min(reserved, remaining[category]);
115
+ allocate('unattributed', category, amount);
116
+ reserved -= amount;
117
+ }
118
+ const interval = turn.interval;
119
+ const eventBuckets = ['tool_output', 'reread_files', 'conversation'];
120
+ let weights = interval ? [interval.toolOutputBytes, interval.reReadBytes, interval.conversationBytes] : [0, 0, 0];
121
+ if (interval && !weights.some(value => value > 0)) {
122
+ // A sole event class owns the whole measured increment even for an
123
+ // empty result; mixed empty payloads have no byte-based split.
124
+ const present = [interval.toolResults > (interval.reReadResults ?? 0), (interval.reReadResults ?? 0) > 0, interval.userMessages > 0];
125
+ if (present.filter(Boolean).length === 1)
126
+ weights = present.map(value => value ? 1 : 0);
127
+ }
128
+ if (weights.some(value => value > 0)) {
129
+ const shares = integerShares(remaining.input + remaining.cacheCreation, weights);
130
+ const inputs = integerShares(remaining.input, shares);
131
+ if (interval.shared && shares.some(value => value > 0))
132
+ result.sharedTurns++;
133
+ eventBuckets.forEach((bucket, i) => {
134
+ allocate(bucket, 'input', inputs[i]);
135
+ allocate(bucket, 'cacheCreation', shares[i] - inputs[i]);
136
+ });
137
+ }
138
+ }
139
+ }
140
+ }
141
+ for (const category of tokenCategories)
142
+ allocate('unattributed', category, remaining[category]);
143
+ turn.uncertainties.forEach(value => uncertainties.add(value));
144
+ sessions.set(turn.sessionId, { prefix, previousOutput: turn.outputTokens });
145
+ result.turnAttributions.push({ turnId: turn.id, sessionId: turn.sessionId, buckets: turnBuckets, totalTokens });
146
+ }
147
+ if (byBucket.get('unattributed').tokens)
148
+ uncertainties.add('unattributed_usage_includes_prior_output_and_missing_event_evidence');
149
+ result.uncertainties = [...uncertainties].sort();
150
+ return result;
151
+ }
@@ -0,0 +1,61 @@
1
+ import { type DollarRange, type PricingTable } from './pricing';
2
+ import { type TranscriptLocation } from './sessions';
3
+ import type { InsightTranscript } from './types';
4
+ export interface BlockReceipt {
5
+ line: number;
6
+ at: number | null;
7
+ sessionId?: string;
8
+ sessionDigest?: string;
9
+ verdict: 'STOP' | 'WARN';
10
+ detectors: string[];
11
+ spawnNumber: number | null;
12
+ blocked: boolean | null;
13
+ }
14
+ export interface ChildEconomics {
15
+ child: string;
16
+ kind: 'fork' | 'fresh';
17
+ forkContextRef: boolean;
18
+ tokens: number | null;
19
+ usd: DollarRange | null;
20
+ unknownModels: string[];
21
+ }
22
+ export interface ChildStatistics {
23
+ count: number;
24
+ measuredCount: number;
25
+ pricedCount: number;
26
+ medianTokens: number | null;
27
+ maxTokens: number | null;
28
+ medianUsd: DollarRange | null;
29
+ maxUsd: DollarRange | null;
30
+ }
31
+ export interface SessionEconomics {
32
+ available: boolean;
33
+ children: ChildEconomics[];
34
+ all: ChildStatistics;
35
+ forks: ChildStatistics;
36
+ fresh: ChildStatistics;
37
+ notes: string[];
38
+ }
39
+ export interface BlocksReport {
40
+ receipts: Array<BlockReceipt & {
41
+ session: string;
42
+ }>;
43
+ sessions: Array<{
44
+ session: string;
45
+ economics: SessionEconomics;
46
+ }>;
47
+ malformedReceiptLines: number;
48
+ }
49
+ /** Read signed envelopes and earlier flat rows without requiring new fields. */
50
+ export declare function readBlockReceipts(filename: string): {
51
+ receipts: BlockReceipt[];
52
+ malformedLines: number;
53
+ };
54
+ export declare function childStatistics(children: ChildEconomics[]): ChildStatistics;
55
+ /** Use the attribution parser for all hosts; only explicit lineage owns a child. */
56
+ export declare function readSessionEconomics(location: TranscriptLocation, locations: TranscriptLocation[], rates?: PricingTable, cache?: Map<string, InsightTranscript>): SessionEconomics;
57
+ export declare function blocksReport(home: string, session?: string, options?: {
58
+ locations?: TranscriptLocation[];
59
+ rates?: PricingTable;
60
+ }): BlocksReport;
61
+ export declare function renderBlocks(report: BlocksReport): string;
@@ -0,0 +1,243 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readBlockReceipts = readBlockReceipts;
4
+ exports.childStatistics = childStatistics;
5
+ exports.readSessionEconomics = readSessionEconomics;
6
+ exports.blocksReport = blocksReport;
7
+ exports.renderBlocks = renderBlocks;
8
+ const node_fs_1 = require("node:fs");
9
+ const node_path_1 = require("node:path");
10
+ const receipt_1 = require("../receipt");
11
+ const live_1 = require("./live");
12
+ const pricing_1 = require("./pricing");
13
+ const render_1 = require("./render");
14
+ const sessions_1 = require("./sessions");
15
+ const transcript_1 = require("./transcript");
16
+ const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {};
17
+ const identifier = (value) => typeof value === 'string' && /^[A-Za-z0-9_.:/-]{1,128}$/.test(value) ? value : undefined;
18
+ const count = (value) => Number.isSafeInteger(value) && Number(value) >= 0 ? Number(value) : null;
19
+ /** Read signed envelopes and earlier flat rows without requiring new fields. */
20
+ function readBlockReceipts(filename) {
21
+ let text;
22
+ try {
23
+ text = (0, node_fs_1.readFileSync)(filename, 'utf8');
24
+ }
25
+ catch (error) {
26
+ if (error.code === 'ENOENT')
27
+ return { receipts: [], malformedLines: 0 };
28
+ throw error;
29
+ }
30
+ const receipts = [];
31
+ let malformedLines = 0;
32
+ for (const [index, line] of text.split('\n').entries()) {
33
+ if (!line.trim())
34
+ continue;
35
+ let row;
36
+ try {
37
+ row = record(JSON.parse(line));
38
+ }
39
+ catch {
40
+ malformedLines++;
41
+ continue;
42
+ }
43
+ const payload = Object.keys(record(row.payload)).length ? record(row.payload) : row;
44
+ if (payload.verdict !== 'STOP' && payload.verdict !== 'WARN')
45
+ continue;
46
+ const measured = record(payload.measured);
47
+ const atValue = payload.at ?? payload.timestamp;
48
+ const at = typeof atValue === 'string' ? Date.parse(atValue) : typeof atValue === 'number' ? atValue : NaN;
49
+ const findings = Array.isArray(payload.findings) ? payload.findings.map(item => record(item).detector) : [];
50
+ const reasons = Array.isArray(payload.reasons) ? payload.reasons : [payload.detector, ...findings];
51
+ const detectors = [...new Set(reasons.flatMap(reason => {
52
+ if (typeof reason !== 'string')
53
+ return [];
54
+ const match = /^([a-z][a-z0-9_-]{0,63})(?::(?:WARN|STOP))?$/.exec(reason);
55
+ return match ? [match[1]] : [];
56
+ }))];
57
+ const sessionId = identifier(payload.sessionId ?? payload.session_id);
58
+ const sessionDigest = typeof payload.sessionDigest === 'string' && /^[a-f0-9]{64}$/i.test(payload.sessionDigest) ? payload.sessionDigest.toLowerCase() : undefined;
59
+ receipts.push({ line: index + 1, at: Number.isFinite(at) && Math.abs(at) <= 8.64e15 ? at : null,
60
+ ...(sessionId ? { sessionId } : {}), ...(sessionDigest ? { sessionDigest } : {}),
61
+ verdict: payload.verdict, detectors, spawnNumber: count(measured.sessionSpawns ?? payload.sessionSpawns ?? payload.spawns),
62
+ blocked: typeof payload.blocked === 'boolean' ? payload.blocked : null });
63
+ }
64
+ return { receipts, malformedLines };
65
+ }
66
+ const median = (values) => {
67
+ if (!values.length)
68
+ return null;
69
+ const sorted = [...values].sort((a, b) => a - b), middle = Math.floor(sorted.length / 2);
70
+ return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
71
+ };
72
+ function childStatistics(children) {
73
+ const measured = children.filter(child => child.tokens !== null), priced = children.filter(child => child.usd !== null);
74
+ return { count: children.length, measuredCount: measured.length, pricedCount: priced.length,
75
+ medianTokens: median(measured.map(child => child.tokens)), maxTokens: measured.length ? Math.max(...measured.map(child => child.tokens)) : null,
76
+ // Dollar statistics never silently omit an unpriced child from the cohort.
77
+ medianUsd: children.length && priced.length === children.length ? { min: median(priced.map(child => child.usd.min)), max: median(priced.map(child => child.usd.max)) } : null,
78
+ maxUsd: children.length && priced.length === children.length ? { min: Math.max(...priced.map(child => child.usd.min)), max: Math.max(...priced.map(child => child.usd.max)) } : null };
79
+ }
80
+ const unavailable = (note) => ({ available: false, children: [], all: childStatistics([]), forks: childStatistics([]), fresh: childStatistics([]), notes: [note] });
81
+ const locationId = (location) => location.host === 'codex'
82
+ ? /([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})$/i.exec(location.sessionId)?.[1] ?? location.sessionId
83
+ : location.sessionId;
84
+ /** Use the attribution parser for all hosts; only explicit lineage owns a child. */
85
+ function readSessionEconomics(location, locations, rates = {}, cache = new Map()) {
86
+ const load = (item, spawnedSession = false) => {
87
+ let parsed = cache.get(item.path);
88
+ if (!parsed) {
89
+ parsed = (0, transcript_1.readInsightTranscript)(item.path, { host: item.host, sessionId: locationId(item), spawnedSession });
90
+ cache.set(item.path, parsed);
91
+ }
92
+ return parsed;
93
+ };
94
+ let parent;
95
+ try {
96
+ parent = load(location);
97
+ }
98
+ catch {
99
+ return unavailable('Session transcript is missing or unreadable; child usage is unavailable.');
100
+ }
101
+ const parentIds = new Set([location.sessionId, locationId(location), parent.state.lineage?.sessionId].filter((value) => Boolean(value)));
102
+ const children = [], notes = [];
103
+ if (location.host === 'claude') {
104
+ const directory = (0, node_path_1.join)(location.path.replace(/\.jsonl$/, ''), 'subagents');
105
+ let names;
106
+ try {
107
+ names = (0, node_fs_1.readdirSync)(directory).filter(name => name.endsWith('.jsonl')).sort();
108
+ }
109
+ catch (error) {
110
+ if (error.code === 'ENOENT') {
111
+ names = [];
112
+ notes.push('No child transcript directory was found. This does not establish that the session spawned no agents.');
113
+ }
114
+ else
115
+ return unavailable('Child transcript directory is unreadable; child usage is unavailable.');
116
+ }
117
+ for (const name of names) {
118
+ try {
119
+ const child = load({ path: (0, node_path_1.join)(directory, name), host: 'claude', sessionId: (0, node_path_1.basename)(name, '.jsonl'), modifiedAt: 0 }, true);
120
+ if (child.state.lineage?.parentSessionId && !parentIds.has(child.state.lineage.parentSessionId)) {
121
+ notes.push('A child file with a different recorded parent was excluded.');
122
+ continue;
123
+ }
124
+ children.push({ id: (0, node_path_1.basename)(name, '.jsonl'), transcript: child });
125
+ }
126
+ catch {
127
+ notes.push('A child transcript could not be read; its usage is unavailable.');
128
+ }
129
+ }
130
+ }
131
+ else {
132
+ for (const item of locations.filter(candidate => candidate.host === 'codex' && candidate.path !== location.path)) {
133
+ try {
134
+ const child = load(item, true);
135
+ if (child.state.lineage?.parentSessionId && parentIds.has(child.state.lineage.parentSessionId))
136
+ children.push({ id: locationId(item), transcript: child });
137
+ }
138
+ catch {
139
+ notes.push('A Codex transcript could not be read; child discovery may be incomplete.');
140
+ }
141
+ }
142
+ notes.push('Codex children require an explicit parent thread id. Inherited usage without a verifiable boundary is unavailable.');
143
+ }
144
+ const parentUsage = new Set(parent.turns.map(turn => turn.id));
145
+ // Shared provider ids collapse both streaming updates and fork history copies.
146
+ const own = (0, transcript_1.deduplicateTurns)(children.flatMap(child => child.transcript.turns).filter(turn => !parentUsage.has(turn.id)));
147
+ const ownedByChild = new Map();
148
+ own.forEach(turn => ownedByChild.set(turn.sessionId, [...ownedByChild.get(turn.sessionId) ?? [], turn]));
149
+ const rows = children.map(({ id, transcript }) => {
150
+ const turns = ownedByChild.get(transcript.sessionId) ?? [];
151
+ const unknownBoundary = transcript.state.forkHistoryBoundaryUnknown === true;
152
+ const prices = turns.map(turn => (0, pricing_1.priceTurn)(turn, rates));
153
+ return { child: id, kind: transcript.state.lineage?.forked ? 'fork' : 'fresh', forkContextRef: transcript.state.lineage?.forkContextRef === true,
154
+ tokens: unknownBoundary || !turns.length ? null : turns.reduce((sum, turn) => sum + (0, render_1.totalTokens)(turn), 0),
155
+ usd: unknownBoundary || !turns.length ? null : (0, render_1.sumPrices)(prices.map(price => price.usd)),
156
+ unknownModels: [...new Set(prices.filter(price => price.usd === null).map(price => price.model ?? 'missing model id'))] };
157
+ });
158
+ if (!rows.length && !notes.length)
159
+ notes.push('No owned child transcripts were found; child economics are unavailable, not evidence of zero spawns.');
160
+ return { available: true, children: rows, all: childStatistics(rows), forks: childStatistics(rows.filter(child => child.kind === 'fork')),
161
+ fresh: childStatistics(rows.filter(child => child.kind === 'fresh')), notes: [...new Set(notes)] };
162
+ }
163
+ function blocksReport(home, session, options = {}) {
164
+ const { receipts, malformedLines } = readBlockReceipts((0, node_path_1.join)(home, 'receipts.ndjson'));
165
+ const locations = options.locations ?? (0, sessions_1.discoverInsightTranscripts)();
166
+ const aliases = new Map(), digests = new Map();
167
+ for (const location of locations)
168
+ for (const alias of new Set([location.sessionId, locationId(location)])) {
169
+ if (!aliases.has(alias))
170
+ aliases.set(alias, location);
171
+ if (!digests.has((0, receipt_1.sha256)(alias)))
172
+ digests.set((0, receipt_1.sha256)(alias), location);
173
+ }
174
+ let selected;
175
+ let requested = session;
176
+ if (session !== 'all') {
177
+ requested = session || process.env.AGENTGUARD_SESSION_ID || process.env.CLAUDE_SESSION_ID || process.env.CODEX_THREAD_ID;
178
+ try {
179
+ selected = (0, sessions_1.selectInsightTranscript)(requested, locations.filter(item => !/[\\/]subagents[\\/]/.test(item.path)));
180
+ }
181
+ catch {
182
+ if (!requested)
183
+ throw new Error('No current session transcript found. Use agentguard-burn blocks all for stored receipts.');
184
+ }
185
+ if (selected) {
186
+ aliases.set(selected.sessionId, selected);
187
+ digests.set((0, receipt_1.sha256)(selected.sessionId), selected);
188
+ }
189
+ }
190
+ const selectedIds = new Set([requested, selected?.sessionId, selected && locationId(selected)].filter((value) => Boolean(value)));
191
+ const selectedDigests = new Set([...selectedIds].map(receipt_1.sha256));
192
+ const sessions = new Map();
193
+ if (selected)
194
+ sessions.set(locationId(selected), selected);
195
+ const rows = receipts.filter(row => session === 'all' || (row.sessionId && selectedIds.has(row.sessionId)) || (row.sessionDigest && selectedDigests.has(row.sessionDigest))).map(row => {
196
+ const location = row.sessionId ? aliases.get(row.sessionId) : row.sessionDigest ? digests.get(row.sessionDigest) : undefined;
197
+ const id = location ? locationId(location) : row.sessionId ?? (row.sessionDigest ? `sha256:${row.sessionDigest}` : `unknown-row-${row.line}`);
198
+ sessions.set(id, location);
199
+ return { ...row, session: id };
200
+ });
201
+ const cache = new Map();
202
+ return { receipts: rows, sessions: [...sessions].map(([id, location]) => ({ session: id,
203
+ economics: location ? readSessionEconomics(location, locations, options.rates, cache) : unavailable('No matching local transcript; child usage is unavailable.') })), malformedReceiptLines: malformedLines };
204
+ }
205
+ function table(headers, rows) {
206
+ const widths = headers.map((header, index) => Math.max(header.length, ...rows.map(row => row[index]?.length ?? 0)));
207
+ return [headers, ...rows].map(row => row.map((cell, index) => cell.padEnd(widths[index])).join(' ').trimEnd()).join('\n');
208
+ }
209
+ const shortSession = (session) => session.startsWith('sha256:') ? session.slice(0, 15) : session.slice(0, 8);
210
+ const number = (value) => value === null ? 'unavailable' : value.toLocaleString('en-US');
211
+ function renderBlocks(report) {
212
+ const sessions = new Map(report.sessions.map(session => [session.session, session.economics]));
213
+ const compact = (item) => !item?.count ? 'unavailable' :
214
+ `${item.count}${item.measuredCount !== item.count ? ` (${item.measuredCount} measured)` : ''}; ${number(item.medianTokens)}/${number(item.maxTokens)}; ${(0, live_1.dollars)(item.medianUsd)}/${(0, live_1.dollars)(item.maxUsd)}`;
215
+ const lines = ['AgentGuard blocks: stored WARN and STOP decisions',
216
+ 'Each child cell: transcript count; median/max measured tokens; median/max list USD.',
217
+ table(['Time (UTC)', 'Session', 'Verdict', 'Detector', 'Spawn', 'Blocked', 'All children', 'Fork children', 'Fresh children'], report.receipts.map(row => {
218
+ const economics = sessions.get(row.session);
219
+ return [row.at === null ? 'unknown' : new Date(row.at).toISOString(), shortSession(row.session), row.verdict, row.detectors.join(',') || 'unknown',
220
+ row.spawnNumber === null ? 'unknown' : String(row.spawnNumber), row.blocked === null ? 'unknown' : row.blocked ? 'yes' : 'no',
221
+ compact(economics?.all), compact(economics?.forks), compact(economics?.fresh)];
222
+ }))];
223
+ if (!report.receipts.length)
224
+ lines.push('No stored WARN or STOP receipts match this selection.');
225
+ for (const { session, economics } of report.sessions) {
226
+ lines.push('', `Session ${shortSession(session)}: its own child transcripts`);
227
+ if (economics.available)
228
+ lines.push(table(['Children', 'Count', 'Measured', 'Median tokens', 'Max tokens', 'Median list USD', 'Max list USD'], [['All', economics.all], ['Forks', economics.forks], ['Fresh', economics.fresh]].map(([label, stats]) => {
229
+ const item = stats;
230
+ return [String(label), item.count ? String(item.count) : '0 found', String(item.measuredCount), number(item.medianTokens), number(item.maxTokens), (0, live_1.dollars)(item.medianUsd), (0, live_1.dollars)(item.maxUsd)];
231
+ })));
232
+ lines.push(...economics.notes);
233
+ if (economics.children.some(child => child.tokens === null))
234
+ lines.push('Some children have no attributable usage or an unknown inherited-history boundary; their token statistics are unavailable.');
235
+ const unpriced = [...new Set(economics.children.flatMap(child => child.unknownModels))];
236
+ if (unpriced.length)
237
+ lines.push(`Tokens only for unpriced models: ${unpriced.join(', ')}. Dollar statistics require every child in the cohort to have a verified rate.`);
238
+ }
239
+ if (report.malformedReceiptLines)
240
+ lines.push(`${report.malformedReceiptLines} malformed receipt lines could not be read.`);
241
+ lines.push('', 'Measured usage is deduplicated by provider response id, including message.id; inherited parent usage is excluded.', 'Claude forks are marked fork-context-ref. Codex forks require host lineage metadata; other discovered children are fresh.', 'Child totals cover the full available transcript, not a prediction at the decision time. Token and dollar medians and maxima are computed separately.', 'Dollars are API list-price equivalents, not your bill. Missing cache TTL produces a price range. Unknown models have tokens only.');
242
+ return lines.join('\n');
243
+ }
@@ -0,0 +1,53 @@
1
+ import type { Policy } from '../types';
2
+ import type { InsightHost, InsightParserState, InsightTurn } from './types';
3
+ import { type PricingTable } from './pricing';
4
+ import { type Pace, type UsageWindow, type LimitForecast } from './pace';
5
+ export interface InsightSettings {
6
+ rewriteWarnDollarsPerHour?: number;
7
+ heavyTurnTokens?: number;
8
+ pricingFile?: string;
9
+ }
10
+ export interface LiveSnapshot {
11
+ schema: 1;
12
+ host: InsightHost;
13
+ sessionId: string;
14
+ updatedAt: number;
15
+ toolEvents: number;
16
+ cursor: {
17
+ offset: number;
18
+ inode: number;
19
+ pathDigest: string;
20
+ };
21
+ parser?: InsightParserState;
22
+ pace: Pace;
23
+ windows: UsageWindow[];
24
+ forecast: LimitForecast;
25
+ lastTurnTokens: number;
26
+ rewriteDollarsToday: {
27
+ min: number;
28
+ max: number;
29
+ } | null;
30
+ unpricedRewritesToday: number;
31
+ heavyAbove: boolean;
32
+ rewriteAbove: boolean;
33
+ }
34
+ export interface LiveInput {
35
+ session_id?: string;
36
+ transcript_path?: string;
37
+ tool_name?: string;
38
+ rate_limits?: unknown;
39
+ }
40
+ export declare function pacePath(home: string, sessionId: string): string;
41
+ export declare function readPricing(home: string, policy?: Policy): PricingTable;
42
+ export declare function dollars(value: {
43
+ min: number;
44
+ max: number;
45
+ } | null): string;
46
+ export declare function hostWindows(value: unknown, source: UsageWindow['source'], at: number): UsageWindow[];
47
+ export declare function heavyTurnMessage(turn: InsightTurn, rates: PricingTable): string;
48
+ /** Local advisory observer. No tool input or output content enters this API. */
49
+ export declare function observeTool(home: string, input: LiveInput, host: InsightHost, policy: Policy, now?: number): {
50
+ snapshot?: LiveSnapshot;
51
+ messages: string[];
52
+ };
53
+ export declare function renderStatusLine(snapshot: LiveSnapshot): string;