@animalabs/agent-framework 0.32.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 +1976 -818
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +7566 -3422
- 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 +82 -20
- 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 +32 -12
- 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 +7804 -3546
- 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 +86 -20
- package/src/types/events.ts +2 -0
- package/src/types/framework.ts +32 -12
- package/src/types/module.ts +181 -68
- package/src/types/trace.ts +542 -40
|
@@ -15,6 +15,8 @@ import { cutToFit, quotedJson, quotedValue, suppliedValue } from '../supplied-te
|
|
|
15
15
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
16
16
|
import { basename, dirname, join } from 'node:path';
|
|
17
17
|
import { GateScript } from './gate-script.js';
|
|
18
|
+
import { SuppressScripts } from './suppress-scripts.js';
|
|
19
|
+
import { firstCatchAll, namesSomething } from './rule-scope.js';
|
|
18
20
|
import { canonical, duplicateName, lastRecipePath, noChanges, reconcileLine, reconcileWithRecipe } from './recipe-reconcile.js';
|
|
19
21
|
import { closestNames, isNearMiss } from '../closest-names.js';
|
|
20
22
|
import { MAX_TIMER_MS } from '../deadline-timer.js';
|
|
@@ -252,7 +254,7 @@ function behaviorFingerprint(b) {
|
|
|
252
254
|
// file for a null or a "" (a passthrough, a behavior's kind, gate.json's
|
|
253
255
|
// default), it now loads without it.
|
|
254
256
|
const CONFIG_FIELDS = ['default', 'policies'];
|
|
255
|
-
const POLICY_FIELDS = ['name', 'match', 'behavior', 'resets', 'passthrough'];
|
|
257
|
+
const POLICY_FIELDS = ['name', 'match', 'behavior', 'resets', 'passthrough', 'suppress'];
|
|
256
258
|
const MATCH_FIELDS = [
|
|
257
259
|
'scope', 'source', 'channel', 'mount', 'pathGlob', 'tagsAny', 'tagsAll', 'tagsNone', 'metadataTrue', 'filter',
|
|
258
260
|
];
|
|
@@ -261,6 +263,7 @@ const BEHAVIOR_KINDS = ['debounce', 'rate_limit', 'passive_sample', 'judge'];
|
|
|
261
263
|
const RATE_LIMIT_FIELDS = ['tokens', 'refillIntervalMs', 'keyBy'];
|
|
262
264
|
const PASSIVE_SAMPLE_FIELDS = ['every', 'keyBy'];
|
|
263
265
|
const JUDGE_FIELDS = ['debounceMs', 'maxWaitMs', 'instructions'];
|
|
266
|
+
const SUPPRESS_FIELDS = ['description', 'script'];
|
|
264
267
|
function isPlainObject(value) {
|
|
265
268
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
266
269
|
}
|
|
@@ -650,6 +653,7 @@ function readRule(raw, unnamed, file = 'gate.json', named = asGateNames) {
|
|
|
650
653
|
const match = readMatch(r, obj.match);
|
|
651
654
|
const resets = readResets(r, obj.resets);
|
|
652
655
|
const passthrough = readPassthrough(r, obj.passthrough, kind);
|
|
656
|
+
const suppress = readSuppress(r, obj.suppress, obj.behavior, match, obj.match);
|
|
653
657
|
const reading = {
|
|
654
658
|
where: r.where, problems: r.problems, notes: r.notes, quiet: kind === 'skip' || kind === 'defer' || r.noTokens === true,
|
|
655
659
|
};
|
|
@@ -659,6 +663,8 @@ function readRule(raw, unnamed, file = 'gate.json', named = asGateNames) {
|
|
|
659
663
|
policy.resets = resets;
|
|
660
664
|
if (passthrough)
|
|
661
665
|
policy.passthrough = true;
|
|
666
|
+
if (suppress)
|
|
667
|
+
policy.suppress = suppress;
|
|
662
668
|
reading.policy = policy;
|
|
663
669
|
if (r.matchesAll)
|
|
664
670
|
reading.matchesAll = true;
|
|
@@ -1071,8 +1077,7 @@ function readMatch(r, value) {
|
|
|
1071
1077
|
// Left with nothing that narrows it once something in it was named, it
|
|
1072
1078
|
// matches every event. Empty lists and nulls alone always meant no
|
|
1073
1079
|
// constraint, and name nothing.
|
|
1074
|
-
|
|
1075
|
-
if (!narrows && r.problems.length + r.notes.length > named)
|
|
1080
|
+
if (!matchNarrows(match) && r.problems.length + r.notes.length > named)
|
|
1076
1081
|
r.matchesAll = true;
|
|
1077
1082
|
return match;
|
|
1078
1083
|
}
|
|
@@ -1120,6 +1125,137 @@ function readPassthrough(r, value, kind) {
|
|
|
1120
1125
|
});
|
|
1121
1126
|
return false;
|
|
1122
1127
|
}
|
|
1128
|
+
/** Whether a match narrows what it matches: anything but an empty scope. */
|
|
1129
|
+
export function matchNarrows(match) {
|
|
1130
|
+
return Object.entries(match).some(([field, v]) => !(field === 'scope' && Array.isArray(v) && v.length === 0));
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Whether a suppress rule's match names what it hides: an event type, or a
|
|
1134
|
+
* source, channel, mount, path or tag pattern that isn't only wildcards
|
|
1135
|
+
* (namesSomething), a metadata flag, or a content filter. A pattern of
|
|
1136
|
+
* wildcards alone, such as `source: "*"`, or only `tagsNone`, takes every
|
|
1137
|
+
* event, which a suppress rule may not.
|
|
1138
|
+
*/
|
|
1139
|
+
function suppressMatchNarrows(match) {
|
|
1140
|
+
if ((match.scope ?? []).length > 0 || (match.metadataTrue ?? []).length > 0 || match.filter !== undefined)
|
|
1141
|
+
return true;
|
|
1142
|
+
if ([match.source, match.channel, match.mount, match.pathGlob].some(namesSomething))
|
|
1143
|
+
return true;
|
|
1144
|
+
return [...(match.tagsAny ?? []), ...(match.tagsAll ?? [])].some(namesSomething);
|
|
1145
|
+
}
|
|
1146
|
+
/** Longest suppress description, in characters: the review sends it with every rule it judges. */
|
|
1147
|
+
export const MAX_SUPPRESS_DESCRIPTION = 2_000;
|
|
1148
|
+
/** Longest suppress script, in characters. */
|
|
1149
|
+
export const MAX_SUPPRESS_SCRIPT = 20_000;
|
|
1150
|
+
const SUPPRESS_EXAMPLE = '{"description": "the eye going on and off"}';
|
|
1151
|
+
/**
|
|
1152
|
+
* A rule's `suppress`. Every problem with it is read as a rule that doesn't
|
|
1153
|
+
* suppress, which is how a release without suppression reads the rule:
|
|
1154
|
+
* its behavior then decides the event, and the event is rendered. The
|
|
1155
|
+
* behavior has to be written out as "defer" (or "skip"), not left to
|
|
1156
|
+
* default, because such a release reads a rule with no behavior as
|
|
1157
|
+
* "always" and would wake the agent on every event the rule was meant to
|
|
1158
|
+
* hide.
|
|
1159
|
+
*/
|
|
1160
|
+
function readSuppress(r, value, behavior, match, rawMatch) {
|
|
1161
|
+
if (value === undefined || value === false)
|
|
1162
|
+
return undefined;
|
|
1163
|
+
const without = "so the rule doesn't suppress, and its behavior decides its events as for any other rule";
|
|
1164
|
+
if (!isPlainObject(value)) {
|
|
1165
|
+
r.problems.push({ text: `suppress is ${shownValue(value)}, where an object such as ${SUPPRESS_EXAMPLE} is needed`, read: without });
|
|
1166
|
+
return undefined;
|
|
1167
|
+
}
|
|
1168
|
+
const before = r.problems.length;
|
|
1169
|
+
// A script given as null or "" is a script that's missing, not one left
|
|
1170
|
+
// out: read as left out, the rule would hide every event its match takes.
|
|
1171
|
+
const { script: _givenScript, ...rest } = value;
|
|
1172
|
+
const emptyScript = Object.hasOwn(value, 'script') && (value.script === null || value.script === '');
|
|
1173
|
+
if (emptyScript) {
|
|
1174
|
+
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 });
|
|
1175
|
+
}
|
|
1176
|
+
const given = withoutEmpty(`${r.where}: suppress`, emptyScript ? rest : value, r.notes);
|
|
1177
|
+
const reported = r.problems.length;
|
|
1178
|
+
readFieldNames(r, 'suppress', given, SUPPRESS_FIELDS);
|
|
1179
|
+
// A field it doesn't read can't be left aside here, as elsewhere in a
|
|
1180
|
+
// rule: one meant as a condition, left out, would hide every event the
|
|
1181
|
+
// match takes.
|
|
1182
|
+
const unread = Object.keys(given).filter((key) => !SUPPRESS_FIELDS.includes(key));
|
|
1183
|
+
if (unread.length > 0 && r.problems.length === reported) {
|
|
1184
|
+
r.problems.push({ text: `suppress has ${quotedList(unread)}, and a suppress rule reads only "description" and "script"`, read: without });
|
|
1185
|
+
}
|
|
1186
|
+
const config = { description: '' };
|
|
1187
|
+
if (typeof given.description !== 'string' || given.description.trim() === '') {
|
|
1188
|
+
r.problems.push({
|
|
1189
|
+
text: `suppress.description is ${given.description === undefined ? 'missing' : shownValue(given.description)}, where the rule's `
|
|
1190
|
+
+ 'purpose in a few words is needed: the review checks what the rule hides against it',
|
|
1191
|
+
read: without,
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
else if ([...given.description].length > MAX_SUPPRESS_DESCRIPTION) {
|
|
1195
|
+
r.problems.push({
|
|
1196
|
+
text: `suppress.description is ${[...given.description].length.toLocaleString('en-US')} characters, over the `
|
|
1197
|
+
+ `${MAX_SUPPRESS_DESCRIPTION.toLocaleString('en-US')} allowed`,
|
|
1198
|
+
read: without,
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
else {
|
|
1202
|
+
config.description = given.description.trim();
|
|
1203
|
+
}
|
|
1204
|
+
if (given.script !== undefined) {
|
|
1205
|
+
if (typeof given.script !== 'string' || given.script.trim() === '') {
|
|
1206
|
+
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 });
|
|
1207
|
+
}
|
|
1208
|
+
else if ([...given.script].length > MAX_SUPPRESS_SCRIPT) {
|
|
1209
|
+
r.problems.push({
|
|
1210
|
+
text: `suppress.script is ${[...given.script].length.toLocaleString('en-US')} characters, over the ${MAX_SUPPRESS_SCRIPT.toLocaleString('en-US')} allowed`,
|
|
1211
|
+
read: without,
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
else {
|
|
1215
|
+
// Parsed only, never run here: the script runs on its worker.
|
|
1216
|
+
try {
|
|
1217
|
+
new Function(`"use strict";\nreturn (${given.script}\n);`);
|
|
1218
|
+
config.script = given.script;
|
|
1219
|
+
}
|
|
1220
|
+
catch (e) {
|
|
1221
|
+
r.problems.push({ text: `suppress.script doesn't parse (${quotedValue(errorText(e))})`, read: without });
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
if (behavior !== 'defer' && behavior !== 'skip') {
|
|
1226
|
+
r.problems.push({
|
|
1227
|
+
text: `suppress is given, and a rule that suppresses needs "behavior": "defer" written out, so a release that `
|
|
1228
|
+
+ `doesn't know "suppress" defers its events instead of waking you on them; it has `
|
|
1229
|
+
+ (behavior === undefined ? 'no behavior' : `behavior ${shownValue(behavior)}`),
|
|
1230
|
+
read: without,
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
// A match field the gate doesn't read would, left aside, hide more than was
|
|
1234
|
+
// written: the condition it was meant to add is gone.
|
|
1235
|
+
if (isPlainObject(rawMatch)) {
|
|
1236
|
+
const rawFilter = isPlainObject(rawMatch.filter) ? Object.keys(rawMatch.filter).filter((key) => !FILTER_FIELDS.includes(key)).map((key) => `filter.${key}`) : [];
|
|
1237
|
+
const unreadMatch = [...Object.keys(rawMatch).filter((key) => !MATCH_FIELDS.includes(key)), ...rawFilter];
|
|
1238
|
+
if (unreadMatch.length > 0) {
|
|
1239
|
+
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 });
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
if (!suppressMatchNarrows(match)) {
|
|
1243
|
+
r.problems.push({
|
|
1244
|
+
text: 'suppress is given, and the rule\'s match names nothing it hides: a suppress rule needs a match that names '
|
|
1245
|
+
+ 'a scope, a source, a channel, tags, a mount or path, metadata or a filter (not a pattern of wildcards alone, or tagsNone alone), '
|
|
1246
|
+
+ 'so it can\'t hide everything',
|
|
1247
|
+
read: without,
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
// Only a rule read without any problem suppresses: one read past a problem
|
|
1251
|
+
// (a slip of a field, a match read without a part of it) matches more, or
|
|
1252
|
+
// decides otherwise, than its author wrote, and hiding is what can't be
|
|
1253
|
+
// seen to go wrong. Its behavior decides its events instead.
|
|
1254
|
+
if (before > 0) {
|
|
1255
|
+
r.problems.push({ text: 'suppress is given on a rule with problems, and a rule suppresses only when it has none', read: without });
|
|
1256
|
+
}
|
|
1257
|
+
return r.problems.length === 0 ? config : undefined;
|
|
1258
|
+
}
|
|
1123
1259
|
/**
|
|
1124
1260
|
* A rule with no problem is in force as written. One with a problem is read
|
|
1125
1261
|
* past it, the way earlier releases read it, so it wakes the agent wherever
|
|
@@ -1613,7 +1749,9 @@ export function findShadowedPolicies(policies, eventTypesFrom) {
|
|
|
1613
1749
|
const out = [];
|
|
1614
1750
|
for (let i = 0; i < policies.length; i++) {
|
|
1615
1751
|
const earlier = policies[i];
|
|
1616
|
-
|
|
1752
|
+
// A suppress rule lets addressed events and the heartbeat's by, so it
|
|
1753
|
+
// can't be said to take every event of a later rule.
|
|
1754
|
+
if (earlier.passthrough || earlier.suppress)
|
|
1617
1755
|
continue;
|
|
1618
1756
|
for (let j = i + 1; j < policies.length; j++) {
|
|
1619
1757
|
const found = subsumedEventTypes(earlier, policies[j], eventTypesFrom);
|
|
@@ -1654,6 +1792,37 @@ function errorText(err) {
|
|
|
1654
1792
|
return 'a value that could not be printed';
|
|
1655
1793
|
}
|
|
1656
1794
|
}
|
|
1795
|
+
class PartialDelivery extends Error {
|
|
1796
|
+
woke;
|
|
1797
|
+
notWoken;
|
|
1798
|
+
constructor(woke, notWoken, cause) {
|
|
1799
|
+
super(errorText(cause), { cause });
|
|
1800
|
+
this.woke = woke;
|
|
1801
|
+
this.notWoken = notWoken;
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
/**
|
|
1805
|
+
* Runs a batch's delivery to `woken`, which pushes each agent it wakes to
|
|
1806
|
+
* `woke`: a throw once any is woken is a PartialDelivery naming them and the
|
|
1807
|
+
* rest; one before any wake is thrown as it is.
|
|
1808
|
+
*/
|
|
1809
|
+
function deliveredTracking(woke, woken, deliver) {
|
|
1810
|
+
try {
|
|
1811
|
+
deliver();
|
|
1812
|
+
}
|
|
1813
|
+
catch (err) {
|
|
1814
|
+
if (woke.length === 0)
|
|
1815
|
+
throw err;
|
|
1816
|
+
throw new PartialDelivery(woke, woken.filter((name) => !woke.includes(name)), err);
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
function deliveredInPart(err) {
|
|
1820
|
+
if (!(err instanceof PartialDelivery))
|
|
1821
|
+
return undefined;
|
|
1822
|
+
const one = err.notWoken.length === 1;
|
|
1823
|
+
return `woke ${joinedList(err.woke)}, and delivering it threw (${err.message}), `
|
|
1824
|
+
+ `so it's dropped for ${joinedList(err.notWoken)}, who ${one ? "isn't" : "aren't"} woken`;
|
|
1825
|
+
}
|
|
1657
1826
|
/**
|
|
1658
1827
|
* Run a caller's callback once the change it's told of is made and traced. A
|
|
1659
1828
|
* throw from it is logged, not thrown, so the call answers as the change it
|
|
@@ -1766,6 +1935,8 @@ export class EventGate {
|
|
|
1766
1935
|
droppedBufferedEvents = 0;
|
|
1767
1936
|
// Per-policy stats
|
|
1768
1937
|
stats = new Map();
|
|
1938
|
+
/** Suppress rules' scripts and their state (suppress-scripts.ts), kept beside gate.json. */
|
|
1939
|
+
suppressScripts;
|
|
1769
1940
|
// The shadow lint's last findings (see refreshShadowWarnings), recomputed
|
|
1770
1941
|
// on every config change and fingerprinted so an unchanged set doesn't
|
|
1771
1942
|
// re-trace.
|
|
@@ -1823,6 +1994,11 @@ export class EventGate {
|
|
|
1823
1994
|
now;
|
|
1824
1995
|
constructor(opts) {
|
|
1825
1996
|
this.configPath = opts.configPath;
|
|
1997
|
+
// Before the rules are read: a start's merge can clear a rule's state, its script's included.
|
|
1998
|
+
this.suppressScripts = new SuppressScripts({
|
|
1999
|
+
statePath: join(dirname(opts.configPath), 'suppress-state.json'),
|
|
2000
|
+
...(opts.suppressScriptTimeoutMs !== undefined ? { timeoutMs: opts.suppressScriptTimeoutMs } : {}),
|
|
2001
|
+
});
|
|
1826
2002
|
this.privilegedUsersPath = opts.privilegedUsersPath;
|
|
1827
2003
|
this.emitTrace = opts.emitTrace;
|
|
1828
2004
|
this.addMessageFn = opts.addMessage;
|
|
@@ -2108,7 +2284,6 @@ export class EventGate {
|
|
|
2108
2284
|
...changes,
|
|
2109
2285
|
// Every name the start added to gate.json, as an append's trace gave.
|
|
2110
2286
|
addedPolicies: names,
|
|
2111
|
-
timestamp: Date.now(),
|
|
2112
2287
|
});
|
|
2113
2288
|
}
|
|
2114
2289
|
if (!appended && (!last.recipe || canonical({ rules: last.recipe.rules, default: last.recipe.default }) !== canonical({ rules: starting.rules.map((rule) => rule.raw), default: given.default }))) {
|
|
@@ -2208,7 +2383,6 @@ export class EventGate {
|
|
|
2208
2383
|
type: 'gate:config-error',
|
|
2209
2384
|
error: why,
|
|
2210
2385
|
configPath: this.configPath,
|
|
2211
|
-
timestamp: Date.now(),
|
|
2212
2386
|
});
|
|
2213
2387
|
}
|
|
2214
2388
|
this.configError = problem;
|
|
@@ -2358,8 +2532,11 @@ export class EventGate {
|
|
|
2358
2532
|
clearStateFor(was, next) {
|
|
2359
2533
|
const behaviorsByName = (config) => {
|
|
2360
2534
|
const byName = new Map();
|
|
2361
|
-
for (const p of config.policies)
|
|
2362
|
-
|
|
2535
|
+
for (const p of config.policies) {
|
|
2536
|
+
// A suppress rule's script is part of what it is: a changed script starts from a fresh state.
|
|
2537
|
+
const fingerprint = behaviorFingerprint(p.behavior) + (p.suppress ? ` suppress:${p.suppress.script ?? ''}` : '');
|
|
2538
|
+
byName.set(p.name, [...(byName.get(p.name) ?? []), fingerprint]);
|
|
2539
|
+
}
|
|
2363
2540
|
return new Map([...byName].map(([name, behaviors]) => [name, JSON.stringify(behaviors)]));
|
|
2364
2541
|
};
|
|
2365
2542
|
const before = behaviorsByName(was);
|
|
@@ -2412,7 +2589,6 @@ export class EventGate {
|
|
|
2412
2589
|
policyCount: newConfig.policies.length,
|
|
2413
2590
|
removedPolicies: removedPolicies.length > 0 ? removedPolicies : undefined,
|
|
2414
2591
|
stateClearedPolicies: stateClearedPolicies.length > 0 ? stateClearedPolicies : undefined,
|
|
2415
|
-
timestamp: now,
|
|
2416
2592
|
});
|
|
2417
2593
|
}
|
|
2418
2594
|
catch {
|
|
@@ -2523,7 +2699,6 @@ export class EventGate {
|
|
|
2523
2699
|
configPath: this.configPath,
|
|
2524
2700
|
policyName: policy.name,
|
|
2525
2701
|
replaced: idx >= 0,
|
|
2526
|
-
timestamp: Date.now(),
|
|
2527
2702
|
});
|
|
2528
2703
|
if (ignored.length > 0) {
|
|
2529
2704
|
// A caller that doesn't take the notes still gets them said, in the log.
|
|
@@ -2570,7 +2745,6 @@ export class EventGate {
|
|
|
2570
2745
|
type: 'gate:policy-removed',
|
|
2571
2746
|
configPath: this.configPath,
|
|
2572
2747
|
policyName: name,
|
|
2573
|
-
timestamp: Date.now(),
|
|
2574
2748
|
});
|
|
2575
2749
|
return true;
|
|
2576
2750
|
}
|
|
@@ -2585,6 +2759,15 @@ export class EventGate {
|
|
|
2585
2759
|
const current = read.state === 'loaded' ? read.config : this.config;
|
|
2586
2760
|
return current.policies.map((p) => p.name);
|
|
2587
2761
|
}
|
|
2762
|
+
/** The rules in force, in order (copies): what the last decision was made by. */
|
|
2763
|
+
policiesInForce() {
|
|
2764
|
+
return this.config.policies.map((p) => structuredClone(p));
|
|
2765
|
+
}
|
|
2766
|
+
/** The rule in force by this name (a copy), or undefined. */
|
|
2767
|
+
policyInForce(name) {
|
|
2768
|
+
const p = this.config.policies.find((x) => x.name === name);
|
|
2769
|
+
return p ? structuredClone(p) : undefined;
|
|
2770
|
+
}
|
|
2588
2771
|
/** @internal For `wake_add_rule`'s and `wake_remove_rule`'s refusals; not a host API. */
|
|
2589
2772
|
ruleNamesForChange() {
|
|
2590
2773
|
const edit = this.rulesForChange();
|
|
@@ -2763,7 +2946,6 @@ export class EventGate {
|
|
|
2763
2946
|
matchedPolicy: 'asleep',
|
|
2764
2947
|
trigger: false,
|
|
2765
2948
|
behavior: 'skip',
|
|
2766
|
-
timestamp: this.now(),
|
|
2767
2949
|
});
|
|
2768
2950
|
return { trigger: false, policyName: 'asleep', behavior: 'skip' };
|
|
2769
2951
|
}
|
|
@@ -2786,6 +2968,12 @@ export class EventGate {
|
|
|
2786
2968
|
}
|
|
2787
2969
|
else {
|
|
2788
2970
|
for (const compiled of this.compiledPolicies) {
|
|
2971
|
+
// A suppress rule doesn't decide a wake for an event suppression was
|
|
2972
|
+
// asked about: one it takes was suppressed before the gate was asked,
|
|
2973
|
+
// and one that reaches here is one it let by. Where suppression wasn't
|
|
2974
|
+
// asked (no view to hide it from), it defers, as its behavior says.
|
|
2975
|
+
if (compiled.policy.suppress && this.passesOverSuppress(info))
|
|
2976
|
+
continue;
|
|
2789
2977
|
if (!this.compiledMatches(compiled, info))
|
|
2790
2978
|
continue;
|
|
2791
2979
|
const candidate = this.computeDecision(compiled.policy, info);
|
|
@@ -2822,7 +3010,6 @@ export class EventGate {
|
|
|
2822
3010
|
trigger: decision.trigger,
|
|
2823
3011
|
behavior: formatBehavior(decision.behavior),
|
|
2824
3012
|
...(observed.length > 0 ? { observed } : {}),
|
|
2825
|
-
timestamp: this.now(),
|
|
2826
3013
|
});
|
|
2827
3014
|
return decision;
|
|
2828
3015
|
}
|
|
@@ -2843,7 +3030,6 @@ export class EventGate {
|
|
|
2843
3030
|
behavior: formatBehavior(policy.behavior),
|
|
2844
3031
|
eventType: info.eventType,
|
|
2845
3032
|
source: info.serverId || undefined,
|
|
2846
|
-
timestamp: this.now(),
|
|
2847
3033
|
});
|
|
2848
3034
|
if (policy.behavior === 'defer' || policy.behavior === 'skip') {
|
|
2849
3035
|
// Report the configured name faithfully ('defer' or legacy 'skip').
|
|
@@ -2975,6 +3161,7 @@ export class EventGate {
|
|
|
2975
3161
|
const debounce = this.debounceTimers.get(name);
|
|
2976
3162
|
if (debounce)
|
|
2977
3163
|
clearTimeout(debounce.timer);
|
|
3164
|
+
this.suppressScripts.forget(name);
|
|
2978
3165
|
this.rateLimitBuckets.delete(name);
|
|
2979
3166
|
this.passiveSampleCounters.delete(name);
|
|
2980
3167
|
this.rateLimitDenied.delete(name);
|
|
@@ -3032,6 +3219,8 @@ export class EventGate {
|
|
|
3032
3219
|
*/
|
|
3033
3220
|
probe(info) {
|
|
3034
3221
|
for (const compiled of this.compiledPolicies) {
|
|
3222
|
+
if (compiled.policy.suppress && this.passesOverSuppress(info))
|
|
3223
|
+
continue; // as evaluate does
|
|
3035
3224
|
if (!this.compiledMatches(compiled, info))
|
|
3036
3225
|
continue;
|
|
3037
3226
|
const trigger = this.wouldTrigger(compiled.policy, info);
|
|
@@ -3042,6 +3231,140 @@ export class EventGate {
|
|
|
3042
3231
|
const trigger = (this.config.default ?? 'always') === 'always';
|
|
3043
3232
|
return { trigger, policyName: null, behavior: this.config.default ?? 'always' };
|
|
3044
3233
|
}
|
|
3234
|
+
/**
|
|
3235
|
+
* The suppress rule that takes this event, or null when none does.
|
|
3236
|
+
* Side-effect-free, like probe: no counter moves, no batch forms, nothing
|
|
3237
|
+
* is traced, and a rule's script runs on a copy of its state, which is
|
|
3238
|
+
* then dropped. (A closure a script builds for itself is its own, and a
|
|
3239
|
+
* 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
|
|
3240
|
+
* framework asks decideSuppression instead, once per event.
|
|
3241
|
+
*
|
|
3242
|
+
* It reads the rules as evaluate does, first match deciding: a rule ahead
|
|
3243
|
+
* of the suppress rule that matches the event decides it instead (it isn't
|
|
3244
|
+
* suppressed), unless it's a passthrough rule that wouldn't fire. A rule
|
|
3245
|
+
* with a script (`suppress.script`) takes an event its match takes only
|
|
3246
|
+
* when the script says to hide it; otherwise the event goes on to the rules
|
|
3247
|
+
* after it, as one the rule doesn't match does. Sleep and gate.js aren't
|
|
3248
|
+
* consulted: they decide wakes, and suppression decides what's rendered.
|
|
3249
|
+
*
|
|
3250
|
+
* Never suppressed, whatever the rules say: an event addressed to the
|
|
3251
|
+
* agent (tagged `chat:addressed`: a mention, a reply, a DM) and the
|
|
3252
|
+
* heartbeat's (the agent's own clock). A suppress rule lets them by to the
|
|
3253
|
+
* rules after it, and its script isn't asked. The framework itself never
|
|
3254
|
+
* asks about console or API input.
|
|
3255
|
+
*/
|
|
3256
|
+
suppressionFor(info) {
|
|
3257
|
+
return this.walkSuppression(info, false);
|
|
3258
|
+
}
|
|
3259
|
+
/**
|
|
3260
|
+
* suppressionFor, for an event that is arriving now: each script asked
|
|
3261
|
+
* keeps the state it leaves, whatever later rules do with the event, and a
|
|
3262
|
+
* suppressed event counts in the passthrough rules ahead of its rule. Ask
|
|
3263
|
+
* once per event.
|
|
3264
|
+
*/
|
|
3265
|
+
decideSuppression(info) {
|
|
3266
|
+
return this.walkSuppression(info, true);
|
|
3267
|
+
}
|
|
3268
|
+
walkSuppression(info, commit) {
|
|
3269
|
+
this.reloadIfChanged();
|
|
3270
|
+
this.syncSuppressScripts();
|
|
3271
|
+
if (info.serverId === 'heartbeat')
|
|
3272
|
+
return null;
|
|
3273
|
+
const addressed = Array.isArray(info.tags) && info.tags.includes('chat:addressed');
|
|
3274
|
+
let event;
|
|
3275
|
+
const observers = [];
|
|
3276
|
+
for (const compiled of this.compiledPolicies) {
|
|
3277
|
+
if (!this.compiledMatches(compiled, info))
|
|
3278
|
+
continue;
|
|
3279
|
+
const suppress = compiled.policy.suppress;
|
|
3280
|
+
if (suppress) {
|
|
3281
|
+
if (addressed)
|
|
3282
|
+
continue;
|
|
3283
|
+
if (suppress.script !== undefined) {
|
|
3284
|
+
event ??= this.suppressScriptEvent(info);
|
|
3285
|
+
const ran = this.suppressScripts.run(compiled.policy.name, event, commit);
|
|
3286
|
+
// A script that failed shows the event: it goes on to the rules after.
|
|
3287
|
+
if (!ran || !('hide' in ran) || !ran.hide)
|
|
3288
|
+
continue;
|
|
3289
|
+
}
|
|
3290
|
+
// A suppressed event never reaches evaluate, so the passthrough rules
|
|
3291
|
+
// ahead of this one count it here, as evaluate would have: none of
|
|
3292
|
+
// them fires for it, and each fires on its schedule. A dry run counts
|
|
3293
|
+
// nothing.
|
|
3294
|
+
if (commit)
|
|
3295
|
+
for (const observer of observers)
|
|
3296
|
+
this.computeDecision(observer, info);
|
|
3297
|
+
return compiled.policy.name;
|
|
3298
|
+
}
|
|
3299
|
+
if (compiled.policy.passthrough && !this.wouldTrigger(compiled.policy, info)) {
|
|
3300
|
+
observers.push(compiled.policy);
|
|
3301
|
+
continue;
|
|
3302
|
+
}
|
|
3303
|
+
return null;
|
|
3304
|
+
}
|
|
3305
|
+
return null;
|
|
3306
|
+
}
|
|
3307
|
+
/** 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. */
|
|
3308
|
+
passesOverSuppress(info) {
|
|
3309
|
+
return info.suppressionAsked === true || info.serverId === 'heartbeat'
|
|
3310
|
+
|| (Array.isArray(info.tags) && info.tags.includes('chat:addressed'));
|
|
3311
|
+
}
|
|
3312
|
+
/** The scripts in step with the rules in force (SuppressScripts.sync: only changed ones are compiled). */
|
|
3313
|
+
syncSuppressScripts() {
|
|
3314
|
+
this.suppressScripts.sync(this.config.policies.map((p) => ({ name: p.name, ...(p.suppress?.script !== undefined ? { script: p.suppress.script } : {}) })));
|
|
3315
|
+
}
|
|
3316
|
+
/** What a suppress script is given for an event: plain JSON, its metadata included where it can be. */
|
|
3317
|
+
suppressScriptEvent(info) {
|
|
3318
|
+
let metadata = {};
|
|
3319
|
+
try {
|
|
3320
|
+
metadata = JSON.parse(JSON.stringify(info.metadata ?? {}));
|
|
3321
|
+
}
|
|
3322
|
+
catch {
|
|
3323
|
+
metadata = {};
|
|
3324
|
+
}
|
|
3325
|
+
const channel = this.routeChannelFor(info) ?? (info.channelId || undefined);
|
|
3326
|
+
const author = this.extractAuthorId(info.metadata);
|
|
3327
|
+
return {
|
|
3328
|
+
eventType: info.eventType,
|
|
3329
|
+
source: info.serverId,
|
|
3330
|
+
...(channel ? { channel } : {}),
|
|
3331
|
+
...(author ? { author } : {}),
|
|
3332
|
+
tags: [...(info.tags ?? [])],
|
|
3333
|
+
text: info.content,
|
|
3334
|
+
at: this.now(),
|
|
3335
|
+
metadata,
|
|
3336
|
+
};
|
|
3337
|
+
}
|
|
3338
|
+
/**
|
|
3339
|
+
* Whether a suppress script compiles to a function, on the scripts' worker:
|
|
3340
|
+
* an error's text, or null. wake_add_rule asks before it saves a rule.
|
|
3341
|
+
*/
|
|
3342
|
+
checkSuppressScript(script) {
|
|
3343
|
+
return this.suppressScripts.check(script);
|
|
3344
|
+
}
|
|
3345
|
+
/** A suppress rule's script state as it stands (a copy), for a host or a test. */
|
|
3346
|
+
suppressScriptState(name) {
|
|
3347
|
+
this.syncSuppressScripts();
|
|
3348
|
+
return this.suppressScripts.stateOf(name);
|
|
3349
|
+
}
|
|
3350
|
+
/**
|
|
3351
|
+
* Count a row the framework suppressed by `name` (suppressionFor's
|
|
3352
|
+
* answer) once it's written. The rule's stats count it as a match, so
|
|
3353
|
+
* gate_status shows how many events each suppress rule took since start.
|
|
3354
|
+
*/
|
|
3355
|
+
noteSuppressed(name, info) {
|
|
3356
|
+
const policyStats = this.stats.get(name) ?? { matchCount: 0, lastMatchTimestamp: null };
|
|
3357
|
+
policyStats.matchCount++;
|
|
3358
|
+
policyStats.lastMatchTimestamp = this.now();
|
|
3359
|
+
this.stats.set(name, policyStats);
|
|
3360
|
+
this.emitTrace({
|
|
3361
|
+
type: 'gate:suppressed',
|
|
3362
|
+
policyName: name,
|
|
3363
|
+
eventType: info.eventType,
|
|
3364
|
+
...(info.serverId ? { serverId: info.serverId } : {}),
|
|
3365
|
+
...(info.channelId ? { channelId: info.channelId } : {}),
|
|
3366
|
+
});
|
|
3367
|
+
}
|
|
3045
3368
|
/** The canonical synthetic events the probe table runs: chat shapes as MCPL delivers them, and a heartbeat. */
|
|
3046
3369
|
probeEvents() {
|
|
3047
3370
|
const chat = (label, eventType, metadata, tags) => ({
|
|
@@ -3100,7 +3423,12 @@ export class EventGate {
|
|
|
3100
3423
|
*/
|
|
3101
3424
|
probeTable() {
|
|
3102
3425
|
return this.probeEvents().map(({ label, info }) => {
|
|
3103
|
-
const
|
|
3426
|
+
const suppressedBy = this.suppressionFor(info);
|
|
3427
|
+
if (suppressedBy !== null) {
|
|
3428
|
+
return { probe: label, eventType: info.eventType, policy: suppressedBy, behavior: 'suppress', wouldWake: false };
|
|
3429
|
+
}
|
|
3430
|
+
// As admission asks: suppression first, so a script that says show lets the probe on.
|
|
3431
|
+
const d = this.probe({ ...info, suppressionAsked: true });
|
|
3104
3432
|
return {
|
|
3105
3433
|
probe: label,
|
|
3106
3434
|
eventType: info.eventType,
|
|
@@ -3147,7 +3475,6 @@ export class EventGate {
|
|
|
3147
3475
|
type: 'gate:shadow-warnings',
|
|
3148
3476
|
reason,
|
|
3149
3477
|
warnings: warnings.map(formatShadowWarning),
|
|
3150
|
-
timestamp: this.now(),
|
|
3151
3478
|
});
|
|
3152
3479
|
}
|
|
3153
3480
|
}
|
|
@@ -3204,6 +3531,7 @@ export class EventGate {
|
|
|
3204
3531
|
routeChannelId: this.routeChannelFor(info),
|
|
3205
3532
|
...(typeof info.metadata?.messageId === 'string' ? { messageId: info.metadata.messageId } : {}),
|
|
3206
3533
|
...(typeof info.metadata?.eventId === 'string' ? { eventId: info.metadata.eventId } : {}),
|
|
3534
|
+
...(info.recipients !== undefined ? { recipients: [...info.recipients] } : {}),
|
|
3207
3535
|
};
|
|
3208
3536
|
const existing = this.debounceTimers.get(policy.name);
|
|
3209
3537
|
// A judge's maxWaitMs caps how far new events push the batch back: it's
|
|
@@ -3258,15 +3586,14 @@ export class EventGate {
|
|
|
3258
3586
|
}
|
|
3259
3587
|
catch (err) {
|
|
3260
3588
|
const one = events.length === 1;
|
|
3261
|
-
console.error(`[gate] Rule ${quotedValue(policyName)}: its pending batch of ${events.length} ${one ? 'event' : 'events'}
|
|
3262
|
-
+ `(${errorText(err)}), so the batch is dropped`);
|
|
3589
|
+
console.error(`[gate] Rule ${quotedValue(policyName)}: its pending batch of ${events.length} ${one ? 'event' : 'events'} `
|
|
3590
|
+
+ (deliveredInPart(err) ?? `couldn't be delivered (${errorText(err)}), so the batch is dropped`));
|
|
3263
3591
|
return;
|
|
3264
3592
|
}
|
|
3265
3593
|
this.emitTrace({
|
|
3266
3594
|
type: 'gate:debounce-delivered',
|
|
3267
3595
|
policyName,
|
|
3268
3596
|
eventCount: events.length,
|
|
3269
|
-
timestamp: Date.now(),
|
|
3270
3597
|
});
|
|
3271
3598
|
}
|
|
3272
3599
|
// =========================================================================
|
|
@@ -3286,11 +3613,37 @@ export class EventGate {
|
|
|
3286
3613
|
type: 'gate:held-dropped',
|
|
3287
3614
|
dropped: batched.length - events.length,
|
|
3288
3615
|
channels: [...new Set(batched.filter((e) => !events.includes(e)).map(heldAt))],
|
|
3289
|
-
timestamp: this.now(),
|
|
3290
3616
|
});
|
|
3291
3617
|
}
|
|
3292
3618
|
if (events.length === 0)
|
|
3293
3619
|
return;
|
|
3620
|
+
// Who each delivery woke, so that a throw after a wake is told as a
|
|
3621
|
+
// delivery in part (PartialDelivery), the batch dropped only for those
|
|
3622
|
+
// it didn't wake.
|
|
3623
|
+
const woke = [];
|
|
3624
|
+
if (events.every((e) => e.recipients === undefined)) {
|
|
3625
|
+
const names = this.getAgentNamesFn();
|
|
3626
|
+
deliveredTracking(woke, names, () => this.deliverBatch(events, names, undefined, woke));
|
|
3627
|
+
return;
|
|
3628
|
+
}
|
|
3629
|
+
const woken = new Set(events.flatMap((e) => e.recipients ?? []));
|
|
3630
|
+
if (events.some((e) => e.recipients === undefined))
|
|
3631
|
+
for (const name of this.getAgentNamesFn())
|
|
3632
|
+
woken.add(name);
|
|
3633
|
+
const groups = new Map();
|
|
3634
|
+
for (const agentName of woken) {
|
|
3635
|
+
const seen = events.filter((e) => e.recipients === undefined || e.recipients.includes(agentName));
|
|
3636
|
+
const key = seen.map((e) => events.indexOf(e)).join(',');
|
|
3637
|
+
const group = groups.get(key) ?? { agents: [], events: seen };
|
|
3638
|
+
group.agents.push(agentName);
|
|
3639
|
+
groups.set(key, group);
|
|
3640
|
+
}
|
|
3641
|
+
deliveredTracking(woke, [...woken], () => {
|
|
3642
|
+
for (const group of groups.values())
|
|
3643
|
+
this.deliverBatch(group.events, group.agents, group.agents, woke);
|
|
3644
|
+
});
|
|
3645
|
+
}
|
|
3646
|
+
deliverBatch(events, woken, audience, woke) {
|
|
3294
3647
|
const policyNames = [...new Set(events.map(e => e.policyName))];
|
|
3295
3648
|
// Events whose content the framework already stored at arrival (MCPL
|
|
3296
3649
|
// channel-incoming / push-event) get a compact REFERENCE — re-quoting
|
|
@@ -3346,7 +3699,12 @@ export class EventGate {
|
|
|
3346
3699
|
const text = `[${NOTICE_TAG.gate}: ${events.length} event${events.length > 1 ? 's' : ''} matched]\n\n${lines.join('\n')}`;
|
|
3347
3700
|
const only = events.length === 1 && events[0].inContext ? events[0] : undefined;
|
|
3348
3701
|
let line;
|
|
3349
|
-
|
|
3702
|
+
// Where a line stored now landed, for the delivery agent's wake, as the
|
|
3703
|
+
// self-wake's notice places its own (readsRowFor): a live turn that
|
|
3704
|
+
// carried and answered the line drops the wake. Other agents' wakes stay
|
|
3705
|
+
// plain by readsRowFor's rule.
|
|
3706
|
+
const placement = {};
|
|
3707
|
+
if (only && this.newestEntry(only, audience)) {
|
|
3350
3708
|
const shownChannel = only.routeChannelId ?? only.channelId;
|
|
3351
3709
|
line = {
|
|
3352
3710
|
entry: entryRef(only),
|
|
@@ -3360,7 +3718,7 @@ export class EventGate {
|
|
|
3360
3718
|
this.addMessageFn('user', [{ type: 'text', text }], {
|
|
3361
3719
|
source: 'gate:debounce',
|
|
3362
3720
|
policies: policyNames,
|
|
3363
|
-
});
|
|
3721
|
+
}, undefined, placement, audience);
|
|
3364
3722
|
}
|
|
3365
3723
|
// Provenance of the batched wake (who/where woke the agent): the newest
|
|
3366
3724
|
// ADDRESSED event first, else the newest event naming a channel or an
|
|
@@ -3370,19 +3728,20 @@ export class EventGate {
|
|
|
3370
3728
|
// back to whoever addressed the agent, not to whichever channel last saw
|
|
3371
3729
|
// any traffic. Ambient-only batches set no locus. A batch whose line the
|
|
3372
3730
|
// gate left to the turn's start carries it too (`line`), one object
|
|
3373
|
-
// shared by every agent
|
|
3731
|
+
// shared by every agent woken with these messages, so it is written once.
|
|
3374
3732
|
const chosen = wakeProvenance(events);
|
|
3375
3733
|
const provenance = line ? { ...chosen, line } : chosen;
|
|
3376
|
-
for (const agentName of
|
|
3377
|
-
this.requestInferenceFn(agentName, 'gate:debounce', 'gate', provenance);
|
|
3734
|
+
for (const agentName of woken) {
|
|
3735
|
+
this.requestInferenceFn(agentName, 'gate:debounce', 'gate', provenance, line ? undefined : placement);
|
|
3736
|
+
woke?.push(agentName);
|
|
3378
3737
|
}
|
|
3379
3738
|
}
|
|
3380
3739
|
/** isNewestEntry for one event; a callback fault keeps the line. */
|
|
3381
|
-
newestEntry(event) {
|
|
3740
|
+
newestEntry(event, recipients) {
|
|
3382
3741
|
if (!this.isNewestEntryFn)
|
|
3383
3742
|
return false;
|
|
3384
3743
|
try {
|
|
3385
|
-
return this.isNewestEntryFn(entryRef(event)) === true;
|
|
3744
|
+
return this.isNewestEntryFn(entryRef(event), recipients) === true;
|
|
3386
3745
|
}
|
|
3387
3746
|
catch {
|
|
3388
3747
|
return false;
|
|
@@ -3496,7 +3855,8 @@ export class EventGate {
|
|
|
3496
3855
|
* the judge to weigh what waiting costs: each agent's pending self-wake
|
|
3497
3856
|
* (skip_reply's wake_in_seconds), the end of a sleep, and the pending
|
|
3498
3857
|
* batches of rules other than `exceptPolicy` (a debounce's wakes the agent
|
|
3499
|
-
* when it fires; a judge rule's goes to the judge, so it only may).
|
|
3858
|
+
* when it fires; a judge rule's goes to the judge, so it only may). A batch,
|
|
3859
|
+
* and a sleep with no agent named, reach the gate's recipients alone.
|
|
3500
3860
|
*/
|
|
3501
3861
|
upcomingWakes(agentNames, exceptPolicy) {
|
|
3502
3862
|
const wakes = [];
|
|
@@ -3504,20 +3864,22 @@ export class EventGate {
|
|
|
3504
3864
|
for (const name of agentNames) {
|
|
3505
3865
|
const due = this.selfWakeDue.get(name);
|
|
3506
3866
|
if (due && this.selfWakeTimers.has(name)) {
|
|
3507
|
-
wakes.push({ at: due.at, certain: true, what: `${agentNames.length > 1 ? `${name}'s` : 'its'} own ${due.source} timer` });
|
|
3867
|
+
wakes.push({ at: due.at, certain: true, kind: 'self-wake', what: `${agentNames.length > 1 ? `${name}'s` : 'its'} own ${due.source} timer` });
|
|
3508
3868
|
}
|
|
3509
3869
|
}
|
|
3510
|
-
|
|
3511
|
-
|
|
3870
|
+
const recipients = this.getAgentNamesFn();
|
|
3871
|
+
const recipient = agentNames.some((name) => recipients.includes(name));
|
|
3872
|
+
if (this.sleepUntil > now && (this.sleepAgent === undefined ? recipient : agentNames.includes(this.sleepAgent))) {
|
|
3873
|
+
wakes.push({ at: this.sleepUntil, certain: true, kind: 'sleep-end', what: 'the end of its sleep' });
|
|
3512
3874
|
}
|
|
3513
3875
|
for (const [name, state] of this.debounceTimers) {
|
|
3514
|
-
if (name === exceptPolicy || state.events.length === 0)
|
|
3876
|
+
if (!recipient || name === exceptPolicy || state.events.length === 0)
|
|
3515
3877
|
continue;
|
|
3516
3878
|
const n = state.events.length;
|
|
3517
3879
|
const batch = `${n} ${n === 1 ? 'event' : 'events'}`;
|
|
3518
3880
|
wakes.push(state.judge
|
|
3519
|
-
? { at: state.dueAt, certain: false, what: `rule ${quotedValue(name)}'s batch of ${batch} goes to the judge` }
|
|
3520
|
-
: { at: state.dueAt, certain: true, what: `rule ${quotedValue(name)}'s debounced batch of ${batch}` });
|
|
3881
|
+
? { at: state.dueAt, certain: false, kind: 'judge-batch', what: `rule ${quotedValue(name)}'s batch of ${batch} goes to the judge` }
|
|
3882
|
+
: { at: state.dueAt, certain: true, kind: 'debounce', what: `rule ${quotedValue(name)}'s debounced batch of ${batch}` });
|
|
3521
3883
|
}
|
|
3522
3884
|
return wakes.sort((a, b) => (a.at ?? 0) - (b.at ?? 0));
|
|
3523
3885
|
}
|
|
@@ -3616,7 +3978,6 @@ export class EventGate {
|
|
|
3616
3978
|
...(!failed && typeof verdict.inputTokens === 'number' ? { inputTokens: verdict.inputTokens } : {}),
|
|
3617
3979
|
...(!failed && typeof verdict.cacheReadTokens === 'number' ? { cacheReadTokens: verdict.cacheReadTokens } : {}),
|
|
3618
3980
|
...(!failed && typeof verdict.outputTokens === 'number' ? { outputTokens: verdict.outputTokens } : {}),
|
|
3619
|
-
timestamp: Date.now(),
|
|
3620
3981
|
});
|
|
3621
3982
|
if (this.disposed)
|
|
3622
3983
|
return;
|
|
@@ -3643,8 +4004,8 @@ export class EventGate {
|
|
|
3643
4004
|
}
|
|
3644
4005
|
catch (err) {
|
|
3645
4006
|
const one = events.length === 1;
|
|
3646
|
-
console.error(`[gate] a judged batch of ${events.length} ${one ? 'event' : 'events'}
|
|
3647
|
-
+ `(${errorText(err)}), so the batch is dropped`);
|
|
4007
|
+
console.error(`[gate] a judged batch of ${events.length} ${one ? 'event' : 'events'} `
|
|
4008
|
+
+ (deliveredInPart(err) ?? `couldn't be delivered (${errorText(err)}), so the batch is dropped`));
|
|
3648
4009
|
}
|
|
3649
4010
|
}
|
|
3650
4011
|
changesJudgedContext(agentName) {
|
|
@@ -3713,8 +4074,8 @@ export class EventGate {
|
|
|
3713
4074
|
* buffer through the same path a turn's end uses. Self-wake and sleep-expiry
|
|
3714
4075
|
* timers deliberately do NOT route through this — their inference requests
|
|
3715
4076
|
* are per-agent targeted pushes that the framework's quiesce wake gate parks
|
|
3716
|
-
* and releases at resume, whereas the buffer's deliverEvents
|
|
3717
|
-
*
|
|
4077
|
+
* and releases at resume, whereas the buffer's deliverEvents wakes every
|
|
4078
|
+
* agent a batch is for.
|
|
3718
4079
|
*/
|
|
3719
4080
|
setQuiesced(quiesced) {
|
|
3720
4081
|
if (this.quiesced === quiesced)
|
|
@@ -3745,8 +4106,12 @@ export class EventGate {
|
|
|
3745
4106
|
}
|
|
3746
4107
|
catch (err) {
|
|
3747
4108
|
const one = events.length === 1;
|
|
3748
|
-
|
|
3749
|
-
|
|
4109
|
+
const held = `${events.length} ${one ? 'event' : 'events'}`;
|
|
4110
|
+
const partly = deliveredInPart(err);
|
|
4111
|
+
console.error(partly
|
|
4112
|
+
? `[gate] a batch of ${held}, held for an agent's turn or the host's quiesce, ${partly}`
|
|
4113
|
+
: `[gate] ${held} held for an agent's turn or the host's quiesce couldn't be delivered (${errorText(err)}), `
|
|
4114
|
+
+ `so ${one ? "it's" : "they're"} dropped`);
|
|
3750
4115
|
}
|
|
3751
4116
|
});
|
|
3752
4117
|
}
|
|
@@ -3815,7 +4180,6 @@ export class EventGate {
|
|
|
3815
4180
|
matchedPolicy: 'asleep',
|
|
3816
4181
|
trigger: false,
|
|
3817
4182
|
behavior: 'skip',
|
|
3818
|
-
timestamp: this.now(),
|
|
3819
4183
|
});
|
|
3820
4184
|
return { until: this.sleepUntil };
|
|
3821
4185
|
}
|
|
@@ -3876,13 +4240,13 @@ export class EventGate {
|
|
|
3876
4240
|
matchedPolicy: 'self-wake',
|
|
3877
4241
|
trigger: true,
|
|
3878
4242
|
behavior: 'always',
|
|
3879
|
-
timestamp: this.now(),
|
|
3880
4243
|
});
|
|
3881
4244
|
// Compact in-window notice: without it the wake turn contains nothing
|
|
3882
4245
|
// new, so the agent can't tell WHAT woke it (its own timer vs. a
|
|
3883
4246
|
// heartbeat) or WHEN. One line, timestamped to the second. Added
|
|
3884
|
-
// before the inference request so it rides the wake turn
|
|
3885
|
-
//
|
|
4247
|
+
// before the inference request so it rides the wake turn: it is stored
|
|
4248
|
+
// at once, and while a turn of the agent's is alive it is kept out of
|
|
4249
|
+
// the window until the agent's next presentation point.
|
|
3886
4250
|
const nowIso = new Date(this.now()).toISOString().slice(0, 19) + 'Z';
|
|
3887
4251
|
// Deliver the notice into the WAKING agent's window — a self-wake
|
|
3888
4252
|
// armed by a non-primary agent (e.g. a subconscious cadence) must not
|
|
@@ -3905,7 +4269,6 @@ export class EventGate {
|
|
|
3905
4269
|
matchedPolicy: 'self-wake',
|
|
3906
4270
|
trigger: false,
|
|
3907
4271
|
behavior: 'skip',
|
|
3908
|
-
timestamp: this.now(),
|
|
3909
4272
|
});
|
|
3910
4273
|
return { inMs: ms, armed: true };
|
|
3911
4274
|
}
|
|
@@ -3953,7 +4316,6 @@ export class EventGate {
|
|
|
3953
4316
|
matchedPolicy: 'asleep',
|
|
3954
4317
|
trigger: true,
|
|
3955
4318
|
behavior: 'always',
|
|
3956
|
-
timestamp: this.now(),
|
|
3957
4319
|
});
|
|
3958
4320
|
const detail = note ? `${reason}: ${note}` : reason;
|
|
3959
4321
|
const targets = agent ? [agent] : this.getAgentNamesFn();
|
|
@@ -4088,6 +4450,7 @@ export class EventGate {
|
|
|
4088
4450
|
return { success: true, data: status };
|
|
4089
4451
|
}
|
|
4090
4452
|
getStatus() {
|
|
4453
|
+
this.syncSuppressScripts();
|
|
4091
4454
|
const policies = this.config.policies.map(p => {
|
|
4092
4455
|
const stats = this.stats.get(p.name);
|
|
4093
4456
|
const debounce = this.debounceTimers.get(p.name);
|
|
@@ -4096,6 +4459,9 @@ export class EventGate {
|
|
|
4096
4459
|
behavior: p.behavior,
|
|
4097
4460
|
matchCount: stats?.matchCount ?? 0,
|
|
4098
4461
|
lastMatchTimestamp: stats?.lastMatchTimestamp ?? null,
|
|
4462
|
+
...(p.suppress
|
|
4463
|
+
? { suppress: { ...p.suppress, ...(p.suppress.script !== undefined && this.suppressScripts.status(p.name) ? { scriptStatus: this.suppressScripts.status(p.name) } : {}) } }
|
|
4464
|
+
: {}),
|
|
4099
4465
|
};
|
|
4100
4466
|
if (typeof p.behavior === 'object' && 'debounce' in p.behavior) {
|
|
4101
4467
|
result.debounceState = {
|
|
@@ -4157,6 +4523,13 @@ export class EventGate {
|
|
|
4157
4523
|
// Fresh, not the cache: the sources the host knows (eventTypesFrom)
|
|
4158
4524
|
// change as servers connect, with no change to the rules.
|
|
4159
4525
|
shadowWarnings: this.lintShadows().map(formatShadowWarning),
|
|
4526
|
+
...(() => {
|
|
4527
|
+
const rule = firstCatchAll(this.config.policies, { lettingBy: true });
|
|
4528
|
+
return rule ? { catchAll: {
|
|
4529
|
+
rule,
|
|
4530
|
+
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.`,
|
|
4531
|
+
} } : {};
|
|
4532
|
+
})(),
|
|
4160
4533
|
totalEvaluations: this.totalEvaluations,
|
|
4161
4534
|
defaultDecisions: {
|
|
4162
4535
|
triggered: this.defaultTriggered,
|
|
@@ -4192,6 +4565,7 @@ export class EventGate {
|
|
|
4192
4565
|
// =========================================================================
|
|
4193
4566
|
dispose() {
|
|
4194
4567
|
this.gateScript.dispose();
|
|
4568
|
+
this.suppressScripts.dispose();
|
|
4195
4569
|
for (const state of this.debounceTimers.values()) {
|
|
4196
4570
|
clearTimeout(state.timer);
|
|
4197
4571
|
}
|