instar 0.28.106 → 0.28.108
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/monitoring/AttributionResolver.d.ts +46 -0
- package/dist/monitoring/AttributionResolver.d.ts.map +1 -0
- package/dist/monitoring/AttributionResolver.js +107 -0
- package/dist/monitoring/AttributionResolver.js.map +1 -0
- package/dist/monitoring/BurnDetector.d.ts +84 -0
- package/dist/monitoring/BurnDetector.d.ts.map +1 -0
- package/dist/monitoring/BurnDetector.js +169 -0
- package/dist/monitoring/BurnDetector.js.map +1 -0
- package/dist/monitoring/BurnThrottleRunbook.d.ts +97 -0
- package/dist/monitoring/BurnThrottleRunbook.d.ts.map +1 -0
- package/dist/monitoring/BurnThrottleRunbook.js +202 -0
- package/dist/monitoring/BurnThrottleRunbook.js.map +1 -0
- package/dist/monitoring/LlmRateGate.d.ts +133 -33
- package/dist/monitoring/LlmRateGate.d.ts.map +1 -1
- package/dist/monitoring/LlmRateGate.js +179 -46
- package/dist/monitoring/LlmRateGate.js.map +1 -1
- package/dist/monitoring/TokenLedger.d.ts +16 -0
- package/dist/monitoring/TokenLedger.d.ts.map +1 -1
- package/dist/monitoring/TokenLedger.js +28 -0
- package/dist/monitoring/TokenLedger.js.map +1 -1
- package/dist/monitoring/attribution-manifest.d.ts +39 -0
- package/dist/monitoring/attribution-manifest.d.ts.map +1 -0
- package/dist/monitoring/attribution-manifest.js +83 -0
- package/dist/monitoring/attribution-manifest.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/0.28.107.md +40 -0
- package/upgrades/0.28.108.md +50 -0
- package/upgrades/side-effects/token-burn-detection-phase-2.md +44 -0
- package/upgrades/side-effects/token-burn-detection-phase-3.md +59 -0
- package/upgrades/side-effects/token-burn-detection-phase-4.md +78 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AttributionResolver — read-side resolver for the burn-detection system.
|
|
3
|
+
*
|
|
4
|
+
* Phase 2 of docs/specs/token-burn-detection-and-self-heal.md.
|
|
5
|
+
*
|
|
6
|
+
* Pure function: given the shape of a TokenLedger event (sessionId, cwd,
|
|
7
|
+
* prompt text, model) returns an attribution_key suitable for grouping
|
|
8
|
+
* "calls of the same structural origin." Used by the Phase 3 BurnDetector
|
|
9
|
+
* to populate attribution_key for events the Phase 1 chokepoint did NOT
|
|
10
|
+
* write directly — chiefly the dominant Claude-CLI path where the JSONL
|
|
11
|
+
* trail carries the prompt and cwd but no source-side label.
|
|
12
|
+
*
|
|
13
|
+
* Authority shape: this is signal-only. It does not gate, throttle, or
|
|
14
|
+
* decide anything. It maps event → key. The detector emits, the
|
|
15
|
+
* Remediator decides. See umbrella spec §"Signal-vs-Authority Decomposition".
|
|
16
|
+
*
|
|
17
|
+
* No I/O, no time-dependent behavior — deterministic for tests + reasoning.
|
|
18
|
+
*/
|
|
19
|
+
export interface AttributionEvent {
|
|
20
|
+
sessionId: string;
|
|
21
|
+
/** cwd / projectPath field from the JSONL line. */
|
|
22
|
+
projectPath?: string | null;
|
|
23
|
+
/** The user prompt that triggered the assistant response. Optional — when missing, resolver falls back to cwd / unknown. */
|
|
24
|
+
prompt?: string | null;
|
|
25
|
+
/** Model string from the JSONL line (`message.model`). */
|
|
26
|
+
model?: string | null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Resolve an attribution_key for an event.
|
|
30
|
+
*
|
|
31
|
+
* Resolution order:
|
|
32
|
+
* 1. Manifest entry that matches the prompt (with optional cwd / model
|
|
33
|
+
* narrowing) → `<component>::<promptFingerprint>`.
|
|
34
|
+
* 2. cwd-based scheduled-job inference — if the cwd contains a path
|
|
35
|
+
* segment like `.instar/jobs/<name>`, return `user-job:<name>`.
|
|
36
|
+
* 3. cwd-based user-extension inference — if the cwd contains
|
|
37
|
+
* `.claude/hooks/` or `.instar/hooks/`, return the hook filename.
|
|
38
|
+
* 4. Fallback: `unknown::<sessionId-prefix>`.
|
|
39
|
+
*
|
|
40
|
+
* The prompt-based check is intentionally first: it's the most informative
|
|
41
|
+
* signal for the bleed-detection use case (one prompt shape running tens
|
|
42
|
+
* of thousands of times). cwd-based inference is the secondary signal for
|
|
43
|
+
* cases where the prompt is variable but the source is recognisable.
|
|
44
|
+
*/
|
|
45
|
+
export declare function resolveAttribution(event: AttributionEvent): string;
|
|
46
|
+
//# sourceMappingURL=AttributionResolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AttributionResolver.d.ts","sourceRoot":"","sources":["../../src/monitoring/AttributionResolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAKH,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,4HAA4H;IAC5H,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,0DAA0D;IAC1D,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAgClE"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AttributionResolver — read-side resolver for the burn-detection system.
|
|
3
|
+
*
|
|
4
|
+
* Phase 2 of docs/specs/token-burn-detection-and-self-heal.md.
|
|
5
|
+
*
|
|
6
|
+
* Pure function: given the shape of a TokenLedger event (sessionId, cwd,
|
|
7
|
+
* prompt text, model) returns an attribution_key suitable for grouping
|
|
8
|
+
* "calls of the same structural origin." Used by the Phase 3 BurnDetector
|
|
9
|
+
* to populate attribution_key for events the Phase 1 chokepoint did NOT
|
|
10
|
+
* write directly — chiefly the dominant Claude-CLI path where the JSONL
|
|
11
|
+
* trail carries the prompt and cwd but no source-side label.
|
|
12
|
+
*
|
|
13
|
+
* Authority shape: this is signal-only. It does not gate, throttle, or
|
|
14
|
+
* decide anything. It maps event → key. The detector emits, the
|
|
15
|
+
* Remediator decides. See umbrella spec §"Signal-vs-Authority Decomposition".
|
|
16
|
+
*
|
|
17
|
+
* No I/O, no time-dependent behavior — deterministic for tests + reasoning.
|
|
18
|
+
*/
|
|
19
|
+
import { buildAttributionKey } from './attributionKey.js';
|
|
20
|
+
import { ATTRIBUTION_MANIFEST } from './attribution-manifest.js';
|
|
21
|
+
/**
|
|
22
|
+
* Resolve an attribution_key for an event.
|
|
23
|
+
*
|
|
24
|
+
* Resolution order:
|
|
25
|
+
* 1. Manifest entry that matches the prompt (with optional cwd / model
|
|
26
|
+
* narrowing) → `<component>::<promptFingerprint>`.
|
|
27
|
+
* 2. cwd-based scheduled-job inference — if the cwd contains a path
|
|
28
|
+
* segment like `.instar/jobs/<name>`, return `user-job:<name>`.
|
|
29
|
+
* 3. cwd-based user-extension inference — if the cwd contains
|
|
30
|
+
* `.claude/hooks/` or `.instar/hooks/`, return the hook filename.
|
|
31
|
+
* 4. Fallback: `unknown::<sessionId-prefix>`.
|
|
32
|
+
*
|
|
33
|
+
* The prompt-based check is intentionally first: it's the most informative
|
|
34
|
+
* signal for the bleed-detection use case (one prompt shape running tens
|
|
35
|
+
* of thousands of times). cwd-based inference is the secondary signal for
|
|
36
|
+
* cases where the prompt is variable but the source is recognisable.
|
|
37
|
+
*/
|
|
38
|
+
export function resolveAttribution(event) {
|
|
39
|
+
// 1. Manifest-based prompt match (with optional cwd/model narrowing).
|
|
40
|
+
if (event.prompt && event.prompt.length > 0) {
|
|
41
|
+
for (const entry of ATTRIBUTION_MANIFEST) {
|
|
42
|
+
if (matchesEntry(entry, event)) {
|
|
43
|
+
return buildAttributionKey(entry.component, event.prompt);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// 2. Scheduled-job inference: a path segment of the cwd is the job name.
|
|
48
|
+
// e.g. /Users/x/.instar/jobs/daily-summary → user-job:daily-summary
|
|
49
|
+
if (event.projectPath) {
|
|
50
|
+
const jobMatch = event.projectPath.match(/[/\\]\.instar[/\\]jobs[/\\]([^/\\]+)/);
|
|
51
|
+
if (jobMatch && jobMatch[1]) {
|
|
52
|
+
return `user-job:${jobMatch[1]}::${shortPromptFp(event.prompt)}`;
|
|
53
|
+
}
|
|
54
|
+
// 3. Hook / extension inference.
|
|
55
|
+
const hookMatch = event.projectPath.match(/[/\\](?:\.claude|\.instar)[/\\]hooks[/\\]([^/\\]+)/);
|
|
56
|
+
if (hookMatch && hookMatch[1]) {
|
|
57
|
+
return `user-hook:${hookMatch[1]}::${shortPromptFp(event.prompt)}`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// 4. Fallback. Use a stable session-id prefix so a single misbehaving
|
|
61
|
+
// session shows as one key (and not exploded by per-prompt fingerprint).
|
|
62
|
+
if (!event.sessionId || event.sessionId.length === 0) {
|
|
63
|
+
return 'unknown::no-session';
|
|
64
|
+
}
|
|
65
|
+
const sidPrefix = event.sessionId.slice(0, 8);
|
|
66
|
+
return `unknown::${sidPrefix}`;
|
|
67
|
+
}
|
|
68
|
+
function matchesEntry(entry, event) {
|
|
69
|
+
// Prompt must match if patterns are defined (Phase 2 always defines them).
|
|
70
|
+
if (entry.promptPatterns && entry.promptPatterns.length > 0) {
|
|
71
|
+
if (!event.prompt)
|
|
72
|
+
return false;
|
|
73
|
+
const hit = entry.promptPatterns.some((re) => re.test(event.prompt));
|
|
74
|
+
if (!hit)
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
// Optional cwd narrowing.
|
|
78
|
+
if (entry.cwdPatterns && entry.cwdPatterns.length > 0) {
|
|
79
|
+
if (!event.projectPath)
|
|
80
|
+
return false;
|
|
81
|
+
const hit = entry.cwdPatterns.some((re) => re.test(event.projectPath));
|
|
82
|
+
if (!hit)
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
// Optional model narrowing.
|
|
86
|
+
if (entry.modelHints && entry.modelHints.length > 0) {
|
|
87
|
+
if (!event.model)
|
|
88
|
+
return false;
|
|
89
|
+
const hit = entry.modelHints.some((m) => event.model.includes(m));
|
|
90
|
+
if (!hit)
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
function shortPromptFp(prompt) {
|
|
96
|
+
if (!prompt || prompt.length === 0)
|
|
97
|
+
return 'nonprompt';
|
|
98
|
+
// Cheap deterministic fingerprint without importing crypto on every call.
|
|
99
|
+
// Phase 1's buildAttributionKey covers the cryptographic case; here we
|
|
100
|
+
// only need to disambiguate prompts at the user-job / user-hook level.
|
|
101
|
+
let h = 0;
|
|
102
|
+
const slice = prompt.slice(0, 128);
|
|
103
|
+
for (let i = 0; i < slice.length; i++)
|
|
104
|
+
h = ((h << 5) - h + slice.charCodeAt(i)) | 0;
|
|
105
|
+
return (h >>> 0).toString(16).padStart(8, '0').slice(0, 8);
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=AttributionResolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AttributionResolver.js","sourceRoot":"","sources":["../../src/monitoring/AttributionResolver.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BurnDetector — emits a structured signal when a single attribution_key
|
|
3
|
+
* crosses configured spend thresholds.
|
|
4
|
+
*
|
|
5
|
+
* Phase 3 of docs/specs/token-burn-detection-and-self-heal.md.
|
|
6
|
+
*
|
|
7
|
+
* Signal-only (per umbrella spec §"Signal-vs-Authority Decomposition"): the
|
|
8
|
+
* detector emits to the existing DegradationReporter. The Phase 4 runbook
|
|
9
|
+
* (Tier-2 Remediator surface) is the only authority that decides whether
|
|
10
|
+
* to alert / throttle / both. The BurnDetector cannot decide anything.
|
|
11
|
+
*
|
|
12
|
+
* Triggers (OR'd):
|
|
13
|
+
* 1. Absolute share — a single attribution_key consumed > absoluteShareThreshold
|
|
14
|
+
* of total 24h spend.
|
|
15
|
+
* 2. Baseline divergence — the key's last-1h rate is > rollingBaselineMultiplier ×
|
|
16
|
+
* its trailing-7-day median rate, AND the 1h rate exceeds rollingBaselineFloor
|
|
17
|
+
* tokens/h.
|
|
18
|
+
*
|
|
19
|
+
* Cold-start (per umbrella spec §"Threshold logic"): the baseline-divergence
|
|
20
|
+
* trigger only fires after a key has been observed for 7 days. Until then,
|
|
21
|
+
* absolute-share is the only signal — that's the trigger that would have
|
|
22
|
+
* caught the 2026-05-15 incident.
|
|
23
|
+
*
|
|
24
|
+
* Polling cadence: 60s, decoupled from the ledger writer. The detector does
|
|
25
|
+
* its own clock (injected `now`) so tests don't need to fake timers globally.
|
|
26
|
+
*/
|
|
27
|
+
import type { TokenLedger } from './TokenLedger.js';
|
|
28
|
+
import type { DegradationReporter } from './DegradationReporter.js';
|
|
29
|
+
export interface BurnDetectionConfig {
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
/** Default 0.25 (25%). */
|
|
32
|
+
absoluteShareThreshold: number;
|
|
33
|
+
/** Default 2 (2x). */
|
|
34
|
+
rollingBaselineMultiplier: number;
|
|
35
|
+
/** Default 10_000_000 tokens/hour. */
|
|
36
|
+
rollingBaselineFloor: number;
|
|
37
|
+
/** Per-key alert cooldown (default 3_600_000ms = 1h). */
|
|
38
|
+
perKeyAlertCooldownMs: number;
|
|
39
|
+
/** Poll interval (default 60_000ms = 60s). */
|
|
40
|
+
pollIntervalMs: number;
|
|
41
|
+
/** Cold-start duration (default 7d). Baseline-divergence trigger disabled until elapsed. */
|
|
42
|
+
coldStartMs: number;
|
|
43
|
+
}
|
|
44
|
+
export declare const DEFAULT_BURN_DETECTION_CONFIG: BurnDetectionConfig;
|
|
45
|
+
export interface BurnSignal {
|
|
46
|
+
attributionKey: string;
|
|
47
|
+
trigger: 'absolute-share' | 'baseline-divergence';
|
|
48
|
+
emittedAt: string;
|
|
49
|
+
observed: {
|
|
50
|
+
tokens24h: number;
|
|
51
|
+
share24h: number;
|
|
52
|
+
tokensLast1h: number;
|
|
53
|
+
projectedDaily: number;
|
|
54
|
+
};
|
|
55
|
+
/** Trailing-7d median tokens/h. Only populated for `baseline-divergence`. */
|
|
56
|
+
baselineMedian7d?: number;
|
|
57
|
+
}
|
|
58
|
+
export interface BurnDetectorDeps {
|
|
59
|
+
ledger: Pick<TokenLedger, 'byAttributionKey' | 'summary'>;
|
|
60
|
+
reporter: Pick<DegradationReporter, 'report'>;
|
|
61
|
+
config?: Partial<BurnDetectionConfig>;
|
|
62
|
+
/** Injectable clock for tests. Defaults to `Date.now`. */
|
|
63
|
+
now?: () => number;
|
|
64
|
+
}
|
|
65
|
+
export declare class BurnDetector {
|
|
66
|
+
private readonly ledger;
|
|
67
|
+
private readonly reporter;
|
|
68
|
+
private readonly config;
|
|
69
|
+
private readonly now;
|
|
70
|
+
/** First time a given attribution_key was seen. Used for cold-start cutoff. */
|
|
71
|
+
private readonly firstSeen;
|
|
72
|
+
/** Last alert emit time per key — gates per-key alert cooldown. */
|
|
73
|
+
private readonly lastAlertAt;
|
|
74
|
+
private timer;
|
|
75
|
+
constructor(deps: BurnDetectorDeps);
|
|
76
|
+
start(): void;
|
|
77
|
+
stop(): void;
|
|
78
|
+
/**
|
|
79
|
+
* One detector cycle. Returns the signals emitted on this tick — useful
|
|
80
|
+
* for tests, and the production loop ignores the return.
|
|
81
|
+
*/
|
|
82
|
+
tick(): BurnSignal[];
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=BurnDetector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BurnDetector.d.ts","sourceRoot":"","sources":["../../src/monitoring/BurnDetector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAqB,MAAM,kBAAkB,CAAC;AACvE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAEpE,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,0BAA0B;IAC1B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,sBAAsB;IACtB,yBAAyB,EAAE,MAAM,CAAC;IAClC,sCAAsC;IACtC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,yDAAyD;IACzD,qBAAqB,EAAE,MAAM,CAAC;IAC9B,8CAA8C;IAC9C,cAAc,EAAE,MAAM,CAAC;IACvB,4FAA4F;IAC5F,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,6BAA6B,EAAE,mBAQ3C,CAAC;AAEF,MAAM,WAAW,UAAU;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,gBAAgB,GAAG,qBAAqB,CAAC;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE;QACR,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,EAAE,MAAM,CAAC;QACjB,YAAY,EAAE,MAAM,CAAC;QACrB,cAAc,EAAE,MAAM,CAAC;KACxB,CAAC;IACF,6EAA6E;IAC7E,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,IAAI,CAAC,WAAW,EAAE,kBAAkB,GAAG,SAAS,CAAC,CAAC;IAC1D,QAAQ,EAAE,IAAI,CAAC,mBAAmB,EAAE,QAAQ,CAAC,CAAC;IAC9C,MAAM,CAAC,EAAE,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACtC,0DAA0D;IAC1D,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA6B;IACpD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA+B;IACxD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsB;IAC7C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,+EAA+E;IAC/E,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA6B;IACvD,mEAAmE;IACnE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IACzD,OAAO,CAAC,KAAK,CAA+C;gBAEhD,IAAI,EAAE,gBAAgB;IAOlC,KAAK,IAAI,IAAI;IAgBb,IAAI,IAAI,IAAI;IAOZ;;;OAGG;IACH,IAAI,IAAI,UAAU,EAAE;CAkGrB"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BurnDetector — emits a structured signal when a single attribution_key
|
|
3
|
+
* crosses configured spend thresholds.
|
|
4
|
+
*
|
|
5
|
+
* Phase 3 of docs/specs/token-burn-detection-and-self-heal.md.
|
|
6
|
+
*
|
|
7
|
+
* Signal-only (per umbrella spec §"Signal-vs-Authority Decomposition"): the
|
|
8
|
+
* detector emits to the existing DegradationReporter. The Phase 4 runbook
|
|
9
|
+
* (Tier-2 Remediator surface) is the only authority that decides whether
|
|
10
|
+
* to alert / throttle / both. The BurnDetector cannot decide anything.
|
|
11
|
+
*
|
|
12
|
+
* Triggers (OR'd):
|
|
13
|
+
* 1. Absolute share — a single attribution_key consumed > absoluteShareThreshold
|
|
14
|
+
* of total 24h spend.
|
|
15
|
+
* 2. Baseline divergence — the key's last-1h rate is > rollingBaselineMultiplier ×
|
|
16
|
+
* its trailing-7-day median rate, AND the 1h rate exceeds rollingBaselineFloor
|
|
17
|
+
* tokens/h.
|
|
18
|
+
*
|
|
19
|
+
* Cold-start (per umbrella spec §"Threshold logic"): the baseline-divergence
|
|
20
|
+
* trigger only fires after a key has been observed for 7 days. Until then,
|
|
21
|
+
* absolute-share is the only signal — that's the trigger that would have
|
|
22
|
+
* caught the 2026-05-15 incident.
|
|
23
|
+
*
|
|
24
|
+
* Polling cadence: 60s, decoupled from the ledger writer. The detector does
|
|
25
|
+
* its own clock (injected `now`) so tests don't need to fake timers globally.
|
|
26
|
+
*/
|
|
27
|
+
export const DEFAULT_BURN_DETECTION_CONFIG = {
|
|
28
|
+
enabled: true,
|
|
29
|
+
absoluteShareThreshold: 0.25,
|
|
30
|
+
rollingBaselineMultiplier: 2,
|
|
31
|
+
rollingBaselineFloor: 10_000_000,
|
|
32
|
+
perKeyAlertCooldownMs: 3_600_000,
|
|
33
|
+
pollIntervalMs: 60_000,
|
|
34
|
+
coldStartMs: 7 * 24 * 60 * 60 * 1000,
|
|
35
|
+
};
|
|
36
|
+
export class BurnDetector {
|
|
37
|
+
ledger;
|
|
38
|
+
reporter;
|
|
39
|
+
config;
|
|
40
|
+
now;
|
|
41
|
+
/** First time a given attribution_key was seen. Used for cold-start cutoff. */
|
|
42
|
+
firstSeen = new Map();
|
|
43
|
+
/** Last alert emit time per key — gates per-key alert cooldown. */
|
|
44
|
+
lastAlertAt = new Map();
|
|
45
|
+
timer = null;
|
|
46
|
+
constructor(deps) {
|
|
47
|
+
this.ledger = deps.ledger;
|
|
48
|
+
this.reporter = deps.reporter;
|
|
49
|
+
this.config = { ...DEFAULT_BURN_DETECTION_CONFIG, ...(deps.config ?? {}) };
|
|
50
|
+
this.now = deps.now ?? (() => Date.now());
|
|
51
|
+
}
|
|
52
|
+
start() {
|
|
53
|
+
if (!this.config.enabled || this.timer)
|
|
54
|
+
return;
|
|
55
|
+
this.timer = setInterval(() => {
|
|
56
|
+
try {
|
|
57
|
+
this.tick();
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
console.warn(`[burn-detector] tick error (non-fatal): ${err.message}`);
|
|
61
|
+
}
|
|
62
|
+
}, this.config.pollIntervalMs);
|
|
63
|
+
// Prevent the interval from keeping the process alive on its own — the
|
|
64
|
+
// server's lifecycle owns shutdown.
|
|
65
|
+
if (this.timer && typeof this.timer.unref === 'function') {
|
|
66
|
+
this.timer.unref();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
stop() {
|
|
70
|
+
if (this.timer) {
|
|
71
|
+
clearInterval(this.timer);
|
|
72
|
+
this.timer = null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* One detector cycle. Returns the signals emitted on this tick — useful
|
|
77
|
+
* for tests, and the production loop ignores the return.
|
|
78
|
+
*/
|
|
79
|
+
tick() {
|
|
80
|
+
if (!this.config.enabled)
|
|
81
|
+
return [];
|
|
82
|
+
const now = this.now();
|
|
83
|
+
const since24h = now - 24 * 60 * 60 * 1000;
|
|
84
|
+
const since1h = now - 60 * 60 * 1000;
|
|
85
|
+
const since7d = now - 7 * 24 * 60 * 60 * 1000;
|
|
86
|
+
const keys24h = this.ledger.byAttributionKey({ sinceMs: since24h });
|
|
87
|
+
if (keys24h.length === 0)
|
|
88
|
+
return [];
|
|
89
|
+
const total24h = keys24h.reduce((sum, k) => sum + k.totalTokens, 0);
|
|
90
|
+
if (total24h <= 0)
|
|
91
|
+
return [];
|
|
92
|
+
// Update first-seen map.
|
|
93
|
+
for (const k of keys24h) {
|
|
94
|
+
if (!this.firstSeen.has(k.attributionKey)) {
|
|
95
|
+
this.firstSeen.set(k.attributionKey, k.firstTs);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const signals = [];
|
|
99
|
+
const keys1h = this.ledger.byAttributionKey({ sinceMs: since1h });
|
|
100
|
+
const keys1hMap = new Map(keys1h.map((k) => [k.attributionKey, k]));
|
|
101
|
+
for (const key24h of keys24h) {
|
|
102
|
+
const share = key24h.totalTokens / total24h;
|
|
103
|
+
const key1h = keys1hMap.get(key24h.attributionKey);
|
|
104
|
+
const tokens1h = key1h ? key1h.totalTokens : 0;
|
|
105
|
+
const projectedDaily = tokens1h * 24;
|
|
106
|
+
// Skip exempt runbook self-attribution prefix (defence in depth — Phase 4
|
|
107
|
+
// runbook also exempts itself at the gate level).
|
|
108
|
+
if (key24h.attributionKey.startsWith('burn-throttle-runbook::'))
|
|
109
|
+
continue;
|
|
110
|
+
// Cooldown.
|
|
111
|
+
const lastAlert = this.lastAlertAt.get(key24h.attributionKey) ?? 0;
|
|
112
|
+
if (now - lastAlert < this.config.perKeyAlertCooldownMs)
|
|
113
|
+
continue;
|
|
114
|
+
let trigger = null;
|
|
115
|
+
let baselineMedian7d;
|
|
116
|
+
// Trigger 1 — absolute share.
|
|
117
|
+
if (share > this.config.absoluteShareThreshold) {
|
|
118
|
+
trigger = 'absolute-share';
|
|
119
|
+
}
|
|
120
|
+
// Trigger 2 — baseline divergence (only after cold-start window).
|
|
121
|
+
if (!trigger) {
|
|
122
|
+
const firstSeen = this.firstSeen.get(key24h.attributionKey) ?? key24h.firstTs;
|
|
123
|
+
if (now - firstSeen >= this.config.coldStartMs && tokens1h >= this.config.rollingBaselineFloor) {
|
|
124
|
+
// 7-day median rate (tokens/h). Use the simple per-key 7d total / 168
|
|
125
|
+
// as a proxy when the table doesn't store hourly buckets — Phase 4 may
|
|
126
|
+
// refine to a true median.
|
|
127
|
+
const keys7d = this.ledger.byAttributionKey({ sinceMs: since7d });
|
|
128
|
+
const key7d = keys7d.find((k) => k.attributionKey === key24h.attributionKey);
|
|
129
|
+
if (key7d) {
|
|
130
|
+
baselineMedian7d = key7d.totalTokens / (7 * 24);
|
|
131
|
+
if (tokens1h > this.config.rollingBaselineMultiplier * baselineMedian7d) {
|
|
132
|
+
trigger = 'baseline-divergence';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (!trigger)
|
|
138
|
+
continue;
|
|
139
|
+
const signal = {
|
|
140
|
+
attributionKey: key24h.attributionKey,
|
|
141
|
+
trigger,
|
|
142
|
+
emittedAt: new Date(now).toISOString(),
|
|
143
|
+
observed: {
|
|
144
|
+
tokens24h: key24h.totalTokens,
|
|
145
|
+
share24h: share,
|
|
146
|
+
tokensLast1h: tokens1h,
|
|
147
|
+
projectedDaily,
|
|
148
|
+
},
|
|
149
|
+
baselineMedian7d,
|
|
150
|
+
};
|
|
151
|
+
signals.push(signal);
|
|
152
|
+
this.lastAlertAt.set(key24h.attributionKey, now);
|
|
153
|
+
// Emit to DegradationReporter. The runbook in Phase 4 will subscribe via
|
|
154
|
+
// the existing Remediator dispatch and decide alert vs throttle.
|
|
155
|
+
this.reporter.report({
|
|
156
|
+
feature: 'token-burn-detection',
|
|
157
|
+
primary: `attribution_key ${key24h.attributionKey} sustained spend within thresholds`,
|
|
158
|
+
fallback: `signal-only: detector flagged the key (Phase 4 runbook will decide alert vs throttle)`,
|
|
159
|
+
reason: trigger === 'absolute-share'
|
|
160
|
+
? `${key24h.attributionKey} consumed ${(share * 100).toFixed(1)}% of 24h spend (threshold ${(this.config.absoluteShareThreshold * 100).toFixed(0)}%)`
|
|
161
|
+
: `${key24h.attributionKey} last-1h rate ${tokens1h.toLocaleString()} tok/h, baseline ${baselineMedian7d?.toLocaleString() ?? '?'} tok/h (multiplier ${this.config.rollingBaselineMultiplier}x)`,
|
|
162
|
+
impact: `Projected ${projectedDaily.toLocaleString()} tokens in next 24h at current rate. ` +
|
|
163
|
+
`Phase 3 is observation-only; Phase 4 wires alerting and bounded auto-throttle.`,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return signals;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=BurnDetector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BurnDetector.js","sourceRoot":"","sources":["../../src/monitoring/BurnDetector.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BurnThrottleRunbook — Tier-2 authority surface for burn-detection signals.
|
|
3
|
+
*
|
|
4
|
+
* Phase 4 of docs/specs/token-burn-detection-and-self-heal.md. This is the
|
|
5
|
+
* ONLY blocking surface in the burn-detection pipeline. The BurnDetector
|
|
6
|
+
* (Phase 3) emits structured signals via DegradationReporter; this runbook
|
|
7
|
+
* subscribes via the existing handler hook and decides:
|
|
8
|
+
* - alert-only (default for `unknown::*` keys and any caller config disables)
|
|
9
|
+
* - throttle (default for known component names)
|
|
10
|
+
* - both (the umbrella spec's primary policy)
|
|
11
|
+
*
|
|
12
|
+
* Authority shape (per umbrella spec §"Signal-vs-Authority Decomposition"):
|
|
13
|
+
* the runbook is the only piece in this pipeline with blocking authority.
|
|
14
|
+
* The detector emits; the gate enforces; the runbook decides.
|
|
15
|
+
*
|
|
16
|
+
* Self-reinforcing-loop guard: the runbook never throttles a key starting
|
|
17
|
+
* with `burn-throttle-runbook::*` — defence-in-depth (the gate also refuses
|
|
18
|
+
* to install such throttles, the detector also exempts the prefix). This
|
|
19
|
+
* three-layer guard is from the convergence audit.
|
|
20
|
+
*/
|
|
21
|
+
import { LlmRateGate, type InstalledThrottle } from './LlmRateGate.js';
|
|
22
|
+
import type { DegradationEvent } from './DegradationReporter.js';
|
|
23
|
+
export interface BurnThrottleConfig {
|
|
24
|
+
/** Whether to auto-throttle (true) or alert-only (false). Default true. */
|
|
25
|
+
autoThrottle: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Whether to auto-throttle keys with no known component (`unknown::*`).
|
|
28
|
+
* Default false — the umbrella spec is conservative here: an unknown key
|
|
29
|
+
* may be a user extension the agent doesn't recognise, and throttling
|
|
30
|
+
* something the agent doesn't understand can break user workflows.
|
|
31
|
+
* Operators opt in to auto-throttle on unknown explicitly.
|
|
32
|
+
*/
|
|
33
|
+
autoThrottleOnUnknown: boolean;
|
|
34
|
+
/** Throttle duration (default 60min). */
|
|
35
|
+
throttleDurationMs: number;
|
|
36
|
+
}
|
|
37
|
+
export declare const DEFAULT_BURN_THROTTLE_CONFIG: BurnThrottleConfig;
|
|
38
|
+
export type RunbookOutcomeKind = 'alert-only-unknown' | 'alert-only-config-disabled' | 'alert-only-self-attribution' | 'throttle-installed' | 'throttle-failed';
|
|
39
|
+
export interface RunbookOutcome {
|
|
40
|
+
kind: RunbookOutcomeKind;
|
|
41
|
+
attributionKey: string;
|
|
42
|
+
decidedAt: string;
|
|
43
|
+
trigger: string;
|
|
44
|
+
/** When kind === 'throttle-installed', the actually-installed throttle. */
|
|
45
|
+
throttle?: InstalledThrottle;
|
|
46
|
+
/** Free-text reason for the decision. */
|
|
47
|
+
reason: string;
|
|
48
|
+
}
|
|
49
|
+
/** Telegram emit function. Phase 5 upgrades to principal-bound buttons. */
|
|
50
|
+
export type TelegramAlertSender = (topicId: number, text: string) => void | Promise<void>;
|
|
51
|
+
export interface BurnThrottleRunbookDeps {
|
|
52
|
+
gate: LlmRateGate;
|
|
53
|
+
config?: Partial<BurnThrottleConfig>;
|
|
54
|
+
/** Telegram emit. Optional — when missing, decisions still happen, just no alert. */
|
|
55
|
+
sendTelegram?: TelegramAlertSender;
|
|
56
|
+
/** Telegram topic ID for alerts. */
|
|
57
|
+
alertTopicId?: number;
|
|
58
|
+
/** Injectable clock for tests. */
|
|
59
|
+
now?: () => number;
|
|
60
|
+
}
|
|
61
|
+
/** Identity tag the runbook uses when installing throttles. */
|
|
62
|
+
export declare const RUNBOOK_ISSUER = "burn-throttle-runbook";
|
|
63
|
+
export declare class BurnThrottleRunbook {
|
|
64
|
+
private readonly gate;
|
|
65
|
+
private readonly config;
|
|
66
|
+
private readonly sendTelegram;
|
|
67
|
+
private readonly alertTopicId;
|
|
68
|
+
private readonly now;
|
|
69
|
+
constructor(deps: BurnThrottleRunbookDeps);
|
|
70
|
+
/**
|
|
71
|
+
* Handle a burn-detection signal. Called from a subscriber hook on
|
|
72
|
+
* DegradationReporter for feature='token-burn-detection' events.
|
|
73
|
+
*
|
|
74
|
+
* Returns the outcome so tests + audit + the verification step (Phase 6)
|
|
75
|
+
* can inspect what happened.
|
|
76
|
+
*/
|
|
77
|
+
handle(event: DegradationEvent): RunbookOutcome;
|
|
78
|
+
private fireTelegram;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Extract the attribution key from a burn-detection DegradationEvent. The
|
|
82
|
+
* BurnDetector formats the `primary` field as "attribution_key <KEY>
|
|
83
|
+
* sustained spend within thresholds" — we parse it back here. The wire
|
|
84
|
+
* shape is internal to the burn-detection pipeline; if it changes, change
|
|
85
|
+
* both ends in the same PR.
|
|
86
|
+
*/
|
|
87
|
+
export declare function extractAttributionKey(event: DegradationEvent): string;
|
|
88
|
+
/**
|
|
89
|
+
* Extract the trigger label from the event's reason field. Returns
|
|
90
|
+
* 'absolute-share' or 'baseline-divergence' or 'unknown-trigger'.
|
|
91
|
+
*/
|
|
92
|
+
export declare function extractTrigger(event: DegradationEvent): string;
|
|
93
|
+
/** Is the attribution key one of the unknown::* fallback shapes? */
|
|
94
|
+
export declare function isUnknownKey(attributionKey: string): boolean;
|
|
95
|
+
/** Compose the ELI16 alert text for Telegram. No backticks, no camelCase config keys, narrative tone. */
|
|
96
|
+
export declare function composeAlertText(event: DegradationEvent, attributionKey: string, trigger: string): string;
|
|
97
|
+
//# sourceMappingURL=BurnThrottleRunbook.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BurnThrottleRunbook.d.ts","sourceRoot":"","sources":["../../src/monitoring/BurnThrottleRunbook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,WAAW,EAAE,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACvE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAEjE,MAAM,WAAW,kBAAkB;IACjC,2EAA2E;IAC3E,YAAY,EAAE,OAAO,CAAC;IACtB;;;;;;OAMG;IACH,qBAAqB,EAAE,OAAO,CAAC;IAC/B,yCAAyC;IACzC,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED,eAAO,MAAM,4BAA4B,EAAE,kBAI1C,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAC1B,oBAAoB,GACpB,4BAA4B,GAC5B,6BAA6B,GAC7B,oBAAoB,GACpB,iBAAiB,CAAC;AAEtB,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,kBAAkB,CAAC;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,yCAAyC;IACzC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,2EAA2E;AAC3E,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1F,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACrC,qFAAqF;IACrF,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC,oCAAoC;IACpC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kCAAkC;IAClC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,+DAA+D;AAC/D,eAAO,MAAM,cAAc,0BAA0B,CAAC;AAEtD,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAc;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;IAC5C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAkC;IAC/D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;gBAEvB,IAAI,EAAE,uBAAuB;IAQzC;;;;;;OAMG;IACH,MAAM,CAAC,KAAK,EAAE,gBAAgB,GAAG,cAAc;IA8F/C,OAAO,CAAC,YAAY;CAYrB;AAID;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAGrE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAI9D;AAED,oEAAoE;AACpE,wBAAgB,YAAY,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAE5D;AAED,yGAAyG;AACzG,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAazG"}
|