@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.
- package/CHANGELOG.md +30 -2
- package/README.md +105 -20
- package/dist/src/adapters/codex.js +11 -1
- package/dist/src/adapters/cursor.js +2 -2
- package/dist/src/calibrate.js +2 -3
- package/dist/src/cli.js +53 -15
- package/dist/src/conformance.d.ts +5 -2
- package/dist/src/conformance.js +30 -17
- package/dist/src/defaults.d.ts +7 -4
- package/dist/src/defaults.js +9 -6
- package/dist/src/detectors/evaluate.d.ts +4 -5
- package/dist/src/detectors/evaluate.js +13 -11
- package/dist/src/eligibility.d.ts +17 -0
- package/dist/src/eligibility.js +29 -0
- package/dist/src/gateway.d.ts +2 -0
- package/dist/src/gateway.js +4 -8
- package/dist/src/history/claude-transcript.d.ts +20 -2
- package/dist/src/history/claude-transcript.js +56 -15
- package/dist/src/hook/pre-tool-use.d.ts +13 -9
- package/dist/src/hook/pre-tool-use.js +63 -31
- package/dist/src/insights/attribution.d.ts +4 -0
- package/dist/src/insights/attribution.js +151 -0
- package/dist/src/insights/blocks.d.ts +61 -0
- package/dist/src/insights/blocks.js +243 -0
- package/dist/src/insights/live.d.ts +53 -0
- package/dist/src/insights/live.js +211 -0
- package/dist/src/insights/pace.d.ts +34 -0
- package/dist/src/insights/pace.js +54 -0
- package/dist/src/insights/pricing.d.ts +48 -0
- package/dist/src/insights/pricing.js +139 -0
- package/dist/src/insights/render.d.ts +8 -0
- package/dist/src/insights/render.js +126 -0
- package/dist/src/insights/sessions.d.ts +12 -0
- package/dist/src/insights/sessions.js +51 -0
- package/dist/src/insights/transcript.d.ts +14 -0
- package/dist/src/insights/transcript.js +505 -0
- package/dist/src/insights/types.d.ts +164 -0
- package/dist/src/insights/types.js +4 -0
- package/dist/src/install.js +14 -5
- package/dist/src/policy.d.ts +4 -0
- package/dist/src/policy.js +57 -0
- package/dist/src/replay/render.js +4 -2
- package/dist/src/replay/simulate.d.ts +5 -0
- package/dist/src/replay/simulate.js +23 -8
- package/dist/src/state/reservations.d.ts +7 -5
- package/dist/src/state/reservations.js +60 -45
- package/dist/src/state/spawn-window.d.ts +10 -0
- package/dist/src/state/spawn-window.js +25 -0
- package/dist/src/types.d.ts +8 -1
- package/docs/USAGE_AND_PRICING.md +132 -0
- package/fixtures/usage-dedup-session/subagents/agent-synthetic-first.jsonl +5 -0
- package/fixtures/usage-dedup-session/subagents/agent-synthetic-second.jsonl +4 -0
- package/fixtures/usage-dedup-session.jsonl +4 -0
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.6 (2026-09-19)
|
|
4
|
+
|
|
5
|
+
- Fix inflated enforcement usage: repeated assistant content blocks and copied child histories now share the same provider response identity as `why`. Persist only usage deltas; rebuild old counters once without replacing their receipt chain head.
|
|
6
|
+
- Fix lifetime fan-out blocking: lifetime count is WARN only, the stop ceiling uses 120 active minutes, and spawn rate defaults to enforcement at 16 proposals within 15 active minutes. Depth above 2 still stops. Atomic reservations use both active windows, including parallel proposals.
|
|
7
|
+
- Fill missing policy fields from defaults with one notice. Preserve explicit settings, including older advisory spawn-rate policies, and bind the normalized policy into new receipt digests.
|
|
8
|
+
- Keep sustained thresholds at WARN 3.5B and STOP 5B. They now fire later because duplicate usage no longer advances the counters. Calibration uses corrected usage and excludes bursts rather than long lifetime spawn counts.
|
|
9
|
+
- Add `blocks [session|all]` with signed receipt context and measured child tokens and list-price economics, separating forks and fresh agents. Reuse the local attribution parser and existing verified pricing table; no transcript contents enter receipts.
|
|
10
|
+
- Keep hook WARN and STOP card text, layout and branding unchanged.
|
|
11
|
+
- Tests: 150 passing by default, one opt-in stress test skipped. The separate 240-process stress run admits exactly 40, denies 200, signs 240 receipts and has no lock failures. All 14 conformance checks pass.
|
|
12
|
+
|
|
13
|
+
## 0.2.5 (2026-09-18)
|
|
14
|
+
|
|
15
|
+
- 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.
|
|
16
|
+
- 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.
|
|
17
|
+
- 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.
|
|
18
|
+
- Validate every bucket against a constructed session, including UTF-8 weights, integer rounding, prefix changes, duplicate events and per-category conservation.
|
|
19
|
+
- Tests: 120 passing by default, one opt-in stress test skipped.
|
|
20
|
+
|
|
21
|
+
## 0.2.4 (2026-09-18)
|
|
22
|
+
|
|
23
|
+
- 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.
|
|
24
|
+
- 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.
|
|
25
|
+
- 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.
|
|
26
|
+
- Bundle dated model pricing and local overrides. Keep unknown models unpriced, content out of persisted metadata, and new reporting paths free of network calls.
|
|
27
|
+
- 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.
|
|
28
|
+
- Validate a 240-process burst: exactly 40 admitted, 200 denied, 240 signed receipts, zero lock failures and a valid chain.
|
|
29
|
+
- Tests: 106 passing by default, one opt-in stress test skipped. The separate stress run also passes.
|
|
30
|
+
|
|
1
31
|
## 0.2.3 (2026-09-15)
|
|
2
32
|
|
|
3
33
|
- Replay evidence refreshed from this machine: 4 STOP, 3 WARN, 431 clean across
|
|
@@ -16,8 +46,6 @@
|
|
|
16
46
|
identifiers, and stop labelling every omitted replay session as clean.
|
|
17
47
|
- Tests: 47 passing by default, one stress test skipped unless AGENTGUARD_STRESS=1.
|
|
18
48
|
|
|
19
|
-
# Changelog
|
|
20
|
-
|
|
21
49
|
## 0.2.2 (2026-09-03)
|
|
22
50
|
|
|
23
51
|
- Codex: `status` and `init codex --write` now say, every time, that the
|
package/README.md
CHANGED
|
@@ -1,28 +1,109 @@
|
|
|
1
1
|
# @agentguard-run/burn
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
59
|
+
Structural limits use active time rather than the age of the session. The
|
|
60
|
+
lifetime spawn count warns at 24 but cannot stop a session on its own. The
|
|
61
|
+
fan-out ceiling permits 40 spawns within the last 120 active minutes and
|
|
62
|
+
stops the next proposal. The separate spawn-rate rule warns at 8 and stops
|
|
63
|
+
candidate 16 within 15 active minutes. Depth above 2 still stops.
|
|
64
|
+
|
|
65
|
+
Economic limits warn at 3.5B and stop at 5B measured tokens per session.
|
|
66
|
+
Repeated Claude assistant content blocks share one provider response id.
|
|
67
|
+
Their usage and copied child history count once, including later increases
|
|
68
|
+
to a response's recorded usage. Old session counters rebuild once from the
|
|
69
|
+
parent and its available child transcripts, retaining the signed chain head.
|
|
70
|
+
The rebuild places events chronologically. A child usage record discovered
|
|
71
|
+
late on a later hook enters the current active-minute bucket, while session
|
|
72
|
+
and bucket totals remain conserved.
|
|
73
|
+
The same numeric sustained thresholds now fire later because inflated usage
|
|
74
|
+
has been removed. Earlier replay figures in the changelog used the old counts.
|
|
75
|
+
|
|
76
|
+
Existing policy files keep explicit settings. Missing fields are filled in
|
|
77
|
+
memory, with a one-time notice. In particular, an old explicit
|
|
78
|
+
`spawnRate.enforce: false` remains advisory until you change it. New policies
|
|
79
|
+
default to `true`; `fanout.windowActiveMinutes` defaults to 120. Run calibration
|
|
80
|
+
only when you want it to write a new shadow policy, or set `AGENTGUARD_HOME`
|
|
81
|
+
to a scratch directory to review fitted values first.
|
|
82
|
+
|
|
83
|
+
## Price a recorded block
|
|
15
84
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
85
|
+
```sh
|
|
86
|
+
agentguard-burn blocks
|
|
87
|
+
agentguard-burn blocks SESSION_ID
|
|
88
|
+
agentguard-burn blocks all
|
|
89
|
+
```
|
|
20
90
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
91
|
+
Each WARN or STOP receipt gets one row with its time, session, detector
|
|
92
|
+
reasons and spawn ordinal. The row includes the session's available child
|
|
93
|
+
count, median and maximum tokens, and median and maximum API list-price
|
|
94
|
+
dollars per child. Forks and fresh agents are reported separately. The
|
|
95
|
+
report reuses the attribution parser and excludes copied provider responses.
|
|
96
|
+
It reads recorded usage only and makes no network calls.
|
|
97
|
+
|
|
98
|
+
These are list-price equivalents, not a bill or a claim of money saved.
|
|
99
|
+
Economics describe the currently available child transcripts of that session,
|
|
100
|
+
not only the children completed before the receipt. Missing child history or
|
|
101
|
+
unknown model pricing stays unavailable. Unknown cache lifetimes produce a
|
|
102
|
+
price range. A fork is identified by its transcript's fork metadata. Codex child discovery
|
|
103
|
+
requires an explicit parent thread id; unknown inherited-history boundaries
|
|
104
|
+
stay unavailable. Use `blocks all json` for structured output. Session
|
|
105
|
+
attribution uses local receipt identifiers without copying transcript content
|
|
106
|
+
into the ledger.
|
|
26
107
|
|
|
27
108
|
Cache-read ratio was about 98% in the original calibration, healthy and pathological alike. It
|
|
28
109
|
is shown as an explanation and never used to decide.
|
|
@@ -85,7 +166,7 @@ What each host can actually see is stated, not implied:
|
|
|
85
166
|
| Ollama proxy | none | none | authoritative | `prompt_eval_count` + `eval_count` on the final chunk |
|
|
86
167
|
| 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 |
|
|
87
168
|
| Cursor (beta) | authoritative | estimated | none | native `subagentStart` deny; hosted-model usage is never exposed |
|
|
88
|
-
| 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 |
|
|
169
|
+
| 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 |
|
|
89
170
|
|
|
90
171
|
An `OK` from a host that cannot see usage is an OK about spawns, and `status`
|
|
91
172
|
says `usage:n/a` next to it. Missing usage never becomes a guessed zero.
|
|
@@ -211,6 +292,11 @@ grabbed the instance it judged, and every write is fenced on the holder's
|
|
|
211
292
|
own nonce still being on the path. In 0.2.3, bounded, staggered retries prevent
|
|
212
293
|
lock waiters from starving the holder. The Sep 15 audit includes one successful
|
|
213
294
|
240-process run after that fix; it does not establish a 192-run guarantee.
|
|
295
|
+
In 0.2.4, the wait is eight seconds and retirement is serialized before
|
|
296
|
+
renaming an abandoned lock. The fresh 240-process run admitted exactly 40,
|
|
297
|
+
denied 200, signed 240 receipts and had zero lock failures. Operational
|
|
298
|
+
lock-wait failures do not count toward enforce eligibility. See the
|
|
299
|
+
[conservative recovery procedure](docs/USAGE_AND_PRICING.md#reservation-lock-recovery).
|
|
214
300
|
|
|
215
301
|
Single-machine by design. Two laptops on one account do not share state, and
|
|
216
302
|
that is stated rather than hidden.
|
|
@@ -228,5 +314,4 @@ a would-block decision in shadow and blocks new work only in enforce mode.
|
|
|
228
314
|
## What it never does
|
|
229
315
|
|
|
230
316
|
No prompts, responses, file contents, or tool inputs are persisted or rendered.
|
|
231
|
-
No telemetry. No provider-quota guesses
|
|
232
|
-
policy, never against a subscription allowance it cannot see.
|
|
317
|
+
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: '
|
|
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:
|
|
131
|
-
subagentStop: [{ command, timeout:
|
|
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/calibrate.js
CHANGED
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
24
|
exports.calibrate = calibrate;
|
|
25
25
|
const defaults_1 = require("./defaults");
|
|
26
|
-
const IMMUTABLE_MAX_SPAWNS = 40;
|
|
27
26
|
const IMMUTABLE_MAX_TOKENS = 6_000_000_000;
|
|
28
27
|
/**
|
|
29
28
|
* Sessions below this are not evidence about what a working session costs.
|
|
@@ -69,10 +68,10 @@ function calibrate(sessions, base = defaults_1.DEFAULT_THRESHOLDS) {
|
|
|
69
68
|
if (trivial > 0) {
|
|
70
69
|
notes.push(`${trivial} short session(s) under ${MIN_WORKING_SESSION_TOKENS / 1e6}M tokens ignored; they are not evidence about working-session cost.`);
|
|
71
70
|
}
|
|
72
|
-
const healthy = working.filter((s) => s.
|
|
71
|
+
const healthy = working.filter((s) => !s.fanoutStop && !s.spawnRateStop && s.totalTokens <= IMMUTABLE_MAX_TOKENS);
|
|
73
72
|
const excluded = working.length - healthy.length;
|
|
74
73
|
if (excluded > 0) {
|
|
75
|
-
notes.push(`${excluded} session(s) excluded as already pathological (
|
|
74
|
+
notes.push(`${excluded} session(s) excluded as already pathological (fan-out, depth or spawn-rate STOP, or >6B tokens). They must not define normal.`);
|
|
76
75
|
}
|
|
77
76
|
const tokens = winsorise(healthy.map((s) => s.totalTokens)).sort((a, b) => a - b);
|
|
78
77
|
const rawWarn = quantile(tokens, 0.9);
|
package/dist/src/cli.js
CHANGED
|
@@ -33,6 +33,12 @@ 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");
|
|
41
|
+
const blocks_1 = require("./insights/blocks");
|
|
36
42
|
const HOME = process.env.AGENTGUARD_HOME ?? (0, node_path_1.join)((0, node_os_1.homedir)(), '.agentguard');
|
|
37
43
|
const PROXY_HOSTS = ['ollama', 'vllm', 'lm-studio', 'openai-compatible'];
|
|
38
44
|
function readStdinJson() {
|
|
@@ -66,18 +72,7 @@ function readDecisions() {
|
|
|
66
72
|
.filter((x) => x !== null);
|
|
67
73
|
}
|
|
68
74
|
function shadowEligibility() {
|
|
69
|
-
|
|
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
|
-
};
|
|
75
|
+
return (0, eligibility_1.computeShadowEligibility)(readDecisions());
|
|
81
76
|
}
|
|
82
77
|
async function main(argv) {
|
|
83
78
|
const [command, ...rest] = argv;
|
|
@@ -87,6 +82,39 @@ async function main(argv) {
|
|
|
87
82
|
};
|
|
88
83
|
const has = (name) => rest.includes(name);
|
|
89
84
|
switch (command) {
|
|
85
|
+
case 'blocks': {
|
|
86
|
+
const session = rest.find(value => value !== 'json' && value !== '--json');
|
|
87
|
+
const report = (0, blocks_1.blocksReport)(HOME, session, { rates: (0, live_1.readPricing)(HOME, (0, pre_tool_use_1.loadPolicy)(HOME)) });
|
|
88
|
+
process.stdout.write((rest.includes('json') || has('--json') ? JSON.stringify(report, null, 2) : (0, blocks_1.renderBlocks)(report)) + '\n');
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
case 'why': {
|
|
92
|
+
const location = (0, sessions_1.selectInsightTranscript)(rest.find(value => value !== 'json'));
|
|
93
|
+
const transcript = (0, transcript_1.readInsightSession)(location.path, { host: location.host, sessionId: location.sessionId });
|
|
94
|
+
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');
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
case 'rewrites': {
|
|
98
|
+
const session = rest.find(value => value !== 'json');
|
|
99
|
+
const locations = session === 'all' ? (0, sessions_1.discoverInsightTranscripts)() : [(0, sessions_1.selectInsightTranscript)(session)];
|
|
100
|
+
const transcripts = locations.map(location => (0, transcript_1.readInsightTranscript)(location.path, { host: location.host, sessionId: location.sessionId }));
|
|
101
|
+
const rates = (0, live_1.readPricing)(HOME, (0, pre_tool_use_1.loadPolicy)(HOME));
|
|
102
|
+
process.stdout.write(rest.includes('json') ? JSON.stringify((0, render_2.rewriteReport)(transcripts, rates), null, 2) + '\n' : (0, render_2.renderRewrites)(transcripts, rates) + '\n');
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
case 'pricing': {
|
|
106
|
+
process.stdout.write((0, render_2.renderPricing)((0, live_1.readPricing)(HOME, (0, pre_tool_use_1.loadPolicy)(HOME))) + '\n');
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
case 'statusline':
|
|
110
|
+
case 'pace': {
|
|
111
|
+
const raw = command === 'statusline' && !process.stdin.isTTY ? readStdinJson() : null;
|
|
112
|
+
const payload = raw && typeof raw === 'object' ? raw : {};
|
|
113
|
+
const location = (0, sessions_1.selectInsightTranscript)(payload.transcript_path || rest[0] || payload.session_id);
|
|
114
|
+
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));
|
|
115
|
+
process.stdout.write((observation.snapshot ? (0, live_1.renderStatusLine)(observation.snapshot) : 'AgentGuard pace unavailable: local usage metadata is not readable.') + '\n');
|
|
116
|
+
return observation.snapshot ? 0 : 1;
|
|
117
|
+
}
|
|
90
118
|
case 'hook': {
|
|
91
119
|
const raw = (0, node_fs_1.readFileSync)(0, 'utf8');
|
|
92
120
|
let input = {};
|
|
@@ -121,7 +149,8 @@ async function main(argv) {
|
|
|
121
149
|
`Calibrated from ${result.sessionsUsed} of ${result.sessionsTotal} sessions (${result.sessionsExcluded} excluded as pathological).`,
|
|
122
150
|
` raw p90 ${(result.rawWarnP90 / 1e9).toFixed(2)}B raw p99 ${(result.rawStopP99 / 1e9).toFixed(2)}B`,
|
|
123
151
|
` sustained WARN ${(t.sustained.warnTokens / 1e9).toFixed(2)}B STOP ${(t.sustained.stopTokens / 1e9).toFixed(2)}B`,
|
|
124
|
-
` fan-out WARN ${t.fanout.warn}
|
|
152
|
+
` fan-out lifetime WARN ${t.fanout.warn}; STOP above ${t.fanout.stop} spawns in ${t.fanout.windowActiveMinutes ?? 120} active minutes; depth above ${t.fanout.maxDepth}`,
|
|
153
|
+
` spawn-rate WARN ${t.spawnRate?.warn ?? 8}; STOP ${t.spawnRate?.stop ?? 16} in ${t.spawnRate?.windowActiveMinutes ?? 15} active minutes; ${t.spawnRate?.enforce === false ? 'shadow only' : 'enforcing in enforce mode'}`,
|
|
125
154
|
` burn-debt ${t.burnDebt.enabled ? `ON, baseline ${Math.round(t.burnDebt.baselinePerActiveMinute / 1e6)}M/active-min` : 'off'}`,
|
|
126
155
|
...result.notes.map((n) => ` note: ${n}`),
|
|
127
156
|
`Policy written to ${(0, node_path_1.join)(HOME, 'burn-policy.json')} in SHADOW mode.`,
|
|
@@ -136,11 +165,14 @@ async function main(argv) {
|
|
|
136
165
|
const override = (0, override_1.readOverride)(HOME);
|
|
137
166
|
process.stdout.write([
|
|
138
167
|
`mode: ${policy.mode}${policy.mode === 'enforce' ? ' (the next STOP blocks)' : ' (recording only; nothing is blocked)'}`,
|
|
139
|
-
`thresholds: fan-out ${policy.thresholds.fanout.warn}
|
|
168
|
+
`thresholds: fan-out lifetime WARN ${policy.thresholds.fanout.warn}; STOP above ${policy.thresholds.fanout.stop} spawns in ${policy.thresholds.fanout.windowActiveMinutes ?? 120} active minutes; depth above ${policy.thresholds.fanout.maxDepth}`,
|
|
169
|
+
`spawn-rate: WARN ${policy.thresholds.spawnRate?.warn ?? 8}; STOP ${policy.thresholds.spawnRate?.stop ?? 16} in ${policy.thresholds.spawnRate?.windowActiveMinutes ?? 15} active minutes; ${policy.thresholds.spawnRate?.enforce === false ? 'shadow only' : 'enforcing in enforce mode'}`,
|
|
170
|
+
`sustained: ${(policy.thresholds.sustained.warnTokens / 1e9).toFixed(1)}B/${(policy.thresholds.sustained.stopTokens / 1e9).toFixed(1)}B local-compute warn at ${lc?.warnConcurrent ?? 4} concurrent${lc?.stopConcurrent ? `, stop at ${lc.stopConcurrent}` : ' (no stop set)'}`,
|
|
140
171
|
policy.calibration ? `calibrated from ${policy.calibration.sessionsSampled} sessions` : 'using shipped defaults (run: agentguard-burn calibrate)',
|
|
141
172
|
`shadow observation: ${e.decisions} decisions over ${e.days.toFixed(1)} days`,
|
|
142
173
|
` would have warned: ${e.warns} would have blocked: ${e.wouldBlock}`,
|
|
143
174
|
`eligible for enforcement: ${e.eligible ? 'yes (agentguard-burn enforce)' : `no (need ${defaults_1.SHADOW_MIN_DECISIONS} decisions and ${defaults_1.SHADOW_MIN_DAYS} days)`}`,
|
|
175
|
+
`lock-wait failures excluded from eligibility: ${e.excludedLockWaitFailures}`,
|
|
144
176
|
override ? `override ACTIVE: ${override.once ? 'the next STOP' : `every STOP until ${new Date(override.until).toLocaleTimeString()}`} passes ("${override.reason}")` : '',
|
|
145
177
|
'',
|
|
146
178
|
(0, status_1.renderHostHealth)(['claude', 'cursor', 'codex'].map((h) => (0, install_1.health)(h))),
|
|
@@ -270,7 +302,13 @@ async function main(argv) {
|
|
|
270
302
|
return 0;
|
|
271
303
|
}
|
|
272
304
|
default:
|
|
273
|
-
process.stdout.write('agentguard-burn <replay|calibrate|status|init|enforce|shadow|resume|proxy|conformance>\n' +
|
|
305
|
+
process.stdout.write('agentguard-burn <blocks|why|rewrites|pace|statusline|pricing|replay|calibrate|status|init|enforce|shadow|resume|proxy|conformance>\n' +
|
|
306
|
+
' blocks [session|all] stored WARN/STOP receipts and that session\'s child usage\n' +
|
|
307
|
+
' why [session] token attribution and API list-price equivalents\n' +
|
|
308
|
+
' rewrites [session|all] full-prefix cache writes by cause\n' +
|
|
309
|
+
' pace [session] ten-minute pace and next-hour projection\n' +
|
|
310
|
+
' statusline [session] one-line local status; accepts Claude status JSON\n' +
|
|
311
|
+
' pricing verified model rates and local overrides\n' +
|
|
274
312
|
' replay [files...] [--json] [--top N] [--min-tokens N]\n' +
|
|
275
313
|
' calibrate fit thresholds to your history (writes shadow policy)\n' +
|
|
276
314
|
' status mode, shadow observations, eligibility, hook health, every host\n' +
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Conformance: the same failure, through every door, stops at the same step.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Fixtures, fitted on real sessions:
|
|
5
5
|
*
|
|
6
6
|
* the storm 42 candidate spawns. First WARN must be spawn 24, first STOP
|
|
7
|
-
* must be spawn 41. Every spawn-capable
|
|
7
|
+
* must be spawn 41 with spawn-rate advisory. Every spawn-capable
|
|
8
|
+
* adapter replays it.
|
|
9
|
+
* rate default policy warns at candidate 8 and stops candidate 16.
|
|
10
|
+
* Pending proposals count even before a transcript sees them.
|
|
8
11
|
* the grind model calls of 250M tokens each. First WARN must be the call
|
|
9
12
|
* after 3.5B, first STOP the call after 5B. Every usage-capable
|
|
10
13
|
* adapter replays it.
|
package/dist/src/conformance.js
CHANGED
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Conformance: the same failure, through every door, stops at the same step.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Fixtures, fitted on real sessions:
|
|
6
6
|
*
|
|
7
7
|
* the storm 42 candidate spawns. First WARN must be spawn 24, first STOP
|
|
8
|
-
* must be spawn 41. Every spawn-capable
|
|
8
|
+
* must be spawn 41 with spawn-rate advisory. Every spawn-capable
|
|
9
|
+
* adapter replays it.
|
|
10
|
+
* rate default policy warns at candidate 8 and stops candidate 16.
|
|
11
|
+
* Pending proposals count even before a transcript sees them.
|
|
9
12
|
* the grind model calls of 250M tokens each. First WARN must be the call
|
|
10
13
|
* after 3.5B, first STOP the call after 5B. Every usage-capable
|
|
11
14
|
* adapter replays it.
|
|
@@ -35,11 +38,11 @@ const STOP_AT = defaults_1.DEFAULT_THRESHOLDS.fanout.stop + 1; // 41
|
|
|
35
38
|
const CALL_TOKENS = 250_000_000;
|
|
36
39
|
const GRIND_WARN_CALL = Math.floor(defaults_1.DEFAULT_THRESHOLDS.sustained.warnTokens / CALL_TOKENS) + 1; // 15
|
|
37
40
|
const GRIND_STOP_CALL = Math.floor(defaults_1.DEFAULT_THRESHOLDS.sustained.stopTokens / CALL_TOKENS) + 1; // 21
|
|
38
|
-
function freshHome() {
|
|
41
|
+
function freshHome(enforceRate = false) {
|
|
39
42
|
const home = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'agb-conf-'));
|
|
40
43
|
// These fixtures share a home to inspect all receipts. Test only the
|
|
41
44
|
// 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 } } };
|
|
45
|
+
const policy = { mode: 'enforce', thresholds: { ...defaults_1.DEFAULT_THRESHOLDS, spawnRate: { ...defaults_1.DEFAULT_THRESHOLDS.spawnRate, enforce: enforceRate }, account: { ...defaults_1.DEFAULT_THRESHOLDS.account, warnConcurrentSessions: Number.MAX_SAFE_INTEGER } } };
|
|
43
46
|
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(home, 'burn-policy.json'), JSON.stringify(policy));
|
|
44
47
|
return home;
|
|
45
48
|
}
|
|
@@ -52,6 +55,7 @@ function boundaries(steps) {
|
|
|
52
55
|
// Hook adapters only expose the first finding's summary. Fan-out is evaluated
|
|
53
56
|
// first in the core, so when it fires it is the one in the message.
|
|
54
57
|
const FANOUT_WARN = /agent spawns this session/;
|
|
58
|
+
const RATE_WARN = /spawns in the last 15 active minutes/;
|
|
55
59
|
function stepFromMessage(denied, message, warnPattern) {
|
|
56
60
|
if (denied)
|
|
57
61
|
return 'STOP';
|
|
@@ -60,34 +64,36 @@ function stepFromMessage(denied, message, warnPattern) {
|
|
|
60
64
|
return warnPattern.test(message) ? 'WARN' : 'WARN:other';
|
|
61
65
|
}
|
|
62
66
|
// ---- storms --------------------------------------------------------------
|
|
63
|
-
function stormRaw(home) {
|
|
64
|
-
const burn = (0, raw_api_1.createRawApiGuard)({ sessionId: 'storm-raw'
|
|
67
|
+
function stormRaw(home, rate = false, startObserved = true) {
|
|
68
|
+
const burn = (0, raw_api_1.createRawApiGuard)({ sessionId: `${rate ? 'rate' : 'storm'}-raw${startObserved ? '' : '-pending'}`, home });
|
|
65
69
|
const steps = [];
|
|
66
70
|
for (let i = 1; i <= STORM; i++) {
|
|
67
71
|
const lease = burn.beforeSpawn({ parentDepth: 0 });
|
|
68
72
|
const d = lease.decision;
|
|
69
|
-
const fanout = d.report.findings.find((f) => f.detector === 'fanout');
|
|
73
|
+
const fanout = d.report.findings.find((f) => f.detector === (rate ? 'spawn_rate' : 'fanout'));
|
|
70
74
|
steps.push(d.blocked ? 'STOP' : fanout?.verdict === 'WARN' ? 'WARN' : d.verdict === 'WARN' ? 'WARN:other' : 'OK');
|
|
71
|
-
if (!d.blocked)
|
|
75
|
+
if (!d.blocked && startObserved)
|
|
72
76
|
lease.started();
|
|
73
77
|
}
|
|
74
78
|
return boundaries(steps);
|
|
75
79
|
}
|
|
76
|
-
function stormCursor(home) {
|
|
80
|
+
function stormCursor(home, rate = false) {
|
|
81
|
+
const sessionId = rate ? 'rate-cursor' : 'storm-cursor';
|
|
77
82
|
const gateway = new gateway_1.Gateway(home);
|
|
78
83
|
const steps = [];
|
|
79
84
|
for (let i = 1; i <= STORM; i++) {
|
|
80
|
-
const out = (0, cursor_1.handleCursorHook)({ hook_event_name: 'subagentStart', conversation_id:
|
|
81
|
-
steps.push(stepFromMessage(out.permission === 'deny', out.agent_message, FANOUT_WARN));
|
|
85
|
+
const out = (0, cursor_1.handleCursorHook)({ hook_event_name: 'subagentStart', conversation_id: sessionId, subagent_id: `sub-${i}`, parent_conversation_id: sessionId }, gateway);
|
|
86
|
+
steps.push(stepFromMessage(out.permission === 'deny', out.agent_message, rate ? RATE_WARN : FANOUT_WARN));
|
|
82
87
|
}
|
|
83
88
|
return boundaries(steps);
|
|
84
89
|
}
|
|
85
|
-
function stormCodex(home) {
|
|
90
|
+
function stormCodex(home, rate = false) {
|
|
91
|
+
const sessionId = rate ? 'rate-codex' : 'storm-codex';
|
|
86
92
|
const gateway = new gateway_1.Gateway(home);
|
|
87
93
|
const steps = [];
|
|
88
94
|
for (let i = 1; i <= STORM; i++) {
|
|
89
|
-
const out = (0, codex_1.handleCodexHook)({ hook_event_name: 'PreToolUse', session_id:
|
|
90
|
-
steps.push(stepFromMessage(out.hookSpecificOutput?.permissionDecision === 'deny', out.systemMessage, FANOUT_WARN));
|
|
95
|
+
const out = (0, codex_1.handleCodexHook)({ hook_event_name: 'PreToolUse', session_id: sessionId, tool_name: 'spawn_agent', tool_use_id: `call-${i}` }, gateway);
|
|
96
|
+
steps.push(stepFromMessage(out.hookSpecificOutput?.permissionDecision === 'deny', out.systemMessage, rate ? RATE_WARN : FANOUT_WARN));
|
|
91
97
|
}
|
|
92
98
|
return boundaries(steps);
|
|
93
99
|
}
|
|
@@ -221,17 +227,23 @@ async function composite(home) {
|
|
|
221
227
|
// ---- runner --------------------------------------------------------------
|
|
222
228
|
async function runConformance() {
|
|
223
229
|
const home = freshHome();
|
|
230
|
+
const rateHome = freshHome(true);
|
|
224
231
|
const checks = [];
|
|
225
232
|
const expectStorm = (name, b) => checks.push({
|
|
226
233
|
name,
|
|
227
234
|
ok: b.firstWarn === WARN_AT && b.firstStop === STOP_AT,
|
|
228
235
|
detail: `fan-out WARN at ${b.firstWarn} (want ${WARN_AT}), STOP at ${b.firstStop} (want ${STOP_AT})${b.firstAnyWarn !== null && b.firstAnyWarn < WARN_AT ? `; advisory spawn-rate WARN at ${b.firstAnyWarn}` : ''}`,
|
|
229
236
|
});
|
|
237
|
+
const expectRate = (name, b) => checks.push({ name, ok: b.firstWarn === defaults_1.DEFAULT_THRESHOLDS.spawnRate.warn && b.firstStop === defaults_1.DEFAULT_THRESHOLDS.spawnRate.stop, detail: `default spawn-rate WARN at ${b.firstWarn} (want 8), STOP at ${b.firstStop} (want 16)` });
|
|
230
238
|
const expectGrind = (name, b) => checks.push({ name, ok: b.firstWarn === GRIND_WARN_CALL && b.firstStop === GRIND_STOP_CALL, detail: `first WARN call ${b.firstWarn} (want ${GRIND_WARN_CALL}), first STOP call ${b.firstStop} (want ${GRIND_STOP_CALL})` });
|
|
231
239
|
try {
|
|
232
240
|
expectStorm('storm · raw middleware', stormRaw(home));
|
|
233
241
|
expectStorm('storm · cursor subagentStart', stormCursor(home));
|
|
234
242
|
expectStorm('storm · codex PreToolUse', stormCodex(home));
|
|
243
|
+
expectRate('rate · raw middleware', stormRaw(rateHome, true));
|
|
244
|
+
expectRate('rate · cursor subagentStart', stormCursor(rateHome, true));
|
|
245
|
+
expectRate('rate · codex PreToolUse', stormCodex(rateHome, true));
|
|
246
|
+
expectRate('rate · pending only, no observed spawns', stormRaw(rateHome, true, false));
|
|
235
247
|
checks.push({ name: 'depth · raw middleware', ok: depthRaw(home), detail: 'depth 1 and 2 admitted, depth 3 denied' });
|
|
236
248
|
expectGrind('grind · raw middleware', grindRaw(home));
|
|
237
249
|
const p = await grindProxy(home);
|
|
@@ -241,17 +253,18 @@ async function runConformance() {
|
|
|
241
253
|
const c = await composite(home);
|
|
242
254
|
checks.push({ name: 'composite · middleware tree + proxy usage, one session', ok: c.ok, detail: c.detail });
|
|
243
255
|
// Receipts: every spawn decision above was signed; verify the chain tail.
|
|
244
|
-
const receipts = require('node:fs')
|
|
245
|
-
.readFileSync((0, node_path_1.join)(
|
|
256
|
+
const receipts = [home, rateHome].flatMap((directory) => require('node:fs')
|
|
257
|
+
.readFileSync((0, node_path_1.join)(directory, 'receipts.ndjson'), 'utf8')
|
|
246
258
|
.split('\n')
|
|
247
259
|
.filter(Boolean)
|
|
248
|
-
.map((l) => JSON.parse(l));
|
|
260
|
+
.map((l) => JSON.parse(l)));
|
|
249
261
|
const allValid = receipts.every((r) => (0, receipt_1.verifyReceipt)(r));
|
|
250
262
|
const contentFree = !receipts.some((r) => JSON.stringify(r.payload).match(/transcript|prompt|\/Users\/|content/i));
|
|
251
263
|
checks.push({ name: `receipts · ${receipts.length} signed, all verify, content-free`, ok: allValid && contentFree && receipts.length > 0, detail: `${receipts.length} receipts, verify=${allValid}, content-free=${contentFree}` });
|
|
252
264
|
}
|
|
253
265
|
finally {
|
|
254
266
|
(0, node_fs_1.rmSync)(home, { recursive: true, force: true });
|
|
267
|
+
(0, node_fs_1.rmSync)(rateHome, { recursive: true, force: true });
|
|
255
268
|
}
|
|
256
269
|
const ok = checks.every((c) => c.ok);
|
|
257
270
|
const text = [
|
package/dist/src/defaults.d.ts
CHANGED
|
@@ -3,10 +3,13 @@ import type { Policy, Thresholds } from './types';
|
|
|
3
3
|
* Shipped thresholds.
|
|
4
4
|
*
|
|
5
5
|
* Originally fitted against 412 real Claude Code sessions. Replayed on the
|
|
6
|
-
* author's machine on 2026-09-
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
+
* That historical snapshot used inflated repeated streaming usage counts and
|
|
9
|
+
* a lifetime fan-out ceiling. Version 0.2.6 deduplicates usage and limits fan-out
|
|
10
|
+
* to 120 active minutes. Lifetime 24 warns; above 40 in the window stops.
|
|
11
|
+
* Spawn rate 8/16 over 15 active minutes warns/stops by default. Sustained
|
|
12
|
+
* thresholds remain 3.5B/5B pending review of deduplicated calibration.
|
|
10
13
|
* This is a dated replay snapshot and an upper bound, not labelled evidence
|
|
11
14
|
* of false-positive rates or guaranteed savings.
|
|
12
15
|
*
|
package/dist/src/defaults.js
CHANGED
|
@@ -5,10 +5,13 @@ exports.SHADOW_MIN_DAYS = exports.SHADOW_MIN_DECISIONS = exports.ACTIVE_GAP_CAP_
|
|
|
5
5
|
* Shipped thresholds.
|
|
6
6
|
*
|
|
7
7
|
* Originally fitted against 412 real Claude Code sessions. Replayed on the
|
|
8
|
-
* author's machine on 2026-09-
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
+
* That historical snapshot used inflated repeated streaming usage counts and
|
|
11
|
+
* a lifetime fan-out ceiling. Version 0.2.6 deduplicates usage and limits fan-out
|
|
12
|
+
* to 120 active minutes. Lifetime 24 warns; above 40 in the window stops.
|
|
13
|
+
* Spawn rate 8/16 over 15 active minutes warns/stops by default. Sustained
|
|
14
|
+
* thresholds remain 3.5B/5B pending review of deduplicated calibration.
|
|
12
15
|
* This is a dated replay snapshot and an upper bound, not labelled evidence
|
|
13
16
|
* of false-positive rates or guaranteed savings.
|
|
14
17
|
*
|
|
@@ -22,7 +25,7 @@ exports.SHADOW_MIN_DAYS = exports.SHADOW_MIN_DECISIONS = exports.ACTIVE_GAP_CAP_
|
|
|
22
25
|
* absent from these thresholds.
|
|
23
26
|
*/
|
|
24
27
|
exports.DEFAULT_THRESHOLDS = {
|
|
25
|
-
fanout: { warn: 24, stop: 40, maxDepth: 2 },
|
|
28
|
+
fanout: { warn: 24, stop: 40, maxDepth: 2, windowActiveMinutes: 120 },
|
|
26
29
|
sustained: { warnTokens: 3_500_000_000, stopTokens: 5_000_000_000 },
|
|
27
30
|
burnDebt: {
|
|
28
31
|
baselinePerActiveMinute: 0,
|
|
@@ -31,7 +34,7 @@ exports.DEFAULT_THRESHOLDS = {
|
|
|
31
34
|
stopDebt: 1_500_000_000,
|
|
32
35
|
enabled: false,
|
|
33
36
|
},
|
|
34
|
-
spawnRate: { windowActiveMinutes: 15, warn: 8, stop: 16, enforce:
|
|
37
|
+
spawnRate: { windowActiveMinutes: 15, warn: 8, stop: 16, enforce: true },
|
|
35
38
|
duplicate: { warnReaders: 3 },
|
|
36
39
|
account: {
|
|
37
40
|
stopTokens: null,
|
|
@@ -5,15 +5,14 @@
|
|
|
5
5
|
* failure modes:
|
|
6
6
|
*
|
|
7
7
|
* structural - fan-out. Many agents, each re-sending context. The 190-spawn
|
|
8
|
-
* session. Caught by
|
|
8
|
+
* session. Caught by spawn windows over active time.
|
|
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
|
|
11
11
|
* see. Caught by a cumulative token ceiling and burn debt.
|
|
12
12
|
*
|
|
13
|
-
* Verdict is the maximum severity across findings.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* safety tool uninstalled.
|
|
13
|
+
* Verdict is the maximum severity across findings. Lifetime fan-out and
|
|
14
|
+
* duplicate work are advisory; only a recent burst, depth or economic
|
|
15
|
+
* threshold can stop new work. Spawn rate can be made advisory by policy.
|
|
17
16
|
*/
|
|
18
17
|
import type { BurnReport, SessionState, Thresholds } from '../types';
|
|
19
18
|
/**
|