@cesarus777/opencode-cgp 0.0.0-stage → 0.2.1

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 (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -2
  3. package/dist/cli.js +1411 -0
  4. package/dist/core.js +1247 -0
  5. package/dist/index.js +2565 -0
  6. package/dist/rpc.js +332 -0
  7. package/dist/tui.js +2286 -0
  8. package/dist/types/adapter.d.ts +20 -0
  9. package/dist/types/chartreport.d.ts +14 -0
  10. package/dist/types/charts.d.ts +54 -0
  11. package/dist/types/config.d.ts +2 -0
  12. package/dist/types/core.d.ts +10 -0
  13. package/dist/types/estimate.d.ts +1 -0
  14. package/dist/types/family.d.ts +39 -0
  15. package/dist/types/history.d.ts +59 -0
  16. package/dist/types/historycharts.d.ts +17 -0
  17. package/dist/types/historyserver.d.ts +7 -0
  18. package/dist/types/index.d.ts +3 -0
  19. package/dist/types/monitor.d.ts +20 -0
  20. package/dist/types/performance.d.ts +57 -0
  21. package/dist/types/policy.d.ts +12 -0
  22. package/dist/types/recovery.d.ts +22 -0
  23. package/dist/types/rpc.d.ts +129 -0
  24. package/dist/types/scheduler.d.ts +18 -0
  25. package/dist/types/snapshot.d.ts +98 -0
  26. package/dist/types/snapshotserver.d.ts +7 -0
  27. package/dist/types/telemetry.d.ts +17 -0
  28. package/dist/types/tui/chartformat.d.ts +11 -0
  29. package/dist/types/tui/charts.d.ts +10 -0
  30. package/dist/types/tui/compact.d.ts +10 -0
  31. package/dist/types/tui/compactformat.d.ts +28 -0
  32. package/dist/types/tui/format.d.ts +8 -0
  33. package/dist/types/tui/inventory.d.ts +18 -0
  34. package/dist/types/tui/panel.d.ts +8 -0
  35. package/dist/types/tui/sidebar.d.ts +13 -0
  36. package/dist/types/tui/state.d.ts +30 -0
  37. package/dist/types/tui/transport.d.ts +40 -0
  38. package/dist/types/tui.d.ts +3 -0
  39. package/dist/types/types.d.ts +153 -0
  40. package/package.json +110 -4
package/dist/index.js ADDED
@@ -0,0 +1,2565 @@
1
+ // @bun
2
+ // src/rpc.ts
3
+ import { Rpc } from "@opencode/plugin/rpc";
4
+ var count = {
5
+ type: "integer",
6
+ minimum: 0,
7
+ maximum: Number.MAX_SAFE_INTEGER
8
+ };
9
+ var text = { type: "string", minLength: 1, maxLength: 512 };
10
+ var enumeration = (...values) => ({ type: "string", enum: values });
11
+ var object = (properties, required = Object.keys(properties)) => ({
12
+ type: "object",
13
+ properties,
14
+ required,
15
+ additionalProperties: false
16
+ });
17
+ var array = (items, maxItems = 128) => ({
18
+ type: "array",
19
+ items,
20
+ maxItems
21
+ });
22
+ var measurement = object({
23
+ tokens: count,
24
+ at: count,
25
+ model: text,
26
+ requestId: text,
27
+ provenance: enumeration("provider-normalized", "serialized-estimate"),
28
+ confidence: enumeration("reported", "approximate"),
29
+ scope: enumeration("completed-request", "prepared-request")
30
+ }, ["tokens", "at", "model", "provenance", "confidence", "scope"]);
31
+ var row = object({
32
+ sessionId: text,
33
+ revision: count,
34
+ observedAt: count,
35
+ parentId: text,
36
+ agent: text,
37
+ model: text,
38
+ measurement,
39
+ limits: object({ context: count, input: count, output: count }, [
40
+ "context",
41
+ "output"
42
+ ]),
43
+ capacityTokens: count,
44
+ outputReserveTokens: count,
45
+ peakTokens: count,
46
+ policy: object({
47
+ id: text,
48
+ configured: object({
49
+ type: enumeration("tokens", "ratio", "remaining"),
50
+ value: { type: "number", minimum: 0 }
51
+ }),
52
+ triggerTokens: { type: "number" },
53
+ comparison: enumeration(">=", ">"),
54
+ reason: enumeration("missing-usage", "model-changed", "invalid-usage", "stale-usage", "missing-model-budget", "incompatible-model-budget", "threshold-crossed", "below-threshold"),
55
+ crossed: { type: "boolean" }
56
+ }, ["id", "configured", "comparison", "reason", "crossed"]),
57
+ compaction: object({
58
+ phase: enumeration("idle", "requesting", "accepted", "running", "awaiting-measurement", "blocked", "uncertain"),
59
+ count,
60
+ armed: { type: "boolean" },
61
+ blockedReason: enumeration("session-removed", "compaction-failed", "reclaim-unverifiable", "insufficient-reclaim", "completion-event-timeout", "admission-outcome-unknown", "restart-needs-lifecycle-evidence"),
62
+ lastAttempt: count,
63
+ lastCompleted: count,
64
+ cooldownUntil: count
65
+ }, ["phase", "armed"]),
66
+ cumulative: object({
67
+ input: count,
68
+ output: count,
69
+ reasoning: count,
70
+ cache: object({ read: count, write: count })
71
+ }),
72
+ cumulativeScope: enumeration("session", "unknown"),
73
+ lastObservedActive: { type: "boolean" }
74
+ }, [
75
+ "sessionId",
76
+ "revision",
77
+ "observedAt",
78
+ "policy",
79
+ "compaction",
80
+ "cumulativeScope",
81
+ "lastObservedActive"
82
+ ]);
83
+ var batchFields = {
84
+ schemaVersion: { const: 1 },
85
+ epoch: text,
86
+ sequence: count,
87
+ generatedAt: count,
88
+ health: object({
89
+ enabled: { type: "boolean" },
90
+ mode: enumeration("observe", "enforce"),
91
+ enforcement: enumeration("observing", "available", "suspended", "disabled"),
92
+ reasons: array(enumeration("event-stream-gap", "persistence-unavailable", "capacity-exceeded", "model-metadata-unavailable"), 8),
93
+ staleMs: count,
94
+ maxSessions: count
95
+ }),
96
+ upserts: array(row),
97
+ removed: array(object({
98
+ sessionId: text,
99
+ revision: count,
100
+ reason: enumeration("deleted", "evicted")
101
+ }))
102
+ };
103
+ var readSchema = object({
104
+ schemaVersion: { const: 1 },
105
+ sessionIds: { ...array(text), uniqueItems: true }
106
+ });
107
+ var batchSchema = object(batchFields);
108
+ var resultSchema = object({
109
+ ...batchFields,
110
+ unavailable: array(object({ sessionId: text, reason: enumeration("not-monitored") }))
111
+ });
112
+ var monitorRpc = Rpc.define({
113
+ id: "context-guard.monitor.v1",
114
+ methods: { read: { input: readSchema, output: resultSchema } },
115
+ events: { changed: { schema: batchSchema } }
116
+ });
117
+ var nonnegative = { type: "number", minimum: 0 };
118
+ var performanceSchema = object({
119
+ observedSince: count,
120
+ at: count,
121
+ sessionCreatedAt: count,
122
+ sessionOutputTokens: count,
123
+ status: enumeration("waiting", "streaming", "idle", "unavailable"),
124
+ partial: { type: "boolean" },
125
+ liveTps: nonnegative,
126
+ averageModelTps: nonnegative,
127
+ averageSessionTps: nonnegative,
128
+ ttftMs: nonnegative,
129
+ latestRequestTps: nonnegative,
130
+ requestId: text,
131
+ requestAgent: text,
132
+ requestModel: text,
133
+ requestOutputTokens: count,
134
+ requestStartedAt: count,
135
+ firstOutputAt: count,
136
+ streamEndedAt: count,
137
+ estimatedOutputTokens: nonnegative,
138
+ modelOutputTokens: nonnegative,
139
+ modelDurationMs: nonnegative,
140
+ completedRequests: count,
141
+ tokenSource: enumeration("estimated", "reported", "unavailable")
142
+ }, [
143
+ "sessionCreatedAt",
144
+ "observedSince",
145
+ "at",
146
+ "status",
147
+ "partial",
148
+ "estimatedOutputTokens",
149
+ "modelOutputTokens",
150
+ "modelDurationMs",
151
+ "completedRequests",
152
+ "tokenSource"
153
+ ]);
154
+ var performanceRow = {
155
+ ...row,
156
+ properties: { ...row.properties, performance: performanceSchema }
157
+ };
158
+ var performanceBatchSchema = object({
159
+ ...batchFields,
160
+ upserts: array(performanceRow)
161
+ });
162
+ var performanceResultSchema = object({
163
+ ...batchFields,
164
+ upserts: array(performanceRow),
165
+ unavailable: resultSchema.properties.unavailable ?? {}
166
+ });
167
+ var performanceRpc = Rpc.define({
168
+ id: "context-guard.monitor.v2",
169
+ methods: { read: { input: readSchema, output: performanceResultSchema } },
170
+ events: { changed: { schema: performanceBatchSchema } }
171
+ });
172
+ function isPerformanceBatch(value) {
173
+ return valid(value, performanceBatchSchema) && uniqueRows(value);
174
+ }
175
+ function isPerformanceResult(value) {
176
+ return valid(value, performanceResultSchema) && uniqueRows(value);
177
+ }
178
+ function isPerformanceSnapshot(value) {
179
+ return valid(value, performanceSchema);
180
+ }
181
+ var chartPointSchema = object({
182
+ time: count,
183
+ agent: text,
184
+ model: text,
185
+ breakBefore: { type: "boolean" },
186
+ partial: { type: "boolean" },
187
+ liveTps: nonnegative,
188
+ averageModelTps: nonnegative,
189
+ averageSessionTps: nonnegative,
190
+ sessionOutputTokens: count
191
+ }, ["time", "breakBefore", "partial"]);
192
+ var chartRequestSchema = object({
193
+ time: count,
194
+ requestId: text,
195
+ started: count,
196
+ agent: text,
197
+ model: text,
198
+ ttftMs: nonnegative,
199
+ outputTokens: count,
200
+ durationMs: nonnegative,
201
+ tps: nonnegative,
202
+ partial: { type: "boolean" }
203
+ }, ["time", "requestId", "started", "partial"]);
204
+ var historyReadSchema = object({
205
+ schemaVersion: { const: 1 },
206
+ sessionId: text,
207
+ from: count,
208
+ to: count,
209
+ maxPoints: { type: "integer", minimum: 16, maximum: 500 }
210
+ });
211
+ var historyResultSchema = object({
212
+ schemaVersion: { const: 1 },
213
+ generatedAt: count,
214
+ enabled: { type: "boolean" },
215
+ truncated: { type: "boolean" },
216
+ metadata: object({
217
+ schema: { const: 1 },
218
+ retainedFrom: count,
219
+ retainedBytes: count,
220
+ skippedBytes: count,
221
+ malformedRecords: count,
222
+ completeness: enumeration("bounded-retention")
223
+ }, [
224
+ "schema",
225
+ "retainedBytes",
226
+ "skippedBytes",
227
+ "malformedRecords",
228
+ "completeness"
229
+ ]),
230
+ data: object({
231
+ schema: { const: 1 },
232
+ omittedSessions: count,
233
+ omittedGaps: count,
234
+ maxPoints: count,
235
+ gaps: array(object({ time: count, sessionId: text, reason: text }), 128),
236
+ series: array(object({
237
+ sessionId: text,
238
+ parentId: text,
239
+ agents: array(text, 1024),
240
+ models: array(text, 1024),
241
+ points: array(chartPointSchema, 500),
242
+ requests: array(chartRequestSchema, 500),
243
+ originalPoints: count
244
+ }, [
245
+ "sessionId",
246
+ "agents",
247
+ "models",
248
+ "points",
249
+ "requests",
250
+ "originalPoints"
251
+ ]), 1)
252
+ }, ["schema", "omittedSessions", "maxPoints", "gaps", "series"])
253
+ });
254
+ var historyRpc = Rpc.define({
255
+ id: "context-guard.history.v1",
256
+ methods: { read: { input: historyReadSchema, output: historyResultSchema } },
257
+ events: {}
258
+ });
259
+ function isHistoryRead(value) {
260
+ return valid(value, historyReadSchema) && record(value) && typeof value.from === "number" && typeof value.to === "number" && value.from <= value.to && value.to - value.from <= 7 * 86400000;
261
+ }
262
+ function isHistoryResult(value) {
263
+ return valid(value, historyResultSchema);
264
+ }
265
+ function record(x) {
266
+ return typeof x === "object" && x !== null && !Array.isArray(x);
267
+ }
268
+ function matches(value, schema) {
269
+ if (!record(schema))
270
+ return false;
271
+ if ("const" in schema && value !== schema.const)
272
+ return false;
273
+ if (Array.isArray(schema.enum) && !schema.enum.includes(value))
274
+ return false;
275
+ if (schema.type === "number" || schema.type === "integer")
276
+ return typeof value === "number" && Number.isFinite(value) && (schema.type !== "integer" || Number.isSafeInteger(value)) && (typeof schema.minimum !== "number" || value >= schema.minimum) && (typeof schema.maximum !== "number" || value <= schema.maximum);
277
+ if (schema.type === "string")
278
+ return typeof value === "string" && value.length > 0 && value.length <= 512;
279
+ if (schema.type === "boolean")
280
+ return typeof value === "boolean";
281
+ if (schema.type === "array")
282
+ return Array.isArray(value) && value.length <= Number(schema.maxItems) && (!schema.uniqueItems || new Set(value).size === value.length) && value.every((v) => matches(v, schema.items));
283
+ if (schema.type === "object") {
284
+ if (!record(value) || !record(schema.properties) || !Array.isArray(schema.required))
285
+ return false;
286
+ const properties = schema.properties;
287
+ return schema.required.every((k) => typeof k === "string" && (k in value)) && Object.keys(value).every((k) => (k in properties) && matches(value[k], properties[k]));
288
+ }
289
+ return "const" in schema;
290
+ }
291
+ function valid(value, schema) {
292
+ try {
293
+ return JSON.stringify(value).length <= 524288 && matches(value, schema);
294
+ } catch {
295
+ return false;
296
+ }
297
+ }
298
+ function isRead(value) {
299
+ return valid(value, readSchema);
300
+ }
301
+ function isBatch(value) {
302
+ return valid(value, batchSchema) && uniqueRows(value);
303
+ }
304
+ function isResult(value) {
305
+ return valid(value, resultSchema) && uniqueRows(value);
306
+ }
307
+ function uniqueRows(value) {
308
+ if (!record(value) || !Array.isArray(value.upserts) || !Array.isArray(value.removed))
309
+ return false;
310
+ const ids = [...value.upserts, ...value.removed].map((v) => record(v) ? v.sessionId : undefined);
311
+ return ids.length <= 128 && new Set(ids).size === ids.length;
312
+ }
313
+
314
+ // src/index.ts
315
+ import { Plugin } from "@opencode/plugin";
316
+
317
+ // src/performance.ts
318
+ var valid2 = (n) => Number.isFinite(n) && n >= 0;
319
+ var rate = (tokens, ms) => valid2(tokens) && ms > 0 ? tokens * 1000 / ms : undefined;
320
+ function outputTokens(tokens) {
321
+ const n = tokens.output + tokens.reasoning;
322
+ return Number.isSafeInteger(tokens.output) && tokens.output >= 0 && Number.isSafeInteger(tokens.reasoning) && tokens.reasoning >= 0 && Number.isSafeInteger(n) ? n : undefined;
323
+ }
324
+
325
+ class PerformanceTracker {
326
+ maxSessions;
327
+ windowMs;
328
+ now;
329
+ rows = new Map;
330
+ constructor(maxSessions, windowMs = 2000, now = Date.now) {
331
+ this.maxSessions = maxSessions;
332
+ this.windowMs = windowMs;
333
+ this.now = now;
334
+ }
335
+ observe(id, created, cumulative) {
336
+ let row2 = this.rows.get(id);
337
+ if (!row2) {
338
+ if (this.rows.size >= this.maxSessions)
339
+ return;
340
+ row2 = {
341
+ observedSince: this.now(),
342
+ created,
343
+ partial: cumulative !== 0,
344
+ output: 0,
345
+ duration: 0,
346
+ completed: 0,
347
+ sequence: -1
348
+ };
349
+ this.rows.set(id, row2);
350
+ }
351
+ if (valid2(created))
352
+ row2.created = created;
353
+ if (cumulative !== undefined && valid2(cumulative))
354
+ row2.cumulative = cumulative;
355
+ }
356
+ accept(id, sequence) {
357
+ const row2 = this.rows.get(id);
358
+ if (!row2 || sequence <= row2.sequence)
359
+ return false;
360
+ row2.sequence = sequence;
361
+ return true;
362
+ }
363
+ start(id, requestId, agent, model, started) {
364
+ const row2 = this.rows.get(id);
365
+ if (!row2 || !valid2(started))
366
+ return;
367
+ const old = row2.request;
368
+ if (old?.id === requestId && old.started === started)
369
+ return;
370
+ if (old && !old.settled)
371
+ row2.partial = true;
372
+ row2.request = {
373
+ id: requestId,
374
+ agent,
375
+ model,
376
+ started,
377
+ chars: 0,
378
+ settled: false,
379
+ events: new Set,
380
+ buckets: new Map
381
+ };
382
+ }
383
+ delta(id, requestId, eventId, at, characters) {
384
+ const row2 = this.rows.get(id), r = row2?.request;
385
+ if (!row2 || !r || r.id !== requestId || r.settled || r.ended !== undefined || !valid2(at) || at < r.started || !valid2(characters) || !characters || r.events.has(eventId))
386
+ return;
387
+ if (r.events.size >= 256)
388
+ r.events.delete(r.events.values().next().value ?? "");
389
+ r.events.add(eventId);
390
+ r.first = r.first === undefined ? at : Math.min(r.first, at);
391
+ r.chars += characters;
392
+ const bucket = Math.floor(at / 100) * 100;
393
+ r.buckets.set(bucket, (r.buckets.get(bucket) ?? 0) + characters);
394
+ this.prune(r, Math.max(at, this.now()));
395
+ while (r.buckets.size > Math.ceil(this.windowMs / 100) + 2) {
396
+ r.buckets.delete(Math.min(...r.buckets.keys()));
397
+ }
398
+ }
399
+ streamed(id, requestId, at) {
400
+ const row2 = this.rows.get(id), r = row2?.request;
401
+ if (!row2 || !r || r.id !== requestId || r.settled || r.ended !== undefined)
402
+ return;
403
+ if (!valid2(at) || at <= r.started || r.first !== undefined && at < r.first) {
404
+ row2.partial = true;
405
+ return;
406
+ }
407
+ r.ended = at;
408
+ }
409
+ settle(id, requestId, tokens) {
410
+ const row2 = this.rows.get(id), r = row2?.request;
411
+ if (!row2)
412
+ return;
413
+ if (!r || r.id !== requestId) {
414
+ row2.partial = true;
415
+ return;
416
+ }
417
+ if (r.settled)
418
+ return;
419
+ r.settled = true;
420
+ if (tokens !== undefined && valid2(tokens))
421
+ r.output = tokens;
422
+ if (tokens !== undefined && valid2(tokens) && r.ended !== undefined) {
423
+ row2.output += tokens;
424
+ row2.duration += r.ended - r.started;
425
+ row2.completed++;
426
+ } else
427
+ row2.partial = true;
428
+ const snapshot = this.snapshot(id);
429
+ if (snapshot) {
430
+ const { latestRequestTps: _previousRate, ...current } = snapshot;
431
+ row2.latest = {
432
+ ...current,
433
+ tokenSource: tokens === undefined ? "unavailable" : "reported",
434
+ ...tokens !== undefined && r.ended !== undefined ? { latestRequestTps: tokens * 1000 / (r.ended - r.started) } : {}
435
+ };
436
+ }
437
+ r.events.clear();
438
+ r.buckets.clear();
439
+ }
440
+ gap(id) {
441
+ for (const [key, row2] of this.rows) {
442
+ if (id !== undefined && key !== id)
443
+ continue;
444
+ row2.partial = true;
445
+ delete row2.request;
446
+ }
447
+ }
448
+ prune(r, at) {
449
+ for (const time of r.buckets.keys())
450
+ if (time < at - this.windowMs)
451
+ r.buckets.delete(time);
452
+ }
453
+ snapshot(id) {
454
+ const row2 = this.rows.get(id);
455
+ if (!row2)
456
+ return;
457
+ const at = this.now(), r = row2.request;
458
+ const active = r && !r.settled && r.ended === undefined;
459
+ if (r)
460
+ this.prune(r, at);
461
+ const live = active ? rate([...r.buckets.values()].reduce((a, b) => a + b, 0) / 4, Math.min(this.windowMs, at - r.started)) : 0;
462
+ const averageModelTps = rate(row2.output, row2.duration);
463
+ const averageSessionTps = row2.cumulative === undefined ? undefined : rate(row2.cumulative, at - row2.created);
464
+ return {
465
+ observedSince: row2.observedSince,
466
+ at,
467
+ sessionCreatedAt: row2.created,
468
+ ...row2.cumulative === undefined ? {} : { sessionOutputTokens: row2.cumulative },
469
+ status: active ? r.first === undefined ? "waiting" : "streaming" : !r && row2.partial ? "unavailable" : "idle",
470
+ partial: row2.partial,
471
+ ...live === undefined || !r && row2.partial ? {} : { liveTps: live },
472
+ ...averageModelTps === undefined ? {} : { averageModelTps },
473
+ ...averageSessionTps === undefined ? {} : { averageSessionTps },
474
+ ...r ? {
475
+ requestId: r.id,
476
+ requestAgent: r.agent,
477
+ requestModel: r.model,
478
+ requestStartedAt: r.started,
479
+ ...r.output === undefined ? {} : { requestOutputTokens: r.output },
480
+ ...r.first === undefined ? {} : { firstOutputAt: r.first, ttftMs: r.first - r.started },
481
+ ...r.ended === undefined ? {} : { streamEndedAt: r.ended }
482
+ } : row2.latest ? {
483
+ ...row2.latest.ttftMs === undefined ? {} : { ttftMs: row2.latest.ttftMs }
484
+ } : {},
485
+ ...row2.latest?.latestRequestTps === undefined ? {} : { latestRequestTps: row2.latest.latestRequestTps },
486
+ estimatedOutputTokens: r ? r.chars / 4 : 0,
487
+ modelOutputTokens: row2.output,
488
+ modelDurationMs: row2.duration,
489
+ completedRequests: row2.completed,
490
+ tokenSource: r && !r.settled ? "estimated" : row2.latest?.tokenSource ?? "unavailable"
491
+ };
492
+ }
493
+ checkpoint() {
494
+ return [...this.rows].map(([sessionId, r]) => ({
495
+ sessionId,
496
+ observedSince: r.observedSince,
497
+ created: r.created,
498
+ output: r.output,
499
+ duration: r.duration,
500
+ completed: r.completed,
501
+ sequence: r.sequence
502
+ }));
503
+ }
504
+ restore(value) {
505
+ if (!Array.isArray(value))
506
+ return;
507
+ for (const item of value.slice(0, this.maxSessions)) {
508
+ if (!item || typeof item !== "object")
509
+ continue;
510
+ const r = item;
511
+ if (typeof r.sessionId !== "string" || !r.sessionId.length || r.sessionId.length > 512 || ![r.observedSince, r.created, r.output, r.duration, r.completed].every((n) => typeof n === "number" && Number.isSafeInteger(n) && n >= 0) || typeof r.sequence !== "number" || !Number.isSafeInteger(r.sequence) || r.sequence < -1)
512
+ continue;
513
+ const { observedSince, created, output, duration, completed, sequence } = r;
514
+ if (typeof observedSince !== "number" || typeof created !== "number" || typeof output !== "number" || typeof duration !== "number" || typeof completed !== "number")
515
+ continue;
516
+ this.rows.set(r.sessionId, {
517
+ observedSince,
518
+ created,
519
+ output,
520
+ duration,
521
+ completed,
522
+ sequence,
523
+ partial: true
524
+ });
525
+ }
526
+ }
527
+ remove(id) {
528
+ this.rows.delete(id);
529
+ }
530
+ ids() {
531
+ return this.rows.keys();
532
+ }
533
+ }
534
+
535
+ // src/charts.ts
536
+ var rateFields = [
537
+ "liveTps",
538
+ "averageModelTps",
539
+ "averageSessionTps",
540
+ "sessionOutputTokens"
541
+ ];
542
+ function downsamplePoints(points, limit) {
543
+ if (!Number.isSafeInteger(limit) || limit < 16 || limit > 5000)
544
+ throw new Error("maxPoints must be an integer from 16 to 5000");
545
+ if (points.length <= limit)
546
+ return [...points];
547
+ const buckets = Math.max(1, Math.floor(limit / (2 + rateFields.length * 2)));
548
+ const selected = new Set;
549
+ for (let b = 0;b < buckets; b++) {
550
+ const start = Math.floor(b * points.length / buckets), end = Math.floor((b + 1) * points.length / buckets);
551
+ selected.add(start);
552
+ selected.add(end - 1);
553
+ for (const field of rateFields) {
554
+ let low, high;
555
+ for (let i = start;i < end; i++) {
556
+ const value = points[i]?.[field];
557
+ if (value === undefined)
558
+ continue;
559
+ if (low === undefined || value < (points[low]?.[field] ?? Infinity))
560
+ low = i;
561
+ if (high === undefined || value > (points[high]?.[field] ?? -Infinity))
562
+ high = i;
563
+ }
564
+ if (low !== undefined)
565
+ selected.add(low);
566
+ if (high !== undefined)
567
+ selected.add(high);
568
+ }
569
+ }
570
+ let previous = -1;
571
+ return [...selected].sort((a, b) => a - b).flatMap((index) => {
572
+ const point = points[index];
573
+ if (!point)
574
+ return [];
575
+ const skippedGap = points.slice(previous + 1, index + 1).some((p) => p.breakBefore);
576
+ previous = index;
577
+ return [{ ...point, breakBefore: point.breakBefore || skippedGap }];
578
+ });
579
+ }
580
+ function buildChartData(records, options = {}) {
581
+ const maxPoints = options.maxPoints ?? 1000, maxSessions = options.maxSessions ?? 128;
582
+ if (!Number.isSafeInteger(maxSessions) || maxSessions < 1 || maxSessions > 1024)
583
+ throw new Error("maxSessions must be from 1 to 1024");
584
+ downsamplePoints([], maxPoints);
585
+ const sorted = [...records].sort((a, b) => a.time - b.time || a.recordId.localeCompare(b.recordId));
586
+ const lastSeen = new Map;
587
+ for (const r of sorted)
588
+ if (r.performance && (!options.sessionId || r.sessionId === options.sessionId))
589
+ lastSeen.set(r.sessionId, r.time);
590
+ const sessions = [...lastSeen].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
591
+ const selected = new Set(sessions.slice(0, maxSessions).map(([id]) => id));
592
+ const series = new Map, gaps = [];
593
+ const seen = new Set, requestSeen = new Set;
594
+ const pendingGaps = new Set;
595
+ const observedSince = new Map;
596
+ for (const r of sorted) {
597
+ if (seen.has(r.recordId))
598
+ continue;
599
+ seen.add(r.recordId);
600
+ if (r.type === "gap") {
601
+ if (r.sessionId === "__history__" || selected.has(r.sessionId)) {
602
+ gaps.push({
603
+ time: r.time,
604
+ sessionId: r.sessionId,
605
+ reason: r.event ?? "observation-gap"
606
+ });
607
+ if (r.sessionId === "__history__")
608
+ for (const id of selected)
609
+ pendingGaps.add(id);
610
+ else
611
+ pendingGaps.add(r.sessionId);
612
+ }
613
+ continue;
614
+ }
615
+ const p = r.performance;
616
+ if (!p || !selected.has(r.sessionId))
617
+ continue;
618
+ let s = series.get(r.sessionId);
619
+ if (!s) {
620
+ s = {
621
+ sessionId: r.sessionId,
622
+ ...r.parentId ? { parentId: r.parentId } : {},
623
+ agents: [],
624
+ models: [],
625
+ points: [],
626
+ requests: [],
627
+ originalPoints: 0
628
+ };
629
+ series.set(r.sessionId, s);
630
+ }
631
+ const agent = p.requestAgent ?? r.agent, model = p.requestModel ?? r.model;
632
+ if (agent && !s.agents.includes(agent))
633
+ s.agents.push(agent);
634
+ if (model && !s.models.includes(model))
635
+ s.models.push(model);
636
+ const previous = s.points.at(-1);
637
+ const restart = observedSince.has(r.sessionId) && observedSince.get(r.sessionId) !== p.observedSince;
638
+ const reset = previous?.sessionOutputTokens !== undefined && p.sessionOutputTokens !== undefined && p.sessionOutputTokens < previous.sessionOutputTokens;
639
+ const discontinuity = pendingGaps.has(r.sessionId) || restart || reset || !previous || previous.partial !== p.partial || previous.liveTps === undefined || p.liveTps === undefined || r.time - previous.time > 2500 || previous.agent !== agent || previous.model !== model;
640
+ const point = {
641
+ time: r.time,
642
+ ...agent ? { agent } : {},
643
+ ...model ? { model } : {},
644
+ breakBefore: discontinuity,
645
+ partial: p.partial,
646
+ ...p.liveTps === undefined ? {} : { liveTps: p.liveTps },
647
+ ...p.averageModelTps === undefined ? {} : { averageModelTps: p.averageModelTps },
648
+ ...p.averageSessionTps === undefined ? {} : { averageSessionTps: p.averageSessionTps },
649
+ ...p.sessionOutputTokens === undefined ? {} : { sessionOutputTokens: p.sessionOutputTokens }
650
+ };
651
+ if (previous?.time === point.time)
652
+ s.points[s.points.length - 1] = {
653
+ ...point,
654
+ breakBefore: point.breakBefore || previous.breakBefore
655
+ };
656
+ else
657
+ s.points.push(point);
658
+ observedSince.set(r.sessionId, p.observedSince);
659
+ pendingGaps.delete(r.sessionId);
660
+ if (r.type === "request" && p.requestId !== undefined && p.requestStartedAt !== undefined) {
661
+ const key = JSON.stringify([
662
+ r.sessionId,
663
+ p.requestId,
664
+ p.requestStartedAt
665
+ ]);
666
+ if (requestSeen.has(key))
667
+ continue;
668
+ requestSeen.add(key);
669
+ const duration = p.streamEndedAt === undefined ? undefined : p.streamEndedAt - p.requestStartedAt;
670
+ const validDuration = duration !== undefined && duration > 0;
671
+ const output = p.tokenSource === "reported" ? p.requestOutputTokens : undefined;
672
+ s.requests.push({
673
+ time: r.time,
674
+ requestId: p.requestId,
675
+ started: p.requestStartedAt,
676
+ ...agent ? { agent } : {},
677
+ ...model ? { model } : {},
678
+ partial: p.partial,
679
+ ...p.ttftMs === undefined ? {} : { ttftMs: p.ttftMs },
680
+ ...output === undefined ? {} : { outputTokens: output },
681
+ ...validDuration ? { durationMs: duration } : {},
682
+ ...output !== undefined && validDuration ? { tps: output * 1000 / duration } : {}
683
+ });
684
+ }
685
+ }
686
+ return {
687
+ schema: 1,
688
+ series: [...series.values()].map((s) => ({
689
+ ...s,
690
+ originalPoints: s.points.length,
691
+ points: downsamplePoints(s.points, maxPoints)
692
+ })),
693
+ gaps: gaps.slice(-1024),
694
+ omittedGaps: Math.max(0, gaps.length - 1024),
695
+ omittedSessions: Math.max(0, sessions.length - selected.size),
696
+ maxPoints
697
+ };
698
+ }
699
+
700
+ // src/history.ts
701
+ import { mkdir, readdir, stat, open, unlink } from "fs/promises";
702
+ import { join } from "path";
703
+ var filename = /^([0-9]+)-[a-f0-9-]+\.jsonl$/;
704
+ async function segments(directory) {
705
+ const names = await readdir(directory).catch((error) => {
706
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
707
+ return [];
708
+ throw error;
709
+ });
710
+ return (await Promise.all(names.filter((n) => filename.test(n)).map(async (name) => ({
711
+ name,
712
+ time: Number(filename.exec(name)?.[1]),
713
+ bytes: (await stat(join(directory, name))).size
714
+ })))).sort((a, b) => a.time - b.time || a.name.localeCompare(b.name));
715
+ }
716
+
717
+ class PerformanceHistory {
718
+ path;
719
+ config;
720
+ now;
721
+ report;
722
+ pending = [];
723
+ working;
724
+ active;
725
+ bytes = 0;
726
+ started = 0;
727
+ disabled = false;
728
+ dropped = 0;
729
+ droppedTotal = 0;
730
+ constructor(path, config, now = Date.now, report = () => {
731
+ }) {
732
+ this.path = path;
733
+ this.config = config;
734
+ this.now = now;
735
+ this.report = report;
736
+ }
737
+ emit(record2) {
738
+ if (!this.config.enabled || this.disabled)
739
+ return;
740
+ const line = JSON.stringify({
741
+ schema: 1,
742
+ recordId: record2.recordId,
743
+ time: record2.time,
744
+ type: record2.type,
745
+ sessionId: record2.sessionId,
746
+ ...record2.parentId ? { parentId: record2.parentId } : {},
747
+ ...record2.agent ? { agent: record2.agent } : {},
748
+ ...record2.model ? { model: record2.model } : {},
749
+ ...record2.event ? { event: record2.event } : {},
750
+ ...record2.droppedRecords === undefined ? {} : { droppedRecords: record2.droppedRecords },
751
+ ...record2.performance ? { performance: scalarPerformance(record2.performance) } : {}
752
+ }) + "\n";
753
+ if (this.pending.length >= 256 || Buffer.byteLength(line) > Math.min(4 * 1024 * 1024, this.config.maxBytes)) {
754
+ this.dropped++;
755
+ this.droppedTotal++;
756
+ if (this.dropped === 1)
757
+ this.report("history-records-dropped");
758
+ return;
759
+ }
760
+ this.pending.push(line);
761
+ this.kick();
762
+ }
763
+ kick() {
764
+ if (this.working || this.disabled)
765
+ return;
766
+ this.working = this.drain().catch(() => {
767
+ this.disabled = true;
768
+ this.pending = [];
769
+ this.report("history-unavailable");
770
+ }).finally(() => {
771
+ this.working = undefined;
772
+ if (this.pending.length)
773
+ this.kick();
774
+ });
775
+ }
776
+ async prune(incoming = 0) {
777
+ const files = await segments(this.path);
778
+ let total = files.reduce((n, f) => n + f.bytes, 0);
779
+ let truncated = false;
780
+ for (const file of files) {
781
+ if (file.time >= this.now() - this.config.retentionDays * 86400000 && total + incoming <= this.config.maxBytes)
782
+ break;
783
+ await unlink(join(this.path, file.name));
784
+ if (file.name === this.active) {
785
+ this.active = undefined;
786
+ this.bytes = 0;
787
+ }
788
+ total -= file.bytes;
789
+ truncated = true;
790
+ }
791
+ if (truncated)
792
+ this.report("history-truncated");
793
+ }
794
+ gapLine(event, droppedRecords) {
795
+ return JSON.stringify({
796
+ schema: 1,
797
+ recordId: crypto.randomUUID(),
798
+ time: this.now(),
799
+ type: "gap",
800
+ sessionId: "__history__",
801
+ event,
802
+ ...droppedRecords === undefined ? {} : { droppedRecords }
803
+ }) + "\n";
804
+ }
805
+ async drain() {
806
+ await mkdir(this.path, { recursive: true, mode: 448 });
807
+ await this.prune();
808
+ while (this.pending.length) {
809
+ let line = this.pending.shift();
810
+ if (!line)
811
+ continue;
812
+ if (this.dropped > 0) {
813
+ this.pending.unshift(line);
814
+ line = this.gapLine("history-records-dropped", this.dropped);
815
+ this.dropped = 0;
816
+ }
817
+ const size = Buffer.byteLength(line);
818
+ const now = this.now();
819
+ if (!this.active || this.bytes + size > Math.min(4 * 1024 * 1024, this.config.maxBytes) || now - this.started >= 3600000) {
820
+ this.active = undefined;
821
+ this.bytes = 0;
822
+ await this.prune(size);
823
+ this.started = now;
824
+ this.active = `${now}-${crypto.randomUUID()}.jsonl`;
825
+ } else
826
+ await this.prune(size);
827
+ if (!this.active) {
828
+ this.started = now;
829
+ this.active = `${now}-${crypto.randomUUID()}.jsonl`;
830
+ }
831
+ const file = await open(join(this.path, this.active), "a", 384);
832
+ try {
833
+ await file.writeFile(line);
834
+ } finally {
835
+ await file.close();
836
+ }
837
+ this.bytes += size;
838
+ }
839
+ }
840
+ health() {
841
+ return { unavailable: this.disabled, droppedRecords: this.droppedTotal };
842
+ }
843
+ async flush() {
844
+ while (this.working)
845
+ await this.working;
846
+ }
847
+ async initialize() {
848
+ if (!this.config.enabled)
849
+ return;
850
+ await mkdir(this.path, { recursive: true, mode: 448 }).then(() => this.prune()).catch(() => {
851
+ this.disabled = true;
852
+ this.report("history-unavailable");
853
+ });
854
+ }
855
+ }
856
+ function scalarPerformance(p) {
857
+ return {
858
+ observedSince: p.observedSince,
859
+ sessionCreatedAt: p.sessionCreatedAt,
860
+ at: p.at,
861
+ status: p.status,
862
+ partial: p.partial,
863
+ estimatedOutputTokens: p.estimatedOutputTokens,
864
+ modelOutputTokens: p.modelOutputTokens,
865
+ modelDurationMs: p.modelDurationMs,
866
+ completedRequests: p.completedRequests,
867
+ tokenSource: p.tokenSource,
868
+ ...Object.fromEntries([
869
+ "sessionOutputTokens",
870
+ "liveTps",
871
+ "averageModelTps",
872
+ "averageSessionTps",
873
+ "ttftMs",
874
+ "latestRequestTps",
875
+ "requestId",
876
+ "requestAgent",
877
+ "requestModel",
878
+ "requestOutputTokens",
879
+ "requestStartedAt",
880
+ "firstOutputAt",
881
+ "streamEndedAt"
882
+ ].flatMap((key) => {
883
+ const value = p[key];
884
+ return value === undefined ? [] : [[key, value]];
885
+ }))
886
+ };
887
+ }
888
+ async function readPerformanceHistory(directory, filter = {}) {
889
+ const records = [];
890
+ const files = await segments(directory);
891
+ const maxBytes = filter.maxBytes ?? 64 * 1024 * 1024;
892
+ let bytes = 0, skippedBytes = 0, malformedRecords = 0;
893
+ const selected = [];
894
+ for (const file of files.slice().reverse()) {
895
+ if (bytes + file.bytes > maxBytes) {
896
+ skippedBytes += file.bytes;
897
+ continue;
898
+ }
899
+ bytes += file.bytes;
900
+ selected.unshift(file);
901
+ }
902
+ for (const file of selected) {
903
+ const handle = await open(join(directory, file.name), "r");
904
+ let source;
905
+ try {
906
+ const buffer = Buffer.alloc(file.bytes);
907
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
908
+ source = buffer.subarray(0, bytesRead).toString("utf8");
909
+ } finally {
910
+ await handle.close();
911
+ }
912
+ for (const line of source.split("\n")) {
913
+ if (!line)
914
+ continue;
915
+ try {
916
+ const value = JSON.parse(line);
917
+ if (!isHistoryRecord(value)) {
918
+ malformedRecords++;
919
+ continue;
920
+ }
921
+ if (filter.sessionId !== undefined && value.sessionId !== filter.sessionId && value.sessionId !== "__history__" || value.type !== "gap" && filter.agent !== undefined && (value.performance?.requestAgent ?? value.agent) !== filter.agent || value.type !== "gap" && filter.model !== undefined && (value.performance?.requestModel ?? value.model) !== filter.model || filter.from !== undefined && value.time < filter.from || filter.to !== undefined && value.time > filter.to)
922
+ continue;
923
+ records.push(value);
924
+ } catch {
925
+ malformedRecords++;
926
+ }
927
+ }
928
+ }
929
+ const metadata = {
930
+ schema: 1,
931
+ ...files[0] ? { retainedFrom: files[0].time } : {},
932
+ retainedBytes: files.reduce((n, f) => n + f.bytes, 0),
933
+ skippedBytes,
934
+ malformedRecords,
935
+ completeness: "bounded-retention"
936
+ };
937
+ return { metadata, records };
938
+ }
939
+ function isRecord(value) {
940
+ return value !== null && typeof value === "object" && !Array.isArray(value);
941
+ }
942
+ function isHistoryRecord(value) {
943
+ if (!isRecord(value))
944
+ return false;
945
+ const r = value;
946
+ const allowed = new Set([
947
+ "schema",
948
+ "recordId",
949
+ "time",
950
+ "type",
951
+ "sessionId",
952
+ "parentId",
953
+ "agent",
954
+ "model",
955
+ "event",
956
+ "performance",
957
+ "droppedRecords"
958
+ ]);
959
+ return Object.keys(r).every((k) => allowed.has(k)) && r.schema === 1 && typeof r.recordId === "string" && typeof r.time === "number" && Number.isSafeInteger(r.time) && r.time >= 0 && typeof r.sessionId === "string" && typeof r.type === "string" && ["sample", "request", "lifecycle", "gap", "terminal"].includes(r.type) && ["parentId", "agent", "model", "event"].every((k) => r[k] === undefined || typeof r[k] === "string") && (r.performance === undefined || isPerformanceSnapshot(r.performance)) && (r.droppedRecords === undefined || typeof r.droppedRecords === "number" && Number.isSafeInteger(r.droppedRecords) && r.droppedRecords >= 0);
960
+ }
961
+
962
+ // src/historyserver.ts
963
+ async function serveHistory(rpc, path, enabled, now = Date.now, authorize = async () => true) {
964
+ if (!rpc)
965
+ return () => {
966
+ };
967
+ let reading = false, stopped = false;
968
+ const registration = await rpc.register(historyRpc, {
969
+ read: async (input, context) => {
970
+ if (!isHistoryRead(input))
971
+ throw new Error("Invalid history request (maximum window: seven days)");
972
+ if (reading || stopped)
973
+ throw new Error("History service busy or stopped; retry later");
974
+ reading = true;
975
+ try {
976
+ const allowed = !enabled || await authorize(input.sessionId, context.signal);
977
+ if (!allowed)
978
+ throw new Error("Session is outside this location");
979
+ const result = enabled ? await readPerformanceHistory(path, {
980
+ sessionId: input.sessionId,
981
+ from: input.from,
982
+ to: input.to
983
+ }) : {
984
+ metadata: {
985
+ schema: 1,
986
+ retainedBytes: 0,
987
+ skippedBytes: 0,
988
+ malformedRecords: 0,
989
+ completeness: "bounded-retention"
990
+ },
991
+ records: []
992
+ };
993
+ if (context.signal.aborted || stopped)
994
+ throw new Error("History read cancelled");
995
+ const data = buildChartData(result.records, {
996
+ sessionId: input.sessionId,
997
+ maxPoints: input.maxPoints,
998
+ maxSessions: 1
999
+ });
1000
+ const truncated = data.gaps.length > 128 || data.series.some((s) => s.requests.length > input.maxPoints || s.originalPoints > s.points.length) || result.metadata.skippedBytes > 0;
1001
+ data.gaps = data.gaps.slice(-128);
1002
+ for (const s of data.series)
1003
+ s.requests = s.requests.slice(-input.maxPoints);
1004
+ const response = {
1005
+ schemaVersion: 1,
1006
+ generatedAt: now(),
1007
+ enabled,
1008
+ truncated,
1009
+ metadata: { ...result.metadata, completeness: "bounded-retention" },
1010
+ data
1011
+ };
1012
+ if (!isHistoryResult(response))
1013
+ throw new Error("History response exceeds the supported bounds");
1014
+ return response;
1015
+ } finally {
1016
+ reading = false;
1017
+ }
1018
+ }
1019
+ });
1020
+ return async () => {
1021
+ stopped = true;
1022
+ await registration.dispose();
1023
+ };
1024
+ }
1025
+
1026
+ // src/policy.ts
1027
+ function glob(pattern, text2) {
1028
+ let p = 0, t = 0, star = -1, retry = 0;
1029
+ while (t < text2.length) {
1030
+ if (pattern[p] === "?" || pattern[p] === text2[t]) {
1031
+ p++;
1032
+ t++;
1033
+ } else if (pattern[p] === "*") {
1034
+ star = p++;
1035
+ retry = t;
1036
+ } else if (star >= 0) {
1037
+ p = star + 1;
1038
+ t = ++retry;
1039
+ } else
1040
+ return false;
1041
+ }
1042
+ while (pattern[p] === "*")
1043
+ p++;
1044
+ return p === pattern.length;
1045
+ }
1046
+
1047
+ class PolicyResolver {
1048
+ config;
1049
+ constructor(config) {
1050
+ this.config = config;
1051
+ }
1052
+ resolve(identity) {
1053
+ let result = {
1054
+ threshold: this.config.defaults.threshold,
1055
+ policy: "defaults"
1056
+ };
1057
+ this.config.policies.forEach((p, i) => {
1058
+ const m = p.match;
1059
+ if (m.role && m.role !== (identity.parentId ? "subagent" : "main"))
1060
+ return;
1061
+ if (m.agent && (!identity.agent || !glob(m.agent, identity.agent)))
1062
+ return;
1063
+ if (m.model && (!identity.model || !glob(m.model, identity.model)))
1064
+ return;
1065
+ result = { threshold: p.threshold, policy: `policies[${i}]` };
1066
+ });
1067
+ return result;
1068
+ }
1069
+ }
1070
+ function usableInput(limits, reserve) {
1071
+ if (!limits)
1072
+ return;
1073
+ const valid3 = (n) => n !== undefined && Number.isFinite(n) && n > 0;
1074
+ const output = reserve ?? (valid3(limits.output) ? limits.output : undefined);
1075
+ const contextBudget = valid3(limits.context) && output !== undefined ? Math.max(0, limits.context - output) : undefined;
1076
+ if (valid3(limits.input))
1077
+ return contextBudget === undefined ? limits.input : Math.min(limits.input, contextBudget);
1078
+ return contextBudget;
1079
+ }
1080
+ function evaluateThreshold(threshold, measurement2, limits, now, staleMs, model, policy, reserve) {
1081
+ const budget = usableInput(limits, reserve);
1082
+ const base = {
1083
+ crossed: false,
1084
+ reason: "missing-usage",
1085
+ policy,
1086
+ ...budget === undefined ? {} : { budget }
1087
+ };
1088
+ const trigger = threshold.type === "tokens" ? threshold.value : budget === undefined ? undefined : threshold.type === "ratio" ? Math.ceil(budget * threshold.value) : budget - threshold.value;
1089
+ if (trigger !== undefined)
1090
+ base.threshold = trigger;
1091
+ if (!measurement2)
1092
+ return base;
1093
+ if (model !== measurement2.model)
1094
+ return { ...base, reason: "model-changed" };
1095
+ if (!Number.isFinite(measurement2.tokens) || measurement2.tokens < 0)
1096
+ return { ...base, reason: "invalid-usage" };
1097
+ if (now - measurement2.at > staleMs || measurement2.at > now)
1098
+ return { ...base, reason: "stale-usage" };
1099
+ if (budget !== undefined && budget > 0)
1100
+ base.percentage = measurement2.tokens / budget * 100;
1101
+ if (trigger === undefined)
1102
+ return { ...base, reason: "missing-model-budget" };
1103
+ if (trigger <= 0)
1104
+ return { ...base, reason: "incompatible-model-budget" };
1105
+ const crossed = threshold.type === "remaining" ? measurement2.tokens > trigger : measurement2.tokens >= trigger;
1106
+ return {
1107
+ ...base,
1108
+ crossed,
1109
+ reason: crossed ? "threshold-crossed" : "below-threshold"
1110
+ };
1111
+ }
1112
+
1113
+ // src/snapshot.ts
1114
+ function project(s, config, now, revision = 0) {
1115
+ const p = new PolicyResolver(config).resolve(s);
1116
+ const d = evaluateThreshold(p.threshold, s.usage.current, s.limits, now, config.safety.staleMs, s.model, p.policy, config.safety.outputReserveTokens);
1117
+ const c = s.compaction;
1118
+ return freezeSnapshot({
1119
+ sessionId: s.sessionId,
1120
+ revision,
1121
+ observedAt: s.lastSeen,
1122
+ ...s.parentId ? { parentId: s.parentId } : {},
1123
+ ...s.agent ? { agent: s.agent } : {},
1124
+ ...s.model ? { model: s.model } : {},
1125
+ ...s.usage.current ? {
1126
+ measurement: {
1127
+ tokens: s.usage.current.tokens,
1128
+ at: s.usage.current.at,
1129
+ model: s.usage.current.model,
1130
+ provenance: s.usage.current.provenance,
1131
+ confidence: s.usage.current.confidence,
1132
+ scope: s.usage.current.scope,
1133
+ ...s.usage.current.requestId ? { requestId: s.usage.current.requestId } : {}
1134
+ }
1135
+ } : {},
1136
+ ...s.limits ? {
1137
+ limits: {
1138
+ context: s.limits.context,
1139
+ output: s.limits.output,
1140
+ ...s.limits.input === undefined ? {} : { input: s.limits.input }
1141
+ }
1142
+ } : {},
1143
+ ...d.budget === undefined ? {} : { capacityTokens: d.budget },
1144
+ ...config.safety.outputReserveTokens === undefined && !s.limits ? {} : {
1145
+ outputReserveTokens: config.safety.outputReserveTokens ?? s.limits?.output ?? 0
1146
+ },
1147
+ peakTokens: s.usage.peak,
1148
+ policy: {
1149
+ id: p.policy,
1150
+ configured: { type: p.threshold.type, value: p.threshold.value },
1151
+ ...d.threshold === undefined ? {} : { triggerTokens: d.threshold },
1152
+ comparison: p.threshold.type === "remaining" ? ">" : ">=",
1153
+ reason: d.reason,
1154
+ crossed: d.crossed
1155
+ },
1156
+ compaction: {
1157
+ phase: c.phase,
1158
+ count: c.count,
1159
+ armed: c.armed,
1160
+ ...c.blockedReason ? { blockedReason: c.blockedReason } : {},
1161
+ ...c.lastAttempt === undefined ? {} : {
1162
+ lastAttempt: c.lastAttempt,
1163
+ cooldownUntil: c.lastAttempt + config.safety.cooldownMs
1164
+ },
1165
+ ...c.lastCompleted === undefined ? {} : { lastCompleted: c.lastCompleted }
1166
+ },
1167
+ ...s.usage.cumulative ? {
1168
+ cumulative: {
1169
+ input: s.usage.cumulative.input,
1170
+ output: s.usage.cumulative.output,
1171
+ reasoning: s.usage.cumulative.reasoning,
1172
+ cache: {
1173
+ read: s.usage.cumulative.cache.read,
1174
+ write: s.usage.cumulative.cache.write
1175
+ }
1176
+ }
1177
+ } : {},
1178
+ cumulativeScope: s.usage.cumulativeScope,
1179
+ lastObservedActive: s.active
1180
+ });
1181
+ }
1182
+
1183
+ class SnapshotStore {
1184
+ config;
1185
+ sessions;
1186
+ now;
1187
+ performanceSnapshot;
1188
+ epoch;
1189
+ sequence = 0;
1190
+ revision = 0;
1191
+ rows = new Map;
1192
+ deleted = new Set;
1193
+ dirty = new Set;
1194
+ listeners = new Set;
1195
+ healthKey = "";
1196
+ reasons = new Set;
1197
+ constructor(config, sessions, now = Date.now, epoch = crypto.randomUUID(), performanceSnapshot) {
1198
+ this.config = config;
1199
+ this.sessions = sessions;
1200
+ this.now = now;
1201
+ this.performanceSnapshot = performanceSnapshot;
1202
+ this.epoch = epoch;
1203
+ }
1204
+ health() {
1205
+ return {
1206
+ enabled: this.config.enabled,
1207
+ mode: this.config.mode,
1208
+ enforcement: !this.config.enabled ? "disabled" : this.reasons.has("event-stream-gap") || this.reasons.has("persistence-unavailable") ? "suspended" : this.config.mode === "observe" ? "observing" : "available",
1209
+ reasons: [...this.reasons].sort(),
1210
+ staleMs: this.config.safety.staleMs,
1211
+ maxSessions: this.config.safety.maxSessions
1212
+ };
1213
+ }
1214
+ degrade(reason) {
1215
+ this.reasons.add(reason);
1216
+ }
1217
+ recover(reason) {
1218
+ this.reasons.delete(reason);
1219
+ }
1220
+ mark(id) {
1221
+ if (this.dirty.size < this.config.safety.maxSessions)
1222
+ this.dirty.add(id);
1223
+ }
1224
+ markAll() {
1225
+ for (const id of this.sessions().keys())
1226
+ this.mark(id);
1227
+ }
1228
+ remove(id) {
1229
+ if (this.rows.has(id))
1230
+ this.deleted.add(id);
1231
+ }
1232
+ listen(fn) {
1233
+ this.listeners.add(fn);
1234
+ return () => {
1235
+ this.listeners.delete(fn);
1236
+ };
1237
+ }
1238
+ batch(upserts, removed) {
1239
+ return {
1240
+ schemaVersion: 1,
1241
+ epoch: this.epoch,
1242
+ sequence: this.sequence,
1243
+ generatedAt: this.now(),
1244
+ health: this.health(),
1245
+ upserts,
1246
+ removed
1247
+ };
1248
+ }
1249
+ flush() {
1250
+ const upserts = [], removed = [];
1251
+ const resident = this.sessions();
1252
+ for (const [id, old] of this.rows)
1253
+ if (old.measurement && old.policy.reason !== "stale-usage" && this.now() - old.measurement.at > this.config.safety.staleMs)
1254
+ this.mark(id);
1255
+ for (const id of resident.keys())
1256
+ if (!this.rows.has(id))
1257
+ this.mark(id);
1258
+ for (const id of this.dirty) {
1259
+ const s = resident.get(id);
1260
+ if (!s)
1261
+ continue;
1262
+ const performance = this.performanceSnapshot?.(id);
1263
+ const next = freezeSnapshot({
1264
+ ...project(s, this.config, this.now()),
1265
+ ...performance ? { performance } : {}
1266
+ });
1267
+ const old = this.rows.get(id);
1268
+ if (old && JSON.stringify({ ...old, revision: 0 }) === JSON.stringify(next))
1269
+ continue;
1270
+ const row2 = freezeSnapshot({ ...next, revision: ++this.revision });
1271
+ this.rows.set(id, row2);
1272
+ upserts.push(row2);
1273
+ }
1274
+ for (const id of this.rows.keys())
1275
+ if (!resident.has(id)) {
1276
+ this.rows.delete(id);
1277
+ removed.push({
1278
+ sessionId: id,
1279
+ revision: ++this.revision,
1280
+ reason: this.deleted.has(id) ? "deleted" : "evicted"
1281
+ });
1282
+ }
1283
+ this.deleted.clear();
1284
+ this.dirty.clear();
1285
+ const healthKey = JSON.stringify(this.health()), changed = healthKey !== this.healthKey;
1286
+ this.healthKey = healthKey;
1287
+ const changes = [
1288
+ ...upserts.map((row2) => ({ kind: "upsert", row: row2 })),
1289
+ ...removed.map((row2) => ({ kind: "remove", row: row2 }))
1290
+ ];
1291
+ for (let i = 0;i < Math.max(changes.length, changed ? 1 : 0); i += 128) {
1292
+ this.sequence++;
1293
+ const slice = changes.slice(i, i + 128);
1294
+ const batch = this.batch(slice.flatMap((change) => change.kind === "upsert" ? [change.row] : []), slice.flatMap((change) => change.kind === "remove" ? [change.row] : []));
1295
+ for (const fn of this.listeners) {
1296
+ try {
1297
+ fn(structuredClone(batch));
1298
+ } catch {
1299
+ }
1300
+ }
1301
+ }
1302
+ }
1303
+ read(input) {
1304
+ this.flush();
1305
+ const ids = [...new Set(input.sessionIds)];
1306
+ return structuredClone({
1307
+ ...this.batch(ids.flatMap((id) => {
1308
+ const row2 = this.rows.get(id);
1309
+ return row2 ? [row2] : [];
1310
+ }), []),
1311
+ unavailable: ids.filter((id) => !this.rows.has(id)).map((sessionId) => ({ sessionId, reason: "not-monitored" }))
1312
+ });
1313
+ }
1314
+ }
1315
+ function freezeSnapshot(row2) {
1316
+ if (row2.performance)
1317
+ Object.freeze(row2.performance);
1318
+ if (row2.measurement)
1319
+ Object.freeze(row2.measurement);
1320
+ if (row2.limits)
1321
+ Object.freeze(row2.limits);
1322
+ Object.freeze(row2.policy.configured);
1323
+ Object.freeze(row2.policy);
1324
+ Object.freeze(row2.compaction);
1325
+ if (row2.cumulative) {
1326
+ Object.freeze(row2.cumulative.cache);
1327
+ Object.freeze(row2.cumulative);
1328
+ }
1329
+ return Object.freeze(row2);
1330
+ }
1331
+
1332
+ // src/snapshotserver.ts
1333
+ async function serveSnapshots(rpc, store, performance = false) {
1334
+ if (!rpc)
1335
+ return () => {
1336
+ };
1337
+ const stripPerformance = (batch) => ({
1338
+ ...batch,
1339
+ upserts: batch.upserts.map(({ performance: _performance, ...row2 }) => row2)
1340
+ });
1341
+ const batchValid = performance ? isPerformanceBatch : isBatch;
1342
+ const resultValid = performance ? isPerformanceResult : isResult;
1343
+ const registration = await rpc.register(performance ? performanceRpc : monitorRpc, {
1344
+ read: async (input) => {
1345
+ if (!isRead(input))
1346
+ throw new Error("Invalid snapshot request");
1347
+ const raw = store.read(input);
1348
+ const result = performance ? raw : stripPerformance(raw);
1349
+ if (!resultValid(result))
1350
+ throw new Error("Invalid snapshot projection");
1351
+ return result;
1352
+ }
1353
+ });
1354
+ const queue = [];
1355
+ const capacity = Math.ceil(store.health().maxSessions / 128) + 2;
1356
+ let busy = false, stopped = false;
1357
+ let retry;
1358
+ let pending = Promise.resolve();
1359
+ function send() {
1360
+ if (busy || stopped || !queue.length)
1361
+ return;
1362
+ const batch = queue[0];
1363
+ if (!batch)
1364
+ return;
1365
+ busy = true;
1366
+ pending = registration.events.emit("changed", { ...batch }).then(() => {
1367
+ if (queue[0] === batch)
1368
+ queue.shift();
1369
+ }, () => {
1370
+ if (!stopped)
1371
+ scheduleRetry();
1372
+ }).finally(() => {
1373
+ busy = false;
1374
+ if (!retry && !stopped)
1375
+ send();
1376
+ });
1377
+ }
1378
+ const unlisten = store.listen((raw) => {
1379
+ const batch = performance ? raw : stripPerformance(raw);
1380
+ if (!batchValid(batch))
1381
+ return;
1382
+ queue.push(batch);
1383
+ if (queue.length > capacity)
1384
+ queue.shift();
1385
+ if (!retry)
1386
+ send();
1387
+ });
1388
+ const retrySend = () => {
1389
+ retry = undefined;
1390
+ send();
1391
+ };
1392
+ function scheduleRetry() {
1393
+ retry = setTimeout(retrySend, 100);
1394
+ }
1395
+ return async () => {
1396
+ stopped = true;
1397
+ unlisten();
1398
+ clearTimeout(retry);
1399
+ queue.length = 0;
1400
+ await pending;
1401
+ await registration.dispose();
1402
+ };
1403
+ }
1404
+
1405
+ // src/config.ts
1406
+ function object2(value, path) {
1407
+ if (!value || typeof value !== "object" || Array.isArray(value))
1408
+ throw new Error(`${path}: expected object`);
1409
+ return value;
1410
+ }
1411
+ function keys(o, allowed, path) {
1412
+ for (const key of Object.keys(o))
1413
+ if (!allowed.includes(key))
1414
+ throw new Error(`${path}.${key}: unknown option`);
1415
+ }
1416
+ function number(v, fallback, path, min = 0) {
1417
+ if (v === undefined)
1418
+ return fallback;
1419
+ if (typeof v !== "number" || !Number.isSafeInteger(v) || v < min)
1420
+ throw new Error(`${path}: expected integer >= ${min}`);
1421
+ return v;
1422
+ }
1423
+ function bool(v, fallback, path) {
1424
+ if (v === undefined)
1425
+ return fallback;
1426
+ if (typeof v !== "boolean")
1427
+ throw new Error(`${path}: expected boolean`);
1428
+ return v;
1429
+ }
1430
+ function selector(v, path) {
1431
+ if (typeof v !== "string" || !v.length || v.length > 256 || /[\[\]{}\\]/.test(v))
1432
+ throw new Error(`${path}: expected selector (only * and ? wildcards)`);
1433
+ return v;
1434
+ }
1435
+ function threshold(v, path) {
1436
+ const o = object2(v, path);
1437
+ keys(o, ["type", "value"], path);
1438
+ if (o.type !== "tokens" && o.type !== "ratio" && o.type !== "remaining")
1439
+ throw new Error(`${path}.type: invalid threshold`);
1440
+ if (typeof o.value !== "number" || !Number.isFinite(o.value) || o.value <= 0 || (o.type === "ratio" ? o.value > 1 : !Number.isSafeInteger(o.value)))
1441
+ throw new Error(`${path}.value: invalid threshold`);
1442
+ return { type: o.type, value: o.value };
1443
+ }
1444
+ function parseConfig(value = {}) {
1445
+ const o = object2(value, "options");
1446
+ keys(o, [
1447
+ "enabled",
1448
+ "mode",
1449
+ "defaults",
1450
+ "policies",
1451
+ "safety",
1452
+ "telemetry",
1453
+ "metrics",
1454
+ "history"
1455
+ ], "options");
1456
+ if (o.mode !== undefined && o.mode !== "observe" && o.mode !== "enforce")
1457
+ throw new Error("mode: expected observe or enforce");
1458
+ const d = object2(o.defaults ?? {}, "defaults");
1459
+ keys(d, ["threshold"], "defaults");
1460
+ const s = object2(o.safety ?? {}, "safety");
1461
+ keys(s, [
1462
+ "cooldownMs",
1463
+ "minReclaimTokens",
1464
+ "maxConsecutiveFailures",
1465
+ "pendingTimeoutMs",
1466
+ "staleMs",
1467
+ "maxSessions",
1468
+ "sessionTtlMs",
1469
+ "outputReserveTokens"
1470
+ ], "safety");
1471
+ const metrics = object2(o.metrics ?? {}, "metrics");
1472
+ keys(metrics, ["enabled", "sampleIntervalMs", "rollingWindowMs"], "metrics");
1473
+ const history = object2(o.history ?? {}, "history");
1474
+ keys(history, ["enabled", "retentionDays", "maxBytes", "path"], "history");
1475
+ if (history.path !== undefined && (typeof history.path !== "string" || !history.path.startsWith("/")))
1476
+ throw new Error("history.path: expected absolute directory path");
1477
+ const t = object2(o.telemetry ?? {}, "telemetry");
1478
+ keys(t, ["enabled", "jsonl", "path", "maxBytes"], "telemetry");
1479
+ if (t.path !== undefined && (typeof t.path !== "string" || !t.path.startsWith("/")))
1480
+ throw new Error("telemetry.path: expected absolute path");
1481
+ if (o.policies !== undefined && (!Array.isArray(o.policies) || o.policies.length > 256))
1482
+ throw new Error("policies: expected array of at most 256 policies");
1483
+ const policies = (o.policies ?? []).map((v, i) => {
1484
+ const p = object2(v, `policies[${i}]`);
1485
+ keys(p, ["match", "threshold"], `policies[${i}]`);
1486
+ const m = object2(p.match, "match");
1487
+ keys(m, ["role", "model", "agent"], "match");
1488
+ if (!Object.keys(m).length)
1489
+ throw new Error("match: at least one selector required");
1490
+ const match = {};
1491
+ if (m.role !== undefined) {
1492
+ if (m.role !== "main" && m.role !== "subagent")
1493
+ throw new Error("match.role: invalid");
1494
+ match.role = m.role;
1495
+ }
1496
+ if (m.model !== undefined) {
1497
+ match.model = selector(m.model, "match.model");
1498
+ if (!match.model.includes("/"))
1499
+ throw new Error("match.model: use provider/model");
1500
+ }
1501
+ if (m.agent !== undefined)
1502
+ match.agent = selector(m.agent, "match.agent");
1503
+ return { match, threshold: threshold(p.threshold, "threshold") };
1504
+ });
1505
+ const config = {
1506
+ enabled: bool(o.enabled, true, "enabled"),
1507
+ mode: o.mode ?? "observe",
1508
+ defaults: {
1509
+ threshold: threshold(d.threshold ?? { type: "tokens", value: 131072 }, "defaults.threshold")
1510
+ },
1511
+ policies,
1512
+ safety: {
1513
+ cooldownMs: number(s.cooldownMs, 60000, "cooldownMs"),
1514
+ minReclaimTokens: number(s.minReclaimTokens, 8192, "minReclaimTokens"),
1515
+ maxConsecutiveFailures: number(s.maxConsecutiveFailures, 3, "maxConsecutiveFailures", 1),
1516
+ pendingTimeoutMs: number(s.pendingTimeoutMs, 300000, "pendingTimeoutMs", 1),
1517
+ staleMs: number(s.staleMs, 300000, "staleMs", 1),
1518
+ maxSessions: number(s.maxSessions, 1024, "maxSessions", 1),
1519
+ sessionTtlMs: number(s.sessionTtlMs, 86400000, "sessionTtlMs", 1),
1520
+ ...s.outputReserveTokens === undefined ? {} : {
1521
+ outputReserveTokens: number(s.outputReserveTokens, 0, "outputReserveTokens")
1522
+ }
1523
+ },
1524
+ metrics: {
1525
+ enabled: bool(metrics.enabled, true, "metrics.enabled"),
1526
+ sampleIntervalMs: number(metrics.sampleIntervalMs, 500, "metrics.sampleIntervalMs", 100),
1527
+ rollingWindowMs: number(metrics.rollingWindowMs, 2000, "metrics.rollingWindowMs", 100)
1528
+ },
1529
+ history: {
1530
+ enabled: bool(history.enabled, true, "history.enabled"),
1531
+ retentionDays: number(history.retentionDays, 7, "history.retentionDays", 1),
1532
+ maxBytes: number(history.maxBytes, 64 * 1024 * 1024, "history.maxBytes", 4096),
1533
+ ...typeof history.path === "string" ? { path: history.path } : {}
1534
+ },
1535
+ telemetry: {
1536
+ enabled: bool(t.enabled, true, "telemetry.enabled"),
1537
+ jsonl: bool(t.jsonl, true, "telemetry.jsonl"),
1538
+ maxBytes: number(t.maxBytes, 8 * 1024 * 1024, "maxBytes", 4096),
1539
+ ...typeof t.path === "string" ? { path: t.path } : {}
1540
+ }
1541
+ };
1542
+ if (config.metrics.rollingWindowMs < config.metrics.sampleIntervalMs || config.metrics.rollingWindowMs > 60000)
1543
+ throw new Error("metrics.rollingWindowMs must be >= sampleIntervalMs and <= 60000");
1544
+ for (const policy of [config.defaults, ...config.policies]) {
1545
+ if (policy.threshold.type === "tokens" && config.safety.minReclaimTokens >= policy.threshold.value)
1546
+ throw new Error("minReclaimTokens must be smaller than each absolute threshold");
1547
+ }
1548
+ if (!config.telemetry.jsonl && config.telemetry.path)
1549
+ throw new Error("telemetry.path requires jsonl");
1550
+ return config;
1551
+ }
1552
+
1553
+ // src/types.ts
1554
+ class NoopBudgetController {
1555
+ evaluate(_input) {
1556
+ return { type: "continue" };
1557
+ }
1558
+ }
1559
+
1560
+ // src/monitor.ts
1561
+ function promptTokens(t) {
1562
+ const fields = [t.input, t.output, t.reasoning, t.cache.read, t.cache.write];
1563
+ if (fields.some((x) => !Number.isSafeInteger(x) || x < 0))
1564
+ return;
1565
+ const total = t.input + t.cache.read + t.cache.write;
1566
+ return total > 0 && Number.isSafeInteger(total) ? total : undefined;
1567
+ }
1568
+
1569
+ class ContextMonitor {
1570
+ config;
1571
+ scheduler;
1572
+ sink;
1573
+ now;
1574
+ budget;
1575
+ sessions = new Map;
1576
+ resolver;
1577
+ constructor(config, scheduler, sink, now = Date.now, budget = new NoopBudgetController) {
1578
+ this.config = config;
1579
+ this.scheduler = scheduler;
1580
+ this.sink = sink;
1581
+ this.now = now;
1582
+ this.budget = budget;
1583
+ this.resolver = new PolicyResolver(config);
1584
+ }
1585
+ touch(identity, limits) {
1586
+ let s = this.sessions.get(identity.sessionId);
1587
+ if (!s) {
1588
+ this.cleanup();
1589
+ if (this.sessions.size >= this.config.safety.maxSessions) {
1590
+ this.sink.emit({
1591
+ time: this.now(),
1592
+ type: "capacity-exceeded",
1593
+ detail: "New session not monitored; increase safety.maxSessions"
1594
+ });
1595
+ return;
1596
+ }
1597
+ s = {
1598
+ ...identity,
1599
+ lastSeen: this.now(),
1600
+ sequence: -1,
1601
+ active: false,
1602
+ usage: {
1603
+ cumulativeScope: "unknown",
1604
+ peak: 0,
1605
+ modelRequests: 0,
1606
+ toolCalls: 0,
1607
+ countersSince: this.now()
1608
+ },
1609
+ compaction: {
1610
+ phase: "idle",
1611
+ armed: true,
1612
+ count: 0,
1613
+ failures: 0,
1614
+ consecutiveFailures: 0
1615
+ }
1616
+ };
1617
+ this.sessions.set(identity.sessionId, s);
1618
+ }
1619
+ if (identity.model && identity.model !== s.model) {
1620
+ delete s.usage.current;
1621
+ delete s.limits;
1622
+ }
1623
+ Object.assign(s, identity);
1624
+ s.lastSeen = this.now();
1625
+ if (limits)
1626
+ s.limits = { ...limits };
1627
+ return s;
1628
+ }
1629
+ accept(s, sequence) {
1630
+ if (sequence <= s.sequence)
1631
+ return false;
1632
+ s.sequence = sequence;
1633
+ s.lastSeen = this.now();
1634
+ return true;
1635
+ }
1636
+ measure(s, measurement2, cumulative) {
1637
+ if (s.usage.current && measurement2.at < s.usage.current.at)
1638
+ return;
1639
+ s.usage.current = measurement2;
1640
+ s.usage.peak = Math.max(s.usage.peak, measurement2.tokens);
1641
+ if (cumulative) {
1642
+ s.usage.cumulative = cumulative;
1643
+ s.usage.cumulativeScope = "session";
1644
+ }
1645
+ this.evaluate(s);
1646
+ }
1647
+ evaluate(s) {
1648
+ const p = this.resolver.resolve(s);
1649
+ s.decision = evaluateThreshold(p.threshold, s.usage.current, s.limits, this.now(), this.config.safety.staleMs, s.model, p.policy, this.config.safety.outputReserveTokens);
1650
+ this.scheduler.measured(s);
1651
+ const action = this.budget.evaluate({
1652
+ session: s,
1653
+ now: this.now(),
1654
+ elapsedMs: this.now() - s.usage.countersSince
1655
+ });
1656
+ if (action.type !== "continue")
1657
+ this.sink.emit({
1658
+ time: this.now(),
1659
+ type: `budget.${action.type}`,
1660
+ session: s,
1661
+ detail: "Advisory extension; only context policies execute in this release"
1662
+ });
1663
+ this.sink.emit({
1664
+ time: this.now(),
1665
+ type: this.config.mode === "observe" && s.decision.crossed ? "would-compact" : "usage",
1666
+ session: s
1667
+ });
1668
+ this.scheduler.consider(s);
1669
+ }
1670
+ remove(id) {
1671
+ const s = this.sessions.get(id);
1672
+ if (s) {
1673
+ s.active = false;
1674
+ s.compaction.phase = "blocked";
1675
+ s.compaction.blockedReason = "session-removed";
1676
+ this.sink.emit({ time: this.now(), type: "session.removed", session: s });
1677
+ this.sessions.delete(id);
1678
+ }
1679
+ }
1680
+ cleanup() {
1681
+ for (const [id, s] of this.sessions) {
1682
+ this.scheduler.tick(s);
1683
+ if (!s.active && ["idle", "blocked"].includes(s.compaction.phase) && this.now() - s.lastSeen > this.config.safety.sessionTtlMs)
1684
+ this.remove(id);
1685
+ }
1686
+ }
1687
+ async getUsage(id) {
1688
+ const s = this.sessions.get(id);
1689
+ if (!s)
1690
+ throw new Error("Unknown session");
1691
+ return structuredClone(s.usage);
1692
+ }
1693
+ }
1694
+
1695
+ // src/scheduler.ts
1696
+ class CompactionScheduler {
1697
+ config;
1698
+ port;
1699
+ sink;
1700
+ now;
1701
+ id;
1702
+ stopped = false;
1703
+ jobs = new Set;
1704
+ constructor(config, port, sink, now = Date.now, id = () => `msg_${crypto.randomUUID().replaceAll("-", "")}`) {
1705
+ this.config = config;
1706
+ this.port = port;
1707
+ this.sink = sink;
1708
+ this.now = now;
1709
+ this.id = id;
1710
+ }
1711
+ consider(s) {
1712
+ if (this.stopped || !this.config.enabled || this.config.mode !== "enforce" || !s.decision?.crossed)
1713
+ return;
1714
+ const c = s.compaction, time = this.now();
1715
+ if (c.phase !== "idle" || !c.armed || c.consecutiveFailures >= this.config.safety.maxConsecutiveFailures)
1716
+ return;
1717
+ if (c.lastAttempt !== undefined && time - c.lastAttempt < this.config.safety.cooldownMs)
1718
+ return;
1719
+ c.phase = "requesting";
1720
+ c.lastAttempt = time;
1721
+ c.requestId = this.id();
1722
+ c.before = s.usage.current?.tokens;
1723
+ c.beforeModel = s.model;
1724
+ c.beforeConfidence = s.usage.current?.confidence;
1725
+ c.armed = false;
1726
+ this.emit("compaction.requesting", s);
1727
+ const requestId = c.requestId;
1728
+ const job = new Promise((resolve) => setTimeout(resolve, 0)).then(async () => {
1729
+ if (this.stopped || c.phase !== "requesting")
1730
+ return;
1731
+ try {
1732
+ await this.port.compact(s.sessionId, requestId);
1733
+ if (c.phase === "requesting")
1734
+ c.phase = "accepted";
1735
+ this.emit("compaction.accepted", s);
1736
+ } catch {
1737
+ if (c.phase === "requesting") {
1738
+ c.phase = "uncertain";
1739
+ c.blockedReason = "admission-outcome-unknown";
1740
+ c.failures++;
1741
+ c.consecutiveFailures++;
1742
+ this.emit("compaction.uncertain", s);
1743
+ }
1744
+ }
1745
+ }).finally(() => this.jobs.delete(job));
1746
+ this.jobs.add(job);
1747
+ }
1748
+ lifecycle(s, event, at) {
1749
+ const c = s.compaction;
1750
+ if (event === "started") {
1751
+ if (c.phase !== "requesting" && c.phase !== "accepted") {
1752
+ c.before = s.usage.current?.tokens;
1753
+ c.beforeModel = s.model;
1754
+ c.beforeConfidence = s.usage.current?.confidence;
1755
+ }
1756
+ c.phase = "running";
1757
+ c.armed = false;
1758
+ c.lastAttempt = at;
1759
+ } else if (event === "ended") {
1760
+ c.phase = "awaiting-measurement";
1761
+ c.lastCompleted = at;
1762
+ c.count++;
1763
+ c.consecutiveFailures = 0;
1764
+ delete s.usage.current;
1765
+ } else {
1766
+ c.failures++;
1767
+ c.consecutiveFailures++;
1768
+ c.lastAttempt = at;
1769
+ c.armed = true;
1770
+ c.phase = c.consecutiveFailures >= this.config.safety.maxConsecutiveFailures ? "blocked" : "idle";
1771
+ c.blockedReason = "compaction-failed";
1772
+ delete s.usage.current;
1773
+ }
1774
+ this.emit(`compaction.${event}`, s);
1775
+ }
1776
+ measured(s) {
1777
+ const c = s.compaction, m = s.usage.current;
1778
+ if (!m)
1779
+ return;
1780
+ if (c.phase === "awaiting-measurement") {
1781
+ if (m.confidence !== "reported" || m.at < (c.lastCompleted ?? 0))
1782
+ return;
1783
+ if (c.beforeModel !== m.model || c.before === undefined || c.beforeConfidence !== "reported") {
1784
+ c.phase = "blocked";
1785
+ c.blockedReason = "reclaim-unverifiable";
1786
+ } else if (c.before - m.tokens < this.config.safety.minReclaimTokens) {
1787
+ c.phase = "blocked";
1788
+ c.blockedReason = "insufficient-reclaim";
1789
+ } else {
1790
+ c.phase = "idle";
1791
+ c.armed = false;
1792
+ delete c.blockedReason;
1793
+ }
1794
+ }
1795
+ const resetAt = (s.decision?.threshold ?? 0) - this.config.safety.minReclaimTokens;
1796
+ if (m.tokens <= resetAt && (c.phase === "idle" || c.phase === "blocked" && c.blockedReason !== "compaction-failed")) {
1797
+ c.armed = true;
1798
+ c.phase = "idle";
1799
+ delete c.blockedReason;
1800
+ }
1801
+ }
1802
+ tick(s) {
1803
+ const c = s.compaction;
1804
+ if ((c.phase === "accepted" || c.phase === "running" || c.phase === "requesting") && this.now() - (c.lastAttempt ?? this.now()) > this.config.safety.pendingTimeoutMs) {
1805
+ c.phase = "uncertain";
1806
+ c.blockedReason = "completion-event-timeout";
1807
+ this.emit("compaction.uncertain", s);
1808
+ }
1809
+ }
1810
+ emit(type, session) {
1811
+ this.sink.emit({ type, time: this.now(), session });
1812
+ }
1813
+ async flush() {
1814
+ await Promise.all(this.jobs);
1815
+ }
1816
+ stop() {
1817
+ this.stopped = true;
1818
+ }
1819
+ }
1820
+
1821
+ // src/estimate.ts
1822
+ function estimate(value, maxChars = 2000000) {
1823
+ let chars = 0, nodes = 0;
1824
+ function visit(v, depth) {
1825
+ if (++nodes > 1e5 || depth > 30 || chars > maxChars)
1826
+ return false;
1827
+ if (typeof v === "string")
1828
+ chars += v.length;
1829
+ else if (typeof v === "number" || typeof v === "boolean")
1830
+ chars += 8;
1831
+ else if (Array.isArray(v)) {
1832
+ for (const item of v)
1833
+ if (!visit(item, depth + 1))
1834
+ return false;
1835
+ } else if (v && typeof v === "object") {
1836
+ if (ArrayBuffer.isView(v) || v instanceof ArrayBuffer)
1837
+ return false;
1838
+ for (const [key, item] of Object.entries(v)) {
1839
+ chars += key.length + 4;
1840
+ if (!visit(item, depth + 1))
1841
+ return false;
1842
+ }
1843
+ }
1844
+ return chars <= maxChars;
1845
+ }
1846
+ return visit(value, 0) ? Math.ceil(chars / 3) : undefined;
1847
+ }
1848
+
1849
+ // src/recovery.ts
1850
+ function record2(v) {
1851
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1852
+ }
1853
+ function nonnegative2(v) {
1854
+ return typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
1855
+ }
1856
+ function decodeRecovery(v, max) {
1857
+ if (!Array.isArray(v))
1858
+ return [];
1859
+ const result = [];
1860
+ for (const item of v.slice(0, max)) {
1861
+ if (!record2(item) || typeof item.sessionId !== "string" || !record2(item.compaction))
1862
+ continue;
1863
+ const c = item.compaction;
1864
+ if (!nonnegative2(c.count) || !nonnegative2(c.failures) || !nonnegative2(c.consecutiveFailures) || typeof c.armed !== "boolean")
1865
+ continue;
1866
+ const phase = c.phase === "idle" || c.phase === "blocked" ? c.phase : "uncertain";
1867
+ const compaction = {
1868
+ phase,
1869
+ armed: c.armed,
1870
+ count: c.count,
1871
+ failures: c.failures,
1872
+ consecutiveFailures: c.consecutiveFailures
1873
+ };
1874
+ if (nonnegative2(c.lastAttempt))
1875
+ compaction.lastAttempt = c.lastAttempt;
1876
+ if (nonnegative2(c.lastCompleted))
1877
+ compaction.lastCompleted = c.lastCompleted;
1878
+ if (typeof c.requestId === "string")
1879
+ compaction.requestId = c.requestId;
1880
+ if (typeof c.blockedReason === "string")
1881
+ compaction.blockedReason = c.blockedReason;
1882
+ if (phase === "uncertain")
1883
+ compaction.blockedReason = "restart-needs-lifecycle-evidence";
1884
+ result.push({
1885
+ sessionId: item.sessionId,
1886
+ compaction,
1887
+ ...nonnegative2(item.sequence) ? { sequence: item.sequence } : {}
1888
+ });
1889
+ }
1890
+ return result;
1891
+ }
1892
+ function encodeRecovery(rows) {
1893
+ return rows.map(({ sessionId, compaction: c, sequence }) => ({
1894
+ sessionId,
1895
+ sequence: sequence ?? -1,
1896
+ compaction: {
1897
+ requestId: c.requestId ?? null,
1898
+ phase: c.phase,
1899
+ armed: c.armed,
1900
+ count: c.count,
1901
+ failures: c.failures,
1902
+ consecutiveFailures: c.consecutiveFailures,
1903
+ lastAttempt: c.lastAttempt ?? null,
1904
+ lastCompleted: c.lastCompleted ?? null,
1905
+ blockedReason: c.blockedReason ?? null
1906
+ }
1907
+ }));
1908
+ }
1909
+
1910
+ // src/telemetry.ts
1911
+ import { mkdir as mkdir2, open as open2, rename, stat as stat2 } from "fs/promises";
1912
+ import { dirname, join as join2 } from "path";
1913
+ import { homedir } from "os";
1914
+ import { createHash } from "crypto";
1915
+ function defaultPath(directory) {
1916
+ const key = createHash("sha256").update(directory).digest("hex").slice(0, 16);
1917
+ return join2(process.env.XDG_STATE_HOME || join2(homedir(), ".local/state"), "opencode-context-guard", `${key}.jsonl`);
1918
+ }
1919
+
1920
+ class JsonlTelemetrySink {
1921
+ path;
1922
+ config;
1923
+ pending = [];
1924
+ working;
1925
+ disabled = false;
1926
+ bytes = 0;
1927
+ ready = false;
1928
+ constructor(path, config) {
1929
+ this.path = path;
1930
+ this.config = config;
1931
+ }
1932
+ emit(event) {
1933
+ if (!this.config.enabled || this.disabled)
1934
+ return;
1935
+ const line = JSON.stringify({ schema: 1, ...event }) + "\n";
1936
+ if (!this.config.jsonl) {
1937
+ console.error(line.trimEnd());
1938
+ return;
1939
+ }
1940
+ if (this.pending.length >= 256) {
1941
+ console.error("context-guard: telemetry queue full; record dropped");
1942
+ return;
1943
+ }
1944
+ this.pending.push(line);
1945
+ this.kick();
1946
+ }
1947
+ kick() {
1948
+ if (!this.working && !this.disabled)
1949
+ this.working = this.drain().finally(() => {
1950
+ this.working = undefined;
1951
+ if (this.pending.length)
1952
+ this.kick();
1953
+ });
1954
+ }
1955
+ async drain() {
1956
+ try {
1957
+ if (!this.ready) {
1958
+ await mkdir2(dirname(this.path), { recursive: true, mode: 448 });
1959
+ this.bytes = await stat2(this.path).then((s) => s.size).catch(() => 0);
1960
+ this.ready = true;
1961
+ }
1962
+ while (this.pending.length) {
1963
+ const line = this.pending.shift();
1964
+ if (!line)
1965
+ continue;
1966
+ const size = Buffer.byteLength(line);
1967
+ if (size > this.config.maxBytes)
1968
+ continue;
1969
+ if (this.bytes + size > this.config.maxBytes) {
1970
+ await rename(this.path, `${this.path}.1`).catch((e) => {
1971
+ if (e.code !== "ENOENT")
1972
+ throw e;
1973
+ });
1974
+ this.bytes = 0;
1975
+ }
1976
+ const file = await open2(this.path, "a", 384);
1977
+ try {
1978
+ await file.writeFile(line);
1979
+ } finally {
1980
+ await file.close();
1981
+ }
1982
+ this.bytes += size;
1983
+ }
1984
+ } catch {
1985
+ this.disabled = true;
1986
+ this.pending = [];
1987
+ console.error("context-guard: telemetry unavailable (check path and permissions)");
1988
+ }
1989
+ }
1990
+ async flush() {
1991
+ while (this.working)
1992
+ await this.working;
1993
+ }
1994
+ }
1995
+ async function readJournal(path, maxBytes = 16 * 1024 * 1024) {
1996
+ const records = [];
1997
+ for (const name of [`${path}.1`, path]) {
1998
+ const size = await stat2(name).then((s) => s.size).catch(() => 0);
1999
+ if (!size)
2000
+ continue;
2001
+ const file = await open2(name, "r");
2002
+ const buffer = Buffer.alloc(Math.min(size, maxBytes));
2003
+ try {
2004
+ await file.read(buffer, 0, buffer.length, Math.max(0, size - maxBytes));
2005
+ } finally {
2006
+ await file.close();
2007
+ }
2008
+ const lines = buffer.toString("utf8").split("\n");
2009
+ if (size > maxBytes)
2010
+ lines.shift();
2011
+ for (const line of lines) {
2012
+ if (!line)
2013
+ continue;
2014
+ try {
2015
+ records.push(JSON.parse(line));
2016
+ } catch {
2017
+ }
2018
+ }
2019
+ }
2020
+ return records;
2021
+ }
2022
+
2023
+ // src/adapter.ts
2024
+ async function setup(ctx, injectedSink) {
2025
+ const config = parseConfig(ctx.options);
2026
+ const disk = new JsonlTelemetrySink(config.telemetry.path ?? defaultPath(ctx.location.directory), config.telemetry);
2027
+ const diagnostics = injectedSink ?? disk;
2028
+ const performance = new PerformanceTracker(config.safety.maxSessions, config.metrics.rollingWindowMs);
2029
+ const store = new SnapshotStore(config, () => monitor?.sessions ?? new Map, Date.now, crypto.randomUUID(), config.metrics.enabled ? (id) => performance.snapshot(id) : undefined);
2030
+ let monitor;
2031
+ const sink = {
2032
+ emit(event) {
2033
+ try {
2034
+ if (event.session)
2035
+ store.mark(event.session.sessionId);
2036
+ if ([
2037
+ "event-stream-gap",
2038
+ "persistence-unavailable",
2039
+ "capacity-exceeded",
2040
+ "model-metadata-unavailable"
2041
+ ].includes(event.type)) {
2042
+ if (event.type === "event-stream-gap")
2043
+ store.degrade("event-stream-gap");
2044
+ if (event.type === "persistence-unavailable")
2045
+ store.degrade("persistence-unavailable");
2046
+ if (event.type === "capacity-exceeded")
2047
+ store.degrade("capacity-exceeded");
2048
+ if (event.type === "model-metadata-unavailable")
2049
+ store.degrade("model-metadata-unavailable");
2050
+ }
2051
+ } catch {
2052
+ }
2053
+ diagnostics.emit(event);
2054
+ }
2055
+ };
2056
+ const stopV1 = await serveSnapshots(ctx.rpc, store).catch(() => {
2057
+ diagnostics.emit({
2058
+ time: Date.now(),
2059
+ type: "snapshot-service-unavailable"
2060
+ });
2061
+ return () => {
2062
+ };
2063
+ });
2064
+ const stopV2 = await serveSnapshots(ctx.rpc, store, true).catch(() => {
2065
+ diagnostics.emit({
2066
+ time: Date.now(),
2067
+ type: "performance-service-unavailable"
2068
+ });
2069
+ return () => {
2070
+ };
2071
+ });
2072
+ const stopHistory = await serveHistory(ctx.historyRpc, config.history.path ?? `${defaultPath(ctx.location.directory)}.metrics`, config.enabled && config.metrics.enabled && config.history.enabled, Date.now, async (sessionId, signal) => {
2073
+ const info = await ctx.session.get({ sessionID: sessionId }, { signal });
2074
+ return info.location.directory === ctx.location.directory;
2075
+ }).catch(() => {
2076
+ diagnostics.emit({ time: Date.now(), type: "history-service-unavailable" });
2077
+ return () => {
2078
+ };
2079
+ });
2080
+ const stopSnapshots = async () => {
2081
+ await stopHistory();
2082
+ await stopV2();
2083
+ await stopV1();
2084
+ };
2085
+ if (!config.enabled) {
2086
+ sink.emit({ time: Date.now(), type: "disabled" });
2087
+ return async () => {
2088
+ await stopSnapshots();
2089
+ await disk.flush();
2090
+ };
2091
+ }
2092
+ const history = new PerformanceHistory(config.history.path ?? `${defaultPath(ctx.location.directory)}.metrics`, {
2093
+ ...config.history,
2094
+ enabled: config.history.enabled && config.metrics.enabled
2095
+ }, Date.now, (type) => diagnostics.emit({ time: Date.now(), type }));
2096
+ await history.initialize();
2097
+ if (config.metrics.enabled) {
2098
+ performance.restore(await ctx.storage.get("performance.v1").catch(() => {
2099
+ return;
2100
+ }));
2101
+ }
2102
+ const performanceResidents = new Set;
2103
+ const recordPerformance = (id, type, event) => {
2104
+ if (!config.metrics.enabled)
2105
+ return;
2106
+ const state = monitor?.sessions.get(id), snapshot = performance.snapshot(id);
2107
+ if (!state || !snapshot)
2108
+ return;
2109
+ history.emit({
2110
+ schema: 1,
2111
+ recordId: crypto.randomUUID(),
2112
+ time: Date.now(),
2113
+ type,
2114
+ sessionId: id,
2115
+ ...state.parentId ? { parentId: state.parentId } : {},
2116
+ ...state.agent ? { agent: state.agent } : {},
2117
+ ...state.model ? { model: state.model } : {},
2118
+ ...event ? { event } : {},
2119
+ performance: snapshot
2120
+ });
2121
+ };
2122
+ const persistPerformance = async () => {
2123
+ if (!config.metrics.enabled)
2124
+ return;
2125
+ await ctx.storage.set("performance.v1", performance.checkpoint().map((row2) => ({ ...row2 }))).catch(() => {
2126
+ diagnostics.emit({
2127
+ time: Date.now(),
2128
+ type: "performance-persistence-unavailable"
2129
+ });
2130
+ });
2131
+ };
2132
+ const controller = new AbortController;
2133
+ let stopped = false, persistenceHealthy = true;
2134
+ let work = Promise.resolve();
2135
+ const serialized = (task) => {
2136
+ const next = work.then(task);
2137
+ work = next.catch(() => {
2138
+ });
2139
+ return next;
2140
+ };
2141
+ const requests = new Map;
2142
+ const saved = await ctx.storage.get("safety").catch(() => {
2143
+ persistenceHealthy = false;
2144
+ return;
2145
+ });
2146
+ const decoded = decodeRecovery(saved, config.safety.maxSessions);
2147
+ if (saved !== undefined && (!Array.isArray(saved) || saved.length !== decoded.length)) {
2148
+ persistenceHealthy = false;
2149
+ }
2150
+ const recoveries = new Map(decoded.map((s) => [s.sessionId, s]));
2151
+ let persistChain = Promise.resolve();
2152
+ const persist = () => {
2153
+ if (!persistenceHealthy)
2154
+ return persistChain;
2155
+ const rows = [...monitor?.sessions.values() ?? []].filter((s) => s.compaction.lastAttempt !== undefined || s.compaction.phase !== "idle").map((s) => ({
2156
+ sessionId: s.sessionId,
2157
+ sequence: s.sequence,
2158
+ compaction: { ...s.compaction }
2159
+ }));
2160
+ const missing = [...recoveries].filter(([id]) => !monitor?.sessions.has(id)).map(([, recovery]) => recovery);
2161
+ const data = encodeRecovery([...rows, ...missing].slice(0, config.safety.maxSessions));
2162
+ persistChain = persistChain.then(() => ctx.storage.set("safety", data)).catch(() => {
2163
+ persistenceHealthy = false;
2164
+ sink.emit({ time: Date.now(), type: "persistence-unavailable" });
2165
+ });
2166
+ return persistChain;
2167
+ };
2168
+ const scheduler = new CompactionScheduler(config, {
2169
+ compact: async (sessionId, id) => {
2170
+ if (!persistenceHealthy)
2171
+ throw new Error("Safety persistence unavailable");
2172
+ await persist();
2173
+ if (!monitor?.sessions.has(sessionId) || monitor?.sessions.get(sessionId)?.compaction.phase !== "requesting")
2174
+ throw new Error("Session no longer requesting");
2175
+ if (!persistenceHealthy || stopped)
2176
+ throw new Error("Safety persistence unavailable");
2177
+ await ctx.session.compact({ sessionID: sessionId, id, delivery: "steer" }, { signal: controller.signal });
2178
+ }
2179
+ }, sink);
2180
+ monitor = new ContextMonitor(config, scheduler, sink);
2181
+ const activeMonitor = monitor;
2182
+ let models = new Map, modelAt = 0, modelRetryAt = 0, modelRetryMs = 1000;
2183
+ async function refreshModels(retryMissing = false) {
2184
+ const now = Date.now();
2185
+ if (now - modelAt < 60000 && (!retryMissing || now < modelRetryAt))
2186
+ return;
2187
+ modelAt = now;
2188
+ modelRetryAt = now + modelRetryMs;
2189
+ modelRetryMs = Math.min(60000, modelRetryMs * 2);
2190
+ try {
2191
+ models = new Map((await ctx.model.list(undefined, { signal: controller.signal })).data.map((m) => [`${m.providerID}/${m.id}`, { ...m.limit }]));
2192
+ let missing = false;
2193
+ for (const s of activeMonitor.sessions.values()) {
2194
+ const limits = s.model ? models.get(s.model) : undefined;
2195
+ if (limits)
2196
+ s.limits = { ...limits };
2197
+ else {
2198
+ delete s.limits;
2199
+ missing ||= s.model !== undefined;
2200
+ }
2201
+ store.mark(s.sessionId);
2202
+ }
2203
+ if (!missing)
2204
+ modelRetryMs = 1000;
2205
+ store.recover("model-metadata-unavailable");
2206
+ } catch {
2207
+ models.clear();
2208
+ for (const s of activeMonitor.sessions.values()) {
2209
+ delete s.limits;
2210
+ store.mark(s.sessionId);
2211
+ }
2212
+ sink.emit({ time: Date.now(), type: "model-metadata-unavailable" });
2213
+ }
2214
+ }
2215
+ async function discover(sessionId) {
2216
+ const info = await ctx.session.get({ sessionID: sessionId }, { signal: controller.signal });
2217
+ if (info.location.directory !== ctx.location.directory)
2218
+ return;
2219
+ const identity = {
2220
+ sessionId,
2221
+ ...info.parentID ? { parentId: info.parentID } : {},
2222
+ ...info.agent ? { agent: info.agent } : {},
2223
+ ...info.model ? { model: `${info.model.providerID}/${info.model.id}` } : {}
2224
+ };
2225
+ if (!activeMonitor.sessions.has(sessionId) && !recoveries.has(sessionId) && activeMonitor.sessions.size + recoveries.size >= config.safety.maxSessions) {
2226
+ sink.emit({ time: Date.now(), type: "capacity-exceeded" });
2227
+ return;
2228
+ }
2229
+ const s = activeMonitor.touch(identity, identity.model ? models.get(identity.model) : undefined);
2230
+ if (s) {
2231
+ if (!identity.model || !models.has(identity.model))
2232
+ delete s.limits;
2233
+ const recovery = recoveries.get(sessionId);
2234
+ if (recovery) {
2235
+ s.compaction = recovery.compaction;
2236
+ s.sequence = recovery.sequence ?? -1;
2237
+ recoveries.delete(sessionId);
2238
+ }
2239
+ if (config.metrics.enabled) {
2240
+ if (!performance.snapshot(sessionId) && [...performance.ids()].length >= config.safety.maxSessions) {
2241
+ const dormant = [...performance.ids()].find((id) => !performanceResidents.has(id));
2242
+ if (dormant)
2243
+ performance.remove(dormant);
2244
+ }
2245
+ performance.observe(sessionId, info.time.created, outputTokens(info.tokens));
2246
+ performanceResidents.add(sessionId);
2247
+ }
2248
+ s.usage.cumulative = info.tokens;
2249
+ s.usage.cumulativeScope = "session";
2250
+ store.mark(s.sessionId);
2251
+ }
2252
+ return s;
2253
+ }
2254
+ const pending = new Map;
2255
+ let draining = false;
2256
+ async function drainHooks() {
2257
+ if (stopped)
2258
+ return;
2259
+ try {
2260
+ await refreshModels([...activeMonitor.sessions.values()].some((s) => s.model !== undefined && !models.has(s.model)));
2261
+ for (const [id, h] of pending) {
2262
+ pending.delete(id);
2263
+ try {
2264
+ const s = activeMonitor.sessions.get(id) ?? await discover(id);
2265
+ if (!s)
2266
+ continue;
2267
+ if (h.at <= Math.max(s.lastExecutionEnd ?? 0, s.compaction.lastCompleted ?? 0))
2268
+ continue;
2269
+ activeMonitor.touch(h.identity, h.identity.model ? models.get(h.identity.model) : undefined);
2270
+ s.active = true;
2271
+ if ((!s.usage.current || Date.now() - s.usage.current.at > config.safety.staleMs) && h.estimate !== undefined && s.model)
2272
+ activeMonitor.measure(s, {
2273
+ tokens: h.estimate,
2274
+ at: h.at,
2275
+ model: s.model,
2276
+ confidence: "approximate",
2277
+ provenance: "serialized-estimate",
2278
+ scope: "prepared-request"
2279
+ });
2280
+ else
2281
+ activeMonitor.evaluate(s);
2282
+ } catch {
2283
+ sink.emit({ time: Date.now(), type: "session-observation-failed" });
2284
+ }
2285
+ }
2286
+ } finally {
2287
+ draining = false;
2288
+ }
2289
+ }
2290
+ const hook = (event) => {
2291
+ if (stopped)
2292
+ return;
2293
+ const id = event.sessionID, model = `${event.model.providerID}/${event.model.id}`, current = activeMonitor.sessions.get(id)?.usage.current;
2294
+ if (!pending.has(id) && pending.size >= config.safety.maxSessions) {
2295
+ sink.emit({ time: Date.now(), type: "hook-capacity-exceeded" });
2296
+ return;
2297
+ }
2298
+ const fallback = !current || current.model !== model || Date.now() - current.at > config.safety.staleMs;
2299
+ const tokens = fallback ? estimate([event.system, event.messages, event.tools]) : undefined;
2300
+ pending.set(id, {
2301
+ identity: { sessionId: id, agent: event.agent, model },
2302
+ at: Date.now(),
2303
+ ...tokens === undefined ? {} : { estimate: tokens }
2304
+ });
2305
+ };
2306
+ const registrations = [];
2307
+ try {
2308
+ registrations.push(await ctx.session.hook("context", hook));
2309
+ registrations.push(await ctx.session.hook("compaction", (e) => {
2310
+ const s = activeMonitor.sessions.get(e.sessionID);
2311
+ if (s) {
2312
+ s.compaction.phase = "running";
2313
+ s.compaction.armed = false;
2314
+ store.mark(s.sessionId);
2315
+ }
2316
+ }));
2317
+ } catch (error) {
2318
+ for (const r of registrations)
2319
+ await r.dispose();
2320
+ await history.flush();
2321
+ await stopSnapshots();
2322
+ throw error;
2323
+ }
2324
+ const relevant = new Set([
2325
+ "session.usage.updated",
2326
+ "session.created",
2327
+ "session.step.started",
2328
+ "session.step.streamed",
2329
+ "session.step.ended",
2330
+ "session.step.failed",
2331
+ "session.compaction.started",
2332
+ "session.compaction.ended",
2333
+ "session.compaction.failed",
2334
+ "session.execution.started",
2335
+ "session.execution.succeeded",
2336
+ "session.execution.failed",
2337
+ "session.execution.interrupted",
2338
+ "session.deleted",
2339
+ "session.model.selected",
2340
+ "session.agent.selected",
2341
+ "session.tool.called"
2342
+ ]);
2343
+ async function handle(event) {
2344
+ if (event.location && event.location.directory !== ctx.location.directory)
2345
+ return;
2346
+ if (event.type === "session.text.delta" || event.type === "session.reasoning.delta") {
2347
+ if (config.metrics.enabled)
2348
+ performance.delta(event.data.sessionID, event.data.assistantMessageID, event.id, event.created, event.data.delta.length);
2349
+ return;
2350
+ }
2351
+ if (!relevant.has(event.type) || !("sessionID" in event.data))
2352
+ return;
2353
+ const id = event.data.sessionID;
2354
+ if (event.location && event.location.directory !== ctx.location.directory)
2355
+ return;
2356
+ if (event.type === "session.deleted") {
2357
+ recordPerformance(id, "terminal", "session.deleted");
2358
+ performance.remove(id);
2359
+ performanceResidents.delete(id);
2360
+ await persistPerformance();
2361
+ store.remove(id);
2362
+ requests.delete(id);
2363
+ activeMonitor.remove(id);
2364
+ recoveries.delete(id);
2365
+ await persist();
2366
+ return;
2367
+ }
2368
+ await refreshModels();
2369
+ const s = await discover(id);
2370
+ if (!s)
2371
+ return;
2372
+ if ("durable" in event && event.durable && !activeMonitor.accept(s, event.durable.seq))
2373
+ return;
2374
+ const perfAccepted = config.metrics.enabled && (!("durable" in event) || !event.durable || performance.accept(id, event.durable.seq));
2375
+ switch (event.type) {
2376
+ case "session.step.started":
2377
+ if (perfAccepted)
2378
+ performance.start(id, event.data.assistantMessageID, event.data.agent, `${event.data.model.providerID}/${event.data.model.id}`, event.data.started);
2379
+ activeMonitor.touch({
2380
+ sessionId: id,
2381
+ agent: event.data.agent,
2382
+ model: `${event.data.model.providerID}/${event.data.model.id}`
2383
+ }, models.get(`${event.data.model.providerID}/${event.data.model.id}`));
2384
+ requests.set(id, {
2385
+ id: event.data.assistantMessageID,
2386
+ model: `${event.data.model.providerID}/${event.data.model.id}`
2387
+ });
2388
+ s.usage.modelRequests++;
2389
+ s.active = true;
2390
+ break;
2391
+ case "session.step.streamed":
2392
+ if (perfAccepted)
2393
+ performance.streamed(id, event.data.assistantMessageID, event.created);
2394
+ break;
2395
+ case "session.step.ended":
2396
+ case "session.step.failed": {
2397
+ if (perfAccepted)
2398
+ performance.settle(id, event.data.assistantMessageID, event.data.tokens ? outputTokens(event.data.tokens) : undefined);
2399
+ const tokens = event.data.tokens && promptTokens(event.data.tokens);
2400
+ const request = requests.get(id);
2401
+ if (tokens !== undefined && request?.id === event.data.assistantMessageID && request.model === s.model)
2402
+ activeMonitor.measure(s, {
2403
+ tokens,
2404
+ at: event.created,
2405
+ requestId: event.data.assistantMessageID,
2406
+ model: request.model,
2407
+ confidence: "reported",
2408
+ provenance: "provider-normalized",
2409
+ scope: "completed-request"
2410
+ });
2411
+ else {
2412
+ delete s.usage.current;
2413
+ sink.emit({
2414
+ time: Date.now(),
2415
+ type: "provider-usage-unavailable",
2416
+ session: s
2417
+ });
2418
+ }
2419
+ break;
2420
+ }
2421
+ case "session.compaction.started":
2422
+ scheduler.lifecycle(s, "started", event.created);
2423
+ break;
2424
+ case "session.compaction.ended":
2425
+ scheduler.lifecycle(s, "ended", event.created);
2426
+ break;
2427
+ case "session.compaction.failed":
2428
+ scheduler.lifecycle(s, "failed", event.created);
2429
+ break;
2430
+ case "session.execution.started":
2431
+ s.active = true;
2432
+ break;
2433
+ case "session.execution.succeeded":
2434
+ case "session.execution.failed":
2435
+ case "session.execution.interrupted":
2436
+ s.active = false;
2437
+ if (performance.snapshot(id)?.tokenSource === "estimated")
2438
+ performance.gap(id);
2439
+ s.lastExecutionEnd = event.created;
2440
+ break;
2441
+ case "session.tool.called":
2442
+ s.usage.toolCalls++;
2443
+ break;
2444
+ }
2445
+ sink.emit({ time: Date.now(), type: "session-state", session: s });
2446
+ if (perfAccepted) {
2447
+ recordPerformance(id, event.type === "session.step.ended" || event.type === "session.step.failed" ? "request" : event.type.startsWith("session.execution.") && event.type !== "session.execution.started" ? "terminal" : "lifecycle", event.type);
2448
+ await persistPerformance();
2449
+ }
2450
+ await persist();
2451
+ }
2452
+ let lastPerformanceSample = 0, lastHistorySample = 0;
2453
+ const timer = setInterval(() => {
2454
+ const now = Date.now();
2455
+ if (config.metrics.enabled && now - lastPerformanceSample >= config.metrics.sampleIntervalMs) {
2456
+ lastPerformanceSample = now;
2457
+ for (const id of performanceResidents) {
2458
+ store.mark(id);
2459
+ const snapshot = performance.snapshot(id);
2460
+ if (now - lastHistorySample >= 1000 && snapshot && (snapshot.status === "waiting" || snapshot.status === "streaming"))
2461
+ recordPerformance(id, "sample");
2462
+ }
2463
+ if (now - lastHistorySample >= 1000)
2464
+ lastHistorySample = now;
2465
+ }
2466
+ store.flush();
2467
+ if (draining || stopped)
2468
+ return;
2469
+ draining = true;
2470
+ serialized(async () => {
2471
+ await drainHooks();
2472
+ activeMonitor.cleanup();
2473
+ if (activeMonitor.sessions.size + recoveries.size < config.safety.maxSessions)
2474
+ store.recover("capacity-exceeded");
2475
+ for (const id of performanceResidents)
2476
+ if (!activeMonitor.sessions.has(id)) {
2477
+ performance.remove(id);
2478
+ performanceResidents.delete(id);
2479
+ }
2480
+ for (const id of requests.keys())
2481
+ if (!activeMonitor.sessions.has(id))
2482
+ requests.delete(id);
2483
+ }).catch(() => {
2484
+ draining = false;
2485
+ });
2486
+ }, 100);
2487
+ timer.unref();
2488
+ const events = (async () => {
2489
+ while (!stopped) {
2490
+ try {
2491
+ for await (const event of ctx.event.subscribe({
2492
+ signal: controller.signal
2493
+ })) {
2494
+ if (stopped)
2495
+ break;
2496
+ await serialized(() => handle(event));
2497
+ }
2498
+ } catch {
2499
+ }
2500
+ if (!stopped) {
2501
+ performance.gap();
2502
+ for (const id of performanceResidents) {
2503
+ store.mark(id);
2504
+ recordPerformance(id, "gap", "event-stream-gap");
2505
+ }
2506
+ await persistPerformance();
2507
+ sink.emit({
2508
+ time: Date.now(),
2509
+ type: "event-stream-gap",
2510
+ detail: "Enforcement suspended until reload; measurements may be incomplete"
2511
+ });
2512
+ scheduler.stop();
2513
+ await new Promise((resolve) => {
2514
+ const finish = () => {
2515
+ clearTimeout(t);
2516
+ controller.signal.removeEventListener("abort", finish);
2517
+ resolve();
2518
+ };
2519
+ const t = setTimeout(finish, 1000);
2520
+ controller.signal.addEventListener("abort", finish, { once: true });
2521
+ });
2522
+ }
2523
+ }
2524
+ })();
2525
+ if (!persistenceHealthy)
2526
+ sink.emit({
2527
+ time: Date.now(),
2528
+ type: "persistence-unavailable",
2529
+ detail: "Enforcement disabled; safety state could not be read"
2530
+ });
2531
+ if (!injectedSink)
2532
+ console.error(`context-guard: ${config.mode}; diagnostics ${disk.path}; metrics ${history.path}`);
2533
+ sink.emit({
2534
+ time: Date.now(),
2535
+ type: "ready",
2536
+ detail: `SDK 2.0.29; runtime ${ctx.app.version}; ${config.mode}; active-subagent=step-boundary; next-request-barrier=false; journal=${disk.path}; metrics=${history.path}`
2537
+ });
2538
+ return async () => {
2539
+ stopped = true;
2540
+ scheduler.stop();
2541
+ controller.abort();
2542
+ clearInterval(timer);
2543
+ pending.clear();
2544
+ const disposed = await Promise.allSettled(registrations.map((r) => r.dispose()));
2545
+ if (disposed.some((r) => r.status === "rejected"))
2546
+ sink.emit({ time: Date.now(), type: "registration-cleanup-failed" });
2547
+ await events;
2548
+ await work;
2549
+ await scheduler.flush();
2550
+ await persist();
2551
+ await persistPerformance();
2552
+ await history.flush();
2553
+ await stopSnapshots();
2554
+ await disk.flush();
2555
+ };
2556
+ }
2557
+
2558
+ // src/index.ts
2559
+ var src_default = Plugin.define({
2560
+ id: "context-guard",
2561
+ setup: (ctx, injectedSink) => setup({ ...ctx, historyRpc: ctx.rpc }, injectedSink)
2562
+ });
2563
+ export {
2564
+ src_default as default
2565
+ };