@animalabs/agent-framework 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/agent.d.ts +11 -1
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +33 -3
- package/dist/src/agent.js.map +1 -1
- package/dist/src/framework.d.ts +184 -18
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1323 -246
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +73 -7
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +399 -22
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/types.d.ts +56 -0
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/capability-grant.d.ts +115 -0
- package/dist/src/mcpl/capability-grant.d.ts.map +1 -0
- package/dist/src/mcpl/capability-grant.js +291 -0
- package/dist/src/mcpl/capability-grant.js.map +1 -0
- package/dist/src/mcpl/capability-mask.d.ts +51 -0
- package/dist/src/mcpl/capability-mask.d.ts.map +1 -0
- package/dist/src/mcpl/capability-mask.js +190 -0
- package/dist/src/mcpl/capability-mask.js.map +1 -0
- package/dist/src/mcpl/channel-registry.d.ts +3 -1
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +163 -36
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.d.ts +8 -3
- package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.js +28 -8
- package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
- package/dist/src/mcpl/errors.d.ts +6 -0
- package/dist/src/mcpl/errors.d.ts.map +1 -1
- package/dist/src/mcpl/errors.js +6 -0
- package/dist/src/mcpl/errors.js.map +1 -1
- package/dist/src/mcpl/feature-set-manager.d.ts +10 -1
- package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
- package/dist/src/mcpl/feature-set-manager.js +45 -4
- package/dist/src/mcpl/feature-set-manager.js.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts +10 -14
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.js +58 -67
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
- package/dist/src/mcpl/index.d.ts +1 -1
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/prose-grammar.d.ts +11 -0
- package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
- package/dist/src/mcpl/prose-grammar.js +21 -0
- package/dist/src/mcpl/prose-grammar.js.map +1 -1
- package/dist/src/mcpl/prose-stream-router.d.ts +11 -2
- package/dist/src/mcpl/prose-stream-router.d.ts.map +1 -1
- package/dist/src/mcpl/prose-stream-router.js +73 -13
- package/dist/src/mcpl/prose-stream-router.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +20 -2
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +93 -9
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +231 -32
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/server-registry.d.ts.map +1 -1
- package/dist/src/mcpl/server-registry.js +4 -3
- package/dist/src/mcpl/server-registry.js.map +1 -1
- package/dist/src/mcpl/tags.d.ts +24 -0
- package/dist/src/mcpl/tags.d.ts.map +1 -0
- package/dist/src/mcpl/tags.js +52 -0
- package/dist/src/mcpl/tags.js.map +1 -0
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +10 -0
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +159 -64
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js +16 -2
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +23 -0
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +48 -0
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/sync.d.ts +11 -2
- package/dist/src/modules/workspace/sync.d.ts.map +1 -1
- package/dist/src/modules/workspace/sync.js +18 -8
- package/dist/src/modules/workspace/sync.js.map +1 -1
- package/dist/src/refusal-reactions.d.ts +27 -0
- package/dist/src/refusal-reactions.d.ts.map +1 -0
- package/dist/src/refusal-reactions.js +35 -0
- package/dist/src/refusal-reactions.js.map +1 -0
- package/dist/src/tool-result-history.d.ts +20 -0
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +26 -5
- package/dist/src/tool-result-history.js.map +1 -1
- package/dist/src/types/agent.d.ts +39 -1
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +16 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +24 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/test/channel-registry-routing.test.js +38 -10
- package/dist/test/channel-registry-routing.test.js.map +1 -1
- package/dist/test/channel-registry-typing.test.js +10 -1
- package/dist/test/channel-registry-typing.test.js.map +1 -1
- package/dist/test/checkpoint-attribution.test.js +168 -8
- package/dist/test/checkpoint-attribution.test.js.map +1 -1
- package/dist/test/conversation-fork-injection-scoping.test.js +8 -3
- package/dist/test/conversation-fork-injection-scoping.test.js.map +1 -1
- package/dist/test/event-gate-passthrough.test.d.ts +15 -0
- package/dist/test/event-gate-passthrough.test.d.ts.map +1 -0
- package/dist/test/event-gate-passthrough.test.js +289 -0
- package/dist/test/event-gate-passthrough.test.js.map +1 -0
- package/dist/test/event-gate-selfwake.test.d.ts +15 -0
- package/dist/test/event-gate-selfwake.test.d.ts.map +1 -0
- package/dist/test/event-gate-selfwake.test.js +125 -0
- package/dist/test/event-gate-selfwake.test.js.map +1 -0
- package/dist/test/explicit-prose-routing.test.js +137 -7
- package/dist/test/explicit-prose-routing.test.js.map +1 -1
- package/dist/test/fixtures/awareness-barrier-mcpl-server.mjs +276 -0
- package/dist/test/fixtures/flaky-mcpl-server.mjs +41 -0
- package/dist/test/machine-close-provenance.test.js +6 -0
- package/dist/test/machine-close-provenance.test.js.map +1 -1
- package/dist/test/mcpl-awareness-barrier.test.js +10 -0
- package/dist/test/mcpl-awareness-barrier.test.js.map +1 -1
- package/dist/test/mcpl-capability-grant.test.d.ts +10 -0
- package/dist/test/mcpl-capability-grant.test.d.ts.map +1 -0
- package/dist/test/mcpl-capability-grant.test.js +109 -0
- package/dist/test/mcpl-capability-grant.test.js.map +1 -0
- package/dist/test/mcpl-capability-scoping.test.d.ts +2 -0
- package/dist/test/mcpl-capability-scoping.test.d.ts.map +1 -0
- package/dist/test/mcpl-capability-scoping.test.js +266 -0
- package/dist/test/mcpl-capability-scoping.test.js.map +1 -0
- package/dist/test/mcpl-gate-roundtrip.test.js +13 -0
- package/dist/test/mcpl-gate-roundtrip.test.js.map +1 -1
- package/dist/test/mcpl-list-status.test.d.ts +2 -0
- package/dist/test/mcpl-list-status.test.d.ts.map +1 -0
- package/dist/test/mcpl-list-status.test.js +78 -0
- package/dist/test/mcpl-list-status.test.js.map +1 -0
- package/dist/test/mcpl-manifest-changed.test.d.ts +2 -0
- package/dist/test/mcpl-manifest-changed.test.d.ts.map +1 -0
- package/dist/test/mcpl-manifest-changed.test.js +177 -0
- package/dist/test/mcpl-manifest-changed.test.js.map +1 -0
- package/dist/test/mcpl-reconnect-events.test.js +27 -2
- package/dist/test/mcpl-reconnect-events.test.js.map +1 -1
- package/dist/test/mcpl-reconnect-reregistration.test.js +53 -17
- package/dist/test/mcpl-reconnect-reregistration.test.js.map +1 -1
- package/dist/test/mcpl-request-timeout.test.js +1 -1
- package/dist/test/mcpl-request-timeout.test.js.map +1 -1
- package/dist/test/physical-window-restart.test.d.ts +14 -0
- package/dist/test/physical-window-restart.test.d.ts.map +1 -0
- package/dist/test/physical-window-restart.test.js +222 -0
- package/dist/test/physical-window-restart.test.js.map +1 -0
- package/dist/test/present-while-acting.test.js +199 -1
- package/dist/test/present-while-acting.test.js.map +1 -1
- package/dist/test/prose-stream-router.test.js +43 -0
- package/dist/test/prose-stream-router.test.js.map +1 -1
- package/dist/test/refusal-reaction-baseline.test.d.ts +2 -0
- package/dist/test/refusal-reaction-baseline.test.d.ts.map +1 -0
- package/dist/test/refusal-reaction-baseline.test.js +74 -0
- package/dist/test/refusal-reaction-baseline.test.js.map +1 -0
- package/dist/test/state-scaling-e2e.test.js +7 -1
- package/dist/test/state-scaling-e2e.test.js.map +1 -1
- package/dist/test/state-scaling.test.js +9 -1
- package/dist/test/state-scaling.test.js.map +1 -1
- package/dist/test/tool-result-spill.test.d.ts +13 -0
- package/dist/test/tool-result-spill.test.d.ts.map +1 -0
- package/dist/test/tool-result-spill.test.js +472 -0
- package/dist/test/tool-result-spill.test.js.map +1 -0
- package/dist/test/trunk-channel-routing.test.js +45 -0
- package/dist/test/trunk-channel-routing.test.js.map +1 -1
- package/dist/test/xml-round-persist.test.d.ts +19 -0
- package/dist/test/xml-round-persist.test.d.ts.map +1 -0
- package/dist/test/xml-round-persist.test.js +235 -0
- package/dist/test/xml-round-persist.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -4
- package/src/agent.ts +38 -4
- package/src/framework.ts +1488 -272
- package/src/gate/event-gate.ts +427 -24
- package/src/gate/types.ts +58 -0
- package/src/index.ts +1 -0
- package/src/mcpl/capability-grant.ts +319 -0
- package/src/mcpl/capability-mask.ts +228 -0
- package/src/mcpl/channel-registry.ts +167 -39
- package/src/mcpl/checkpoint-manager.ts +28 -8
- package/src/mcpl/errors.ts +7 -0
- package/src/mcpl/feature-set-manager.ts +50 -5
- package/src/mcpl/hook-orchestrator.ts +64 -86
- package/src/mcpl/index.ts +0 -2
- package/src/mcpl/prose-grammar.ts +21 -0
- package/src/mcpl/prose-stream-router.ts +65 -14
- package/src/mcpl/push-handler.ts +18 -2
- package/src/mcpl/server-connection.ts +262 -34
- package/src/mcpl/server-registry.ts +4 -3
- package/src/mcpl/tags.ts +50 -0
- package/src/mcpl/transport.ts +11 -0
- package/src/mcpl/types.ts +176 -78
- package/src/modules/workspace/index.ts +48 -0
- package/src/modules/workspace/sync.ts +30 -7
- package/src/refusal-reactions.ts +37 -0
- package/src/tool-result-history.ts +27 -5
- package/src/types/agent.ts +40 -1
- package/src/types/framework.ts +17 -0
- package/src/types/trace.ts +22 -0
package/src/gate/event-gate.ts
CHANGED
|
@@ -25,6 +25,8 @@ import type {
|
|
|
25
25
|
GateEventInfo,
|
|
26
26
|
GatePolicyStats,
|
|
27
27
|
GateStatus,
|
|
28
|
+
ShadowWarning,
|
|
29
|
+
GateProbeRow,
|
|
28
30
|
} from './types.js';
|
|
29
31
|
|
|
30
32
|
// Re-export types consumers need
|
|
@@ -310,11 +312,137 @@ function validatePolicy(raw: unknown): GatePolicy {
|
|
|
310
312
|
if (names.length > 0) resets = names;
|
|
311
313
|
}
|
|
312
314
|
|
|
315
|
+
// Validate passthrough — only meaningful on the counting behaviors, where
|
|
316
|
+
// "matched but didn't fire" is a distinct outcome that can fall through.
|
|
317
|
+
// On always/defer/skip/debounce the flag would be a no-op at best and a
|
|
318
|
+
// misleading promise at worst, so reject it loudly instead of ignoring it.
|
|
319
|
+
let passthrough: boolean | undefined;
|
|
320
|
+
if (obj.passthrough !== undefined) {
|
|
321
|
+
if (typeof obj.passthrough !== 'boolean') {
|
|
322
|
+
throw new Error(`Policy "${obj.name}": passthrough must be a boolean`);
|
|
323
|
+
}
|
|
324
|
+
if (obj.passthrough) {
|
|
325
|
+
const counting = typeof behavior === 'object'
|
|
326
|
+
&& ('rate_limit' in behavior || 'passive_sample' in behavior);
|
|
327
|
+
if (!counting) {
|
|
328
|
+
throw new Error(
|
|
329
|
+
`Policy "${obj.name}": passthrough is only valid with rate_limit or `
|
|
330
|
+
+ `passive_sample behaviors — for "${formatBehavior(behavior)}" there is `
|
|
331
|
+
+ 'no non-firing match to fall through on',
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
passthrough = true;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
313
338
|
const policy: GatePolicy = { name: obj.name, match, behavior };
|
|
314
339
|
if (resets) policy.resets = resets;
|
|
340
|
+
if (passthrough) policy.passthrough = passthrough;
|
|
315
341
|
return policy;
|
|
316
342
|
}
|
|
317
343
|
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
// Shadow lint — prove (conservatively) when rule ordering makes a rule dead
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
/** Set-inclusion helper for exact-string lists. */
|
|
349
|
+
function subsetOf(a: string[], b: string[]): boolean {
|
|
350
|
+
return a.every((x) => b.includes(x));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** A glob field on the EARLIER rule is at least as broad as the LATER rule's
|
|
354
|
+
* when it's absent, the universal "*", or literally identical. Anything
|
|
355
|
+
* cleverer (real glob subsumption) is out of conservative scope. */
|
|
356
|
+
function globAtLeastAsBroad(earlier: string | undefined, later: string | undefined): boolean {
|
|
357
|
+
if (earlier === undefined || earlier === '*') return true;
|
|
358
|
+
return earlier === later;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Event types for which `earlier` provably matches EVERY event that `later`
|
|
363
|
+
* would match — i.e. the types for which `later` is unreachable when
|
|
364
|
+
* `earlier` sits before it and consumes its matches. Returns null when
|
|
365
|
+
* subsumption can't be proven (which is not proof of safety — the lint is
|
|
366
|
+
* deliberately one-sided so every warning it emits is true).
|
|
367
|
+
*/
|
|
368
|
+
function subsumedEventTypes(earlier: GatePolicy, later: GatePolicy): string[] | 'all' | null {
|
|
369
|
+
const e = earlier.match;
|
|
370
|
+
const l = later.match;
|
|
371
|
+
|
|
372
|
+
// Scope: which of `later`'s event types does `earlier` cover?
|
|
373
|
+
let covered: string[] | 'all';
|
|
374
|
+
if (!e.scope || e.scope.length === 0) {
|
|
375
|
+
covered = l.scope && l.scope.length > 0 ? l.scope : 'all';
|
|
376
|
+
} else if (!l.scope || l.scope.length === 0) {
|
|
377
|
+
// `later` matches all types; `earlier` only consumes its own scope, so
|
|
378
|
+
// the shadow is partial — exactly those types.
|
|
379
|
+
covered = e.scope;
|
|
380
|
+
} else {
|
|
381
|
+
const overlap = l.scope.filter((t) => e.scope!.includes(t));
|
|
382
|
+
if (overlap.length === 0) return null;
|
|
383
|
+
covered = overlap;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Every remaining discriminator on `earlier` must be at least as broad as
|
|
387
|
+
// `later`'s, otherwise some `later` events slip past `earlier`.
|
|
388
|
+
if (!globAtLeastAsBroad(e.source, l.source)) return null;
|
|
389
|
+
if (!globAtLeastAsBroad(e.channel, l.channel)) return null;
|
|
390
|
+
if (!globAtLeastAsBroad(e.mount, l.mount)) return null;
|
|
391
|
+
if (!globAtLeastAsBroad(e.pathGlob, l.pathGlob)) return null;
|
|
392
|
+
|
|
393
|
+
if (e.filter) {
|
|
394
|
+
if (!l.filter || e.filter.type !== l.filter.type || e.filter.pattern !== l.filter.pattern) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// metadataTrue / tagsAny are OR-lists: earlier subsumes when later's list
|
|
400
|
+
// is a subset of earlier's (any event satisfying later then satisfies
|
|
401
|
+
// earlier too). tagsAll / tagsNone are AND/NOT constraints: earlier
|
|
402
|
+
// subsumes when its constraints are a subset of later's.
|
|
403
|
+
if (e.metadataTrue && e.metadataTrue.length > 0) {
|
|
404
|
+
if (!l.metadataTrue || !subsetOf(l.metadataTrue, e.metadataTrue)) return null;
|
|
405
|
+
}
|
|
406
|
+
if (e.tagsAny && e.tagsAny.length > 0) {
|
|
407
|
+
if (!l.tagsAny || !subsetOf(l.tagsAny, e.tagsAny)) return null;
|
|
408
|
+
}
|
|
409
|
+
if (e.tagsAll && !subsetOf(e.tagsAll, l.tagsAll ?? [])) return null;
|
|
410
|
+
if (e.tagsNone && !subsetOf(e.tagsNone, l.tagsNone ?? [])) return null;
|
|
411
|
+
|
|
412
|
+
return covered;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Find rules made unreachable by earlier, broader rules (first match wins).
|
|
417
|
+
* The 2026-08-08 Mythos incident is the canonical catch: a prepended
|
|
418
|
+
* `{scope:[mcpl:channel-incoming]}` sampler silently made the
|
|
419
|
+
* direct-address and mention rules dead for the whole incoming lane.
|
|
420
|
+
* `passthrough` observers shadow nothing — non-firing matches fall through.
|
|
421
|
+
*/
|
|
422
|
+
export function findShadowedPolicies(policies: GatePolicy[]): ShadowWarning[] {
|
|
423
|
+
const out: ShadowWarning[] = [];
|
|
424
|
+
for (let i = 0; i < policies.length; i++) {
|
|
425
|
+
const earlier = policies[i];
|
|
426
|
+
if (earlier.passthrough) continue;
|
|
427
|
+
for (let j = i + 1; j < policies.length; j++) {
|
|
428
|
+
const covered = subsumedEventTypes(earlier, policies[j]);
|
|
429
|
+
if (covered) {
|
|
430
|
+
out.push({ earlier: earlier.name, later: policies[j].name, eventTypes: covered });
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return out;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** Human-readable one-liner for a ShadowWarning, used in tool results,
|
|
438
|
+
* traces, and gate_status alike so the same hazard reads the same way
|
|
439
|
+
* everywhere. */
|
|
440
|
+
export function formatShadowWarning(w: ShadowWarning): string {
|
|
441
|
+
const types = w.eventTypes === 'all' ? 'ALL event types' : w.eventTypes.join(', ');
|
|
442
|
+
return `rule "${w.earlier}" makes "${w.later}" unreachable for ${types} `
|
|
443
|
+
+ `(first match wins — every event "${w.later}" would match is consumed by "${w.earlier}" first)`;
|
|
444
|
+
}
|
|
445
|
+
|
|
318
446
|
// ---------------------------------------------------------------------------
|
|
319
447
|
// EventGate
|
|
320
448
|
// ---------------------------------------------------------------------------
|
|
@@ -349,6 +477,11 @@ export class EventGate {
|
|
|
349
477
|
// Per-policy stats
|
|
350
478
|
private stats = new Map<string, PolicyStats>();
|
|
351
479
|
|
|
480
|
+
// Shadow-lint cache (see refreshShadowWarnings) — recomputed on every
|
|
481
|
+
// config change, fingerprinted so an unchanged hazard set doesn't re-trace.
|
|
482
|
+
private shadowWarningsCache: ShadowWarning[] = [];
|
|
483
|
+
private shadowWarningsFingerprint = '[]';
|
|
484
|
+
|
|
352
485
|
// Observability counters for events that didn't match any policy
|
|
353
486
|
private totalEvaluations = 0;
|
|
354
487
|
private defaultTriggered = 0;
|
|
@@ -366,6 +499,13 @@ export class EventGate {
|
|
|
366
499
|
private sleepTimer: ReturnType<typeof setTimeout> | null = null;
|
|
367
500
|
private sleepAgent: string | undefined;
|
|
368
501
|
|
|
502
|
+
// Self-wake timers per agent (skip_reply's wake_in_seconds). Unlike sleep,
|
|
503
|
+
// a self-wake suppresses NOTHING — it means "if nothing else wakes me by
|
|
504
|
+
// then, wake me". Any turn start for the agent cancels its pending
|
|
505
|
+
// self-wake (an external wake supersedes it; the timer would otherwise
|
|
506
|
+
// land as a redundant empty wake right after the turn).
|
|
507
|
+
private selfWakeTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
508
|
+
|
|
369
509
|
// Privileged users who may wake the agent through sleep. Hot-reloaded from
|
|
370
510
|
// privilegedUsersPath on change.
|
|
371
511
|
private privilegedUsersPath: string | undefined;
|
|
@@ -448,6 +588,7 @@ export class EventGate {
|
|
|
448
588
|
// Load config
|
|
449
589
|
this.config = this.loadConfig() ?? opts.initialConfig ?? { ...DEFAULT_CONFIG };
|
|
450
590
|
this.compiledPolicies = this.compilePolicies(this.config);
|
|
591
|
+
this.refreshShadowWarnings('startup');
|
|
451
592
|
|
|
452
593
|
// Programmable gate: an optional agent-authored `gate.js` next to gate.json.
|
|
453
594
|
// Absent ⇒ inactive (zero overhead beyond a throttled stat check).
|
|
@@ -561,6 +702,7 @@ export class EventGate {
|
|
|
561
702
|
this.config = newConfig;
|
|
562
703
|
this.compiledPolicies = this.compilePolicies(newConfig);
|
|
563
704
|
this.lastReloadTimestamp = now;
|
|
705
|
+
this.refreshShadowWarnings('reload');
|
|
564
706
|
|
|
565
707
|
this.emitTrace({
|
|
566
708
|
type: 'gate:config-reloaded',
|
|
@@ -591,23 +733,46 @@ export class EventGate {
|
|
|
591
733
|
* without waiting for the throttled mtime reload.
|
|
592
734
|
*
|
|
593
735
|
* The freshest on-disk config is re-read first, so a concurrent operator edit
|
|
594
|
-
* isn't clobbered. A policy with the same `name` is replaced IN PLACE
|
|
595
|
-
* preserved);
|
|
596
|
-
* —
|
|
597
|
-
*
|
|
736
|
+
* isn't clobbered. A policy with the same `name` is replaced IN PLACE when no
|
|
737
|
+
* `position` is given (order preserved); WITH a `position` it is moved there
|
|
738
|
+
* — so re-issuing a rule is also how you reposition it. New policies append
|
|
739
|
+
* by default; `'prepend'` puts one at the very front (ahead of EVERYTHING,
|
|
740
|
+
* including addressed-message wake rules — see the shadow lint), and the
|
|
741
|
+
* anchored forms `{before: name}` / `{after: name}` insert relative to an
|
|
742
|
+
* existing rule, which is almost always the safer way to order a sampler
|
|
743
|
+
* or debounce against a specific catch-all.
|
|
598
744
|
*/
|
|
599
|
-
addPolicy(
|
|
745
|
+
addPolicy(
|
|
746
|
+
rawPolicy: unknown,
|
|
747
|
+
options?: { position?: 'append' | 'prepend' | { before: string } | { after: string } },
|
|
748
|
+
): GatePolicy {
|
|
600
749
|
const policy = validatePolicy(rawPolicy);
|
|
601
750
|
const current = this.loadConfig() ?? this.config;
|
|
602
751
|
const policies = [...current.policies];
|
|
603
752
|
const idx = policies.findIndex((p) => p.name === policy.name);
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
policies
|
|
753
|
+
const position = options?.position;
|
|
754
|
+
|
|
755
|
+
if (idx >= 0 && position === undefined) {
|
|
756
|
+
policies[idx] = policy; // replace in place, order preserved
|
|
608
757
|
} else {
|
|
609
|
-
policies.
|
|
758
|
+
if (idx >= 0) policies.splice(idx, 1); // repositioning a known rule
|
|
759
|
+
if (position === 'prepend') {
|
|
760
|
+
policies.unshift(policy);
|
|
761
|
+
} else if (position !== undefined && typeof position === 'object') {
|
|
762
|
+
const anchorName = 'before' in position ? position.before : position.after;
|
|
763
|
+
const anchorIdx = policies.findIndex((p) => p.name === anchorName);
|
|
764
|
+
if (anchorIdx < 0) {
|
|
765
|
+
throw new Error(
|
|
766
|
+
`addPolicy: no policy named "${anchorName}" to anchor on — `
|
|
767
|
+
+ `have: [${policies.map((p) => p.name).join(', ')}]`,
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
policies.splice('before' in position ? anchorIdx : anchorIdx + 1, 0, policy);
|
|
771
|
+
} else {
|
|
772
|
+
policies.push(policy);
|
|
773
|
+
}
|
|
610
774
|
}
|
|
775
|
+
|
|
611
776
|
this.writeConfig({ ...current, policies });
|
|
612
777
|
this.emitTrace({
|
|
613
778
|
type: 'gate:policy-added',
|
|
@@ -665,6 +830,7 @@ export class EventGate {
|
|
|
665
830
|
writeFileSync(this.configPath, JSON.stringify(config, null, 2) + '\n');
|
|
666
831
|
this.config = config;
|
|
667
832
|
this.compiledPolicies = this.compilePolicies(config);
|
|
833
|
+
this.refreshShadowWarnings('policy-mutation');
|
|
668
834
|
this.configSource = 'file';
|
|
669
835
|
this.configErrors = [];
|
|
670
836
|
this.lastReloadTimestamp = Date.now();
|
|
@@ -676,18 +842,10 @@ export class EventGate {
|
|
|
676
842
|
}
|
|
677
843
|
|
|
678
844
|
// =========================================================================
|
|
679
|
-
// Policy matching — first match wins
|
|
845
|
+
// Policy matching — first match wins (passthrough observers excepted; the
|
|
846
|
+
// match/decide loop lives inline in evaluate() and probe())
|
|
680
847
|
// =========================================================================
|
|
681
848
|
|
|
682
|
-
private matchPolicy(info: GateEventInfo): GatePolicy | null {
|
|
683
|
-
for (const compiled of this.compiledPolicies) {
|
|
684
|
-
if (this.compiledMatches(compiled, info)) {
|
|
685
|
-
return compiled.policy;
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
return null;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
849
|
private compiledMatches(compiled: CompiledPolicy, info: GateEventInfo): boolean {
|
|
692
850
|
const match = compiled.policy.match;
|
|
693
851
|
|
|
@@ -797,13 +955,32 @@ export class EventGate {
|
|
|
797
955
|
// Programmable gate (gate.js) takes precedence when present and returns a
|
|
798
956
|
// behavior; null/undefined (inactive, error, or a deliberate pass) falls
|
|
799
957
|
// through to the declarative gate.json policies below.
|
|
958
|
+
//
|
|
959
|
+
// Declarative policies: first match wins — EXCEPT `passthrough` observers,
|
|
960
|
+
// which consume the event only when their counting behavior actually
|
|
961
|
+
// fires. A passthrough policy that matched-but-didn't-fire records its
|
|
962
|
+
// count and evaluation continues, so a density sampler placed early can
|
|
963
|
+
// no longer swallow the addressed-message rules behind it.
|
|
800
964
|
const scripted = this.gateScript.evaluate(info);
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
965
|
+
let decision: GateDecision | null = null;
|
|
966
|
+
const observed: string[] = [];
|
|
967
|
+
if (scripted != null) {
|
|
968
|
+
decision = this.computeDecision({ name: 'gate.js', match: {}, behavior: scripted }, info);
|
|
969
|
+
} else {
|
|
970
|
+
for (const compiled of this.compiledPolicies) {
|
|
971
|
+
if (!this.compiledMatches(compiled, info)) continue;
|
|
972
|
+
const candidate = this.computeDecision(compiled.policy, info);
|
|
973
|
+
if (candidate.trigger || !compiled.policy.passthrough) {
|
|
974
|
+
decision = candidate;
|
|
975
|
+
break;
|
|
976
|
+
}
|
|
977
|
+
observed.push(compiled.policy.name); // counted, didn't fire, fell through
|
|
978
|
+
}
|
|
979
|
+
}
|
|
804
980
|
|
|
805
981
|
this.totalEvaluations++;
|
|
806
|
-
if (!
|
|
982
|
+
if (!decision) {
|
|
983
|
+
decision = this.computeDecision(null, info);
|
|
807
984
|
const bucket = this.defaultByEventType.get(info.eventType)
|
|
808
985
|
?? { triggered: 0, skipped: 0 };
|
|
809
986
|
if (decision.trigger) {
|
|
@@ -815,6 +992,7 @@ export class EventGate {
|
|
|
815
992
|
}
|
|
816
993
|
this.defaultByEventType.set(info.eventType, bucket);
|
|
817
994
|
}
|
|
995
|
+
if (observed.length > 0) decision.observed = observed;
|
|
818
996
|
|
|
819
997
|
this.emitTrace({
|
|
820
998
|
type: 'gate:decision',
|
|
@@ -824,6 +1002,7 @@ export class EventGate {
|
|
|
824
1002
|
matchedPolicy: decision.policyName,
|
|
825
1003
|
trigger: decision.trigger,
|
|
826
1004
|
behavior: formatBehavior(decision.behavior),
|
|
1005
|
+
...(observed.length > 0 ? { observed } : {}),
|
|
827
1006
|
timestamp: this.now(),
|
|
828
1007
|
});
|
|
829
1008
|
|
|
@@ -995,6 +1174,164 @@ export class EventGate {
|
|
|
995
1174
|
this.passiveSampleFires.delete(name);
|
|
996
1175
|
}
|
|
997
1176
|
|
|
1177
|
+
// =========================================================================
|
|
1178
|
+
// Dry-run probes — side-effect-free "what would this event do right now"
|
|
1179
|
+
// =========================================================================
|
|
1180
|
+
|
|
1181
|
+
/**
|
|
1182
|
+
* Non-mutating twin of the behavior decision: would this policy trigger for
|
|
1183
|
+
* this event, given CURRENT counter/bucket state? Reads state, never writes
|
|
1184
|
+
* it — no stats, no traces, no counter increments, no debounce batching.
|
|
1185
|
+
*/
|
|
1186
|
+
private wouldTrigger(policy: GatePolicy, info: GateEventInfo): boolean {
|
|
1187
|
+
const b = policy.behavior;
|
|
1188
|
+
if (b === 'always') return true;
|
|
1189
|
+
if (b === 'defer' || b === 'skip') return false;
|
|
1190
|
+
if (typeof b === 'object') {
|
|
1191
|
+
if ('debounce' in b) return false; // batches; never triggers inline
|
|
1192
|
+
if ('rate_limit' in b) {
|
|
1193
|
+
const cfg = b.rate_limit;
|
|
1194
|
+
const key = this.resolveKey(info.metadata, cfg.keyBy);
|
|
1195
|
+
const bucket = this.rateLimitBuckets.get(policy.name)?.get(key);
|
|
1196
|
+
if (!bucket) return true; // fresh bucket starts full
|
|
1197
|
+
let tokens = bucket.tokens;
|
|
1198
|
+
if (tokens < cfg.tokens) {
|
|
1199
|
+
const elapsed = Math.max(0, this.now() - bucket.lastRefill);
|
|
1200
|
+
tokens = Math.min(cfg.tokens, tokens + Math.floor(elapsed / cfg.refillIntervalMs));
|
|
1201
|
+
}
|
|
1202
|
+
return tokens > 0;
|
|
1203
|
+
}
|
|
1204
|
+
if ('passive_sample' in b) {
|
|
1205
|
+
const cfg = b.passive_sample;
|
|
1206
|
+
const key = this.resolveKey(info.metadata, cfg.keyBy);
|
|
1207
|
+
const count = this.passiveSampleCounters.get(policy.name)?.get(key) ?? 0;
|
|
1208
|
+
return count + 1 >= cfg.every;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
return false;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* Evaluate an event against the declarative policy table WITHOUT side
|
|
1216
|
+
* effects: no counters advance, no debounce batches form, no stats or
|
|
1217
|
+
* traces are recorded, and sleep suppression is ignored (a probe asks
|
|
1218
|
+
* about the table, not the moment). gate.js is not consulted — a script
|
|
1219
|
+
* may be stateful, and a probe must never perturb state.
|
|
1220
|
+
*/
|
|
1221
|
+
probe(info: GateEventInfo): GateDecision {
|
|
1222
|
+
for (const compiled of this.compiledPolicies) {
|
|
1223
|
+
if (!this.compiledMatches(compiled, info)) continue;
|
|
1224
|
+
const trigger = this.wouldTrigger(compiled.policy, info);
|
|
1225
|
+
if (trigger || !compiled.policy.passthrough) {
|
|
1226
|
+
return { trigger, policyName: compiled.policy.name, behavior: compiled.policy.behavior };
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
const trigger = (this.config.default ?? 'always') === 'always';
|
|
1230
|
+
return { trigger, policyName: null, behavior: this.config.default ?? 'always' };
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* Probe a canonical set of synthetic chat/heartbeat events and report which
|
|
1235
|
+
* rule wins each and whether it would wake. Backs the before/after table in
|
|
1236
|
+
* the wake_add_rule result — the point where a rule change's real semantics
|
|
1237
|
+
* must become legible (af#105; the Mythos stand-back-density incident would
|
|
1238
|
+
* have been visible as `dm: discord-direct-address → stand-back-density`).
|
|
1239
|
+
* Probes are representative shapes, not an exhaustive event space — rules
|
|
1240
|
+
* keyed on specific channels/mounts may not match any probe.
|
|
1241
|
+
*/
|
|
1242
|
+
probeTable(): GateProbeRow[] {
|
|
1243
|
+
const chat = (
|
|
1244
|
+
label: string,
|
|
1245
|
+
eventType: string,
|
|
1246
|
+
metadata: Record<string, unknown>,
|
|
1247
|
+
tags: string[],
|
|
1248
|
+
): { label: string; info: GateEventInfo } => ({
|
|
1249
|
+
label,
|
|
1250
|
+
info: {
|
|
1251
|
+
content: `[probe] ${label}`,
|
|
1252
|
+
eventType,
|
|
1253
|
+
serverId: 'discord',
|
|
1254
|
+
channelId: '',
|
|
1255
|
+
metadata: { ...metadata, eventType },
|
|
1256
|
+
tags,
|
|
1257
|
+
},
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
const probes = [
|
|
1261
|
+
chat('dm (open channel)', 'mcpl:channel-incoming', { isDM: true, isMention: true },
|
|
1262
|
+
['chat:dm', 'chat:private', 'chat:addressed', 'chat:from-human']),
|
|
1263
|
+
chat('dm (closed channel)', 'mcpl:push-event', { isDM: true, isMention: true },
|
|
1264
|
+
['chat:dm', 'chat:private', 'chat:addressed', 'chat:from-human']),
|
|
1265
|
+
chat('explicit mention (open channel)', 'mcpl:channel-incoming',
|
|
1266
|
+
{ isMention: true, isExplicitMention: true },
|
|
1267
|
+
['chat:mention', 'chat:addressed', 'chat:from-human']),
|
|
1268
|
+
chat('explicit mention (closed channel)', 'mcpl:push-event',
|
|
1269
|
+
{ isMention: true, isExplicitMention: true },
|
|
1270
|
+
['chat:mention', 'chat:addressed', 'chat:from-human']),
|
|
1271
|
+
chat('reply to you', 'mcpl:channel-incoming', { isMention: true, isReplyToBot: true },
|
|
1272
|
+
['chat:reply', 'chat:addressed', 'chat:from-human']),
|
|
1273
|
+
chat('ambient (human)', 'mcpl:channel-incoming', {},
|
|
1274
|
+
['chat:ambient', 'chat:from-human']),
|
|
1275
|
+
chat('ambient (bot)', 'mcpl:channel-incoming', { isBot: true },
|
|
1276
|
+
['chat:ambient', 'chat:from-bot']),
|
|
1277
|
+
{
|
|
1278
|
+
label: 'heartbeat',
|
|
1279
|
+
info: {
|
|
1280
|
+
content: '[probe] heartbeat',
|
|
1281
|
+
eventType: 'mcpl:push-event',
|
|
1282
|
+
serverId: 'heartbeat',
|
|
1283
|
+
channelId: '',
|
|
1284
|
+
metadata: { source: 'heartbeat', eventType: 'mcpl:push-event' },
|
|
1285
|
+
} as GateEventInfo,
|
|
1286
|
+
},
|
|
1287
|
+
];
|
|
1288
|
+
|
|
1289
|
+
return probes.map(({ label, info }) => {
|
|
1290
|
+
const d = this.probe(info);
|
|
1291
|
+
return {
|
|
1292
|
+
probe: label,
|
|
1293
|
+
eventType: info.eventType,
|
|
1294
|
+
policy: d.policyName,
|
|
1295
|
+
behavior: d.policyName === null
|
|
1296
|
+
? `default:${formatBehavior(d.behavior)}`
|
|
1297
|
+
: formatBehavior(d.behavior),
|
|
1298
|
+
wouldWake: d.trigger,
|
|
1299
|
+
};
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// =========================================================================
|
|
1304
|
+
// Shadow lint surface
|
|
1305
|
+
// =========================================================================
|
|
1306
|
+
|
|
1307
|
+
/** Current shadow-lint findings for the live policy order. Fresh compute —
|
|
1308
|
+
* cheap (policy lists are tens of entries at most). */
|
|
1309
|
+
lintShadows(): ShadowWarning[] {
|
|
1310
|
+
return findShadowedPolicies(this.config.policies);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
/**
|
|
1314
|
+
* Recompute shadow warnings and, when the set changed and is non-empty,
|
|
1315
|
+
* emit a `gate:shadow-warnings` trace so the hazard lands in ops
|
|
1316
|
+
* observability at the moment the order changed — not at the next missed
|
|
1317
|
+
* DM. Called from writeConfig and reloadIfChanged.
|
|
1318
|
+
*/
|
|
1319
|
+
private refreshShadowWarnings(reason: string): void {
|
|
1320
|
+
const warnings = this.lintShadows();
|
|
1321
|
+
const fingerprint = JSON.stringify(warnings);
|
|
1322
|
+
if (fingerprint === this.shadowWarningsFingerprint) return;
|
|
1323
|
+
this.shadowWarningsFingerprint = fingerprint;
|
|
1324
|
+
this.shadowWarningsCache = warnings;
|
|
1325
|
+
if (warnings.length > 0) {
|
|
1326
|
+
this.emitTrace({
|
|
1327
|
+
type: 'gate:shadow-warnings',
|
|
1328
|
+
reason,
|
|
1329
|
+
warnings: warnings.map(formatShadowWarning),
|
|
1330
|
+
timestamp: this.now(),
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
998
1335
|
// =========================================================================
|
|
999
1336
|
// shouldTriggerInference callback adapter
|
|
1000
1337
|
// =========================================================================
|
|
@@ -1135,6 +1472,14 @@ export class EventGate {
|
|
|
1135
1472
|
|
|
1136
1473
|
onInferenceStarted(agentName: string): void {
|
|
1137
1474
|
this.inferring.add(agentName);
|
|
1475
|
+
// A turn is starting — whatever caused it supersedes any pending
|
|
1476
|
+
// self-wake. (Arming happens MID-turn via skip_reply, i.e. after this
|
|
1477
|
+
// ran for the arming turn, so a fresh arm always survives its own turn.)
|
|
1478
|
+
const selfWake = this.selfWakeTimers.get(agentName);
|
|
1479
|
+
if (selfWake) {
|
|
1480
|
+
clearTimeout(selfWake);
|
|
1481
|
+
this.selfWakeTimers.delete(agentName);
|
|
1482
|
+
}
|
|
1138
1483
|
}
|
|
1139
1484
|
|
|
1140
1485
|
onInferenceEnded(agentName: string): void {
|
|
@@ -1206,6 +1551,59 @@ export class EventGate {
|
|
|
1206
1551
|
return { until: this.sleepUntil };
|
|
1207
1552
|
}
|
|
1208
1553
|
|
|
1554
|
+
// =========================================================================
|
|
1555
|
+
// Self-wake (skip_reply wake_in_seconds)
|
|
1556
|
+
// =========================================================================
|
|
1557
|
+
|
|
1558
|
+
/**
|
|
1559
|
+
* Arm a one-shot self-wake for `agentName` in `seconds` (clamped to
|
|
1560
|
+
* [1, 3600]). Fires a normal inference request when it elapses — no
|
|
1561
|
+
* suppression window, unlike sleep. Re-arming replaces the pending timer.
|
|
1562
|
+
* Cancelled by the agent's next turn start (see onInferenceStarted):
|
|
1563
|
+
* the semantics are "if nothing else wakes me by then, wake me".
|
|
1564
|
+
*/
|
|
1565
|
+
armSelfWake(agentName: string, seconds: number, source = 'skip_reply'): { inMs: number } {
|
|
1566
|
+
const ms = Math.max(1_000, Math.min(3_600_000, Math.floor(seconds * 1000)));
|
|
1567
|
+
const prev = this.selfWakeTimers.get(agentName);
|
|
1568
|
+
if (prev) clearTimeout(prev);
|
|
1569
|
+
const timer = setTimeout(() => {
|
|
1570
|
+
this.selfWakeTimers.delete(agentName);
|
|
1571
|
+
this.emitTrace({
|
|
1572
|
+
type: 'gate:decision',
|
|
1573
|
+
eventType: 'selfwake:fired',
|
|
1574
|
+
matchedPolicy: 'self-wake',
|
|
1575
|
+
trigger: true,
|
|
1576
|
+
behavior: 'always',
|
|
1577
|
+
timestamp: this.now(),
|
|
1578
|
+
});
|
|
1579
|
+
// Compact in-window notice: without it the wake turn contains nothing
|
|
1580
|
+
// new, so the agent can't tell WHAT woke it (its own timer vs. a
|
|
1581
|
+
// heartbeat) or WHEN. One line, timestamped to the second. Added
|
|
1582
|
+
// before the inference request so it rides the wake turn (addMessage's
|
|
1583
|
+
// turn-alive guard defers it to that turn's start if needed).
|
|
1584
|
+
const nowIso = new Date(this.now()).toISOString().slice(0, 19) + 'Z';
|
|
1585
|
+
this.addMessageFn('user', [{
|
|
1586
|
+
type: 'text',
|
|
1587
|
+
text: `[self-wake] your ${source} timer (${Math.round(ms / 1000)}s) elapsed — now ${nowIso}`,
|
|
1588
|
+
}], { source: 'gate:self-wake' });
|
|
1589
|
+
this.requestInferenceFn(
|
|
1590
|
+
agentName,
|
|
1591
|
+
`self-scheduled wake (${source}, ${Math.round(ms / 1000)}s)`,
|
|
1592
|
+
'self-wake',
|
|
1593
|
+
);
|
|
1594
|
+
}, ms);
|
|
1595
|
+
this.selfWakeTimers.set(agentName, timer);
|
|
1596
|
+
this.emitTrace({
|
|
1597
|
+
type: 'gate:decision',
|
|
1598
|
+
eventType: 'selfwake:set',
|
|
1599
|
+
matchedPolicy: 'self-wake',
|
|
1600
|
+
trigger: false,
|
|
1601
|
+
behavior: 'skip',
|
|
1602
|
+
timestamp: this.now(),
|
|
1603
|
+
});
|
|
1604
|
+
return { inMs: ms };
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1209
1607
|
/** End sleep immediately. Returns true if the agent was asleep. */
|
|
1210
1608
|
clearSleep(): boolean {
|
|
1211
1609
|
const wasAsleep = this.sleepUntil > this.now();
|
|
@@ -1368,6 +1766,7 @@ export class EventGate {
|
|
|
1368
1766
|
script: this.gateScript.status(),
|
|
1369
1767
|
policies,
|
|
1370
1768
|
errors: [...this.configErrors],
|
|
1769
|
+
shadowWarnings: this.shadowWarningsCache.map(formatShadowWarning),
|
|
1371
1770
|
totalEvaluations: this.totalEvaluations,
|
|
1372
1771
|
defaultDecisions: {
|
|
1373
1772
|
triggered: this.defaultTriggered,
|
|
@@ -1387,6 +1786,10 @@ export class EventGate {
|
|
|
1387
1786
|
clearTimeout(state.timer);
|
|
1388
1787
|
}
|
|
1389
1788
|
this.debounceTimers.clear();
|
|
1789
|
+
for (const timer of this.selfWakeTimers.values()) {
|
|
1790
|
+
clearTimeout(timer);
|
|
1791
|
+
}
|
|
1792
|
+
this.selfWakeTimers.clear();
|
|
1390
1793
|
this.rateLimitBuckets.clear();
|
|
1391
1794
|
this.passiveSampleCounters.clear();
|
|
1392
1795
|
this.rateLimitDenied.clear();
|
package/src/gate/types.ts
CHANGED
|
@@ -118,6 +118,22 @@ export interface GatePolicy {
|
|
|
118
118
|
* coupling the behaviors to each other in code. Unknown names are ignored.
|
|
119
119
|
*/
|
|
120
120
|
resets?: string[];
|
|
121
|
+
/**
|
|
122
|
+
* Observer semantics for the counting behaviors (`passive_sample`,
|
|
123
|
+
* `rate_limit`): when the behavior does NOT fire for a matching event
|
|
124
|
+
* (sampler below N, bucket empty), evaluation CONTINUES to later policies
|
|
125
|
+
* instead of consuming the event with trigger=false. When it DOES fire,
|
|
126
|
+
* it triggers as usual and evaluation stops.
|
|
127
|
+
*
|
|
128
|
+
* This is how to express "additionally wake me every Nth event" without
|
|
129
|
+
* silently swallowing the events in between — a plain (consuming)
|
|
130
|
+
* `passive_sample` placed early makes every later rule unreachable for
|
|
131
|
+
* its scope, which is almost never what a density governor intends
|
|
132
|
+
* (2026-08-08 Mythos stand-back-density incident). Validation rejects
|
|
133
|
+
* `passthrough` on `always`/`defer`/`skip`/`debounce`, where "didn't
|
|
134
|
+
* fire" has no meaning distinct from the behavior itself.
|
|
135
|
+
*/
|
|
136
|
+
passthrough?: boolean;
|
|
121
137
|
}
|
|
122
138
|
|
|
123
139
|
// ---------------------------------------------------------------------------
|
|
@@ -162,6 +178,42 @@ export interface GateDecision {
|
|
|
162
178
|
policyName: string | null;
|
|
163
179
|
/** The behavior that was applied. */
|
|
164
180
|
behavior: GateBehavior;
|
|
181
|
+
/**
|
|
182
|
+
* Names of `passthrough` policies that matched and counted this event but
|
|
183
|
+
* did not fire, so evaluation continued past them. Absent when none did.
|
|
184
|
+
*/
|
|
185
|
+
observed?: string[];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* A rule-ordering hazard: `earlier` matches (and consumes) every event that
|
|
190
|
+
* `later` would match, for the listed event types — so `later` can never
|
|
191
|
+
* fire for them. Produced by the conservative shadow lint; a warning here is
|
|
192
|
+
* always a true statement about the config, but a config with no warnings is
|
|
193
|
+
* not guaranteed hazard-free (the lint only proves what it can prove).
|
|
194
|
+
*/
|
|
195
|
+
export interface ShadowWarning {
|
|
196
|
+
/** Name of the earlier (consuming) policy. */
|
|
197
|
+
earlier: string;
|
|
198
|
+
/** Name of the later policy it makes unreachable. */
|
|
199
|
+
later: string;
|
|
200
|
+
/** Event types affected — 'all' when both rules are unscoped. */
|
|
201
|
+
eventTypes: string[] | 'all';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** One row of the dry-run probe table (see EventGate.probeTable). */
|
|
205
|
+
export interface GateProbeRow {
|
|
206
|
+
/** Short label for the synthetic event probed (e.g. "dm", "mention"). */
|
|
207
|
+
probe: string;
|
|
208
|
+
/** Event type the probe used. */
|
|
209
|
+
eventType: string;
|
|
210
|
+
/** Winning policy name, or null when the default applied. */
|
|
211
|
+
policy: string | null;
|
|
212
|
+
/** Compact behavior description (formatBehavior output or the default). */
|
|
213
|
+
behavior: string;
|
|
214
|
+
/** Whether this event WOULD trigger inference right now. For counting
|
|
215
|
+
* behaviors this reflects current counter/bucket state. */
|
|
216
|
+
wouldWake: boolean;
|
|
165
217
|
}
|
|
166
218
|
|
|
167
219
|
/** Information about an event being evaluated. */
|
|
@@ -211,6 +263,12 @@ export interface GateStatus {
|
|
|
211
263
|
script: import('./gate-script.js').GateScriptStatus;
|
|
212
264
|
policies: GatePolicyStats[];
|
|
213
265
|
errors: string[];
|
|
266
|
+
/**
|
|
267
|
+
* Active shadow-lint findings for the current policy order (see
|
|
268
|
+
* ShadowWarning). Rendered so an agent inspecting gate_status sees the
|
|
269
|
+
* same hazards wake_add_rule warns about at install time.
|
|
270
|
+
*/
|
|
271
|
+
shadowWarnings: string[];
|
|
214
272
|
/** Total events the gate has evaluated since startup. */
|
|
215
273
|
totalEvaluations: number;
|
|
216
274
|
/**
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ export type { StartStreamResult } from './agent.js';
|
|
|
8
8
|
export { ProcessQueueImpl } from './queue.js';
|
|
9
9
|
export { ModuleRegistry } from './module-registry.js';
|
|
10
10
|
export { formatZonedDateTime, formatZonedTime, isValidTimeZone, resolveTimeZone } from './timezone.js';
|
|
11
|
+
export { REFUSAL_REACTIONS, REFUSAL_REACTION_FALLBACK, REFUSAL_REACTION_BASELINE } from './refusal-reactions.js';
|
|
11
12
|
|
|
12
13
|
// Built-in modules
|
|
13
14
|
export * from './modules/index.js';
|