instar 0.28.109 → 0.28.111

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 (39) hide show
  1. package/dashboard/proposals.html +418 -0
  2. package/dist/monitoring/BurnVerifier.d.ts +68 -0
  3. package/dist/monitoring/BurnVerifier.d.ts.map +1 -0
  4. package/dist/monitoring/BurnVerifier.js +151 -0
  5. package/dist/monitoring/BurnVerifier.js.map +1 -0
  6. package/dist/remediation/NovelFailureReviewer.d.ts +212 -0
  7. package/dist/remediation/NovelFailureReviewer.d.ts.map +1 -0
  8. package/dist/remediation/NovelFailureReviewer.js +605 -0
  9. package/dist/remediation/NovelFailureReviewer.js.map +1 -0
  10. package/dist/remediation/Remediator.d.ts.map +1 -1
  11. package/dist/remediation/Remediator.js +1 -0
  12. package/dist/remediation/Remediator.js.map +1 -1
  13. package/dist/remediation/TrustElevationSource.d.ts +8 -1
  14. package/dist/remediation/TrustElevationSource.d.ts.map +1 -1
  15. package/dist/remediation/TrustElevationSource.js +8 -1
  16. package/dist/remediation/TrustElevationSource.js.map +1 -1
  17. package/dist/remediation/audit/AuditProjection.d.ts +7 -0
  18. package/dist/remediation/audit/AuditProjection.d.ts.map +1 -1
  19. package/dist/remediation/audit/AuditProjection.js +16 -0
  20. package/dist/remediation/audit/AuditProjection.js.map +1 -1
  21. package/dist/remediation/audit/AuditWriter.d.ts +8 -0
  22. package/dist/remediation/audit/AuditWriter.d.ts.map +1 -1
  23. package/dist/remediation/audit/AuditWriter.js +5 -1
  24. package/dist/remediation/audit/AuditWriter.js.map +1 -1
  25. package/dist/server/AgentServer.d.ts +4 -0
  26. package/dist/server/AgentServer.d.ts.map +1 -1
  27. package/dist/server/AgentServer.js +90 -0
  28. package/dist/server/AgentServer.js.map +1 -1
  29. package/dist/server/routes/remediation-proposals.d.ts +114 -0
  30. package/dist/server/routes/remediation-proposals.d.ts.map +1 -0
  31. package/dist/server/routes/remediation-proposals.js +350 -0
  32. package/dist/server/routes/remediation-proposals.js.map +1 -0
  33. package/package.json +1 -1
  34. package/src/data/builtin-manifest.json +3 -3
  35. package/upgrades/0.28.110.md +41 -0
  36. package/upgrades/0.28.111.md +43 -0
  37. package/upgrades/side-effects/s1-novel-failure-reviewer.md +79 -0
  38. package/upgrades/side-effects/s2-dashboard-proposals.md +76 -0
  39. package/upgrades/side-effects/token-burn-detection-phase-6.md +63 -0
@@ -0,0 +1,418 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>Remediation Proposals — Instar Dashboard</title>
7
+ <link rel="icon" href="/dashboard/favicon.png" />
8
+ <style>
9
+ :root {
10
+ --bg: #0c0c10;
11
+ --bg-card: #15161c;
12
+ --bg-hover: #1c1d24;
13
+ --fg: #e4e4eb;
14
+ --fg-dim: #888894;
15
+ --accent: #6aa9ff;
16
+ --border: #2a2b34;
17
+ --red: #e57373;
18
+ --amber: #e0a060;
19
+ --green: #80c080;
20
+ --untrusted: #2a2018;
21
+ --untrusted-border: #6a4828;
22
+ }
23
+ * { box-sizing: border-box; }
24
+ body {
25
+ margin: 0;
26
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
27
+ background: var(--bg);
28
+ color: var(--fg);
29
+ font-size: 14px;
30
+ line-height: 1.5;
31
+ }
32
+ .page {
33
+ max-width: 1100px;
34
+ margin: 0 auto;
35
+ padding: 24px 20px 60px;
36
+ }
37
+ header.top {
38
+ display: flex;
39
+ align-items: baseline;
40
+ gap: 16px;
41
+ margin-bottom: 24px;
42
+ }
43
+ h1 { font-size: 20px; font-weight: 600; margin: 0; }
44
+ .sub { color: var(--fg-dim); font-size: 13px; }
45
+ .auth-bar {
46
+ display: flex;
47
+ gap: 8px;
48
+ align-items: center;
49
+ margin-bottom: 16px;
50
+ flex-wrap: wrap;
51
+ }
52
+ .auth-bar input {
53
+ flex: 1;
54
+ min-width: 200px;
55
+ padding: 6px 10px;
56
+ background: var(--bg-card);
57
+ border: 1px solid var(--border);
58
+ border-radius: 4px;
59
+ color: var(--fg);
60
+ font-family: ui-monospace, monospace;
61
+ font-size: 12px;
62
+ }
63
+ .btn {
64
+ padding: 6px 14px;
65
+ background: var(--bg-card);
66
+ border: 1px solid var(--border);
67
+ border-radius: 4px;
68
+ color: var(--fg);
69
+ cursor: pointer;
70
+ font-size: 13px;
71
+ }
72
+ .btn:hover { background: var(--bg-hover); border-color: var(--accent); }
73
+ .btn-primary { border-color: var(--accent); color: var(--accent); }
74
+ .btn-danger { border-color: var(--red); color: var(--red); }
75
+ .btn:disabled { opacity: 0.5; cursor: not-allowed; }
76
+ .status-line {
77
+ color: var(--fg-dim);
78
+ font-size: 12px;
79
+ margin-bottom: 12px;
80
+ min-height: 16px;
81
+ }
82
+ .status-line.err { color: var(--red); }
83
+ .status-line.ok { color: var(--green); }
84
+ section.bucket {
85
+ margin-top: 28px;
86
+ }
87
+ section.bucket h2 {
88
+ font-size: 14px;
89
+ font-weight: 600;
90
+ color: var(--fg-dim);
91
+ margin: 0 0 10px 0;
92
+ text-transform: uppercase;
93
+ letter-spacing: 0.06em;
94
+ }
95
+ section.bucket .count {
96
+ color: var(--fg-dim);
97
+ font-weight: 400;
98
+ margin-left: 6px;
99
+ }
100
+ .empty {
101
+ color: var(--fg-dim);
102
+ font-style: italic;
103
+ padding: 16px;
104
+ background: var(--bg-card);
105
+ border: 1px dashed var(--border);
106
+ border-radius: 6px;
107
+ text-align: center;
108
+ font-size: 13px;
109
+ }
110
+ .card {
111
+ background: var(--bg-card);
112
+ border: 1px solid var(--border);
113
+ border-radius: 6px;
114
+ padding: 14px 16px;
115
+ margin-bottom: 10px;
116
+ }
117
+ .card.queued { opacity: 0.78; }
118
+ .card-head {
119
+ display: flex;
120
+ justify-content: space-between;
121
+ align-items: baseline;
122
+ gap: 12px;
123
+ margin-bottom: 8px;
124
+ }
125
+ .card-title {
126
+ font-weight: 600;
127
+ font-size: 14px;
128
+ }
129
+ .card-meta {
130
+ color: var(--fg-dim);
131
+ font-size: 11px;
132
+ font-family: ui-monospace, monospace;
133
+ }
134
+ .card-body {
135
+ font-size: 13px;
136
+ color: var(--fg);
137
+ }
138
+ .card-actions {
139
+ display: flex;
140
+ gap: 8px;
141
+ margin-top: 12px;
142
+ }
143
+ .untrusted-frame {
144
+ background: var(--untrusted);
145
+ border: 1px solid var(--untrusted-border);
146
+ border-radius: 4px;
147
+ padding: 10px 12px;
148
+ margin-top: 8px;
149
+ font-size: 12px;
150
+ }
151
+ .untrusted-frame .untrusted-label {
152
+ color: var(--amber);
153
+ font-size: 10px;
154
+ text-transform: uppercase;
155
+ letter-spacing: 0.08em;
156
+ margin-bottom: 6px;
157
+ font-weight: 600;
158
+ }
159
+ .untrusted-frame .untrusted-content {
160
+ white-space: pre-wrap;
161
+ word-break: break-word;
162
+ }
163
+ .kv {
164
+ display: grid;
165
+ grid-template-columns: 130px 1fr;
166
+ gap: 4px 10px;
167
+ font-size: 12px;
168
+ margin-top: 8px;
169
+ }
170
+ .kv .k { color: var(--fg-dim); }
171
+ .kv .v {
172
+ font-family: ui-monospace, monospace;
173
+ word-break: break-all;
174
+ }
175
+ .redaction-note {
176
+ color: var(--amber);
177
+ font-size: 11px;
178
+ margin-top: 6px;
179
+ }
180
+ </style>
181
+ </head>
182
+ <body>
183
+ <div class="page">
184
+ <header class="top">
185
+ <h1>Remediation Proposals</h1>
186
+ <span class="sub">
187
+ Pending <code>NovelFailureReviewer</code> outputs — review-needed,
188
+ un-promoted. Untrusted LLM-summarized content rendered in a clearly
189
+ marked frame per spec §A10.
190
+ </span>
191
+ </header>
192
+
193
+ <div class="auth-bar">
194
+ <input
195
+ id="authToken"
196
+ type="password"
197
+ placeholder="Bearer token (from .instar/config.json#authToken)"
198
+ autocomplete="off"
199
+ />
200
+ <button class="btn btn-primary" id="refreshBtn" type="button">Refresh</button>
201
+ </div>
202
+
203
+ <div class="status-line" id="status"></div>
204
+
205
+ <section class="bucket" id="visibleSection">
206
+ <h2>Visible <span class="count" id="visibleCount">(0)</span></h2>
207
+ <div id="visibleList"></div>
208
+ </section>
209
+
210
+ <section class="bucket" id="queuedSection">
211
+ <h2>Queued (silent, behind the visible 3) <span class="count" id="queuedCount">(0)</span></h2>
212
+ <div id="queuedList"></div>
213
+ </section>
214
+
215
+ <section class="bucket" id="dismissedSection">
216
+ <h2>Dismissed <span class="count" id="dismissedCount">(0)</span></h2>
217
+ <div id="dismissedList"></div>
218
+ </section>
219
+ </div>
220
+
221
+ <script>
222
+ // ── State ────────────────────────────────────────────────────────
223
+ const TOKEN_STORAGE_KEY = 'instar.remediation-proposals.authToken';
224
+
225
+ function loadToken() {
226
+ try {
227
+ return localStorage.getItem(TOKEN_STORAGE_KEY) || '';
228
+ } catch { return ''; }
229
+ }
230
+ function saveToken(t) {
231
+ try { localStorage.setItem(TOKEN_STORAGE_KEY, t); } catch { /* noop */ }
232
+ }
233
+
234
+ const authInput = document.getElementById('authToken');
235
+ const statusEl = document.getElementById('status');
236
+ const refreshBtn = document.getElementById('refreshBtn');
237
+ authInput.value = loadToken();
238
+ authInput.addEventListener('change', () => saveToken(authInput.value));
239
+
240
+ function setStatus(msg, kind) {
241
+ statusEl.textContent = msg;
242
+ statusEl.className = 'status-line' + (kind ? ' ' + kind : '');
243
+ }
244
+
245
+ function authHeaders() {
246
+ const t = authInput.value.trim();
247
+ const headers = { 'X-Instar-Request': '1' };
248
+ if (t) headers['Authorization'] = 'Bearer ' + t;
249
+ return headers;
250
+ }
251
+
252
+ function escapeHtml(s) {
253
+ if (s == null) return '';
254
+ return String(s)
255
+ .replace(/&/g, '&amp;')
256
+ .replace(/</g, '&lt;')
257
+ .replace(/>/g, '&gt;')
258
+ .replace(/"/g, '&quot;')
259
+ .replace(/'/g, '&#39;');
260
+ }
261
+
262
+ function fmtDate(ts) {
263
+ if (typeof ts !== 'number') return 'unknown time';
264
+ try {
265
+ const d = new Date(ts);
266
+ return d.toISOString().replace('T', ' ').slice(0, 19);
267
+ } catch { return 'invalid time'; }
268
+ }
269
+
270
+ function renderCard(p, opts) {
271
+ const queued = !!(opts && opts.queued);
272
+ const dismissed = !!(opts && opts.dismissed);
273
+ const id = escapeHtml(p.proposalId || '(missing id)');
274
+ const summary = escapeHtml(p.llmSummary || '(no summary)');
275
+ const hypothesis = escapeHtml(p.hypothesis || '');
276
+ const errCode = escapeHtml(p.suggestedErrorCode || '');
277
+ const agent = escapeHtml(p.producingAgentId || '');
278
+ const gen = fmtDate(p.generatedAt);
279
+ const occ = p.occurrencesObserved != null ? p.occurrencesObserved : '?';
280
+ const lifetimes = p.processLifetimes != null ? p.processLifetimes : '?';
281
+ const redacted = p.redactionApplied === true;
282
+ const showDismiss = !queued && !dismissed && p.status === 'outstanding';
283
+ return [
284
+ '<div class="card ' + (queued ? 'queued' : '') + '" data-id="' + id + '">',
285
+ ' <div class="card-head">',
286
+ ' <div class="card-title">[REVIEW NEEDED] ' + (errCode ? escapeHtml(errCode) : '(no error code)') + '</div>',
287
+ ' <div class="card-meta">' + id + '</div>',
288
+ ' </div>',
289
+ ' <div class="card-body">',
290
+ ' <div class="untrusted-frame">',
291
+ ' <div class="untrusted-label">Untrusted LLM-summarized content</div>',
292
+ ' <div class="untrusted-content">' + summary + (hypothesis ? '\n\nHypothesis: ' + hypothesis : '') + '</div>',
293
+ ' </div>',
294
+ ' <div class="kv">',
295
+ ' <div class="k">Cluster sig</div><div class="v">' + escapeHtml(p.clusterSignature || '') + '</div>',
296
+ ' <div class="k">Occurrences</div><div class="v">' + occ + ' across ' + lifetimes + ' process lifetimes</div>',
297
+ ' <div class="k">Producing agent</div><div class="v">' + agent + '</div>',
298
+ ' <div class="k">Generated</div><div class="v">' + gen + '</div>',
299
+ ' <div class="k">Status</div><div class="v">' + escapeHtml(p.status || 'unknown') + '</div>',
300
+ ' </div>',
301
+ redacted ? '<div class="redaction-note">reason.full + forensic.rawResponse hidden — collaborative trust required to view.</div>' : '',
302
+ ' </div>',
303
+ ' <div class="card-actions">',
304
+ ' <button class="btn" type="button" data-action="detail" data-id="' + id + '">View detail</button>',
305
+ showDismiss ? ' <button class="btn btn-danger" type="button" data-action="dismiss" data-id="' + id + '">Dismiss</button>' : '',
306
+ ' </div>',
307
+ '</div>',
308
+ ].join('\n');
309
+ }
310
+
311
+ function renderList(elementId, arr, opts) {
312
+ const el = document.getElementById(elementId);
313
+ if (!arr || arr.length === 0) {
314
+ el.innerHTML = '<div class="empty">None.</div>';
315
+ return;
316
+ }
317
+ el.innerHTML = arr.map((p) => renderCard(p, opts)).join('');
318
+ }
319
+
320
+ async function loadProposals() {
321
+ setStatus('Loading...', '');
322
+ try {
323
+ const res = await fetch('/remediation/proposals', { headers: authHeaders() });
324
+ if (res.status === 401) {
325
+ setStatus('Auth required — enter your bearer token above.', 'err');
326
+ return;
327
+ }
328
+ if (!res.ok) {
329
+ const body = await res.text();
330
+ setStatus('Error ' + res.status + ': ' + body, 'err');
331
+ return;
332
+ }
333
+ const data = await res.json();
334
+ document.getElementById('visibleCount').textContent = '(' + data.visible.length + ')';
335
+ document.getElementById('queuedCount').textContent = '(' + data.queued.length + ')';
336
+ document.getElementById('dismissedCount').textContent = '(' + (data.dismissed || []).length + ')';
337
+ renderList('visibleList', data.visible, { queued: false });
338
+ renderList('queuedList', data.queued, { queued: true });
339
+ renderList('dismissedList', data.dismissed || [], { dismissed: true });
340
+ const collaborative = data.trust && data.trust.hasCollaborative;
341
+ setStatus(
342
+ 'Loaded ' + data.visible.length + ' visible / ' + data.queued.length + ' queued. Trust: ' +
343
+ (collaborative ? 'collaborative (full view)' : 'below collaborative (redacted view)'),
344
+ 'ok',
345
+ );
346
+ } catch (err) {
347
+ setStatus('Network error: ' + (err && err.message ? err.message : err), 'err');
348
+ }
349
+ }
350
+
351
+ async function dismissProposal(id) {
352
+ if (!id) return;
353
+ if (!confirm('Dismiss proposal ' + id + '? This removes it from the outstanding-3 cap.')) return;
354
+ try {
355
+ const res = await fetch('/remediation/proposals/' + encodeURIComponent(id) + '/dismiss', {
356
+ method: 'POST',
357
+ headers: authHeaders(),
358
+ });
359
+ if (res.status === 403) {
360
+ setStatus('Dismiss refused: collaborative trust required.', 'err');
361
+ return;
362
+ }
363
+ if (res.status === 429) {
364
+ const body = await res.json().catch(() => ({}));
365
+ setStatus('Dismiss rate-limited. ' + (body.error || ''), 'err');
366
+ return;
367
+ }
368
+ if (!res.ok) {
369
+ const body = await res.text();
370
+ setStatus('Dismiss failed ' + res.status + ': ' + body, 'err');
371
+ return;
372
+ }
373
+ setStatus('Dismissed.', 'ok');
374
+ loadProposals();
375
+ } catch (err) {
376
+ setStatus('Network error during dismiss: ' + err, 'err');
377
+ }
378
+ }
379
+
380
+ async function showDetail(id) {
381
+ try {
382
+ const res = await fetch('/remediation/proposals/' + encodeURIComponent(id), {
383
+ headers: authHeaders(),
384
+ });
385
+ if (!res.ok) {
386
+ const body = await res.text();
387
+ setStatus('Detail fetch failed ' + res.status + ': ' + body, 'err');
388
+ return;
389
+ }
390
+ const data = await res.json();
391
+ const w = window.open('', '_blank', 'width=720,height=600');
392
+ if (!w) return;
393
+ const json = JSON.stringify(data.proposal, null, 2);
394
+ w.document.write(
395
+ '<!doctype html><html><head><title>Proposal ' + escapeHtml(id) +
396
+ '</title><style>body{background:#0c0c10;color:#e4e4eb;font-family:ui-monospace,monospace;padding:16px;font-size:12px;white-space:pre-wrap}</style></head>' +
397
+ '<body>' + escapeHtml(json) + '</body></html>',
398
+ );
399
+ w.document.close();
400
+ } catch (err) {
401
+ setStatus('Network error: ' + err, 'err');
402
+ }
403
+ }
404
+
405
+ refreshBtn.addEventListener('click', loadProposals);
406
+ document.addEventListener('click', (ev) => {
407
+ const t = ev.target;
408
+ if (!(t instanceof HTMLElement)) return;
409
+ const action = t.dataset.action;
410
+ const id = t.dataset.id;
411
+ if (action === 'dismiss') dismissProposal(id);
412
+ else if (action === 'detail') showDetail(id);
413
+ });
414
+
415
+ if (authInput.value) loadProposals();
416
+ </script>
417
+ </body>
418
+ </html>
@@ -0,0 +1,68 @@
1
+ /**
2
+ * BurnVerifier — post-throttle verification + follow-up alerts.
3
+ *
4
+ * Phase 6 (final phase) of docs/specs/token-burn-detection-and-self-heal.md.
5
+ * Five minutes after the runbook installs a throttle, this module re-samples
6
+ * the token-ledger telemetry for the affected attribution_key. If the
7
+ * post-throttle rate dropped materially, a structured "I caught it, I
8
+ * slowed it down, here's the before-and-after" Telegram message goes out
9
+ * (the umbrella spec's "fixed, here's the before/after" follow-up shape).
10
+ *
11
+ * If the post-throttle rate did NOT drop, the verifier escalates: the
12
+ * throttle was pointed at the wrong key, or the path doesn't honour the
13
+ * gate. Operator gets a "I tried but it did not work" message with the
14
+ * specific shape that distinguishes from "I caught it" so they know to
15
+ * intervene manually.
16
+ *
17
+ * Pure timer + telemetry-read; no LLM calls (the umbrella spec's
18
+ * §"Why it's safe — What if the watcher itself burns tokens?" guarantee).
19
+ */
20
+ import type { TokenLedger } from './TokenLedger.js';
21
+ import type { RunbookOutcome } from './BurnThrottleRunbook.js';
22
+ import type { DegradationEvent } from './DegradationReporter.js';
23
+ export interface VerificationResult {
24
+ attributionKey: string;
25
+ preThrottleRate: number;
26
+ postThrottleRate: number;
27
+ ratio: number;
28
+ successfullyThrottled: boolean;
29
+ verifiedAt: string;
30
+ }
31
+ export interface BurnVerifierDeps {
32
+ ledger: Pick<TokenLedger, 'byAttributionKey'>;
33
+ sendTelegram?: (topicId: number, text: string) => void | Promise<void>;
34
+ alertTopicId?: number;
35
+ now?: () => number;
36
+ config?: {
37
+ verifyDelayMs?: number;
38
+ sampleWindowMs?: number;
39
+ successRatio?: number;
40
+ };
41
+ schedule?: (cb: () => void, delayMs: number) => void;
42
+ }
43
+ export declare class BurnVerifier {
44
+ private readonly ledger;
45
+ private readonly sendTelegram;
46
+ private readonly alertTopicId;
47
+ private readonly now;
48
+ private readonly verifyDelayMs;
49
+ private readonly sampleWindowMs;
50
+ private readonly successRatio;
51
+ private readonly schedule;
52
+ constructor(deps: BurnVerifierDeps);
53
+ /**
54
+ * Called by the subscriber when the runbook installs a throttle. Pulls
55
+ * the pre-throttle rate from the originating event, schedules a re-sample
56
+ * after `verifyDelayMs`, and on fire sends the structured follow-up.
57
+ */
58
+ scheduleVerification(outcome: RunbookOutcome, event: DegradationEvent): void;
59
+ /**
60
+ * Run the verification re-sample. Exposed for tests + the manual-rerun
61
+ * surface (Phase 6+ dashboard control).
62
+ */
63
+ runVerification(attributionKey: string, preThrottleRate: number): VerificationResult;
64
+ private fireFollowUp;
65
+ private fireTelegram;
66
+ }
67
+ export declare function extractTokensLast1h(event: DegradationEvent): number;
68
+ //# sourceMappingURL=BurnVerifier.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BurnVerifier.d.ts","sourceRoot":"","sources":["../../src/monitoring/BurnVerifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAMjE,MAAM,WAAW,kBAAkB;IACjC,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,qBAAqB,EAAE,OAAO,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,IAAI,CAAC,WAAW,EAAE,kBAAkB,CAAC,CAAC;IAC9C,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE;QACP,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;IACF,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACtD;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA6B;IACpD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAwE;IACrG,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA4C;gBAEzD,IAAI,EAAE,gBAAgB;IAgBlC;;;;OAIG;IACH,oBAAoB,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAc5E;;;OAGG;IACH,eAAe,CAAC,cAAc,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,kBAAkB;IAqBpF,OAAO,CAAC,YAAY;IAepB,OAAO,CAAC,YAAY;CAQrB;AAID,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAUnE"}
@@ -0,0 +1,151 @@
1
+ /**
2
+ * BurnVerifier — post-throttle verification + follow-up alerts.
3
+ *
4
+ * Phase 6 (final phase) of docs/specs/token-burn-detection-and-self-heal.md.
5
+ * Five minutes after the runbook installs a throttle, this module re-samples
6
+ * the token-ledger telemetry for the affected attribution_key. If the
7
+ * post-throttle rate dropped materially, a structured "I caught it, I
8
+ * slowed it down, here's the before-and-after" Telegram message goes out
9
+ * (the umbrella spec's "fixed, here's the before/after" follow-up shape).
10
+ *
11
+ * If the post-throttle rate did NOT drop, the verifier escalates: the
12
+ * throttle was pointed at the wrong key, or the path doesn't honour the
13
+ * gate. Operator gets a "I tried but it did not work" message with the
14
+ * specific shape that distinguishes from "I caught it" so they know to
15
+ * intervene manually.
16
+ *
17
+ * Pure timer + telemetry-read; no LLM calls (the umbrella spec's
18
+ * §"Why it's safe — What if the watcher itself burns tokens?" guarantee).
19
+ */
20
+ const DEFAULT_SUCCESS_RATIO = 0.5;
21
+ const DEFAULT_VERIFY_DELAY_MS = 5 * 60 * 1000;
22
+ const DEFAULT_SAMPLE_WINDOW_MS = 5 * 60 * 1000;
23
+ export class BurnVerifier {
24
+ ledger;
25
+ sendTelegram;
26
+ alertTopicId;
27
+ now;
28
+ verifyDelayMs;
29
+ sampleWindowMs;
30
+ successRatio;
31
+ schedule;
32
+ constructor(deps) {
33
+ this.ledger = deps.ledger;
34
+ this.sendTelegram = deps.sendTelegram;
35
+ this.alertTopicId = deps.alertTopicId ?? 8615;
36
+ this.now = deps.now ?? (() => Date.now());
37
+ this.verifyDelayMs = deps.config?.verifyDelayMs ?? DEFAULT_VERIFY_DELAY_MS;
38
+ this.sampleWindowMs = deps.config?.sampleWindowMs ?? DEFAULT_SAMPLE_WINDOW_MS;
39
+ this.successRatio = deps.config?.successRatio ?? DEFAULT_SUCCESS_RATIO;
40
+ this.schedule = deps.schedule ?? ((cb, delayMs) => {
41
+ const t = setTimeout(cb, delayMs);
42
+ if (typeof t.unref === 'function') {
43
+ t.unref();
44
+ }
45
+ });
46
+ }
47
+ /**
48
+ * Called by the subscriber when the runbook installs a throttle. Pulls
49
+ * the pre-throttle rate from the originating event, schedules a re-sample
50
+ * after `verifyDelayMs`, and on fire sends the structured follow-up.
51
+ */
52
+ scheduleVerification(outcome, event) {
53
+ if (outcome.kind !== 'throttle-installed')
54
+ return;
55
+ const preThrottleRate = extractTokensLast1h(event);
56
+ const attributionKey = outcome.attributionKey;
57
+ this.schedule(() => {
58
+ try {
59
+ this.runVerification(attributionKey, preThrottleRate);
60
+ }
61
+ catch (err) {
62
+ console.warn(`[burn-verifier] verification threw (non-fatal): ${err.message}`);
63
+ }
64
+ }, this.verifyDelayMs);
65
+ }
66
+ /**
67
+ * Run the verification re-sample. Exposed for tests + the manual-rerun
68
+ * surface (Phase 6+ dashboard control).
69
+ */
70
+ runVerification(attributionKey, preThrottleRate) {
71
+ const sinceMs = this.now() - this.sampleWindowMs;
72
+ const rows = this.ledger.byAttributionKey({ sinceMs });
73
+ const row = rows.find((r) => r.attributionKey === attributionKey);
74
+ const postThrottleTokens = row ? row.totalTokens : 0;
75
+ const postThrottleRate = postThrottleTokens * (60 * 60 * 1000 / this.sampleWindowMs);
76
+ const ratio = preThrottleRate > 0 ? postThrottleRate / preThrottleRate : 0;
77
+ const successfullyThrottled = ratio < this.successRatio;
78
+ const result = {
79
+ attributionKey,
80
+ preThrottleRate,
81
+ postThrottleRate,
82
+ ratio,
83
+ successfullyThrottled,
84
+ verifiedAt: new Date(this.now()).toISOString(),
85
+ };
86
+ this.fireFollowUp(result);
87
+ return result;
88
+ }
89
+ fireFollowUp(r) {
90
+ const friendlyName = humanize(r.attributionKey);
91
+ const text = r.successfullyThrottled
92
+ ? `Caught and contained. ${friendlyName}: before the slowdown it was running at about ` +
93
+ `${formatRate(r.preThrottleRate)}; now it is running at about ${formatRate(r.postThrottleRate)} ` +
94
+ `— a ${formatReduction(r.ratio)} drop. The throttle will lift on its own at the configured time, ` +
95
+ `or you can release it sooner from the original alert.`
96
+ : `Slowdown did not take effect. I tried to slow ${friendlyName} down, but the rate did ` +
97
+ `not drop (was ${formatRate(r.preThrottleRate)}, still at ${formatRate(r.postThrottleRate)}). ` +
98
+ `This usually means one of two things: the attribution is pointing at the wrong code path ` +
99
+ `and the real offender is elsewhere, or the offending path does not honour the slowdown. ` +
100
+ `You may need to look at this one manually.`;
101
+ this.fireTelegram(text);
102
+ }
103
+ fireTelegram(text) {
104
+ if (!this.sendTelegram)
105
+ return;
106
+ try {
107
+ void this.sendTelegram(this.alertTopicId, text);
108
+ }
109
+ catch {
110
+ // Intentional swallow.
111
+ }
112
+ }
113
+ }
114
+ /* ---------- pure helpers ---------- */
115
+ export function extractTokensLast1h(event) {
116
+ const baselineM = /last-1h rate ([\d,]+)\s*tok\/h/.exec(event.reason || '');
117
+ if (baselineM && baselineM[1]) {
118
+ return Number(baselineM[1].replace(/,/g, ''));
119
+ }
120
+ const projectedM = /Projected ([\d,]+) tokens in next 24h/.exec(event.impact || '');
121
+ if (projectedM && projectedM[1]) {
122
+ return Number(projectedM[1].replace(/,/g, '')) / 24;
123
+ }
124
+ return 0;
125
+ }
126
+ function humanize(attributionKey) {
127
+ if (attributionKey.startsWith('unknown::'))
128
+ return 'an unknown component';
129
+ if (attributionKey.startsWith('user-job:')) {
130
+ const m = /^user-job:([^:]+)/.exec(attributionKey);
131
+ return m ? `your scheduled job "${m[1]}"` : 'a scheduled job';
132
+ }
133
+ if (attributionKey.startsWith('user-hook:')) {
134
+ const m = /^user-hook:([^:]+)/.exec(attributionKey);
135
+ return m ? `your hook "${m[1]}"` : 'a hook';
136
+ }
137
+ const m = /^([^:]+)::/.exec(attributionKey);
138
+ return m ? `the ${m[1]} component` : attributionKey;
139
+ }
140
+ function formatRate(tokensPerHour) {
141
+ if (tokensPerHour >= 1_000_000)
142
+ return `${(tokensPerHour / 1_000_000).toFixed(1)} million tokens per hour`;
143
+ if (tokensPerHour >= 1_000)
144
+ return `${(tokensPerHour / 1_000).toFixed(1)} thousand tokens per hour`;
145
+ return `${Math.round(tokensPerHour)} tokens per hour`;
146
+ }
147
+ function formatReduction(ratio) {
148
+ const pct = Math.round((1 - ratio) * 100);
149
+ return `${pct}%`;
150
+ }
151
+ //# sourceMappingURL=BurnVerifier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BurnVerifier.js","sourceRoot":"","sources":["../../src/monitoring/BurnVerifier.ts"],"names":[],"mappings":"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"}