@animalabs/agent-framework 0.31.0 → 0.33.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/README.md +204 -14
- package/dist/src/admission/admit.d.ts +18 -0
- package/dist/src/admission/admit.d.ts.map +1 -1
- package/dist/src/admission/admit.js +16 -3
- package/dist/src/admission/admit.js.map +1 -1
- package/dist/src/agent.d.ts +185 -78
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +279 -102
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server-core.d.ts.map +1 -1
- package/dist/src/api/mcp-server-core.js +50 -12
- package/dist/src/api/mcp-server-core.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +4 -20
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/code-execution/isolate/scripts.d.ts +12 -8
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +24 -0
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/config-checks.d.ts +7 -0
- package/dist/src/config-checks.d.ts.map +1 -1
- package/dist/src/config-checks.js +14 -0
- package/dist/src/config-checks.js.map +1 -1
- package/dist/src/context-manager-errors.d.ts +25 -0
- package/dist/src/context-manager-errors.d.ts.map +1 -0
- package/dist/src/context-manager-errors.js +31 -0
- package/dist/src/context-manager-errors.js.map +1 -0
- package/dist/src/context-options.d.ts +67 -0
- package/dist/src/context-options.d.ts.map +1 -0
- package/dist/src/context-options.js +85 -0
- package/dist/src/context-options.js.map +1 -0
- package/dist/src/deferred-import.d.ts +93 -0
- package/dist/src/deferred-import.d.ts.map +1 -0
- package/dist/src/deferred-import.js +177 -0
- package/dist/src/deferred-import.js.map +1 -0
- package/dist/src/dendrite/audience.d.ts +223 -0
- package/dist/src/dendrite/audience.d.ts.map +1 -0
- package/dist/src/dendrite/audience.js +202 -0
- package/dist/src/dendrite/audience.js.map +1 -0
- package/dist/src/dendrite/index.d.ts +5 -3
- package/dist/src/dendrite/index.d.ts.map +1 -1
- package/dist/src/dendrite/index.js +2 -1
- package/dist/src/dendrite/index.js.map +1 -1
- package/dist/src/dendrite/presets.d.ts +10 -5
- package/dist/src/dendrite/presets.d.ts.map +1 -1
- package/dist/src/dendrite/presets.js +17 -5
- package/dist/src/dendrite/presets.js.map +1 -1
- package/dist/src/dendrite/registry.d.ts +62 -7
- package/dist/src/dendrite/registry.d.ts.map +1 -1
- package/dist/src/dendrite/registry.js +179 -10
- package/dist/src/dendrite/registry.js.map +1 -1
- package/dist/src/dendrite/types.d.ts +91 -8
- package/dist/src/dendrite/types.d.ts.map +1 -1
- package/dist/src/foreign-rounds.d.ts +69 -0
- package/dist/src/foreign-rounds.d.ts.map +1 -0
- package/dist/src/foreign-rounds.js +152 -0
- package/dist/src/foreign-rounds.js.map +1 -0
- package/dist/src/framework.d.ts +1977 -820
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +7571 -3457
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +85 -15
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +421 -47
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/index.d.ts +1 -1
- package/dist/src/gate/index.d.ts.map +1 -1
- package/dist/src/gate/index.js.map +1 -1
- package/dist/src/gate/rule-scope.d.ts +30 -0
- package/dist/src/gate/rule-scope.d.ts.map +1 -0
- package/dist/src/gate/rule-scope.js +41 -0
- package/dist/src/gate/rule-scope.js.map +1 -0
- package/dist/src/gate/suppress-script-worker.d.ts +2 -0
- package/dist/src/gate/suppress-script-worker.d.ts.map +1 -0
- package/dist/src/gate/suppress-script-worker.js +103 -0
- package/dist/src/gate/suppress-script-worker.js.map +1 -0
- package/dist/src/gate/suppress-scripts.d.ts +105 -0
- package/dist/src/gate/suppress-scripts.d.ts.map +1 -0
- package/dist/src/gate/suppress-scripts.js +403 -0
- package/dist/src/gate/suppress-scripts.js.map +1 -0
- package/dist/src/gate/suppression-review.d.ts +143 -0
- package/dist/src/gate/suppression-review.d.ts.map +1 -0
- package/dist/src/gate/suppression-review.js +463 -0
- package/dist/src/gate/suppression-review.js.map +1 -0
- package/dist/src/gate/suppression-stats.d.ts +125 -0
- package/dist/src/gate/suppression-stats.d.ts.map +1 -0
- package/dist/src/gate/suppression-stats.js +214 -0
- package/dist/src/gate/suppression-stats.js.map +1 -0
- package/dist/src/gate/types.d.ts +135 -7
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/gate/wake-onboarding.d.ts +119 -0
- package/dist/src/gate/wake-onboarding.d.ts.map +1 -0
- package/dist/src/gate/wake-onboarding.js +277 -0
- package/dist/src/gate/wake-onboarding.js.map +1 -0
- package/dist/src/held-writes.d.ts +31 -23
- package/dist/src/held-writes.d.ts.map +1 -1
- package/dist/src/held-writes.js +44 -59
- package/dist/src/held-writes.js.map +1 -1
- package/dist/src/index.d.ts +5 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/jsonrpc-envelope.d.ts +69 -0
- package/dist/src/jsonrpc-envelope.d.ts.map +1 -0
- package/dist/src/jsonrpc-envelope.js +66 -0
- package/dist/src/jsonrpc-envelope.js.map +1 -0
- package/dist/src/mcpl/channel-registry.d.ts +131 -15
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +290 -46
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.d.ts +2 -4
- package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
- package/dist/src/mcpl/feature-set-manager.d.ts +2 -4
- package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
- package/dist/src/mcpl/feature-set-manager.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +2 -1
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
- package/dist/src/mcpl/inbound-source.js +2 -1
- package/dist/src/mcpl/inbound-source.js.map +1 -1
- package/dist/src/mcpl/inference-router.d.ts +6 -6
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +132 -22
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +105 -15
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts +9 -7
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +13 -10
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +134 -56
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tool-classes.d.ts.map +1 -1
- package/dist/src/mcpl/tool-classes.js +3 -0
- package/dist/src/mcpl/tool-classes.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +8 -2
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +24 -1
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +68 -18
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts +45 -8
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +237 -86
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/history/index.d.ts +59 -0
- package/dist/src/modules/history/index.d.ts.map +1 -1
- package/dist/src/modules/history/index.js +432 -67
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/history/semantic.d.ts +6 -5
- package/dist/src/modules/history/semantic.d.ts.map +1 -1
- package/dist/src/modules/history/semantic.js +15 -22
- package/dist/src/modules/history/semantic.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +1 -1
- package/dist/src/modules/workspace/index.d.ts +28 -7
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +76 -22
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/notice-tags.d.ts +10 -1
- package/dist/src/notice-tags.d.ts.map +1 -1
- package/dist/src/notice-tags.js +10 -1
- package/dist/src/notice-tags.js.map +1 -1
- package/dist/src/operator-log.d.ts +36 -3
- package/dist/src/operator-log.d.ts.map +1 -1
- package/dist/src/operator-log.js +2 -2
- package/dist/src/operator-log.js.map +1 -1
- package/dist/src/presentation.d.ts +746 -0
- package/dist/src/presentation.d.ts.map +1 -0
- package/dist/src/presentation.js +1435 -0
- package/dist/src/presentation.js.map +1 -0
- package/dist/src/prose-drafts.d.ts +15 -11
- package/dist/src/prose-drafts.d.ts.map +1 -1
- package/dist/src/prose-drafts.js +16 -12
- package/dist/src/prose-drafts.js.map +1 -1
- package/dist/src/read-marks.d.ts +192 -0
- package/dist/src/read-marks.d.ts.map +1 -0
- package/dist/src/read-marks.js +242 -0
- package/dist/src/read-marks.js.map +1 -0
- package/dist/src/recovery/offline-branch.d.ts +91 -6
- package/dist/src/recovery/offline-branch.d.ts.map +1 -1
- package/dist/src/recovery/offline-branch.js +273 -29
- package/dist/src/recovery/offline-branch.js.map +1 -1
- package/dist/src/recovery/recover-cli.js +22 -3
- package/dist/src/recovery/recover-cli.js.map +1 -1
- package/dist/src/send-tools.d.ts +5 -9
- package/dist/src/send-tools.d.ts.map +1 -1
- package/dist/src/send-tools.js +40 -7
- package/dist/src/send-tools.js.map +1 -1
- package/dist/src/stored-injections.d.ts +2 -2
- package/dist/src/stored-injections.js +2 -2
- package/dist/src/stored-removal.d.ts +17 -4
- package/dist/src/stored-removal.d.ts.map +1 -1
- package/dist/src/stored-removal.js +29 -5
- package/dist/src/stored-removal.js.map +1 -1
- package/dist/src/tool-result-guard.d.ts +12 -1
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +42 -8
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +1159 -79
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +3709 -363
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +114 -17
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/tune-out/tools.js +87 -27
- package/dist/src/tune-out/tools.js.map +1 -1
- package/dist/src/tune-out/window-anchor.d.ts +143 -0
- package/dist/src/tune-out/window-anchor.d.ts.map +1 -0
- package/dist/src/tune-out/window-anchor.js +202 -0
- package/dist/src/tune-out/window-anchor.js.map +1 -0
- package/dist/src/types/agent.d.ts +86 -31
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +2 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +43 -21
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +180 -69
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +510 -36
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/admission/admit.ts +32 -3
- package/src/agent.ts +323 -120
- package/src/api/mcp-server-core.ts +54 -17
- package/src/api/server.ts +4 -23
- package/src/code-execution/isolate/scripts.ts +28 -3
- package/src/config-checks.ts +14 -0
- package/src/context-manager-errors.ts +37 -0
- package/src/context-options.ts +96 -0
- package/src/deferred-import.ts +239 -0
- package/src/dendrite/audience.ts +315 -0
- package/src/dendrite/index.ts +14 -2
- package/src/dendrite/presets.ts +18 -6
- package/src/dendrite/registry.ts +209 -12
- package/src/dendrite/types.ts +92 -8
- package/src/foreign-rounds.ts +199 -0
- package/src/framework.ts +7814 -3585
- package/src/gate/event-gate.ts +428 -65
- package/src/gate/index.ts +3 -0
- package/src/gate/rule-scope.ts +45 -0
- package/src/gate/suppress-script-worker.ts +89 -0
- package/src/gate/suppress-scripts.ts +414 -0
- package/src/gate/suppression-review.ts +517 -0
- package/src/gate/suppression-stats.ts +314 -0
- package/src/gate/types.ts +141 -7
- package/src/gate/wake-onboarding.ts +309 -0
- package/src/held-writes.ts +49 -60
- package/src/index.ts +9 -2
- package/src/jsonrpc-envelope.ts +81 -0
- package/src/mcpl/channel-registry.ts +353 -85
- package/src/mcpl/checkpoint-manager.ts +3 -2
- package/src/mcpl/feature-set-manager.ts +2 -1
- package/src/mcpl/inbound-source.ts +2 -1
- package/src/mcpl/inference-router.ts +7 -4
- package/src/mcpl/push-coalescer.ts +193 -34
- package/src/mcpl/push-handler.ts +10 -5
- package/src/mcpl/server-connection.ts +141 -60
- package/src/mcpl/tool-classes.ts +3 -0
- package/src/mcpl/types.ts +8 -2
- package/src/module-registry.ts +73 -17
- package/src/modules/discord/index.ts +229 -81
- package/src/modules/history/index.ts +439 -70
- package/src/modules/history/semantic.ts +15 -21
- package/src/modules/workspace/disk-agreement.ts +1 -1
- package/src/modules/workspace/index.ts +85 -23
- package/src/notice-tags.ts +10 -1
- package/src/operator-log.ts +36 -3
- package/src/presentation.ts +1676 -0
- package/src/prose-drafts.ts +16 -12
- package/src/read-marks.ts +328 -0
- package/src/recovery/offline-branch.ts +346 -32
- package/src/recovery/recover-cli.ts +21 -3
- package/src/send-tools.ts +40 -7
- package/src/stored-injections.ts +2 -2
- package/src/stored-removal.ts +31 -7
- package/src/tool-result-guard.ts +41 -7
- package/src/tune-out/coordinator.ts +4089 -457
- package/src/tune-out/tools.ts +160 -43
- package/src/tune-out/window-anchor.ts +277 -0
- package/src/types/agent.ts +90 -31
- package/src/types/events.ts +2 -0
- package/src/types/framework.ts +43 -21
- package/src/types/module.ts +181 -68
- package/src/types/trace.ts +542 -40
- package/dist/src/reasoning-loss-notice.d.ts +0 -23
- package/dist/src/reasoning-loss-notice.d.ts.map +0 -1
- package/dist/src/reasoning-loss-notice.js +0 -52
- package/dist/src/reasoning-loss-notice.js.map +0 -1
- package/dist/src/reasoning-loss.d.ts +0 -24
- package/dist/src/reasoning-loss.d.ts.map +0 -1
- package/dist/src/reasoning-loss.js +0 -102
- package/dist/src/reasoning-loss.js.map +0 -1
- package/src/reasoning-loss-notice.ts +0 -64
- package/src/reasoning-loss.ts +0 -120
package/src/gate/event-gate.ts
CHANGED
|
@@ -16,16 +16,20 @@ import { cutToFit, quotedJson, quotedValue, suppliedValue } from '../supplied-te
|
|
|
16
16
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
17
17
|
import { basename, dirname, join } from 'node:path';
|
|
18
18
|
import { GateScript } from './gate-script.js';
|
|
19
|
+
import { SuppressScripts, type SuppressScriptEvent } from './suppress-scripts.js';
|
|
20
|
+
import { firstCatchAll, namesSomething } from './rule-scope.js';
|
|
19
21
|
import { canonical, duplicateName, lastRecipePath, noChanges, reconcileLine, reconcileWithRecipe, ruleName, type WrittenConfig } from './recipe-reconcile.js';
|
|
20
22
|
import { closestNames, isNearMiss } from '../closest-names.js';
|
|
21
23
|
import { MAX_TIMER_MS } from '../deadline-timer.js';
|
|
22
24
|
import { safeSlice } from '../safe-slice.js';
|
|
23
25
|
|
|
24
26
|
import type { ToolDefinition } from '../types/events.js';
|
|
27
|
+
import type { TraceEventInput } from '../types/trace.js';
|
|
25
28
|
import type {
|
|
26
29
|
GateConfig,
|
|
27
30
|
GatePolicy,
|
|
28
31
|
GatePolicyMatch,
|
|
32
|
+
SuppressConfig,
|
|
29
33
|
GateBehavior,
|
|
30
34
|
GateDecision,
|
|
31
35
|
GateEntryRef,
|
|
@@ -146,15 +150,20 @@ interface PendingEvent {
|
|
|
146
150
|
* a channel message's id, a push event's event id (isNewestEntry). */
|
|
147
151
|
messageId?: string;
|
|
148
152
|
eventId?: string;
|
|
153
|
+
/** GateEventInfo.recipients. */
|
|
154
|
+
recipients?: readonly string[];
|
|
149
155
|
}
|
|
150
156
|
|
|
151
157
|
/** Where the host put a message the gate wrote: the agent whose window it
|
|
152
|
-
* went to
|
|
153
|
-
*
|
|
158
|
+
* went to; its stored id, once stored (absent while a surgery's hold, or
|
|
159
|
+
* its agent's recovery, keeps the write in memory); and `presented: false`
|
|
160
|
+
* when it wasn't presented to the agent: kept in memory, or stored while a
|
|
161
|
+
* turn of the agent's was alive, or with a presentation at rest that
|
|
162
|
+
* failed (#291's presentation index). The host marks the wake that goes with it from these. */
|
|
154
163
|
export interface WritePlacement {
|
|
155
164
|
agent?: string;
|
|
156
|
-
deferredId?: string;
|
|
157
165
|
messageId?: string;
|
|
166
|
+
presented?: false;
|
|
158
167
|
}
|
|
159
168
|
|
|
160
169
|
/**
|
|
@@ -259,15 +268,6 @@ interface PolicyStats {
|
|
|
259
268
|
lastMatchTimestamp: number | null;
|
|
260
269
|
}
|
|
261
270
|
|
|
262
|
-
// ---------------------------------------------------------------------------
|
|
263
|
-
// Trace event shape (subset — avoids importing the full TraceEvent union)
|
|
264
|
-
// ---------------------------------------------------------------------------
|
|
265
|
-
|
|
266
|
-
interface TraceEventLike {
|
|
267
|
-
type: string;
|
|
268
|
-
[key: string]: unknown;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
271
|
// ---------------------------------------------------------------------------
|
|
272
272
|
// Glob matching (simple * wildcards)
|
|
273
273
|
// ---------------------------------------------------------------------------
|
|
@@ -447,7 +447,7 @@ function behaviorFingerprint(b: import('./types.js').GateBehavior): string {
|
|
|
447
447
|
// default), it now loads without it.
|
|
448
448
|
|
|
449
449
|
const CONFIG_FIELDS = ['default', 'policies'];
|
|
450
|
-
const POLICY_FIELDS = ['name', 'match', 'behavior', 'resets', 'passthrough'];
|
|
450
|
+
const POLICY_FIELDS = ['name', 'match', 'behavior', 'resets', 'passthrough', 'suppress'];
|
|
451
451
|
const MATCH_FIELDS = [
|
|
452
452
|
'scope', 'source', 'channel', 'mount', 'pathGlob', 'tagsAny', 'tagsAll', 'tagsNone', 'metadataTrue', 'filter',
|
|
453
453
|
];
|
|
@@ -456,6 +456,7 @@ const BEHAVIOR_KINDS = ['debounce', 'rate_limit', 'passive_sample', 'judge'] as
|
|
|
456
456
|
const RATE_LIMIT_FIELDS = ['tokens', 'refillIntervalMs', 'keyBy'];
|
|
457
457
|
const PASSIVE_SAMPLE_FIELDS = ['every', 'keyBy'];
|
|
458
458
|
const JUDGE_FIELDS = ['debounceMs', 'maxWaitMs', 'instructions'];
|
|
459
|
+
const SUPPRESS_FIELDS = ['description', 'script'];
|
|
459
460
|
|
|
460
461
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
461
462
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -937,6 +938,7 @@ function readRule(raw: unknown, unnamed: string, file = 'gate.json', named: (pat
|
|
|
937
938
|
const match = readMatch(r, obj.match);
|
|
938
939
|
const resets = readResets(r, obj.resets);
|
|
939
940
|
const passthrough = readPassthrough(r, obj.passthrough, kind);
|
|
941
|
+
const suppress = readSuppress(r, obj.suppress, obj.behavior, match, obj.match);
|
|
940
942
|
const reading: RuleReading = {
|
|
941
943
|
where: r.where, problems: r.problems, notes: r.notes, quiet: kind === 'skip' || kind === 'defer' || r.noTokens === true,
|
|
942
944
|
};
|
|
@@ -944,6 +946,7 @@ function readRule(raw: unknown, unnamed: string, file = 'gate.json', named: (pat
|
|
|
944
946
|
const policy: GatePolicy = { name, match, behavior };
|
|
945
947
|
if (resets) policy.resets = resets;
|
|
946
948
|
if (passthrough) policy.passthrough = true;
|
|
949
|
+
if (suppress) policy.suppress = suppress;
|
|
947
950
|
reading.policy = policy;
|
|
948
951
|
if (r.matchesAll) reading.matchesAll = true;
|
|
949
952
|
}
|
|
@@ -1333,8 +1336,7 @@ function readMatch(r: Reader, value: unknown): GatePolicyMatch {
|
|
|
1333
1336
|
// Left with nothing that narrows it once something in it was named, it
|
|
1334
1337
|
// matches every event. Empty lists and nulls alone always meant no
|
|
1335
1338
|
// constraint, and name nothing.
|
|
1336
|
-
|
|
1337
|
-
if (!narrows && r.problems.length + r.notes.length > named) r.matchesAll = true;
|
|
1339
|
+
if (!matchNarrows(match) && r.problems.length + r.notes.length > named) r.matchesAll = true;
|
|
1338
1340
|
return match;
|
|
1339
1341
|
}
|
|
1340
1342
|
|
|
@@ -1381,6 +1383,133 @@ function readPassthrough(r: Reader, value: unknown, kind: BehaviorKind | undefin
|
|
|
1381
1383
|
return false;
|
|
1382
1384
|
}
|
|
1383
1385
|
|
|
1386
|
+
/** Whether a match narrows what it matches: anything but an empty scope. */
|
|
1387
|
+
export function matchNarrows(match: GatePolicyMatch): boolean {
|
|
1388
|
+
return Object.entries(match).some(([field, v]) => !(field === 'scope' && Array.isArray(v) && v.length === 0));
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
/**
|
|
1392
|
+
* Whether a suppress rule's match names what it hides: an event type, or a
|
|
1393
|
+
* source, channel, mount, path or tag pattern that isn't only wildcards
|
|
1394
|
+
* (namesSomething), a metadata flag, or a content filter. A pattern of
|
|
1395
|
+
* wildcards alone, such as `source: "*"`, or only `tagsNone`, takes every
|
|
1396
|
+
* event, which a suppress rule may not.
|
|
1397
|
+
*/
|
|
1398
|
+
function suppressMatchNarrows(match: GatePolicyMatch): boolean {
|
|
1399
|
+
if ((match.scope ?? []).length > 0 || (match.metadataTrue ?? []).length > 0 || match.filter !== undefined) return true;
|
|
1400
|
+
if ([match.source, match.channel, match.mount, match.pathGlob].some(namesSomething)) return true;
|
|
1401
|
+
return [...(match.tagsAny ?? []), ...(match.tagsAll ?? [])].some(namesSomething);
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
/** Longest suppress description, in characters: the review sends it with every rule it judges. */
|
|
1405
|
+
export const MAX_SUPPRESS_DESCRIPTION = 2_000;
|
|
1406
|
+
/** Longest suppress script, in characters. */
|
|
1407
|
+
export const MAX_SUPPRESS_SCRIPT = 20_000;
|
|
1408
|
+
const SUPPRESS_EXAMPLE = '{"description": "the eye going on and off"}';
|
|
1409
|
+
|
|
1410
|
+
/**
|
|
1411
|
+
* A rule's `suppress`. Every problem with it is read as a rule that doesn't
|
|
1412
|
+
* suppress, which is how a release without suppression reads the rule:
|
|
1413
|
+
* its behavior then decides the event, and the event is rendered. The
|
|
1414
|
+
* behavior has to be written out as "defer" (or "skip"), not left to
|
|
1415
|
+
* default, because such a release reads a rule with no behavior as
|
|
1416
|
+
* "always" and would wake the agent on every event the rule was meant to
|
|
1417
|
+
* hide.
|
|
1418
|
+
*/
|
|
1419
|
+
function readSuppress(r: Reader, value: unknown, behavior: unknown, match: GatePolicyMatch, rawMatch: unknown): SuppressConfig | undefined {
|
|
1420
|
+
if (value === undefined || value === false) return undefined;
|
|
1421
|
+
const without = "so the rule doesn't suppress, and its behavior decides its events as for any other rule";
|
|
1422
|
+
if (!isPlainObject(value)) {
|
|
1423
|
+
r.problems.push({ text: `suppress is ${shownValue(value)}, where an object such as ${SUPPRESS_EXAMPLE} is needed`, read: without });
|
|
1424
|
+
return undefined;
|
|
1425
|
+
}
|
|
1426
|
+
const before = r.problems.length;
|
|
1427
|
+
// A script given as null or "" is a script that's missing, not one left
|
|
1428
|
+
// out: read as left out, the rule would hide every event its match takes.
|
|
1429
|
+
const { script: _givenScript, ...rest } = value;
|
|
1430
|
+
const emptyScript = Object.hasOwn(value, 'script') && (value.script === null || value.script === '');
|
|
1431
|
+
if (emptyScript) {
|
|
1432
|
+
r.problems.push({ text: `suppress.script is ${shownValue(value.script)}, where the source of a function such as "(event, state) => false" is needed`, read: without });
|
|
1433
|
+
}
|
|
1434
|
+
const given = withoutEmpty(`${r.where}: suppress`, emptyScript ? rest : value, r.notes);
|
|
1435
|
+
const reported = r.problems.length;
|
|
1436
|
+
readFieldNames(r, 'suppress', given, SUPPRESS_FIELDS);
|
|
1437
|
+
// A field it doesn't read can't be left aside here, as elsewhere in a
|
|
1438
|
+
// rule: one meant as a condition, left out, would hide every event the
|
|
1439
|
+
// match takes.
|
|
1440
|
+
const unread = Object.keys(given).filter((key) => !SUPPRESS_FIELDS.includes(key));
|
|
1441
|
+
if (unread.length > 0 && r.problems.length === reported) {
|
|
1442
|
+
r.problems.push({ text: `suppress has ${quotedList(unread)}, and a suppress rule reads only "description" and "script"`, read: without });
|
|
1443
|
+
}
|
|
1444
|
+
const config: SuppressConfig = { description: '' };
|
|
1445
|
+
if (typeof given.description !== 'string' || given.description.trim() === '') {
|
|
1446
|
+
r.problems.push({
|
|
1447
|
+
text: `suppress.description is ${given.description === undefined ? 'missing' : shownValue(given.description)}, where the rule's `
|
|
1448
|
+
+ 'purpose in a few words is needed: the review checks what the rule hides against it',
|
|
1449
|
+
read: without,
|
|
1450
|
+
});
|
|
1451
|
+
} else if ([...given.description].length > MAX_SUPPRESS_DESCRIPTION) {
|
|
1452
|
+
r.problems.push({
|
|
1453
|
+
text: `suppress.description is ${[...given.description].length.toLocaleString('en-US')} characters, over the `
|
|
1454
|
+
+ `${MAX_SUPPRESS_DESCRIPTION.toLocaleString('en-US')} allowed`,
|
|
1455
|
+
read: without,
|
|
1456
|
+
});
|
|
1457
|
+
} else {
|
|
1458
|
+
config.description = given.description.trim();
|
|
1459
|
+
}
|
|
1460
|
+
if (given.script !== undefined) {
|
|
1461
|
+
if (typeof given.script !== 'string' || given.script.trim() === '') {
|
|
1462
|
+
r.problems.push({ text: `suppress.script is ${shownValue(given.script)}, where the source of a function such as "(event, state) => false" is needed`, read: without });
|
|
1463
|
+
} else if ([...given.script].length > MAX_SUPPRESS_SCRIPT) {
|
|
1464
|
+
r.problems.push({
|
|
1465
|
+
text: `suppress.script is ${[...given.script].length.toLocaleString('en-US')} characters, over the ${MAX_SUPPRESS_SCRIPT.toLocaleString('en-US')} allowed`,
|
|
1466
|
+
read: without,
|
|
1467
|
+
});
|
|
1468
|
+
} else {
|
|
1469
|
+
// Parsed only, never run here: the script runs on its worker.
|
|
1470
|
+
try {
|
|
1471
|
+
new Function(`"use strict";\nreturn (${given.script}\n);`);
|
|
1472
|
+
config.script = given.script;
|
|
1473
|
+
} catch (e) {
|
|
1474
|
+
r.problems.push({ text: `suppress.script doesn't parse (${quotedValue(errorText(e))})`, read: without });
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
if (behavior !== 'defer' && behavior !== 'skip') {
|
|
1479
|
+
r.problems.push({
|
|
1480
|
+
text: `suppress is given, and a rule that suppresses needs "behavior": "defer" written out, so a release that `
|
|
1481
|
+
+ `doesn't know "suppress" defers its events instead of waking you on them; it has `
|
|
1482
|
+
+ (behavior === undefined ? 'no behavior' : `behavior ${shownValue(behavior)}`),
|
|
1483
|
+
read: without,
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
// A match field the gate doesn't read would, left aside, hide more than was
|
|
1487
|
+
// written: the condition it was meant to add is gone.
|
|
1488
|
+
if (isPlainObject(rawMatch)) {
|
|
1489
|
+
const rawFilter = isPlainObject(rawMatch.filter) ? Object.keys(rawMatch.filter).filter((key) => !FILTER_FIELDS.includes(key)).map((key) => `filter.${key}`) : [];
|
|
1490
|
+
const unreadMatch = [...Object.keys(rawMatch).filter((key) => !MATCH_FIELDS.includes(key)), ...rawFilter];
|
|
1491
|
+
if (unreadMatch.length > 0) {
|
|
1492
|
+
r.problems.push({ text: `suppress is given, and match has ${quotedList(unreadMatch)}, which the gate doesn't read, so the rule would hide more than was written`, read: without });
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
if (!suppressMatchNarrows(match)) {
|
|
1496
|
+
r.problems.push({
|
|
1497
|
+
text: 'suppress is given, and the rule\'s match names nothing it hides: a suppress rule needs a match that names '
|
|
1498
|
+
+ 'a scope, a source, a channel, tags, a mount or path, metadata or a filter (not a pattern of wildcards alone, or tagsNone alone), '
|
|
1499
|
+
+ 'so it can\'t hide everything',
|
|
1500
|
+
read: without,
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
// Only a rule read without any problem suppresses: one read past a problem
|
|
1504
|
+
// (a slip of a field, a match read without a part of it) matches more, or
|
|
1505
|
+
// decides otherwise, than its author wrote, and hiding is what can't be
|
|
1506
|
+
// seen to go wrong. Its behavior decides its events instead.
|
|
1507
|
+
if (before > 0) {
|
|
1508
|
+
r.problems.push({ text: 'suppress is given on a rule with problems, and a rule suppresses only when it has none', read: without });
|
|
1509
|
+
}
|
|
1510
|
+
return r.problems.length === 0 ? config : undefined;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1384
1513
|
/** What a load or a start does with a rule. */
|
|
1385
1514
|
type RuleOutcome = 'as-written' | 'read' | 'left-out';
|
|
1386
1515
|
|
|
@@ -1962,7 +2091,9 @@ export function findShadowedPolicies(policies: GatePolicy[], eventTypesFrom?: Ev
|
|
|
1962
2091
|
const out: ShadowWarning[] = [];
|
|
1963
2092
|
for (let i = 0; i < policies.length; i++) {
|
|
1964
2093
|
const earlier = policies[i];
|
|
1965
|
-
|
|
2094
|
+
// A suppress rule lets addressed events and the heartbeat's by, so it
|
|
2095
|
+
// can't be said to take every event of a later rule.
|
|
2096
|
+
if (earlier.passthrough || earlier.suppress) continue;
|
|
1966
2097
|
for (let j = i + 1; j < policies.length; j++) {
|
|
1967
2098
|
const found = subsumedEventTypes(earlier, policies[j], eventTypesFrom);
|
|
1968
2099
|
if (found) {
|
|
@@ -2004,6 +2135,33 @@ function errorText(err: unknown): string {
|
|
|
2004
2135
|
}
|
|
2005
2136
|
}
|
|
2006
2137
|
|
|
2138
|
+
class PartialDelivery extends Error {
|
|
2139
|
+
constructor(readonly woke: readonly string[], readonly notWoken: readonly string[], cause: unknown) {
|
|
2140
|
+
super(errorText(cause), { cause });
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
/**
|
|
2145
|
+
* Runs a batch's delivery to `woken`, which pushes each agent it wakes to
|
|
2146
|
+
* `woke`: a throw once any is woken is a PartialDelivery naming them and the
|
|
2147
|
+
* rest; one before any wake is thrown as it is.
|
|
2148
|
+
*/
|
|
2149
|
+
function deliveredTracking(woke: string[], woken: readonly string[], deliver: () => void): void {
|
|
2150
|
+
try {
|
|
2151
|
+
deliver();
|
|
2152
|
+
} catch (err) {
|
|
2153
|
+
if (woke.length === 0) throw err;
|
|
2154
|
+
throw new PartialDelivery(woke, woken.filter((name) => !woke.includes(name)), err);
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
function deliveredInPart(err: unknown): string | undefined {
|
|
2159
|
+
if (!(err instanceof PartialDelivery)) return undefined;
|
|
2160
|
+
const one = err.notWoken.length === 1;
|
|
2161
|
+
return `woke ${joinedList(err.woke)}, and delivering it threw (${err.message}), `
|
|
2162
|
+
+ `so it's dropped for ${joinedList(err.notWoken)}, who ${one ? "isn't" : "aren't"} woken`;
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2007
2165
|
/**
|
|
2008
2166
|
* Run a caller's callback once the change it's told of is made and traced. A
|
|
2009
2167
|
* throw from it is logged, not thrown, so the call answers as the change it
|
|
@@ -2123,6 +2281,8 @@ export class EventGate {
|
|
|
2123
2281
|
|
|
2124
2282
|
// Per-policy stats
|
|
2125
2283
|
private stats = new Map<string, PolicyStats>();
|
|
2284
|
+
/** Suppress rules' scripts and their state (suppress-scripts.ts), kept beside gate.json. */
|
|
2285
|
+
private readonly suppressScripts: SuppressScripts;
|
|
2126
2286
|
|
|
2127
2287
|
// The shadow lint's last findings (see refreshShadowWarnings), recomputed
|
|
2128
2288
|
// on every config change and fingerprinted so an unchanged set doesn't
|
|
@@ -2172,7 +2332,7 @@ export class EventGate {
|
|
|
2172
2332
|
private lastPrivilegedCheck = 0;
|
|
2173
2333
|
|
|
2174
2334
|
// Dependency-injected callbacks
|
|
2175
|
-
private emitTrace: (event:
|
|
2335
|
+
private emitTrace: (event: TraceEventInput) => void;
|
|
2176
2336
|
private addMessageFn: (
|
|
2177
2337
|
participant: string,
|
|
2178
2338
|
content: Array<{ type: 'text'; text: string }>,
|
|
@@ -2181,6 +2341,10 @@ export class EventGate {
|
|
|
2181
2341
|
forAgent?: string,
|
|
2182
2342
|
/** Out-param: where the message landed, for the wake that goes with it. */
|
|
2183
2343
|
placement?: WritePlacement,
|
|
2344
|
+
/** The agents the row is for; absent: the host's default for the write.
|
|
2345
|
+
* The host stamps those of them that read where it stores the row, and
|
|
2346
|
+
* its default when none does. */
|
|
2347
|
+
audience?: readonly string[],
|
|
2184
2348
|
) => unknown;
|
|
2185
2349
|
private requestInferenceFn: (agentName: string, reason: string, source: string, provenance?: WakeProvenance, after?: WritePlacement) => void;
|
|
2186
2350
|
private getAgentNamesFn: () => string[];
|
|
@@ -2188,7 +2352,7 @@ export class EventGate {
|
|
|
2188
2352
|
private isHeldFn: (serverId: string | undefined, channelId: string | undefined) => boolean;
|
|
2189
2353
|
private resolveRouteChannelFn: (info: GateEventInfo) => string | undefined;
|
|
2190
2354
|
private channelShownFn?: (channel: { serverId: string; channelId: string }) => string;
|
|
2191
|
-
private isNewestEntryFn?: (entry: GateEntryRef) => boolean;
|
|
2355
|
+
private isNewestEntryFn?: (entry: GateEntryRef, recipients?: readonly string[]) => boolean;
|
|
2192
2356
|
/** Clock injection — keeps the new rate_limit / passive_sample paths
|
|
2193
2357
|
* testable without monkey-patching Date.now globally. */
|
|
2194
2358
|
private now: () => number;
|
|
@@ -2197,13 +2361,14 @@ export class EventGate {
|
|
|
2197
2361
|
configPath: string;
|
|
2198
2362
|
initialConfig?: GateConfig;
|
|
2199
2363
|
privilegedUsersPath?: string;
|
|
2200
|
-
emitTrace: (event:
|
|
2364
|
+
emitTrace: (event: TraceEventInput) => void;
|
|
2201
2365
|
addMessage: (
|
|
2202
2366
|
participant: string,
|
|
2203
2367
|
content: Array<{ type: 'text'; text: string }>,
|
|
2204
2368
|
metadata?: Record<string, unknown>,
|
|
2205
2369
|
forAgent?: string,
|
|
2206
2370
|
placement?: WritePlacement,
|
|
2371
|
+
audience?: readonly string[],
|
|
2207
2372
|
) => unknown;
|
|
2208
2373
|
/** `after`: the placement of the message the wake is for, when it has
|
|
2209
2374
|
* one, so the host can drop the wake once a request that carried the
|
|
@@ -2221,6 +2386,8 @@ export class EventGate {
|
|
|
2221
2386
|
now?: () => number;
|
|
2222
2387
|
/** Per-event timeout (ms) for the optional gate.js script. Default 50. */
|
|
2223
2388
|
scriptTimeoutMs?: number;
|
|
2389
|
+
/** How long a suppress rule's script may take for one event before the event is shown (ms). Default 50. */
|
|
2390
|
+
suppressScriptTimeoutMs?: number;
|
|
2224
2391
|
/** The reply judge `judge` rules ask (JudgeConfig). Absent: a judge
|
|
2225
2392
|
* rule's batch is delivered as a debounce's, and gate_status says so. */
|
|
2226
2393
|
judge?: JudgeFn;
|
|
@@ -2251,8 +2418,10 @@ export class EventGate {
|
|
|
2251
2418
|
* waiting to be written there. A batched wake for that one message
|
|
2252
2419
|
* doesn't write its line when it fires: it hands the line to the wake
|
|
2253
2420
|
* (WakeProvenance.line), for a fresh start of the wake's turn to decide
|
|
2254
|
-
* (WakeLine).
|
|
2255
|
-
|
|
2421
|
+
* (WakeLine). With `recipients`, the windows are those agents', each of
|
|
2422
|
+
* which the event must be newest in. Absent: every batched wake writes
|
|
2423
|
+
* its line, as before. */
|
|
2424
|
+
isNewestEntry?: (entry: GateEntryRef, recipients?: readonly string[]) => boolean;
|
|
2256
2425
|
/** The event types the gate can see from a source, when the host knows
|
|
2257
2426
|
* them (an MCPL server's id: MCPL_GATE_EVENT_TYPES), else undefined.
|
|
2258
2427
|
* The rule-order notes (findShadowedPolicies) leave out types a rule's
|
|
@@ -2260,6 +2429,11 @@ export class EventGate {
|
|
|
2260
2429
|
eventTypesFrom?: EventTypesFrom;
|
|
2261
2430
|
}) {
|
|
2262
2431
|
this.configPath = opts.configPath;
|
|
2432
|
+
// Before the rules are read: a start's merge can clear a rule's state, its script's included.
|
|
2433
|
+
this.suppressScripts = new SuppressScripts({
|
|
2434
|
+
statePath: join(dirname(opts.configPath), 'suppress-state.json'),
|
|
2435
|
+
...(opts.suppressScriptTimeoutMs !== undefined ? { timeoutMs: opts.suppressScriptTimeoutMs } : {}),
|
|
2436
|
+
});
|
|
2263
2437
|
this.privilegedUsersPath = opts.privilegedUsersPath;
|
|
2264
2438
|
this.emitTrace = opts.emitTrace;
|
|
2265
2439
|
this.addMessageFn = opts.addMessage;
|
|
@@ -2541,7 +2715,6 @@ export class EventGate {
|
|
|
2541
2715
|
...changes,
|
|
2542
2716
|
// Every name the start added to gate.json, as an append's trace gave.
|
|
2543
2717
|
addedPolicies: names,
|
|
2544
|
-
timestamp: Date.now(),
|
|
2545
2718
|
});
|
|
2546
2719
|
}
|
|
2547
2720
|
if (!appended && (!last.recipe || canonical({ rules: last.recipe.rules, default: last.recipe.default }) !== canonical({ rules: starting.rules.map((rule) => rule.raw), default: given.default }))) {
|
|
@@ -2652,7 +2825,6 @@ export class EventGate {
|
|
|
2652
2825
|
type: 'gate:config-error',
|
|
2653
2826
|
error: why,
|
|
2654
2827
|
configPath: this.configPath,
|
|
2655
|
-
timestamp: Date.now(),
|
|
2656
2828
|
});
|
|
2657
2829
|
}
|
|
2658
2830
|
this.configError = problem;
|
|
@@ -2798,7 +2970,11 @@ export class EventGate {
|
|
|
2798
2970
|
private clearStateFor(was: GateConfig, next: GateConfig): { removed: string[]; cleared: string[] } {
|
|
2799
2971
|
const behaviorsByName = (config: GateConfig): Map<string, string> => {
|
|
2800
2972
|
const byName = new Map<string, string[]>();
|
|
2801
|
-
for (const p of config.policies)
|
|
2973
|
+
for (const p of config.policies) {
|
|
2974
|
+
// A suppress rule's script is part of what it is: a changed script starts from a fresh state.
|
|
2975
|
+
const fingerprint = behaviorFingerprint(p.behavior) + (p.suppress ? ` suppress:${p.suppress.script ?? ''}` : '');
|
|
2976
|
+
byName.set(p.name, [...(byName.get(p.name) ?? []), fingerprint]);
|
|
2977
|
+
}
|
|
2802
2978
|
return new Map([...byName].map(([name, behaviors]) => [name, JSON.stringify(behaviors)]));
|
|
2803
2979
|
};
|
|
2804
2980
|
const before = behaviorsByName(was);
|
|
@@ -2851,7 +3027,6 @@ export class EventGate {
|
|
|
2851
3027
|
removedPolicies: removedPolicies.length > 0 ? removedPolicies : undefined,
|
|
2852
3028
|
stateClearedPolicies:
|
|
2853
3029
|
stateClearedPolicies.length > 0 ? stateClearedPolicies : undefined,
|
|
2854
|
-
timestamp: now,
|
|
2855
3030
|
});
|
|
2856
3031
|
} catch {
|
|
2857
3032
|
// The rules in force stay, and the next check tries again.
|
|
@@ -2977,7 +3152,6 @@ export class EventGate {
|
|
|
2977
3152
|
configPath: this.configPath,
|
|
2978
3153
|
policyName: policy.name,
|
|
2979
3154
|
replaced: idx >= 0,
|
|
2980
|
-
timestamp: Date.now(),
|
|
2981
3155
|
});
|
|
2982
3156
|
if (ignored.length > 0) {
|
|
2983
3157
|
// A caller that doesn't take the notes still gets them said, in the log.
|
|
@@ -3020,7 +3194,6 @@ export class EventGate {
|
|
|
3020
3194
|
type: 'gate:policy-removed',
|
|
3021
3195
|
configPath: this.configPath,
|
|
3022
3196
|
policyName: name,
|
|
3023
|
-
timestamp: Date.now(),
|
|
3024
3197
|
});
|
|
3025
3198
|
return true;
|
|
3026
3199
|
}
|
|
@@ -3037,6 +3210,17 @@ export class EventGate {
|
|
|
3037
3210
|
return current.policies.map((p) => p.name);
|
|
3038
3211
|
}
|
|
3039
3212
|
|
|
3213
|
+
/** The rules in force, in order (copies): what the last decision was made by. */
|
|
3214
|
+
policiesInForce(): GatePolicy[] {
|
|
3215
|
+
return this.config.policies.map((p) => structuredClone(p));
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
/** The rule in force by this name (a copy), or undefined. */
|
|
3219
|
+
policyInForce(name: string): GatePolicy | undefined {
|
|
3220
|
+
const p = this.config.policies.find((x) => x.name === name);
|
|
3221
|
+
return p ? structuredClone(p) : undefined;
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3040
3224
|
/** @internal For `wake_add_rule`'s and `wake_remove_rule`'s refusals; not a host API. */
|
|
3041
3225
|
ruleNamesForChange(): { names: string[]; note: string } | { refused: string } {
|
|
3042
3226
|
const edit = this.rulesForChange();
|
|
@@ -3223,7 +3407,6 @@ export class EventGate {
|
|
|
3223
3407
|
matchedPolicy: 'asleep',
|
|
3224
3408
|
trigger: false,
|
|
3225
3409
|
behavior: 'skip',
|
|
3226
|
-
timestamp: this.now(),
|
|
3227
3410
|
});
|
|
3228
3411
|
return { trigger: false, policyName: 'asleep', behavior: 'skip' };
|
|
3229
3412
|
}
|
|
@@ -3246,6 +3429,11 @@ export class EventGate {
|
|
|
3246
3429
|
decision = this.computeDecision({ name: 'gate.js', match: {}, behavior: scripted }, info);
|
|
3247
3430
|
} else {
|
|
3248
3431
|
for (const compiled of this.compiledPolicies) {
|
|
3432
|
+
// A suppress rule doesn't decide a wake for an event suppression was
|
|
3433
|
+
// asked about: one it takes was suppressed before the gate was asked,
|
|
3434
|
+
// and one that reaches here is one it let by. Where suppression wasn't
|
|
3435
|
+
// asked (no view to hide it from), it defers, as its behavior says.
|
|
3436
|
+
if (compiled.policy.suppress && this.passesOverSuppress(info)) continue;
|
|
3249
3437
|
if (!this.compiledMatches(compiled, info)) continue;
|
|
3250
3438
|
const candidate = this.computeDecision(compiled.policy, info);
|
|
3251
3439
|
if (candidate.trigger || !compiled.policy.passthrough) {
|
|
@@ -3281,7 +3469,6 @@ export class EventGate {
|
|
|
3281
3469
|
trigger: decision.trigger,
|
|
3282
3470
|
behavior: formatBehavior(decision.behavior),
|
|
3283
3471
|
...(observed.length > 0 ? { observed } : {}),
|
|
3284
|
-
timestamp: this.now(),
|
|
3285
3472
|
});
|
|
3286
3473
|
|
|
3287
3474
|
return decision;
|
|
@@ -3306,7 +3493,6 @@ export class EventGate {
|
|
|
3306
3493
|
behavior: formatBehavior(policy.behavior),
|
|
3307
3494
|
eventType: info.eventType,
|
|
3308
3495
|
source: info.serverId || undefined,
|
|
3309
|
-
timestamp: this.now(),
|
|
3310
3496
|
});
|
|
3311
3497
|
|
|
3312
3498
|
if (policy.behavior === 'defer' || policy.behavior === 'skip') {
|
|
@@ -3452,6 +3638,7 @@ export class EventGate {
|
|
|
3452
3638
|
private clearPolicyState(name: string): void {
|
|
3453
3639
|
const debounce = this.debounceTimers.get(name);
|
|
3454
3640
|
if (debounce) clearTimeout(debounce.timer);
|
|
3641
|
+
this.suppressScripts.forget(name);
|
|
3455
3642
|
this.rateLimitBuckets.delete(name);
|
|
3456
3643
|
this.passiveSampleCounters.delete(name);
|
|
3457
3644
|
this.rateLimitDenied.delete(name);
|
|
@@ -3507,6 +3694,7 @@ export class EventGate {
|
|
|
3507
3694
|
*/
|
|
3508
3695
|
probe(info: GateEventInfo): GateDecision {
|
|
3509
3696
|
for (const compiled of this.compiledPolicies) {
|
|
3697
|
+
if (compiled.policy.suppress && this.passesOverSuppress(info)) continue; // as evaluate does
|
|
3510
3698
|
if (!this.compiledMatches(compiled, info)) continue;
|
|
3511
3699
|
const trigger = this.wouldTrigger(compiled.policy, info);
|
|
3512
3700
|
if (trigger || !compiled.policy.passthrough) {
|
|
@@ -3517,6 +3705,135 @@ export class EventGate {
|
|
|
3517
3705
|
return { trigger, policyName: null, behavior: this.config.default ?? 'always' };
|
|
3518
3706
|
}
|
|
3519
3707
|
|
|
3708
|
+
/**
|
|
3709
|
+
* The suppress rule that takes this event, or null when none does.
|
|
3710
|
+
* Side-effect-free, like probe: no counter moves, no batch forms, nothing
|
|
3711
|
+
* is traced, and a rule's script runs on a copy of its state, which is
|
|
3712
|
+
* then dropped. (A closure a script builds for itself is its own, and a
|
|
3713
|
+
* dry run's timeout counts toward setting a script aside, as it costs the same.) The probe table and other dry runs ask this; the
|
|
3714
|
+
* framework asks decideSuppression instead, once per event.
|
|
3715
|
+
*
|
|
3716
|
+
* It reads the rules as evaluate does, first match deciding: a rule ahead
|
|
3717
|
+
* of the suppress rule that matches the event decides it instead (it isn't
|
|
3718
|
+
* suppressed), unless it's a passthrough rule that wouldn't fire. A rule
|
|
3719
|
+
* with a script (`suppress.script`) takes an event its match takes only
|
|
3720
|
+
* when the script says to hide it; otherwise the event goes on to the rules
|
|
3721
|
+
* after it, as one the rule doesn't match does. Sleep and gate.js aren't
|
|
3722
|
+
* consulted: they decide wakes, and suppression decides what's rendered.
|
|
3723
|
+
*
|
|
3724
|
+
* Never suppressed, whatever the rules say: an event addressed to the
|
|
3725
|
+
* agent (tagged `chat:addressed`: a mention, a reply, a DM) and the
|
|
3726
|
+
* heartbeat's (the agent's own clock). A suppress rule lets them by to the
|
|
3727
|
+
* rules after it, and its script isn't asked. The framework itself never
|
|
3728
|
+
* asks about console or API input.
|
|
3729
|
+
*/
|
|
3730
|
+
suppressionFor(info: GateEventInfo): string | null {
|
|
3731
|
+
return this.walkSuppression(info, false);
|
|
3732
|
+
}
|
|
3733
|
+
|
|
3734
|
+
/**
|
|
3735
|
+
* suppressionFor, for an event that is arriving now: each script asked
|
|
3736
|
+
* keeps the state it leaves, whatever later rules do with the event, and a
|
|
3737
|
+
* suppressed event counts in the passthrough rules ahead of its rule. Ask
|
|
3738
|
+
* once per event.
|
|
3739
|
+
*/
|
|
3740
|
+
decideSuppression(info: GateEventInfo): string | null {
|
|
3741
|
+
return this.walkSuppression(info, true);
|
|
3742
|
+
}
|
|
3743
|
+
|
|
3744
|
+
private walkSuppression(info: GateEventInfo, commit: boolean): string | null {
|
|
3745
|
+
this.reloadIfChanged();
|
|
3746
|
+
this.syncSuppressScripts();
|
|
3747
|
+
if (info.serverId === 'heartbeat') return null;
|
|
3748
|
+
const addressed = Array.isArray(info.tags) && info.tags.includes('chat:addressed');
|
|
3749
|
+
let event: SuppressScriptEvent | undefined;
|
|
3750
|
+
const observers: GatePolicy[] = [];
|
|
3751
|
+
for (const compiled of this.compiledPolicies) {
|
|
3752
|
+
if (!this.compiledMatches(compiled, info)) continue;
|
|
3753
|
+
const suppress = compiled.policy.suppress;
|
|
3754
|
+
if (suppress) {
|
|
3755
|
+
if (addressed) continue;
|
|
3756
|
+
if (suppress.script !== undefined) {
|
|
3757
|
+
event ??= this.suppressScriptEvent(info);
|
|
3758
|
+
const ran = this.suppressScripts.run(compiled.policy.name, event, commit);
|
|
3759
|
+
// A script that failed shows the event: it goes on to the rules after.
|
|
3760
|
+
if (!ran || !('hide' in ran) || !ran.hide) continue;
|
|
3761
|
+
}
|
|
3762
|
+
// A suppressed event never reaches evaluate, so the passthrough rules
|
|
3763
|
+
// ahead of this one count it here, as evaluate would have: none of
|
|
3764
|
+
// them fires for it, and each fires on its schedule. A dry run counts
|
|
3765
|
+
// nothing.
|
|
3766
|
+
if (commit) for (const observer of observers) this.computeDecision(observer, info);
|
|
3767
|
+
return compiled.policy.name;
|
|
3768
|
+
}
|
|
3769
|
+
if (compiled.policy.passthrough && !this.wouldTrigger(compiled.policy, info)) { observers.push(compiled.policy); continue; }
|
|
3770
|
+
return null;
|
|
3771
|
+
}
|
|
3772
|
+
return null;
|
|
3773
|
+
}
|
|
3774
|
+
|
|
3775
|
+
/** Whether evaluate and probe pass over a suppress rule for this event: suppression was asked about it, or it's one suppression always lets by. */
|
|
3776
|
+
private passesOverSuppress(info: GateEventInfo): boolean {
|
|
3777
|
+
return info.suppressionAsked === true || info.serverId === 'heartbeat'
|
|
3778
|
+
|| (Array.isArray(info.tags) && info.tags.includes('chat:addressed'));
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3781
|
+
/** The scripts in step with the rules in force (SuppressScripts.sync: only changed ones are compiled). */
|
|
3782
|
+
private syncSuppressScripts(): void {
|
|
3783
|
+
this.suppressScripts.sync(this.config.policies.map((p) => ({ name: p.name, ...(p.suppress?.script !== undefined ? { script: p.suppress.script } : {}) })));
|
|
3784
|
+
}
|
|
3785
|
+
|
|
3786
|
+
/** What a suppress script is given for an event: plain JSON, its metadata included where it can be. */
|
|
3787
|
+
private suppressScriptEvent(info: GateEventInfo): SuppressScriptEvent {
|
|
3788
|
+
let metadata: Record<string, unknown> = {};
|
|
3789
|
+
try { metadata = JSON.parse(JSON.stringify(info.metadata ?? {})) as Record<string, unknown>; } catch { metadata = {}; }
|
|
3790
|
+
const channel = this.routeChannelFor(info) ?? (info.channelId || undefined);
|
|
3791
|
+
const author = this.extractAuthorId(info.metadata);
|
|
3792
|
+
return {
|
|
3793
|
+
eventType: info.eventType,
|
|
3794
|
+
source: info.serverId,
|
|
3795
|
+
...(channel ? { channel } : {}),
|
|
3796
|
+
...(author ? { author } : {}),
|
|
3797
|
+
tags: [...(info.tags ?? [])],
|
|
3798
|
+
text: info.content,
|
|
3799
|
+
at: this.now(),
|
|
3800
|
+
metadata,
|
|
3801
|
+
};
|
|
3802
|
+
}
|
|
3803
|
+
|
|
3804
|
+
/**
|
|
3805
|
+
* Whether a suppress script compiles to a function, on the scripts' worker:
|
|
3806
|
+
* an error's text, or null. wake_add_rule asks before it saves a rule.
|
|
3807
|
+
*/
|
|
3808
|
+
checkSuppressScript(script: string): string | null {
|
|
3809
|
+
return this.suppressScripts.check(script);
|
|
3810
|
+
}
|
|
3811
|
+
|
|
3812
|
+
/** A suppress rule's script state as it stands (a copy), for a host or a test. */
|
|
3813
|
+
suppressScriptState(name: string): Record<string, unknown> | undefined {
|
|
3814
|
+
this.syncSuppressScripts();
|
|
3815
|
+
return this.suppressScripts.stateOf(name);
|
|
3816
|
+
}
|
|
3817
|
+
|
|
3818
|
+
/**
|
|
3819
|
+
* Count a row the framework suppressed by `name` (suppressionFor's
|
|
3820
|
+
* answer) once it's written. The rule's stats count it as a match, so
|
|
3821
|
+
* gate_status shows how many events each suppress rule took since start.
|
|
3822
|
+
*/
|
|
3823
|
+
noteSuppressed(name: string, info: Pick<GateEventInfo, 'eventType' | 'serverId' | 'channelId'>): void {
|
|
3824
|
+
const policyStats = this.stats.get(name) ?? { matchCount: 0, lastMatchTimestamp: null };
|
|
3825
|
+
policyStats.matchCount++;
|
|
3826
|
+
policyStats.lastMatchTimestamp = this.now();
|
|
3827
|
+
this.stats.set(name, policyStats);
|
|
3828
|
+
this.emitTrace({
|
|
3829
|
+
type: 'gate:suppressed',
|
|
3830
|
+
policyName: name,
|
|
3831
|
+
eventType: info.eventType,
|
|
3832
|
+
...(info.serverId ? { serverId: info.serverId } : {}),
|
|
3833
|
+
...(info.channelId ? { channelId: info.channelId } : {}),
|
|
3834
|
+
});
|
|
3835
|
+
}
|
|
3836
|
+
|
|
3520
3837
|
/** The canonical synthetic events the probe table runs: chat shapes as MCPL delivers them, and a heartbeat. */
|
|
3521
3838
|
private probeEvents(): Array<{ label: string; info: GateEventInfo }> {
|
|
3522
3839
|
const chat = (
|
|
@@ -3592,7 +3909,12 @@ export class EventGate {
|
|
|
3592
3909
|
*/
|
|
3593
3910
|
probeTable(): GateProbeRow[] {
|
|
3594
3911
|
return this.probeEvents().map(({ label, info }) => {
|
|
3595
|
-
const
|
|
3912
|
+
const suppressedBy = this.suppressionFor(info);
|
|
3913
|
+
if (suppressedBy !== null) {
|
|
3914
|
+
return { probe: label, eventType: info.eventType, policy: suppressedBy, behavior: 'suppress', wouldWake: false };
|
|
3915
|
+
}
|
|
3916
|
+
// As admission asks: suppression first, so a script that says show lets the probe on.
|
|
3917
|
+
const d = this.probe({ ...info, suppressionAsked: true });
|
|
3596
3918
|
return {
|
|
3597
3919
|
probe: label,
|
|
3598
3920
|
eventType: info.eventType,
|
|
@@ -3642,7 +3964,6 @@ export class EventGate {
|
|
|
3642
3964
|
type: 'gate:shadow-warnings',
|
|
3643
3965
|
reason,
|
|
3644
3966
|
warnings: warnings.map(formatShadowWarning),
|
|
3645
|
-
timestamp: this.now(),
|
|
3646
3967
|
});
|
|
3647
3968
|
}
|
|
3648
3969
|
}
|
|
@@ -3705,6 +4026,7 @@ export class EventGate {
|
|
|
3705
4026
|
routeChannelId: this.routeChannelFor(info),
|
|
3706
4027
|
...(typeof info.metadata?.messageId === 'string' ? { messageId: info.metadata.messageId } : {}),
|
|
3707
4028
|
...(typeof info.metadata?.eventId === 'string' ? { eventId: info.metadata.eventId } : {}),
|
|
4029
|
+
...(info.recipients !== undefined ? { recipients: [...info.recipients] } : {}),
|
|
3708
4030
|
};
|
|
3709
4031
|
|
|
3710
4032
|
const existing = this.debounceTimers.get(policy.name);
|
|
@@ -3764,8 +4086,8 @@ export class EventGate {
|
|
|
3764
4086
|
} catch (err) {
|
|
3765
4087
|
const one = events.length === 1;
|
|
3766
4088
|
console.error(
|
|
3767
|
-
`[gate] Rule ${quotedValue(policyName)}: its pending batch of ${events.length} ${one ? 'event' : 'events'}
|
|
3768
|
-
+ `(${errorText(err)}), so the batch is dropped
|
|
4089
|
+
`[gate] Rule ${quotedValue(policyName)}: its pending batch of ${events.length} ${one ? 'event' : 'events'} `
|
|
4090
|
+
+ (deliveredInPart(err) ?? `couldn't be delivered (${errorText(err)}), so the batch is dropped`),
|
|
3769
4091
|
);
|
|
3770
4092
|
return;
|
|
3771
4093
|
}
|
|
@@ -3774,7 +4096,6 @@ export class EventGate {
|
|
|
3774
4096
|
type: 'gate:debounce-delivered',
|
|
3775
4097
|
policyName,
|
|
3776
4098
|
eventCount: events.length,
|
|
3777
|
-
timestamp: Date.now(),
|
|
3778
4099
|
});
|
|
3779
4100
|
}
|
|
3780
4101
|
|
|
@@ -3796,11 +4117,34 @@ export class EventGate {
|
|
|
3796
4117
|
type: 'gate:held-dropped',
|
|
3797
4118
|
dropped: batched.length - events.length,
|
|
3798
4119
|
channels: [...new Set(batched.filter((e) => !events.includes(e)).map(heldAt))],
|
|
3799
|
-
timestamp: this.now(),
|
|
3800
4120
|
});
|
|
3801
4121
|
}
|
|
3802
4122
|
if (events.length === 0) return;
|
|
4123
|
+
// Who each delivery woke, so that a throw after a wake is told as a
|
|
4124
|
+
// delivery in part (PartialDelivery), the batch dropped only for those
|
|
4125
|
+
// it didn't wake.
|
|
4126
|
+
const woke: string[] = [];
|
|
4127
|
+
if (events.every((e) => e.recipients === undefined)) {
|
|
4128
|
+
const names = this.getAgentNamesFn();
|
|
4129
|
+
deliveredTracking(woke, names, () => this.deliverBatch(events, names, undefined, woke));
|
|
4130
|
+
return;
|
|
4131
|
+
}
|
|
4132
|
+
const woken = new Set(events.flatMap((e) => e.recipients ?? []));
|
|
4133
|
+
if (events.some((e) => e.recipients === undefined)) for (const name of this.getAgentNamesFn()) woken.add(name);
|
|
4134
|
+
const groups = new Map<string, { agents: string[]; events: PendingEvent[] }>();
|
|
4135
|
+
for (const agentName of woken) {
|
|
4136
|
+
const seen = events.filter((e) => e.recipients === undefined || e.recipients.includes(agentName));
|
|
4137
|
+
const key = seen.map((e) => events.indexOf(e)).join(',');
|
|
4138
|
+
const group = groups.get(key) ?? { agents: [], events: seen };
|
|
4139
|
+
group.agents.push(agentName);
|
|
4140
|
+
groups.set(key, group);
|
|
4141
|
+
}
|
|
4142
|
+
deliveredTracking(woke, [...woken], () => {
|
|
4143
|
+
for (const group of groups.values()) this.deliverBatch(group.events, group.agents, group.agents, woke);
|
|
4144
|
+
});
|
|
4145
|
+
}
|
|
3803
4146
|
|
|
4147
|
+
private deliverBatch(events: PendingEvent[], woken: readonly string[], audience?: readonly string[], woke?: string[]): void {
|
|
3804
4148
|
const policyNames = [...new Set(events.map(e => e.policyName))];
|
|
3805
4149
|
|
|
3806
4150
|
// Events whose content the framework already stored at arrival (MCPL
|
|
@@ -3859,7 +4203,12 @@ export class EventGate {
|
|
|
3859
4203
|
const text = `[${NOTICE_TAG.gate}: ${events.length} event${events.length > 1 ? 's' : ''} matched]\n\n${lines.join('\n')}`;
|
|
3860
4204
|
const only = events.length === 1 && events[0].inContext ? events[0] : undefined;
|
|
3861
4205
|
let line: WakeLine | undefined;
|
|
3862
|
-
|
|
4206
|
+
// Where a line stored now landed, for the delivery agent's wake, as the
|
|
4207
|
+
// self-wake's notice places its own (readsRowFor): a live turn that
|
|
4208
|
+
// carried and answered the line drops the wake. Other agents' wakes stay
|
|
4209
|
+
// plain by readsRowFor's rule.
|
|
4210
|
+
const placement: WritePlacement = {};
|
|
4211
|
+
if (only && this.newestEntry(only, audience)) {
|
|
3863
4212
|
const shownChannel = only.routeChannelId ?? only.channelId;
|
|
3864
4213
|
line = {
|
|
3865
4214
|
entry: entryRef(only),
|
|
@@ -3872,7 +4221,7 @@ export class EventGate {
|
|
|
3872
4221
|
this.addMessageFn('user', [{ type: 'text', text }], {
|
|
3873
4222
|
source: 'gate:debounce',
|
|
3874
4223
|
policies: policyNames,
|
|
3875
|
-
});
|
|
4224
|
+
}, undefined, placement, audience);
|
|
3876
4225
|
}
|
|
3877
4226
|
|
|
3878
4227
|
// Provenance of the batched wake (who/where woke the agent): the newest
|
|
@@ -3883,19 +4232,20 @@ export class EventGate {
|
|
|
3883
4232
|
// back to whoever addressed the agent, not to whichever channel last saw
|
|
3884
4233
|
// any traffic. Ambient-only batches set no locus. A batch whose line the
|
|
3885
4234
|
// gate left to the turn's start carries it too (`line`), one object
|
|
3886
|
-
// shared by every agent
|
|
4235
|
+
// shared by every agent woken with these messages, so it is written once.
|
|
3887
4236
|
const chosen = wakeProvenance(events);
|
|
3888
4237
|
const provenance = line ? { ...chosen, line } : chosen;
|
|
3889
|
-
for (const agentName of
|
|
3890
|
-
this.requestInferenceFn(agentName, 'gate:debounce', 'gate', provenance);
|
|
4238
|
+
for (const agentName of woken) {
|
|
4239
|
+
this.requestInferenceFn(agentName, 'gate:debounce', 'gate', provenance, line ? undefined : placement);
|
|
4240
|
+
woke?.push(agentName);
|
|
3891
4241
|
}
|
|
3892
4242
|
}
|
|
3893
4243
|
|
|
3894
4244
|
/** isNewestEntry for one event; a callback fault keeps the line. */
|
|
3895
|
-
private newestEntry(event: PendingEvent): boolean {
|
|
4245
|
+
private newestEntry(event: PendingEvent, recipients?: readonly string[]): boolean {
|
|
3896
4246
|
if (!this.isNewestEntryFn) return false;
|
|
3897
4247
|
try {
|
|
3898
|
-
return this.isNewestEntryFn(entryRef(event)) === true;
|
|
4248
|
+
return this.isNewestEntryFn(entryRef(event), recipients) === true;
|
|
3899
4249
|
} catch {
|
|
3900
4250
|
return false;
|
|
3901
4251
|
}
|
|
@@ -4014,7 +4364,8 @@ export class EventGate {
|
|
|
4014
4364
|
* the judge to weigh what waiting costs: each agent's pending self-wake
|
|
4015
4365
|
* (skip_reply's wake_in_seconds), the end of a sleep, and the pending
|
|
4016
4366
|
* batches of rules other than `exceptPolicy` (a debounce's wakes the agent
|
|
4017
|
-
* when it fires; a judge rule's goes to the judge, so it only may).
|
|
4367
|
+
* when it fires; a judge rule's goes to the judge, so it only may). A batch,
|
|
4368
|
+
* and a sleep with no agent named, reach the gate's recipients alone.
|
|
4018
4369
|
*/
|
|
4019
4370
|
upcomingWakes(agentNames: string[], exceptPolicy?: string): UpcomingWake[] {
|
|
4020
4371
|
const wakes: UpcomingWake[] = [];
|
|
@@ -4022,19 +4373,21 @@ export class EventGate {
|
|
|
4022
4373
|
for (const name of agentNames) {
|
|
4023
4374
|
const due = this.selfWakeDue.get(name);
|
|
4024
4375
|
if (due && this.selfWakeTimers.has(name)) {
|
|
4025
|
-
wakes.push({ at: due.at, certain: true, what: `${agentNames.length > 1 ? `${name}'s` : 'its'} own ${due.source} timer` });
|
|
4376
|
+
wakes.push({ at: due.at, certain: true, kind: 'self-wake', what: `${agentNames.length > 1 ? `${name}'s` : 'its'} own ${due.source} timer` });
|
|
4026
4377
|
}
|
|
4027
4378
|
}
|
|
4028
|
-
|
|
4029
|
-
|
|
4379
|
+
const recipients = this.getAgentNamesFn();
|
|
4380
|
+
const recipient = agentNames.some((name) => recipients.includes(name));
|
|
4381
|
+
if (this.sleepUntil > now && (this.sleepAgent === undefined ? recipient : agentNames.includes(this.sleepAgent))) {
|
|
4382
|
+
wakes.push({ at: this.sleepUntil, certain: true, kind: 'sleep-end', what: 'the end of its sleep' });
|
|
4030
4383
|
}
|
|
4031
4384
|
for (const [name, state] of this.debounceTimers) {
|
|
4032
|
-
if (name === exceptPolicy || state.events.length === 0) continue;
|
|
4385
|
+
if (!recipient || name === exceptPolicy || state.events.length === 0) continue;
|
|
4033
4386
|
const n = state.events.length;
|
|
4034
4387
|
const batch = `${n} ${n === 1 ? 'event' : 'events'}`;
|
|
4035
4388
|
wakes.push(state.judge
|
|
4036
|
-
? { at: state.dueAt, certain: false, what: `rule ${quotedValue(name)}'s batch of ${batch} goes to the judge` }
|
|
4037
|
-
: { at: state.dueAt, certain: true, what: `rule ${quotedValue(name)}'s debounced batch of ${batch}` });
|
|
4389
|
+
? { at: state.dueAt, certain: false, kind: 'judge-batch', what: `rule ${quotedValue(name)}'s batch of ${batch} goes to the judge` }
|
|
4390
|
+
: { at: state.dueAt, certain: true, kind: 'debounce', what: `rule ${quotedValue(name)}'s debounced batch of ${batch}` });
|
|
4038
4391
|
}
|
|
4039
4392
|
return wakes.sort((a, b) => (a.at ?? 0) - (b.at ?? 0));
|
|
4040
4393
|
}
|
|
@@ -4131,7 +4484,6 @@ export class EventGate {
|
|
|
4131
4484
|
...(!failed && typeof verdict.inputTokens === 'number' ? { inputTokens: verdict.inputTokens } : {}),
|
|
4132
4485
|
...(!failed && typeof verdict.cacheReadTokens === 'number' ? { cacheReadTokens: verdict.cacheReadTokens } : {}),
|
|
4133
4486
|
...(!failed && typeof verdict.outputTokens === 'number' ? { outputTokens: verdict.outputTokens } : {}),
|
|
4134
|
-
timestamp: Date.now(),
|
|
4135
4487
|
});
|
|
4136
4488
|
if (this.disposed) return;
|
|
4137
4489
|
const judge = this.currentJudge(policyName, state.judge);
|
|
@@ -4156,8 +4508,8 @@ export class EventGate {
|
|
|
4156
4508
|
} catch (err) {
|
|
4157
4509
|
const one = events.length === 1;
|
|
4158
4510
|
console.error(
|
|
4159
|
-
`[gate] a judged batch of ${events.length} ${one ? 'event' : 'events'}
|
|
4160
|
-
+ `(${errorText(err)}), so the batch is dropped
|
|
4511
|
+
`[gate] a judged batch of ${events.length} ${one ? 'event' : 'events'} `
|
|
4512
|
+
+ (deliveredInPart(err) ?? `couldn't be delivered (${errorText(err)}), so the batch is dropped`),
|
|
4161
4513
|
);
|
|
4162
4514
|
}
|
|
4163
4515
|
}
|
|
@@ -4231,8 +4583,8 @@ export class EventGate {
|
|
|
4231
4583
|
* buffer through the same path a turn's end uses. Self-wake and sleep-expiry
|
|
4232
4584
|
* timers deliberately do NOT route through this — their inference requests
|
|
4233
4585
|
* are per-agent targeted pushes that the framework's quiesce wake gate parks
|
|
4234
|
-
* and releases at resume, whereas the buffer's deliverEvents
|
|
4235
|
-
*
|
|
4586
|
+
* and releases at resume, whereas the buffer's deliverEvents wakes every
|
|
4587
|
+
* agent a batch is for.
|
|
4236
4588
|
*/
|
|
4237
4589
|
setQuiesced(quiesced: boolean): void {
|
|
4238
4590
|
if (this.quiesced === quiesced) return;
|
|
@@ -4260,10 +4612,12 @@ export class EventGate {
|
|
|
4260
4612
|
this.deliverEvents(events);
|
|
4261
4613
|
} catch (err) {
|
|
4262
4614
|
const one = events.length === 1;
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4615
|
+
const held = `${events.length} ${one ? 'event' : 'events'}`;
|
|
4616
|
+
const partly = deliveredInPart(err);
|
|
4617
|
+
console.error(partly
|
|
4618
|
+
? `[gate] a batch of ${held}, held for an agent's turn or the host's quiesce, ${partly}`
|
|
4619
|
+
: `[gate] ${held} held for an agent's turn or the host's quiesce couldn't be delivered (${errorText(err)}), `
|
|
4620
|
+
+ `so ${one ? "it's" : "they're"} dropped`);
|
|
4267
4621
|
}
|
|
4268
4622
|
});
|
|
4269
4623
|
}
|
|
@@ -4347,7 +4701,6 @@ export class EventGate {
|
|
|
4347
4701
|
matchedPolicy: 'asleep',
|
|
4348
4702
|
trigger: false,
|
|
4349
4703
|
behavior: 'skip',
|
|
4350
|
-
timestamp: this.now(),
|
|
4351
4704
|
});
|
|
4352
4705
|
return { until: this.sleepUntil };
|
|
4353
4706
|
}
|
|
@@ -4405,13 +4758,13 @@ export class EventGate {
|
|
|
4405
4758
|
matchedPolicy: 'self-wake',
|
|
4406
4759
|
trigger: true,
|
|
4407
4760
|
behavior: 'always',
|
|
4408
|
-
timestamp: this.now(),
|
|
4409
4761
|
});
|
|
4410
4762
|
// Compact in-window notice: without it the wake turn contains nothing
|
|
4411
4763
|
// new, so the agent can't tell WHAT woke it (its own timer vs. a
|
|
4412
4764
|
// heartbeat) or WHEN. One line, timestamped to the second. Added
|
|
4413
|
-
// before the inference request so it rides the wake turn
|
|
4414
|
-
//
|
|
4765
|
+
// before the inference request so it rides the wake turn: it is stored
|
|
4766
|
+
// at once, and while a turn of the agent's is alive it is kept out of
|
|
4767
|
+
// the window until the agent's next presentation point.
|
|
4415
4768
|
const nowIso = new Date(this.now()).toISOString().slice(0, 19) + 'Z';
|
|
4416
4769
|
// Deliver the notice into the WAKING agent's window — a self-wake
|
|
4417
4770
|
// armed by a non-primary agent (e.g. a subconscious cadence) must not
|
|
@@ -4440,7 +4793,6 @@ export class EventGate {
|
|
|
4440
4793
|
matchedPolicy: 'self-wake',
|
|
4441
4794
|
trigger: false,
|
|
4442
4795
|
behavior: 'skip',
|
|
4443
|
-
timestamp: this.now(),
|
|
4444
4796
|
});
|
|
4445
4797
|
return { inMs: ms, armed: true };
|
|
4446
4798
|
}
|
|
@@ -4483,7 +4835,6 @@ export class EventGate {
|
|
|
4483
4835
|
matchedPolicy: 'asleep',
|
|
4484
4836
|
trigger: true,
|
|
4485
4837
|
behavior: 'always',
|
|
4486
|
-
timestamp: this.now(),
|
|
4487
4838
|
});
|
|
4488
4839
|
const detail = note ? `${reason}: ${note}` : reason;
|
|
4489
4840
|
const targets = agent ? [agent] : this.getAgentNamesFn();
|
|
@@ -4613,6 +4964,7 @@ export class EventGate {
|
|
|
4613
4964
|
}
|
|
4614
4965
|
|
|
4615
4966
|
getStatus(): GateStatus {
|
|
4967
|
+
this.syncSuppressScripts();
|
|
4616
4968
|
const policies: GatePolicyStats[] = this.config.policies.map(p => {
|
|
4617
4969
|
const stats = this.stats.get(p.name);
|
|
4618
4970
|
const debounce = this.debounceTimers.get(p.name);
|
|
@@ -4621,6 +4973,9 @@ export class EventGate {
|
|
|
4621
4973
|
behavior: p.behavior,
|
|
4622
4974
|
matchCount: stats?.matchCount ?? 0,
|
|
4623
4975
|
lastMatchTimestamp: stats?.lastMatchTimestamp ?? null,
|
|
4976
|
+
...(p.suppress
|
|
4977
|
+
? { suppress: { ...p.suppress, ...(p.suppress.script !== undefined && this.suppressScripts.status(p.name) ? { scriptStatus: this.suppressScripts.status(p.name)! } : {}) } }
|
|
4978
|
+
: {}),
|
|
4624
4979
|
};
|
|
4625
4980
|
if (typeof p.behavior === 'object' && 'debounce' in p.behavior) {
|
|
4626
4981
|
result.debounceState = {
|
|
@@ -4684,6 +5039,13 @@ export class EventGate {
|
|
|
4684
5039
|
// Fresh, not the cache: the sources the host knows (eventTypesFrom)
|
|
4685
5040
|
// change as servers connect, with no change to the rules.
|
|
4686
5041
|
shadowWarnings: this.lintShadows().map(formatShadowWarning),
|
|
5042
|
+
...((): Pick<GateStatus, 'catchAll'> => {
|
|
5043
|
+
const rule = firstCatchAll(this.config.policies, { lettingBy: true });
|
|
5044
|
+
return rule ? { catchAll: {
|
|
5045
|
+
rule,
|
|
5046
|
+
note: `${JSON.stringify(rule)} lets MCPL events by without waking you and without naming a server, a channel or a tag, so a server connected later is handled by it until a rule for that server goes ahead of it.`,
|
|
5047
|
+
} } : {};
|
|
5048
|
+
})(),
|
|
4687
5049
|
totalEvaluations: this.totalEvaluations,
|
|
4688
5050
|
defaultDecisions: {
|
|
4689
5051
|
triggered: this.defaultTriggered,
|
|
@@ -4720,6 +5082,7 @@ export class EventGate {
|
|
|
4720
5082
|
|
|
4721
5083
|
dispose(): void {
|
|
4722
5084
|
this.gateScript.dispose();
|
|
5085
|
+
this.suppressScripts.dispose();
|
|
4723
5086
|
for (const state of this.debounceTimers.values()) {
|
|
4724
5087
|
clearTimeout(state.timer);
|
|
4725
5088
|
}
|