@agentguard-run/burn 0.2.2 → 0.2.5

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 (43) hide show
  1. package/CHANGELOG.md +39 -2
  2. package/LICENSE +9 -10
  3. package/README.md +96 -16
  4. package/dist/src/adapters/codex.js +11 -1
  5. package/dist/src/adapters/cursor.js +2 -2
  6. package/dist/src/cli.js +40 -13
  7. package/dist/src/conformance.js +3 -1
  8. package/dist/src/defaults.d.ts +9 -8
  9. package/dist/src/defaults.js +9 -8
  10. package/dist/src/detectors/evaluate.d.ts +5 -2
  11. package/dist/src/detectors/evaluate.js +26 -2
  12. package/dist/src/eligibility.d.ts +17 -0
  13. package/dist/src/eligibility.js +29 -0
  14. package/dist/src/gateway.d.ts +2 -0
  15. package/dist/src/gateway.js +6 -3
  16. package/dist/src/hook/pre-tool-use.d.ts +2 -0
  17. package/dist/src/hook/pre-tool-use.js +93 -63
  18. package/dist/src/insights/attribution.d.ts +4 -0
  19. package/dist/src/insights/attribution.js +151 -0
  20. package/dist/src/insights/live.d.ts +53 -0
  21. package/dist/src/insights/live.js +211 -0
  22. package/dist/src/insights/pace.d.ts +34 -0
  23. package/dist/src/insights/pace.js +54 -0
  24. package/dist/src/insights/pricing.d.ts +48 -0
  25. package/dist/src/insights/pricing.js +139 -0
  26. package/dist/src/insights/render.d.ts +8 -0
  27. package/dist/src/insights/render.js +126 -0
  28. package/dist/src/insights/sessions.d.ts +12 -0
  29. package/dist/src/insights/sessions.js +51 -0
  30. package/dist/src/insights/transcript.d.ts +12 -0
  31. package/dist/src/insights/transcript.js +492 -0
  32. package/dist/src/insights/types.d.ts +157 -0
  33. package/dist/src/insights/types.js +4 -0
  34. package/dist/src/install.js +14 -5
  35. package/dist/src/replay/render.js +1 -1
  36. package/dist/src/state/account.d.ts +3 -0
  37. package/dist/src/state/account.js +39 -0
  38. package/dist/src/state/reservations.d.ts +5 -5
  39. package/dist/src/state/reservations.js +60 -45
  40. package/dist/src/types.d.ts +6 -0
  41. package/docs/USAGE_AND_PRICING.md +132 -0
  42. package/fixtures/codex-0.151.0-pretooluse.json +11 -11
  43. package/package.json +4 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.5 (2026-09-18)
4
+
5
+ - Attribute session usage from measured deltas: fixed-prefix context, cached history, tool output, repeated Read results and conversation. Use bytes only to split a measured increment, with mixed intervals counted as shared.
6
+ - Preserve every recorded token across output, rewrites, child fan-out and the residual. The Method footer explains each bucket, baseline resets and the prior-output subtraction retained in the residual.
7
+ - Keep baseline and event state across responses for correct last-turn shares and category pricing. Repeated usage snapshots retain their original event interval; old pace caches rebuild local metadata once.
8
+ - Validate every bucket against a constructed session, including UTF-8 weights, integer rounding, prefix changes, duplicate events and per-category conservation.
9
+ - Tests: 120 passing by default, one opt-in stress test skipped.
10
+
11
+ ## 0.2.4 (2026-09-18)
12
+
13
+ - Add local `why`, `rewrites`, `pace`, `statusline` and `pricing` commands. Assistant usage is reconciled by response identity, with unknown attribution kept explicit and child usage identified from transcript metadata.
14
+ - Explain full-prefix cache writes, measured lifetimes, cause evidence and API list cost. Warn on configurable rewrite spend and heavy context crossings without blocking ordinary tools.
15
+ - Observe every Claude Code and Codex tool call. Track cached and uncached ten-minute pace. Forecast limits only from fresh host percentage observations; provide a companion terminal line where Codex cannot run a custom footer command.
16
+ - Bundle dated model pricing and local overrides. Keep unknown models unpriced, content out of persisted metadata, and new reporting paths free of network calls.
17
+ - Raise reservation lock wait to eight seconds, serialize stale-lock retirement, and exclude operational lock-wait failures from enforce eligibility. Old ledgers and receipts remain readable.
18
+ - Validate a 240-process burst: exactly 40 admitted, 200 denied, 240 signed receipts, zero lock failures and a valid chain.
19
+ - Tests: 106 passing by default, one opt-in stress test skipped. The separate stress run also passes.
20
+
21
+ ## 0.2.3 (2026-09-15)
22
+
23
+ - Replay evidence refreshed from this machine: 4 STOP, 3 WARN, 431 clean across
24
+ 438 sessions and 616 spawns. Hero: 32%, 32.58B observed, 10.27B after STOP,
25
+ $4,639 API-list scenario. Fan-out sessions have 59, 190 and 226 spawns;
26
+ the sustained-burn session has 9.15B tokens and 26 spawns. These are a dated
27
+ snapshot and an upper bound, without a false-positive classification.
28
+ - Account concurrency warnings and optional active-window token WARN/STOP
29
+ budgets read both hook and gateway session files. Shadow never blocks.
30
+ - Claude hook decisions now write the same content-free, chained Ed25519
31
+ receipts as gateway decisions, with host/action on ledger rows. Old rows and
32
+ receipts remain readable. Session refresh and receipt chaining share the lock.
33
+ - Add an opt-in 240-hook-process stress test at cap 40. Staggered lock retries
34
+ reduce contention while preserving the existing fail-closed deadline.
35
+ - Correct the license package/file enumeration, use synthetic Codex fixture
36
+ identifiers, and stop labelling every omitted replay session as clean.
37
+ - Tests: 47 passing by default, one stress test skipped unless AGENTGUARD_STRESS=1.
38
+
3
39
  ## 0.2.2 (2026-09-03)
4
40
 
5
41
  - Codex: `status` and `init codex --write` now say, every time, that the
@@ -75,7 +111,7 @@ The cross-tool layer. One policy, one lock, one receipt across hosts.
75
111
  contention). Under 240 concurrent hook processes this admitted 41 to 46
76
112
  spawns against a cap of 40. Lock instances now carry a nonce; reclaims
77
113
  are verified against the instance judged; writes are fenced on the
78
- holder's nonce. 192 runs at 240 concurrent processes admit exactly 40.
114
+ holder's nonce. See 0.2.3 for the reproducible 240-process audit result.
79
115
  - A lock owner record could be read half-written and parsed as "held since
80
116
  1970". The record is now written atomically and malformed records are
81
117
  never trusted.
@@ -92,7 +128,8 @@ The cross-tool layer. One policy, one lock, one receipt across hosts.
92
128
 
93
129
  ### Unchanged
94
130
  - The Claude Code hook, transcript reader, detectors and thresholds. The
95
- existing 24 tests pass as they were.
131
+ original tests remain covered; 0.2.2 has 40 passing tests, and 0.2.3 has
132
+ 47 default passing tests plus one opt-in stress test.
96
133
 
97
134
  ## 0.1.1 (2026-09-03)
98
135
  - The artifact: block-digit hero, per-session sparklines with the STOP marked,
package/LICENSE CHANGED
@@ -2,13 +2,12 @@ AgentGuard(R) Burn - Alpha License
2
2
  Copyright (c) 2026 Dunecrest Ventures Inc.
3
3
 
4
4
  1. SCOPE.
5
- This software is the npm package @agentguard-run/spend, comprising every file
5
+ This software is the npm package @agentguard-run/burn, comprising every file
6
6
  present in an installed copy of that package. That includes the TypeScript
7
- sources under src/, the compiled output under dist/, the command-line entry
8
- point under bin/, the documentation under docs/, the fixtures under tests/, and
9
- the root files README.md, README.es-419.md, README.pt-BR.md, CHANGELOG.md,
10
- PATENTS.md and package.json. Any file the package ships that Section 3 does not
11
- expressly name is licensed under this Section 1; the enumeration above is
7
+ type definitions and compiled output under dist/src/, the command-line entry
8
+ point dist/src/cli.js, the fixtures under fixtures/, and the root files
9
+ README.md, CHANGELOG.md, LICENSE and package.json. Any file the package ships
10
+ that Section 3 does not expressly name is licensed under this Section 1; the enumeration above is
12
11
  illustrative, and adding a file to the package does not place it outside these
13
12
  terms.
14
13
 
@@ -40,8 +39,8 @@ Licensor. All patent rights are expressly reserved.
40
39
  The following assets, and ONLY these assets, are released under the BSD
41
40
  2-Clause License, reproduced in full below:
42
41
 
43
- - The documentation files README.md, README.es-419.md and README.pt-BR.md
44
- - The test fixture under tests/
42
+ - The documentation file README.md
43
+ - The test fixtures under fixtures/
45
44
 
46
45
  Copyright (c) 2026 Dunecrest Ventures Inc.
47
46
 
@@ -74,8 +73,8 @@ consistent with Section 2 above. The BSD 2-Clause License is used for these
74
73
  assets precisely because it is silent as to patents.
75
74
 
76
75
  Nothing else the package ships is included in the grant above. The code under
77
- src/, dist/ and bin/, the documentation under docs/, and the root files
78
- CHANGELOG.md and PATENTS.md remain licensed only under the alpha evaluation
76
+ dist/src/, and the root files CHANGELOG.md, LICENSE and package.json remain
77
+ licensed only under the alpha evaluation
79
78
  terms of Section 1. The TypeScript type definitions, policy engine, decision
80
79
  log, store implementation, cost table, and wrapper code are in that group.
81
80
 
package/README.md CHANGED
@@ -1,25 +1,80 @@
1
1
  # @agentguard-run/burn
2
2
 
3
- Local runaway-agent circuit breaker for AI coding agents.
3
+ See where a coding session's tokens went and what the next heavy turn could cost in time and limits. Burn explains recorded usage, warns about large cache rewrites and keeps its existing runaway-session circuit breaker. Everything stays on this machine.
4
4
 
5
- Raw token counters did not help. You see "360k tokens" and keep going. This
6
- tool does the three things a counter does not: it decides whether a session is
7
- pathological, it blocks the next agent spawn when it is, and it tells you what
8
- to do about it. Nothing leaves the machine.
5
+ ## Install in five lines
6
+
7
+ ```sh
8
+ npm i -g @agentguard-run/burn
9
+ agentguard-burn init claude
10
+ agentguard-burn init codex
11
+ agentguard-burn statusline
12
+ agentguard-burn why
13
+ ```
14
+
15
+ The two init commands print hook configuration for review. Merge the relevant snippet into your host configuration. They do not write it. For Codex, review and trust the hook through `/hooks`. Upgrading an existing hook requires the new snippet: it matches every tool so pace can update on ordinary tool calls, while admission still gates spawns.
16
+
17
+ For Claude Code, configure its `statusLine` command as `agentguard-burn statusline`. It receives the host JSON on stdin. Codex 0.154.0 has no external status command slot. Use `agentguard-burn statusline SESSION_ID` in a companion terminal and the native Codex limit indicators. See [host fields and setup](docs/USAGE_AND_PRICING.md#what-the-hosts-expose).
18
+
19
+ ## Why this session
20
+
21
+ ```sh
22
+ agentguard-burn why
23
+ agentguard-burn why SESSION_ID
24
+ agentguard-burn rewrites SESSION_ID
25
+ agentguard-burn rewrites all
26
+ agentguard-burn pace SESSION_ID
27
+ agentguard-burn pricing
28
+ ```
29
+
30
+ A session argument can also be a transcript path. Without one, Burn uses the session environment variable when available, then the most recently modified local transcript. Claude child transcripts are included in `why`. Token shares are shares of recorded tokens, not shares of dollars. Repeated usage records for one provider response count once. Copied Codex history is reconciled before counting new usage.
31
+
32
+ The table covers instruction stack + system, history re-sent, repeated file reads, subagent fan-out, tool output, conversation, full-prefix rewrites, output and unattributed usage. The fixed-prefix baseline is the first assistant response's measured context. It includes the initial user message, so the label is an operational baseline rather than a direct measurement of instruction files. A rewrite classified as prefix change resets that baseline and is counted in the footer; the new baseline can include history already present at that point.
33
+
34
+ Later cache reads up to the baseline go to instruction stack + system. Reads above it go to history re-sent. Fresh input plus cache creation is the measured arriving increment. Burn subtracts the previous response's output before assigning that increment to intervening tool results and user messages. Result byte sizes only split that measured total; bytes are never converted into tokens. A result for a previously read Read path goes to re-read files. Mixed user and tool intervals are marked shared and counted in the footer.
35
+
36
+ Rewrite tokens and child transcript usage keep their own buckets. Prior output deducted from an input increment remains in the unattributed residual so every recorded input and output token is counted exactly once. Missing event evidence also remains there. Integer allocation preserves every token, and the Method footer explains each row.
37
+
38
+ A full-prefix rewrite writes more than 150,000 tokens and more than half the actual input context. Burn reports the observed cache lifetime and surrounding evidence: idle beyond that lifetime, immediate compaction, first subagent response, or residual prefix change. An unknown lifetime stays unknown. Each write is priced at its measured lifetime, or a range when that lifetime is unavailable.
39
+
40
+ Every tool hook updates a local pace file under the Burn home. Pace uses ten wall-clock minutes: cached means cache reads; uncached means fresh input, writes and output. The next-hour projection assumes that pace continues. A time-to-limit estimate requires rising, fresh host percentage samples from the same quota pool and reset window. No token-to-limit conversion or cache weighting is assumed. Without sufficient host data, the line says why the limit is unknown.
41
+
42
+ The new warnings are advisory in both modes. Cache rewrites warn once when their last-hour API list equivalent exceeds $5. A context crossing 500,000 tokens warns about processing time and compaction, estimates a cache rewrite, and suggests a fresh session with a handoff note. These warnings do not deny a tool call.
43
+
44
+ Optional settings in your existing `burn-policy.json`:
45
+
46
+ ```json
47
+ {
48
+ "insights": {
49
+ "rewriteWarnDollarsPerHour": 5,
50
+ "heavyTurnTokens": 500000
51
+ }
52
+ }
53
+ ```
54
+
55
+ Merge these fields into the existing policy. All existing thresholds and modes remain supported. Prices are exact-model API list equivalents, not a subscription bill. Unknown models show tokens only. Every rate has a source and verification date. A local override file and full pricing table are documented in [Usage and pricing](docs/USAGE_AND_PRICING.md).
9
56
 
10
57
  ## Two safety planes
11
58
 
12
- Fitted against 412 real Claude Code sessions, which showed two independent
59
+ Originally fitted against 412 real Claude Code sessions, then replayed on
60
+ 2026-09-18 against 453 sessions. The history shows two independent
13
61
  failure shapes that one detector cannot cover:
14
62
 
15
63
  | Plane | Catches | Rule | Validated on |
16
64
  |---|---|---|---|
17
- | Structural | fan-out storms: many agents re-sending context | WARN 24 spawns, STOP before spawn 41, no agents-spawning-agents past depth 2 | 174- and 226-spawn sessions; STOP would have intercepted 91% of the runaway |
65
+ | Structural | fan-out storms: many agents re-sending context | WARN 24 spawns, STOP before spawn 41, no agents-spawning-agents past depth 2 | 59-, 190- and 226-spawn sessions; the 190-spawn session has a 4.35B post-STOP tail (92%) |
18
66
  | Economic | slow grinds: few agents, enormous total | WARN 3.5B, STOP 5B tokens per session | the 9.15B / 26-spawn session the spawn cap could not see |
19
67
 
20
- Across all 412 sessions: 3 STOP, 2 WARN, 407 clean. Zero false STOPs.
68
+ The 2026-09-18 replay snapshot: **5 STOP, 3 WARN, 445 clean across 453
69
+ sessions and 662 spawns**. Replay hero: **30%**. **10.80B** of **36.30B**
70
+ recorded tokens came after a STOP boundary; API-list scenario **$4,832**.
71
+ This is an upper bound assuming no override or restart. Replay does not label
72
+ false positives. Active histories can change the next replay result. The existing replay
73
+ counters retain their published behavior and can include repeated streaming
74
+ usage records. The new `why` report reconciles provider responses, so its
75
+ totals can differ. Do not treat replay as a reconciled invoice.
21
76
 
22
- Cache-read ratio was 98% on every session, healthy and pathological alike. It
77
+ Cache-read ratio was about 98% in the original calibration, healthy and pathological alike. It
23
78
  is shown as an explanation and never used to decide.
24
79
 
25
80
  ## Start with replay, not with the hook
@@ -80,7 +135,7 @@ What each host can actually see is stated, not implied:
80
135
  | Ollama proxy | none | none | authoritative | `prompt_eval_count` + `eval_count` on the final chunk |
81
136
  | vLLM / LM Studio / OpenAI-compatible proxy | none | none | authoritative when the server sends `usage`, else reported missing | non-streaming `usage`, or the final SSE usage event |
82
137
  | Cursor (beta) | authoritative | estimated | none | native `subagentStart` deny; hosted-model usage is never exposed |
83
- | Codex (beta) | authoritative | estimated | estimated | `PreToolUse` on `spawn_agent`; live deny, allow and override canary passed on codex-cli 0.151.0; transcript parsed best-effort |
138
+ | Codex (beta) | authoritative | estimated | estimated | `PreToolUse` on every tool, with admission on `spawn_agent`; live deny, allow and override canary passed on codex-cli 0.151.0; transcript parsed best-effort |
84
139
 
85
140
  An `OK` from a host that cannot see usage is an OK about spawns, and `status`
86
141
  says `usage:n/a` next to it. Missing usage never becomes a guessed zero.
@@ -185,22 +240,47 @@ Ten parallel `Agent` calls launch ten hook processes that all read the same
185
240
  transcript and all see the same count. A naive cap is cosmetic during exactly
186
241
  the burst it exists for. Spawns are admitted through an atomic, cross-process
187
242
  reservation under a machine-wide lock; the test suite launches 60 real OS
188
- processes against a cap of 40 and asserts exactly 40 are admitted, through
189
- the Claude hook and again through the Cursor hook.
243
+ reservation processes against a cap of 40 and asserts exactly 40 are admitted,
244
+ and separately exercises 60 Cursor hook processes. The opt-in stress test runs
245
+ 240 actual Claude hook processes against cap 40:
246
+
247
+ ```sh
248
+ npm run build
249
+ AGENTGUARD_STRESS=1 node --test dist/tests/stress.test.js
250
+ ```
251
+
252
+ It checks exactly 40 admitted and 200 denied, verifies the receipt chain for
253
+ coordinated decisions, and reports lock failures separately. Infrastructure
254
+ failures deny in enforce mode before a receipt can be chained, as in the gateway.
190
255
 
191
256
  0.2.0 fixed the lock itself. Under 240 concurrent hook processes the 0.1
192
257
  lock could tear down a live sibling's lock (a waiter judged "owner is dead"
193
258
  about an instance that had already been released and replaced) and admit
194
259
  41 to 46. Lock instances now carry a nonce; a reclaim only counts if it
195
260
  grabbed the instance it judged, and every write is fenced on the holder's
196
- own nonce still being on the path. 192 runs at 240 concurrent processes:
197
- exactly 40, every time.
261
+ own nonce still being on the path. In 0.2.3, bounded, staggered retries prevent
262
+ lock waiters from starving the holder. The Sep 15 audit includes one successful
263
+ 240-process run after that fix; it does not establish a 192-run guarantee.
264
+ In 0.2.4, the wait is eight seconds and retirement is serialized before
265
+ renaming an abandoned lock. The fresh 240-process run admitted exactly 40,
266
+ denied 200, signed 240 receipts and had zero lock failures. Operational
267
+ lock-wait failures do not count toward enforce eligibility. See the
268
+ [conservative recovery procedure](docs/USAGE_AND_PRICING.md#reservation-lock-recovery).
198
269
 
199
270
  Single-machine by design. Two laptops on one account do not share state, and
200
271
  that is stated rather than hidden.
201
272
 
273
+ ## Account thresholds
274
+
275
+ `account.warnConcurrentSessions` warns when more than that many sessions have
276
+ been active on this machine in the last 30 minutes, matching `status` liveness.
277
+ Optional `account.warnTokens` and `account.stopTokens` sum the last
278
+ `account.windowActiveMinutes` of token buckets from each active session.
279
+ Both token limits default to `null`. Closed gateway sessions are excluded;
280
+ hook and gateway session files are both read. WARN never blocks. STOP records
281
+ a would-block decision in shadow and blocks new work only in enforce mode.
282
+
202
283
  ## What it never does
203
284
 
204
285
  No prompts, responses, file contents, or tool inputs are persisted or rendered.
205
- No telemetry. No provider-quota guesses: it projects against your configured
206
- policy, never against a subscription allowance it cannot see.
286
+ No telemetry. No provider-quota guesses. Account forecasts use only observed host percentages, never an invented allowance or cache weighting.
@@ -33,11 +33,21 @@ const node_crypto_1 = require("node:crypto");
33
33
  const node_fs_1 = require("node:fs");
34
34
  const events_1 = require("../events");
35
35
  const render_1 = require("../replay/render");
36
+ const live_1 = require("../insights/live");
37
+ const pre_tool_use_1 = require("../hook/pre-tool-use");
36
38
  /** Fields this renderer must never emit. Codex rejects the whole object if they appear. */
37
39
  exports.CODEX_FORBIDDEN_FIELDS = ['continue', 'stopReason', 'suppressOutput', 'decision', 'reason'];
38
40
  const HOST = 'codex';
39
41
  const SPAWN_TOOLS = new Set(['Agent', 'spawn_agent', 'Task']);
40
42
  function handleCodexHook(raw, gateway, now = Date.now()) {
43
+ const parsed = parse(raw);
44
+ if (!parsed)
45
+ return {};
46
+ const observation = (0, live_1.observeTool)(gateway.dataDirectory, { session_id: parsed.sessionId, transcript_path: parsed.transcriptPath, tool_name: parsed.toolName }, 'codex', (0, pre_tool_use_1.loadPolicy)(gateway.dataDirectory), now);
47
+ const output = parsed.event === 'PreToolUse' && SPAWN_TOOLS.has(parsed.toolName) ? handleCodexSpawnHook(raw, gateway, now) : {};
48
+ return observation.messages.length ? { ...output, systemMessage: [output.systemMessage, ...observation.messages].filter(Boolean).join('\n') } : output;
49
+ }
50
+ function handleCodexSpawnHook(raw, gateway, now) {
41
51
  const input = parse(raw);
42
52
  if (!input)
43
53
  return {};
@@ -190,7 +200,7 @@ function walk(value, path) {
190
200
  function codexHooksSnippet(command) {
191
201
  return {
192
202
  hooks: {
193
- PreToolUse: [{ matcher: '^(Agent|spawn_agent)$', hooks: [{ type: 'command', command, timeout: 5 }] }],
203
+ PreToolUse: [{ matcher: '.*', hooks: [{ type: 'command', command, timeout: 15 }] }],
194
204
  },
195
205
  };
196
206
  }
@@ -127,8 +127,8 @@ function cursorHooksSnippet(command) {
127
127
  return {
128
128
  version: 1,
129
129
  hooks: {
130
- subagentStart: [{ command, timeout: 5, failClosed: true }],
131
- subagentStop: [{ command, timeout: 5 }],
130
+ subagentStart: [{ command, timeout: 15, failClosed: true }],
131
+ subagentStop: [{ command, timeout: 15 }],
132
132
  sessionEnd: [{ command, timeout: 3 }],
133
133
  },
134
134
  };
package/dist/src/cli.js CHANGED
@@ -33,6 +33,11 @@ const server_1 = require("./proxy/server");
33
33
  const render_1 = require("./replay/render");
34
34
  const simulate_1 = require("./replay/simulate");
35
35
  const status_1 = require("./status");
36
+ const eligibility_1 = require("./eligibility");
37
+ const sessions_1 = require("./insights/sessions");
38
+ const transcript_1 = require("./insights/transcript");
39
+ const live_1 = require("./insights/live");
40
+ const render_2 = require("./insights/render");
36
41
  const HOME = process.env.AGENTGUARD_HOME ?? (0, node_path_1.join)((0, node_os_1.homedir)(), '.agentguard');
37
42
  const PROXY_HOSTS = ['ollama', 'vllm', 'lm-studio', 'openai-compatible'];
38
43
  function readStdinJson() {
@@ -66,18 +71,7 @@ function readDecisions() {
66
71
  .filter((x) => x !== null);
67
72
  }
68
73
  function shadowEligibility() {
69
- const decisions = readDecisions();
70
- const first = decisions.length ? Number(decisions[0].at) : Date.now();
71
- const days = (Date.now() - first) / 86_400_000;
72
- const wouldBlock = decisions.filter((d) => d.wouldDeny === true).length;
73
- const warns = decisions.filter((d) => d.verdict === 'WARN').length;
74
- return {
75
- eligible: decisions.length >= defaults_1.SHADOW_MIN_DECISIONS && days >= defaults_1.SHADOW_MIN_DAYS,
76
- decisions: decisions.length,
77
- days,
78
- wouldBlock,
79
- warns,
80
- };
74
+ return (0, eligibility_1.computeShadowEligibility)(readDecisions());
81
75
  }
82
76
  async function main(argv) {
83
77
  const [command, ...rest] = argv;
@@ -87,6 +81,33 @@ async function main(argv) {
87
81
  };
88
82
  const has = (name) => rest.includes(name);
89
83
  switch (command) {
84
+ case 'why': {
85
+ const location = (0, sessions_1.selectInsightTranscript)(rest.find(value => value !== 'json'));
86
+ const transcript = (0, transcript_1.readInsightSession)(location.path, { host: location.host, sessionId: location.sessionId });
87
+ process.stdout.write(rest.includes('json') ? JSON.stringify(transcript, null, 2) + '\n' : (0, render_2.renderWhy)(transcript, (0, live_1.readPricing)(HOME, (0, pre_tool_use_1.loadPolicy)(HOME))) + '\n');
88
+ return 0;
89
+ }
90
+ case 'rewrites': {
91
+ const session = rest.find(value => value !== 'json');
92
+ const locations = session === 'all' ? (0, sessions_1.discoverInsightTranscripts)() : [(0, sessions_1.selectInsightTranscript)(session)];
93
+ const transcripts = locations.map(location => (0, transcript_1.readInsightTranscript)(location.path, { host: location.host, sessionId: location.sessionId }));
94
+ const rates = (0, live_1.readPricing)(HOME, (0, pre_tool_use_1.loadPolicy)(HOME));
95
+ process.stdout.write(rest.includes('json') ? JSON.stringify((0, render_2.rewriteReport)(transcripts, rates), null, 2) + '\n' : (0, render_2.renderRewrites)(transcripts, rates) + '\n');
96
+ return 0;
97
+ }
98
+ case 'pricing': {
99
+ process.stdout.write((0, render_2.renderPricing)((0, live_1.readPricing)(HOME, (0, pre_tool_use_1.loadPolicy)(HOME))) + '\n');
100
+ return 0;
101
+ }
102
+ case 'statusline':
103
+ case 'pace': {
104
+ const raw = command === 'statusline' && !process.stdin.isTTY ? readStdinJson() : null;
105
+ const payload = raw && typeof raw === 'object' ? raw : {};
106
+ const location = (0, sessions_1.selectInsightTranscript)(payload.transcript_path || rest[0] || payload.session_id);
107
+ const observation = (0, live_1.observeTool)(HOME, { ...payload, session_id: payload.session_id || location.sessionId, transcript_path: location.path }, location.host, (0, pre_tool_use_1.loadPolicy)(HOME));
108
+ process.stdout.write((observation.snapshot ? (0, live_1.renderStatusLine)(observation.snapshot) : 'AgentGuard pace unavailable: local usage metadata is not readable.') + '\n');
109
+ return observation.snapshot ? 0 : 1;
110
+ }
90
111
  case 'hook': {
91
112
  const raw = (0, node_fs_1.readFileSync)(0, 'utf8');
92
113
  let input = {};
@@ -141,6 +162,7 @@ async function main(argv) {
141
162
  `shadow observation: ${e.decisions} decisions over ${e.days.toFixed(1)} days`,
142
163
  ` would have warned: ${e.warns} would have blocked: ${e.wouldBlock}`,
143
164
  `eligible for enforcement: ${e.eligible ? 'yes (agentguard-burn enforce)' : `no (need ${defaults_1.SHADOW_MIN_DECISIONS} decisions and ${defaults_1.SHADOW_MIN_DAYS} days)`}`,
165
+ `lock-wait failures excluded from eligibility: ${e.excludedLockWaitFailures}`,
144
166
  override ? `override ACTIVE: ${override.once ? 'the next STOP' : `every STOP until ${new Date(override.until).toLocaleTimeString()}`} passes ("${override.reason}")` : '',
145
167
  '',
146
168
  (0, status_1.renderHostHealth)(['claude', 'cursor', 'codex'].map((h) => (0, install_1.health)(h))),
@@ -270,7 +292,12 @@ async function main(argv) {
270
292
  return 0;
271
293
  }
272
294
  default:
273
- process.stdout.write('agentguard-burn <replay|calibrate|status|init|enforce|shadow|resume|proxy|conformance>\n' +
295
+ process.stdout.write('agentguard-burn <why|rewrites|pace|statusline|pricing|replay|calibrate|status|init|enforce|shadow|resume|proxy|conformance>\n' +
296
+ ' why [session] token attribution and API list-price equivalents\n' +
297
+ ' rewrites [session|all] full-prefix cache writes by cause\n' +
298
+ ' pace [session] ten-minute pace and next-hour projection\n' +
299
+ ' statusline [session] one-line local status; accepts Claude status JSON\n' +
300
+ ' pricing verified model rates and local overrides\n' +
274
301
  ' replay [files...] [--json] [--top N] [--min-tokens N]\n' +
275
302
  ' calibrate fit thresholds to your history (writes shadow policy)\n' +
276
303
  ' status mode, shadow observations, eligibility, hook health, every host\n' +
@@ -37,7 +37,9 @@ const GRIND_WARN_CALL = Math.floor(defaults_1.DEFAULT_THRESHOLDS.sustained.warnT
37
37
  const GRIND_STOP_CALL = Math.floor(defaults_1.DEFAULT_THRESHOLDS.sustained.stopTokens / CALL_TOKENS) + 1; // 21
38
38
  function freshHome() {
39
39
  const home = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'agb-conf-'));
40
- const policy = { mode: 'enforce', thresholds: defaults_1.DEFAULT_THRESHOLDS };
40
+ // These fixtures share a home to inspect all receipts. Test only the
41
+ // session boundaries here; account concurrency has its own integration tests.
42
+ const policy = { mode: 'enforce', thresholds: { ...defaults_1.DEFAULT_THRESHOLDS, account: { ...defaults_1.DEFAULT_THRESHOLDS.account, warnConcurrentSessions: Number.MAX_SAFE_INTEGER } } };
41
43
  (0, node_fs_1.writeFileSync)((0, node_path_1.join)(home, 'burn-policy.json'), JSON.stringify(policy));
42
44
  return home;
43
45
  }
@@ -2,14 +2,15 @@ import type { Policy, Thresholds } from './types';
2
2
  /**
3
3
  * Shipped thresholds.
4
4
  *
5
- * These are not intuition. They were fitted against 412 real Claude Code
6
- * sessions on the author's machine on 2026-09-03:
7
- *
8
- * fan-out 24/40 : on the 172-spawn runaway, STOP at spawn 40 would have
9
- * prevented 91% of that session's tokens.
10
- * sustained 3.5B/5B : across all 412 sessions this produced exactly 1 STOP,
11
- * the 9.15B slow grind the fan-out cap missed, 2 WARNs, and
12
- * 409 clean sessions. Zero false STOPs.
5
+ * Originally fitted against 412 real Claude Code sessions. Replayed on the
6
+ * author's machine on 2026-09-18: 453 sessions, 662 spawns, 36.30B recorded tokens;
7
+ * 5 STOP, 3 WARN, 445 clean. The recorded post-STOP tail is 10.80B (30%).
8
+ * Legacy replay counters can include repeated streaming usage records. The
9
+ * new insights report reconciles provider responses instead.
10
+ * Fan-out 24/40 stops before spawn 41 on the 59-, 190- and 226-spawn
11
+ * sessions; sustained 3.5B/5B catches the 9.15B / 26-spawn slow grind.
12
+ * This is a dated replay snapshot and an upper bound, not labelled evidence
13
+ * of false-positive rates or guaranteed savings.
13
14
  *
14
15
  * A previous draft shipped a 50M session budget. Routine sessions on the same
15
16
  * machine run 1.4B to 3.5B, so that default would have fired within minutes on
@@ -4,14 +4,15 @@ exports.SHADOW_MIN_DAYS = exports.SHADOW_MIN_DECISIONS = exports.ACTIVE_GAP_CAP_
4
4
  /**
5
5
  * Shipped thresholds.
6
6
  *
7
- * These are not intuition. They were fitted against 412 real Claude Code
8
- * sessions on the author's machine on 2026-09-03:
9
- *
10
- * fan-out 24/40 : on the 172-spawn runaway, STOP at spawn 40 would have
11
- * prevented 91% of that session's tokens.
12
- * sustained 3.5B/5B : across all 412 sessions this produced exactly 1 STOP,
13
- * the 9.15B slow grind the fan-out cap missed, 2 WARNs, and
14
- * 409 clean sessions. Zero false STOPs.
7
+ * Originally fitted against 412 real Claude Code sessions. Replayed on the
8
+ * author's machine on 2026-09-18: 453 sessions, 662 spawns, 36.30B recorded tokens;
9
+ * 5 STOP, 3 WARN, 445 clean. The recorded post-STOP tail is 10.80B (30%).
10
+ * Legacy replay counters can include repeated streaming usage records. The
11
+ * new insights report reconciles provider responses instead.
12
+ * Fan-out 24/40 stops before spawn 41 on the 59-, 190- and 226-spawn
13
+ * sessions; sustained 3.5B/5B catches the 9.15B / 26-spawn slow grind.
14
+ * This is a dated replay snapshot and an upper bound, not labelled evidence
15
+ * of false-positive rates or guaranteed savings.
15
16
  *
16
17
  * A previous draft shipped a 50M session budget. Routine sessions on the same
17
18
  * machine run 1.4B to 3.5B, so that default would have fired within minutes on
@@ -4,7 +4,7 @@
4
4
  * Two independent safety planes, because the data showed two independent
5
5
  * failure modes:
6
6
  *
7
- * structural - fan-out. Many agents, each re-sending context. The 172-spawn
7
+ * structural - fan-out. Many agents, each re-sending context. The 190-spawn
8
8
  * session. Caught by an absolute spawn cap.
9
9
  * economic - sustained burn. Few agents, long session, enormous total. The
10
10
  * 9.15B session with only 26 spawns, which a spawn cap cannot
@@ -21,5 +21,8 @@ import type { BurnReport, SessionState, Thresholds } from '../types';
21
21
  * would have if the pending spawn were allowed; the decision must be about the
22
22
  * proposal, not about a violation the transcript has already recorded.
23
23
  */
24
- export declare function evaluate(state: SessionState, thresholds: Thresholds, proposedSpawnDepth?: number | null): BurnReport;
24
+ export declare function evaluate(state: SessionState, thresholds: Thresholds, proposedSpawnDepth?: number | null, account?: {
25
+ sessions: SessionState[];
26
+ now: number;
27
+ }): BurnReport;
25
28
  export declare function fmt(n: number): string;
@@ -5,7 +5,7 @@
5
5
  * Two independent safety planes, because the data showed two independent
6
6
  * failure modes:
7
7
  *
8
- * structural - fan-out. Many agents, each re-sending context. The 172-spawn
8
+ * structural - fan-out. Many agents, each re-sending context. The 190-spawn
9
9
  * session. Caught by an absolute spawn cap.
10
10
  * economic - sustained burn. Few agents, long session, enormous total. The
11
11
  * 9.15B session with only 26 spawns, which a spawn cap cannot
@@ -19,6 +19,7 @@
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
20
  exports.evaluate = evaluate;
21
21
  exports.fmt = fmt;
22
+ const account_1 = require("../state/account");
22
23
  const session_1 = require("../state/session");
23
24
  const RANK = { OK: 0, WARN: 1, STOP: 2 };
24
25
  function worst(a, b) {
@@ -29,7 +30,7 @@ function worst(a, b) {
29
30
  * would have if the pending spawn were allowed; the decision must be about the
30
31
  * proposal, not about a violation the transcript has already recorded.
31
32
  */
32
- function evaluate(state, thresholds, proposedSpawnDepth = null) {
33
+ function evaluate(state, thresholds, proposedSpawnDepth = null, account) {
33
34
  const findings = [];
34
35
  let verdict = 'OK';
35
36
  // ---- structural plane: fan-out --------------------------------------
@@ -132,6 +133,27 @@ function evaluate(state, thresholds, proposedSpawnDepth = null) {
132
133
  });
133
134
  verdict = worst(verdict, 'WARN');
134
135
  }
136
+ // Machine activity uses status's 30-minute liveness window. Token budgets
137
+ // sum each live session's last configured number of active-minute buckets.
138
+ if (account && thresholds.account) {
139
+ const t = thresholds.account;
140
+ const live = account.sessions.filter((s) => account.now - s.lastEventAt <= account_1.ACTIVE_SESSION_WINDOW_MS);
141
+ if (live.length > t.warnConcurrentSessions) {
142
+ findings.push({ detector: 'account', verdict: 'WARN',
143
+ summary: `${live.length} concurrent active sessions on this machine; warning threshold is ${t.warnConcurrentSessions}.`,
144
+ observed: live.length, threshold: t.warnConcurrentSessions });
145
+ verdict = worst(verdict, 'WARN');
146
+ }
147
+ const tokens = live.reduce((sum, s) => sum + (0, session_1.windowSum)(s.tokensByActiveMinute, s.activeMinutes, t.windowActiveMinutes), 0);
148
+ const severity = t.stopTokens != null && tokens >= t.stopTokens ? 'STOP'
149
+ : t.warnTokens != null && tokens >= t.warnTokens ? 'WARN' : null;
150
+ if (severity) {
151
+ findings.push({ detector: 'account', verdict: severity,
152
+ summary: `Active sessions consumed ${fmt(tokens)} tokens over their last ${t.windowActiveMinutes} active minutes.`,
153
+ observed: tokens, threshold: (severity === 'STOP' ? t.stopTokens : t.warnTokens) });
154
+ verdict = worst(verdict, severity);
155
+ }
156
+ }
135
157
  // ---- advisory: duplicate work --------------------------------------
136
158
  let duplicated = 0;
137
159
  let worstSurfaceReaders = 0;
@@ -192,6 +214,8 @@ function prescribe(state, findings, t) {
192
214
  if (has('burn_debt')) {
193
215
  out.push('Burn is sustained above your own baseline. Narrow the task or split it into a new session.');
194
216
  }
217
+ if (has('account'))
218
+ out.push('Pause new work across active sessions on this machine until the account window clears.');
195
219
  if (has('duplicate_work')) {
196
220
  out.push('Assign one reader per file and pass a short digest down, instead of letting every agent re-read it.');
197
221
  }
@@ -0,0 +1,17 @@
1
+ export interface ShadowEligibility {
2
+ eligible: boolean;
3
+ decisions: number;
4
+ days: number;
5
+ wouldBlock: number;
6
+ warns: number;
7
+ excludedLockWaitFailures: number;
8
+ }
9
+ /** Existing lock-failure ledger rows remain readable without a schema change. */
10
+ export declare function isLockWaitFailure(row: Readonly<Record<string, unknown>>): boolean;
11
+ /**
12
+ * A reservation-lock timeout is an operational failure, not an observation of
13
+ * a threshold. Keep it in the audit ledger but do not let a burst of timeouts
14
+ * satisfy the observation count or start the enforcement waiting period.
15
+ * Older decision rows need no new fields and retain their existing behavior.
16
+ */
17
+ export declare function computeShadowEligibility(rows: ReadonlyArray<Readonly<Record<string, unknown>>>, now?: number): ShadowEligibility;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isLockWaitFailure = isLockWaitFailure;
4
+ exports.computeShadowEligibility = computeShadowEligibility;
5
+ const defaults_1 = require("./defaults");
6
+ /** Existing lock-failure ledger rows remain readable without a schema change. */
7
+ function isLockWaitFailure(row) {
8
+ return row.failClosed === true && typeof row.reason === 'string'
9
+ && /\bcould not acquire (?:the )?reservation lock\b/i.test(row.reason);
10
+ }
11
+ /**
12
+ * A reservation-lock timeout is an operational failure, not an observation of
13
+ * a threshold. Keep it in the audit ledger but do not let a burst of timeouts
14
+ * satisfy the observation count or start the enforcement waiting period.
15
+ * Older decision rows need no new fields and retain their existing behavior.
16
+ */
17
+ function computeShadowEligibility(rows, now = Date.now()) {
18
+ const decisions = rows.filter(row => !isLockWaitFailure(row));
19
+ const first = decisions.length ? Number(decisions[0].at) : now;
20
+ const days = (now - first) / 86_400_000;
21
+ return {
22
+ eligible: decisions.length >= defaults_1.SHADOW_MIN_DECISIONS && days >= defaults_1.SHADOW_MIN_DAYS,
23
+ decisions: decisions.length,
24
+ days,
25
+ wouldBlock: decisions.filter(row => row.wouldDeny === true).length,
26
+ warns: decisions.filter(row => row.verdict === 'WARN').length,
27
+ excludedLockWaitFailures: rows.length - decisions.length,
28
+ };
29
+ }
@@ -80,6 +80,8 @@ export interface GatewayOptions {
80
80
  }
81
81
  export declare class Gateway {
82
82
  private readonly home;
83
+ /** Shared local storage for optional content-free usage observations. */
84
+ get dataDirectory(): string;
83
85
  private readonly store;
84
86
  private readonly signer;
85
87
  private readonly now;