@animalabs/agent-framework 0.32.0 → 0.34.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 +340 -22
- 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 +208 -80
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +345 -113
- 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/append-all.d.ts +5 -0
- package/dist/src/append-all.d.ts.map +1 -0
- package/dist/src/append-all.js +8 -0
- package/dist/src/append-all.js.map +1 -0
- 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 +70 -0
- package/dist/src/context-options.d.ts.map +1 -0
- package/dist/src/context-options.js +90 -0
- package/dist/src/context-options.js.map +1 -0
- package/dist/src/cut-off-round.js +4 -4
- 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 +183 -12
- 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/failure-text.d.ts +36 -0
- package/dist/src/failure-text.d.ts.map +1 -0
- package/dist/src/failure-text.js +89 -0
- package/dist/src/failure-text.js.map +1 -0
- 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 +2064 -842
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +8586 -3739
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +125 -21
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +487 -72
- 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/reply-judge.d.ts +4 -0
- package/dist/src/gate/reply-judge.d.ts.map +1 -1
- package/dist/src/gate/reply-judge.js +12 -3
- package/dist/src/gate/reply-judge.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 +142 -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/heartbeat/heartbeat.js +1 -1
- package/dist/src/heartbeat/heartbeat.js.map +1 -1
- 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 +6 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/inference-bounds.d.ts +2 -1
- package/dist/src/inference-bounds.d.ts.map +1 -1
- package/dist/src/inference-bounds.js +9 -6
- package/dist/src/inference-bounds.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 +188 -28
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +426 -70
- 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/modern-connection.d.ts +109 -15
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +593 -86
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +3 -1
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +15 -9
- package/dist/src/mcpl/protocol-family.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 +50 -28
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +165 -72
- 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/tool-presence.d.ts.map +1 -1
- package/dist/src/mcpl/tool-presence.js +10 -6
- package/dist/src/mcpl/tool-presence.js.map +1 -1
- package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
- package/dist/src/mcpl/toolset-host.js +7 -6
- package/dist/src/mcpl/toolset-host.js.map +1 -1
- package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
- package/dist/src/mcpl/toolsets-module.js +2 -1
- package/dist/src/mcpl/toolsets-module.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +33 -6
- 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 +238 -87
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/health/index.js +2 -2
- package/dist/src/modules/health/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 +18 -25
- 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 +80 -25
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +2 -1
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +3 -2
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/notice-tags.d.ts +20 -9
- package/dist/src/notice-tags.d.ts.map +1 -1
- package/dist/src/notice-tags.js +20 -9
- 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 +747 -0
- package/dist/src/presentation.d.ts.map +1 -0
- package/dist/src/presentation.js +1437 -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/reasoning-loss.d.ts +5 -0
- package/dist/src/reasoning-loss.d.ts.map +1 -0
- package/dist/src/reasoning-loss.js +8 -0
- package/dist/src/reasoning-loss.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 +277 -32
- package/dist/src/recovery/offline-branch.js.map +1 -1
- package/dist/src/recovery/recover-cli.js +24 -4
- 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/sent-rows.js +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/supplied-text.d.ts +1 -1
- package/dist/src/supplied-text.js +3 -3
- package/dist/src/supplied-text.js.map +1 -1
- package/dist/src/thinking-drops.d.ts +2 -0
- package/dist/src/thinking-drops.d.ts.map +1 -1
- package/dist/src/thinking-drops.js.map +1 -1
- package/dist/src/tool-presentation.d.ts.map +1 -1
- package/dist/src/tool-presentation.js +38 -9
- package/dist/src/tool-presentation.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 +1170 -81
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +3732 -369
- 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 +89 -28
- 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 +121 -25
- 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 +72 -17
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/inference-log.d.ts +1 -0
- package/dist/src/types/inference-log.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 +573 -39
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/admission/admit.ts +32 -3
- package/src/agent.ts +392 -132
- package/src/api/mcp-server-core.ts +54 -17
- package/src/api/server.ts +4 -23
- package/src/append-all.ts +6 -0
- 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 +106 -0
- package/src/cut-off-round.ts +4 -4
- 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 +213 -14
- package/src/dendrite/types.ts +92 -8
- package/src/failure-text.ts +88 -0
- package/src/foreign-rounds.ts +199 -0
- package/src/framework.ts +8796 -3813
- package/src/gate/event-gate.ts +518 -91
- package/src/gate/index.ts +3 -0
- package/src/gate/reply-judge.ts +12 -3
- 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 +148 -7
- package/src/gate/wake-onboarding.ts +309 -0
- package/src/heartbeat/heartbeat.ts +1 -1
- package/src/held-writes.ts +49 -60
- package/src/index.ts +10 -2
- package/src/inference-bounds.ts +11 -6
- package/src/jsonrpc-envelope.ts +81 -0
- package/src/mcpl/channel-registry.ts +513 -116
- 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/modern-connection.ts +629 -91
- package/src/mcpl/protocol-family.ts +15 -9
- package/src/mcpl/push-coalescer.ts +193 -34
- package/src/mcpl/push-handler.ts +10 -5
- package/src/mcpl/server-connection.ts +169 -75
- package/src/mcpl/tool-classes.ts +3 -0
- package/src/mcpl/tool-presence.ts +10 -5
- package/src/mcpl/toolset-host.ts +7 -6
- package/src/mcpl/toolsets-module.ts +2 -1
- package/src/mcpl/types.ts +33 -6
- package/src/module-registry.ts +73 -17
- package/src/modules/discord/index.ts +230 -82
- package/src/modules/health/index.ts +2 -2
- package/src/modules/history/index.ts +439 -70
- package/src/modules/history/semantic.ts +18 -24
- package/src/modules/workspace/disk-agreement.ts +1 -1
- package/src/modules/workspace/index.ts +89 -26
- package/src/modules/workspace/observe.ts +2 -1
- package/src/modules/workspace/reconcile.ts +3 -2
- package/src/notice-tags.ts +21 -9
- package/src/operator-log.ts +36 -3
- package/src/presentation.ts +1678 -0
- package/src/prose-drafts.ts +16 -12
- package/src/read-marks.ts +328 -0
- package/src/reasoning-loss.ts +7 -0
- package/src/recovery/offline-branch.ts +350 -35
- package/src/recovery/recover-cli.ts +23 -4
- package/src/send-tools.ts +40 -7
- package/src/sent-rows.ts +1 -1
- package/src/stored-injections.ts +2 -2
- package/src/stored-removal.ts +31 -7
- package/src/supplied-text.ts +3 -3
- package/src/thinking-drops.ts +2 -0
- package/src/tool-presentation.ts +34 -9
- package/src/tool-result-guard.ts +41 -7
- package/src/tune-out/coordinator.ts +4121 -472
- package/src/tune-out/tools.ts +162 -44
- package/src/tune-out/window-anchor.ts +277 -0
- package/src/types/agent.ts +122 -25
- package/src/types/events.ts +2 -0
- package/src/types/framework.ts +73 -17
- package/src/types/inference-log.ts +1 -0
- package/src/types/module.ts +181 -68
- package/src/types/trace.ts +607 -43
|
@@ -15,10 +15,16 @@ 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';
|
|
21
23
|
import { safeSlice } from '../safe-slice.js';
|
|
24
|
+
import { failureWords } from '../failure-text.js';
|
|
25
|
+
import { JudgeFailure } from './reply-judge.js';
|
|
26
|
+
import { isSystemNotice } from '../mcpl/tags.js';
|
|
27
|
+
import { appendAll } from '../append-all.js';
|
|
22
28
|
// ---------------------------------------------------------------------------
|
|
23
29
|
// Constants
|
|
24
30
|
// ---------------------------------------------------------------------------
|
|
@@ -102,15 +108,25 @@ function isMetadataTruthy(value) {
|
|
|
102
108
|
return Boolean(value);
|
|
103
109
|
}
|
|
104
110
|
/**
|
|
105
|
-
* Provenance of a batched wake: the newest ADDRESSED
|
|
106
|
-
* wins (mention / reply / DM outranks newer ambient
|
|
107
|
-
* own locus rule); else the newest
|
|
108
|
-
*
|
|
111
|
+
* Provenance of a batched wake: the newest ADDRESSED event that names a
|
|
112
|
+
* channel or an author wins (mention / reply / DM outranks newer ambient
|
|
113
|
+
* chatter — the framework's own locus rule); else the newest event that
|
|
114
|
+
* names either. An addressed event's resolved route counts as its channel;
|
|
115
|
+
* an ambient one's doesn't. Channel, author and addressed all come from
|
|
116
|
+
* that one event, so they can never disagree. A `chat:system` notice is
|
|
117
|
+
* never chosen for the batch's route, channel or addressed flag, even one
|
|
118
|
+
* addressed by its place (a pin in a DM), since a notice never decides where
|
|
119
|
+
* the agent's speech goes; a coalesced render whose latest occurrence is a
|
|
120
|
+
* notice isn't taken as one (GateEventInfo.folds). A batch with no such event
|
|
121
|
+
* carries only its `batchChannels` when it has them; else its chosen event is
|
|
122
|
+
* its newest notice that has an author, of which it takes only the author
|
|
123
|
+
* and time.
|
|
109
124
|
*/
|
|
110
125
|
export function wakeProvenance(events) {
|
|
126
|
+
const said = events.filter((e) => !e.systemNotice);
|
|
111
127
|
let newest;
|
|
112
128
|
let newestAddressed;
|
|
113
|
-
for (const e of
|
|
129
|
+
for (const e of said) {
|
|
114
130
|
// A resolved route counts as naming a channel only for an ADDRESSED
|
|
115
131
|
// event: a push whose origin declares only `mcplChannelId` (no raw
|
|
116
132
|
// channel id, no author) is still a routable addressed message, and
|
|
@@ -126,22 +142,40 @@ export function wakeProvenance(events) {
|
|
|
126
142
|
newestAddressed = e;
|
|
127
143
|
}
|
|
128
144
|
const pick = newestAddressed ?? newest;
|
|
129
|
-
|
|
130
|
-
|
|
145
|
+
const channels = said.some((e) => e.addressed) ? [] : batchRoutes(said);
|
|
146
|
+
if (!pick && channels.length === 0) {
|
|
147
|
+
const notice = events.reduce((n, e) => (e.systemNotice && e.authorId && (!n || e.timestamp >= n.timestamp) ? e : n), undefined);
|
|
148
|
+
return notice ? { counterparty: counterpartyOf(notice), at: notice.timestamp } : undefined;
|
|
149
|
+
}
|
|
131
150
|
// push-event channel ids are the adapter's raw ids (a Discord snowflake):
|
|
132
151
|
// not a composite channel id, so not reported as one
|
|
133
|
-
const channelId = pick
|
|
134
|
-
const ns = pick.serverId || (pick.channelId ? pick.channelId.split(':')[0] : '') || 'channel';
|
|
152
|
+
const channelId = pick?.channelId && pick.eventType !== 'mcpl:push-event' ? pick.channelId : undefined;
|
|
135
153
|
return {
|
|
136
154
|
...(channelId ? { channelId } : {}),
|
|
137
|
-
...(pick
|
|
138
|
-
...(pick
|
|
139
|
-
...(pick
|
|
155
|
+
...(pick?.authorId ? { counterparty: counterpartyOf(pick) } : {}),
|
|
156
|
+
...(pick?.addressed ? { addressed: true } : {}),
|
|
157
|
+
...(pick?.addressed && pick.routeChannelId
|
|
140
158
|
? { routeChannelId: pick.routeChannelId, ...(pick.serverId ? { routeServerId: pick.serverId } : {}) }
|
|
141
159
|
: {}),
|
|
142
|
-
|
|
160
|
+
...(channels.length > 0 ? { batchChannels: channels } : {}),
|
|
161
|
+
...(pick ? { at: pick.timestamp } : {}),
|
|
143
162
|
};
|
|
144
163
|
}
|
|
164
|
+
function counterpartyOf(e) {
|
|
165
|
+
const ns = e.serverId || (e.channelId ? e.channelId.split(':')[0] : '') || 'channel';
|
|
166
|
+
return `${ns}:user:${e.authorId}`;
|
|
167
|
+
}
|
|
168
|
+
function batchRoutes(events) {
|
|
169
|
+
const routes = [];
|
|
170
|
+
for (const e of events) {
|
|
171
|
+
if (!e.routeChannelId)
|
|
172
|
+
continue;
|
|
173
|
+
if (routes.some((r) => r.channelId === e.routeChannelId && r.serverId === e.serverId))
|
|
174
|
+
continue;
|
|
175
|
+
routes.push({ channelId: e.routeChannelId, ...(e.serverId ? { serverId: e.serverId } : {}) });
|
|
176
|
+
}
|
|
177
|
+
return routes;
|
|
178
|
+
}
|
|
145
179
|
/** A `[Gate: …]` line's reference to messages already in the window: how
|
|
146
180
|
* many, in which channel, and how long ago the oldest arrived, as of `now`. */
|
|
147
181
|
function referenceLine(count, channel, oldest, now) {
|
|
@@ -252,7 +286,7 @@ function behaviorFingerprint(b) {
|
|
|
252
286
|
// file for a null or a "" (a passthrough, a behavior's kind, gate.json's
|
|
253
287
|
// default), it now loads without it.
|
|
254
288
|
const CONFIG_FIELDS = ['default', 'policies'];
|
|
255
|
-
const POLICY_FIELDS = ['name', 'match', 'behavior', 'resets', 'passthrough'];
|
|
289
|
+
const POLICY_FIELDS = ['name', 'match', 'behavior', 'resets', 'passthrough', 'suppress'];
|
|
256
290
|
const MATCH_FIELDS = [
|
|
257
291
|
'scope', 'source', 'channel', 'mount', 'pathGlob', 'tagsAny', 'tagsAll', 'tagsNone', 'metadataTrue', 'filter',
|
|
258
292
|
];
|
|
@@ -261,6 +295,7 @@ const BEHAVIOR_KINDS = ['debounce', 'rate_limit', 'passive_sample', 'judge'];
|
|
|
261
295
|
const RATE_LIMIT_FIELDS = ['tokens', 'refillIntervalMs', 'keyBy'];
|
|
262
296
|
const PASSIVE_SAMPLE_FIELDS = ['every', 'keyBy'];
|
|
263
297
|
const JUDGE_FIELDS = ['debounceMs', 'maxWaitMs', 'instructions'];
|
|
298
|
+
const SUPPRESS_FIELDS = ['description', 'script'];
|
|
264
299
|
function isPlainObject(value) {
|
|
265
300
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
266
301
|
}
|
|
@@ -650,6 +685,7 @@ function readRule(raw, unnamed, file = 'gate.json', named = asGateNames) {
|
|
|
650
685
|
const match = readMatch(r, obj.match);
|
|
651
686
|
const resets = readResets(r, obj.resets);
|
|
652
687
|
const passthrough = readPassthrough(r, obj.passthrough, kind);
|
|
688
|
+
const suppress = readSuppress(r, obj.suppress, obj.behavior, match, obj.match);
|
|
653
689
|
const reading = {
|
|
654
690
|
where: r.where, problems: r.problems, notes: r.notes, quiet: kind === 'skip' || kind === 'defer' || r.noTokens === true,
|
|
655
691
|
};
|
|
@@ -659,6 +695,8 @@ function readRule(raw, unnamed, file = 'gate.json', named = asGateNames) {
|
|
|
659
695
|
policy.resets = resets;
|
|
660
696
|
if (passthrough)
|
|
661
697
|
policy.passthrough = true;
|
|
698
|
+
if (suppress)
|
|
699
|
+
policy.suppress = suppress;
|
|
662
700
|
reading.policy = policy;
|
|
663
701
|
if (r.matchesAll)
|
|
664
702
|
reading.matchesAll = true;
|
|
@@ -1071,8 +1109,7 @@ function readMatch(r, value) {
|
|
|
1071
1109
|
// Left with nothing that narrows it once something in it was named, it
|
|
1072
1110
|
// matches every event. Empty lists and nulls alone always meant no
|
|
1073
1111
|
// constraint, and name nothing.
|
|
1074
|
-
|
|
1075
|
-
if (!narrows && r.problems.length + r.notes.length > named)
|
|
1112
|
+
if (!matchNarrows(match) && r.problems.length + r.notes.length > named)
|
|
1076
1113
|
r.matchesAll = true;
|
|
1077
1114
|
return match;
|
|
1078
1115
|
}
|
|
@@ -1120,6 +1157,137 @@ function readPassthrough(r, value, kind) {
|
|
|
1120
1157
|
});
|
|
1121
1158
|
return false;
|
|
1122
1159
|
}
|
|
1160
|
+
/** Whether a match narrows what it matches: anything but an empty scope. */
|
|
1161
|
+
export function matchNarrows(match) {
|
|
1162
|
+
return Object.entries(match).some(([field, v]) => !(field === 'scope' && Array.isArray(v) && v.length === 0));
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Whether a suppress rule's match names what it hides: an event type, or a
|
|
1166
|
+
* source, channel, mount, path or tag pattern that isn't only wildcards
|
|
1167
|
+
* (namesSomething), a metadata flag, or a content filter. A pattern of
|
|
1168
|
+
* wildcards alone, such as `source: "*"`, or only `tagsNone`, takes every
|
|
1169
|
+
* event, which a suppress rule may not.
|
|
1170
|
+
*/
|
|
1171
|
+
function suppressMatchNarrows(match) {
|
|
1172
|
+
if ((match.scope ?? []).length > 0 || (match.metadataTrue ?? []).length > 0 || match.filter !== undefined)
|
|
1173
|
+
return true;
|
|
1174
|
+
if ([match.source, match.channel, match.mount, match.pathGlob].some(namesSomething))
|
|
1175
|
+
return true;
|
|
1176
|
+
return [...(match.tagsAny ?? []), ...(match.tagsAll ?? [])].some(namesSomething);
|
|
1177
|
+
}
|
|
1178
|
+
/** Longest suppress description, in characters: the review sends it with every rule it judges. */
|
|
1179
|
+
export const MAX_SUPPRESS_DESCRIPTION = 2_000;
|
|
1180
|
+
/** Longest suppress script, in characters. */
|
|
1181
|
+
export const MAX_SUPPRESS_SCRIPT = 20_000;
|
|
1182
|
+
const SUPPRESS_EXAMPLE = '{"description": "the eye going on and off"}';
|
|
1183
|
+
/**
|
|
1184
|
+
* A rule's `suppress`. Every problem with it is read as a rule that doesn't
|
|
1185
|
+
* suppress, which is how a release without suppression reads the rule:
|
|
1186
|
+
* its behavior then decides the event, and the event is rendered. The
|
|
1187
|
+
* behavior has to be written out as "defer" (or "skip"), not left to
|
|
1188
|
+
* default, because such a release reads a rule with no behavior as
|
|
1189
|
+
* "always" and would wake the agent on every event the rule was meant to
|
|
1190
|
+
* hide.
|
|
1191
|
+
*/
|
|
1192
|
+
function readSuppress(r, value, behavior, match, rawMatch) {
|
|
1193
|
+
if (value === undefined || value === false)
|
|
1194
|
+
return undefined;
|
|
1195
|
+
const without = "so the rule doesn't suppress, and its behavior decides its events as for any other rule";
|
|
1196
|
+
if (!isPlainObject(value)) {
|
|
1197
|
+
r.problems.push({ text: `suppress is ${shownValue(value)}, where an object such as ${SUPPRESS_EXAMPLE} is needed`, read: without });
|
|
1198
|
+
return undefined;
|
|
1199
|
+
}
|
|
1200
|
+
const before = r.problems.length;
|
|
1201
|
+
// A script given as null or "" is a script that's missing, not one left
|
|
1202
|
+
// out: read as left out, the rule would hide every event its match takes.
|
|
1203
|
+
const { script: _givenScript, ...rest } = value;
|
|
1204
|
+
const emptyScript = Object.hasOwn(value, 'script') && (value.script === null || value.script === '');
|
|
1205
|
+
if (emptyScript) {
|
|
1206
|
+
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 });
|
|
1207
|
+
}
|
|
1208
|
+
const given = withoutEmpty(`${r.where}: suppress`, emptyScript ? rest : value, r.notes);
|
|
1209
|
+
const reported = r.problems.length;
|
|
1210
|
+
readFieldNames(r, 'suppress', given, SUPPRESS_FIELDS);
|
|
1211
|
+
// A field it doesn't read can't be left aside here, as elsewhere in a
|
|
1212
|
+
// rule: one meant as a condition, left out, would hide every event the
|
|
1213
|
+
// match takes.
|
|
1214
|
+
const unread = Object.keys(given).filter((key) => !SUPPRESS_FIELDS.includes(key));
|
|
1215
|
+
if (unread.length > 0 && r.problems.length === reported) {
|
|
1216
|
+
r.problems.push({ text: `suppress has ${quotedList(unread)}, and a suppress rule reads only "description" and "script"`, read: without });
|
|
1217
|
+
}
|
|
1218
|
+
const config = { description: '' };
|
|
1219
|
+
if (typeof given.description !== 'string' || given.description.trim() === '') {
|
|
1220
|
+
r.problems.push({
|
|
1221
|
+
text: `suppress.description is ${given.description === undefined ? 'missing' : shownValue(given.description)}, where the rule's `
|
|
1222
|
+
+ 'purpose in a few words is needed: the review checks what the rule hides against it',
|
|
1223
|
+
read: without,
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
else if ([...given.description].length > MAX_SUPPRESS_DESCRIPTION) {
|
|
1227
|
+
r.problems.push({
|
|
1228
|
+
text: `suppress.description is ${[...given.description].length.toLocaleString('en-US')} characters, over the `
|
|
1229
|
+
+ `${MAX_SUPPRESS_DESCRIPTION.toLocaleString('en-US')} allowed`,
|
|
1230
|
+
read: without,
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
else {
|
|
1234
|
+
config.description = given.description.trim();
|
|
1235
|
+
}
|
|
1236
|
+
if (given.script !== undefined) {
|
|
1237
|
+
if (typeof given.script !== 'string' || given.script.trim() === '') {
|
|
1238
|
+
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 });
|
|
1239
|
+
}
|
|
1240
|
+
else if ([...given.script].length > MAX_SUPPRESS_SCRIPT) {
|
|
1241
|
+
r.problems.push({
|
|
1242
|
+
text: `suppress.script is ${[...given.script].length.toLocaleString('en-US')} characters, over the ${MAX_SUPPRESS_SCRIPT.toLocaleString('en-US')} allowed`,
|
|
1243
|
+
read: without,
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
else {
|
|
1247
|
+
// Parsed only, never run here: the script runs on its worker.
|
|
1248
|
+
try {
|
|
1249
|
+
new Function(`"use strict";\nreturn (${given.script}\n);`);
|
|
1250
|
+
config.script = given.script;
|
|
1251
|
+
}
|
|
1252
|
+
catch (e) {
|
|
1253
|
+
r.problems.push({ text: `suppress.script doesn't parse (${quotedValue(errorText(e))})`, read: without });
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
if (behavior !== 'defer' && behavior !== 'skip') {
|
|
1258
|
+
r.problems.push({
|
|
1259
|
+
text: `suppress is given, and a rule that suppresses needs "behavior": "defer" written out, so a release that `
|
|
1260
|
+
+ `doesn't know "suppress" defers its events instead of waking you on them; it has `
|
|
1261
|
+
+ (behavior === undefined ? 'no behavior' : `behavior ${shownValue(behavior)}`),
|
|
1262
|
+
read: without,
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
// A match field the gate doesn't read would, left aside, hide more than was
|
|
1266
|
+
// written: the condition it was meant to add is gone.
|
|
1267
|
+
if (isPlainObject(rawMatch)) {
|
|
1268
|
+
const rawFilter = isPlainObject(rawMatch.filter) ? Object.keys(rawMatch.filter).filter((key) => !FILTER_FIELDS.includes(key)).map((key) => `filter.${key}`) : [];
|
|
1269
|
+
const unreadMatch = [...Object.keys(rawMatch).filter((key) => !MATCH_FIELDS.includes(key)), ...rawFilter];
|
|
1270
|
+
if (unreadMatch.length > 0) {
|
|
1271
|
+
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 });
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
if (!suppressMatchNarrows(match)) {
|
|
1275
|
+
r.problems.push({
|
|
1276
|
+
text: 'suppress is given, and the rule\'s match names nothing it hides: a suppress rule needs a match that names '
|
|
1277
|
+
+ 'a scope, a source, a channel, tags, a mount or path, metadata or a filter (not a pattern of wildcards alone, or tagsNone alone), '
|
|
1278
|
+
+ 'so it can\'t hide everything',
|
|
1279
|
+
read: without,
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
// Only a rule read without any problem suppresses: one read past a problem
|
|
1283
|
+
// (a slip of a field, a match read without a part of it) matches more, or
|
|
1284
|
+
// decides otherwise, than its author wrote, and hiding is what can't be
|
|
1285
|
+
// seen to go wrong. Its behavior decides its events instead.
|
|
1286
|
+
if (before > 0) {
|
|
1287
|
+
r.problems.push({ text: 'suppress is given on a rule with problems, and a rule suppresses only when it has none', read: without });
|
|
1288
|
+
}
|
|
1289
|
+
return r.problems.length === 0 ? config : undefined;
|
|
1290
|
+
}
|
|
1123
1291
|
/**
|
|
1124
1292
|
* A rule with no problem is in force as written. One with a problem is read
|
|
1125
1293
|
* past it, the way earlier releases read it, so it wakes the agent wherever
|
|
@@ -1266,7 +1434,7 @@ function readConfig(raw, file, start) {
|
|
|
1266
1434
|
}
|
|
1267
1435
|
}
|
|
1268
1436
|
for (const rule of rules)
|
|
1269
|
-
sentences
|
|
1437
|
+
appendAll(sentences, readingSentences(rule.reading, rule.outcome));
|
|
1270
1438
|
// A list with no rules, or none the gate reads, where no sentence above said it has none.
|
|
1271
1439
|
if (notUsed !== undefined && !sentences.some((said) => said.includes(notUsed))) {
|
|
1272
1440
|
sentences.push(`${File} has no rules${rules.length > 0 ? ' but ones the gate leaves out' : ''}, so ${notUsedWhy}.`);
|
|
@@ -1613,7 +1781,9 @@ export function findShadowedPolicies(policies, eventTypesFrom) {
|
|
|
1613
1781
|
const out = [];
|
|
1614
1782
|
for (let i = 0; i < policies.length; i++) {
|
|
1615
1783
|
const earlier = policies[i];
|
|
1616
|
-
|
|
1784
|
+
// A suppress rule lets addressed events and the heartbeat's by, so it
|
|
1785
|
+
// can't be said to take every event of a later rule.
|
|
1786
|
+
if (earlier.passthrough || earlier.suppress)
|
|
1617
1787
|
continue;
|
|
1618
1788
|
for (let j = i + 1; j < policies.length; j++) {
|
|
1619
1789
|
const found = subsumedEventTypes(earlier, policies[j], eventTypesFrom);
|
|
@@ -1645,15 +1815,53 @@ export function formatShadowWarning(w) {
|
|
|
1645
1815
|
+ `Nothing to do unless ${later} is meant for ${types} events; then ${move}. `
|
|
1646
1816
|
+ `A scope on ${later} naming the event types it's for clears this note.`;
|
|
1647
1817
|
}
|
|
1818
|
+
function judgeFailureWords(err) {
|
|
1819
|
+
if (err instanceof JudgeFailure)
|
|
1820
|
+
return err.message;
|
|
1821
|
+
return err instanceof Error ? failureWords(err, 'the call failed') : 'the call failed';
|
|
1822
|
+
}
|
|
1648
1823
|
/** The text of a thrown value, for a log line. It never throws itself, whatever was thrown. */
|
|
1649
1824
|
function errorText(err) {
|
|
1650
1825
|
try {
|
|
1826
|
+
if (err instanceof JudgeFailure && err.detail !== undefined)
|
|
1827
|
+
return `${err.message} (${err.detail})`;
|
|
1651
1828
|
return err instanceof Error ? err.message : String(err);
|
|
1652
1829
|
}
|
|
1653
1830
|
catch {
|
|
1654
1831
|
return 'a value that could not be printed';
|
|
1655
1832
|
}
|
|
1656
1833
|
}
|
|
1834
|
+
class PartialDelivery extends Error {
|
|
1835
|
+
woke;
|
|
1836
|
+
notWoken;
|
|
1837
|
+
constructor(woke, notWoken, cause) {
|
|
1838
|
+
super(errorText(cause), { cause });
|
|
1839
|
+
this.woke = woke;
|
|
1840
|
+
this.notWoken = notWoken;
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
/**
|
|
1844
|
+
* Runs a batch's delivery to `woken`, which pushes each agent it wakes to
|
|
1845
|
+
* `woke`: a throw once any is woken is a PartialDelivery naming them and the
|
|
1846
|
+
* rest; one before any wake is thrown as it is.
|
|
1847
|
+
*/
|
|
1848
|
+
function deliveredTracking(woke, woken, deliver) {
|
|
1849
|
+
try {
|
|
1850
|
+
deliver();
|
|
1851
|
+
}
|
|
1852
|
+
catch (err) {
|
|
1853
|
+
if (woke.length === 0)
|
|
1854
|
+
throw err;
|
|
1855
|
+
throw new PartialDelivery(woke, woken.filter((name) => !woke.includes(name)), err);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
function deliveredInPart(err) {
|
|
1859
|
+
if (!(err instanceof PartialDelivery))
|
|
1860
|
+
return undefined;
|
|
1861
|
+
const one = err.notWoken.length === 1;
|
|
1862
|
+
return `woke ${joinedList(err.woke)}, and delivering it threw (${err.message}), `
|
|
1863
|
+
+ `so it's dropped for ${joinedList(err.notWoken)}, who ${one ? "isn't" : "aren't"} woken`;
|
|
1864
|
+
}
|
|
1657
1865
|
/**
|
|
1658
1866
|
* Run a caller's callback once the change it's told of is made and traced. A
|
|
1659
1867
|
* throw from it is logged, not thrown, so the call answers as the change it
|
|
@@ -1766,6 +1974,8 @@ export class EventGate {
|
|
|
1766
1974
|
droppedBufferedEvents = 0;
|
|
1767
1975
|
// Per-policy stats
|
|
1768
1976
|
stats = new Map();
|
|
1977
|
+
/** Suppress rules' scripts and their state (suppress-scripts.ts), kept beside gate.json. */
|
|
1978
|
+
suppressScripts;
|
|
1769
1979
|
// The shadow lint's last findings (see refreshShadowWarnings), recomputed
|
|
1770
1980
|
// on every config change and fingerprinted so an unchanged set doesn't
|
|
1771
1981
|
// re-trace.
|
|
@@ -1823,6 +2033,11 @@ export class EventGate {
|
|
|
1823
2033
|
now;
|
|
1824
2034
|
constructor(opts) {
|
|
1825
2035
|
this.configPath = opts.configPath;
|
|
2036
|
+
// Before the rules are read: a start's merge can clear a rule's state, its script's included.
|
|
2037
|
+
this.suppressScripts = new SuppressScripts({
|
|
2038
|
+
statePath: join(dirname(opts.configPath), 'suppress-state.json'),
|
|
2039
|
+
...(opts.suppressScriptTimeoutMs !== undefined ? { timeoutMs: opts.suppressScriptTimeoutMs } : {}),
|
|
2040
|
+
});
|
|
1826
2041
|
this.privilegedUsersPath = opts.privilegedUsersPath;
|
|
1827
2042
|
this.emitTrace = opts.emitTrace;
|
|
1828
2043
|
this.addMessageFn = opts.addMessage;
|
|
@@ -2108,7 +2323,6 @@ export class EventGate {
|
|
|
2108
2323
|
...changes,
|
|
2109
2324
|
// Every name the start added to gate.json, as an append's trace gave.
|
|
2110
2325
|
addedPolicies: names,
|
|
2111
|
-
timestamp: Date.now(),
|
|
2112
2326
|
});
|
|
2113
2327
|
}
|
|
2114
2328
|
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 +2422,6 @@ export class EventGate {
|
|
|
2208
2422
|
type: 'gate:config-error',
|
|
2209
2423
|
error: why,
|
|
2210
2424
|
configPath: this.configPath,
|
|
2211
|
-
timestamp: Date.now(),
|
|
2212
2425
|
});
|
|
2213
2426
|
}
|
|
2214
2427
|
this.configError = problem;
|
|
@@ -2358,8 +2571,11 @@ export class EventGate {
|
|
|
2358
2571
|
clearStateFor(was, next) {
|
|
2359
2572
|
const behaviorsByName = (config) => {
|
|
2360
2573
|
const byName = new Map();
|
|
2361
|
-
for (const p of config.policies)
|
|
2362
|
-
|
|
2574
|
+
for (const p of config.policies) {
|
|
2575
|
+
// A suppress rule's script is part of what it is: a changed script starts from a fresh state.
|
|
2576
|
+
const fingerprint = behaviorFingerprint(p.behavior) + (p.suppress ? ` suppress:${p.suppress.script ?? ''}` : '');
|
|
2577
|
+
byName.set(p.name, [...(byName.get(p.name) ?? []), fingerprint]);
|
|
2578
|
+
}
|
|
2363
2579
|
return new Map([...byName].map(([name, behaviors]) => [name, JSON.stringify(behaviors)]));
|
|
2364
2580
|
};
|
|
2365
2581
|
const before = behaviorsByName(was);
|
|
@@ -2412,7 +2628,6 @@ export class EventGate {
|
|
|
2412
2628
|
policyCount: newConfig.policies.length,
|
|
2413
2629
|
removedPolicies: removedPolicies.length > 0 ? removedPolicies : undefined,
|
|
2414
2630
|
stateClearedPolicies: stateClearedPolicies.length > 0 ? stateClearedPolicies : undefined,
|
|
2415
|
-
timestamp: now,
|
|
2416
2631
|
});
|
|
2417
2632
|
}
|
|
2418
2633
|
catch {
|
|
@@ -2523,7 +2738,6 @@ export class EventGate {
|
|
|
2523
2738
|
configPath: this.configPath,
|
|
2524
2739
|
policyName: policy.name,
|
|
2525
2740
|
replaced: idx >= 0,
|
|
2526
|
-
timestamp: Date.now(),
|
|
2527
2741
|
});
|
|
2528
2742
|
if (ignored.length > 0) {
|
|
2529
2743
|
// A caller that doesn't take the notes still gets them said, in the log.
|
|
@@ -2570,7 +2784,6 @@ export class EventGate {
|
|
|
2570
2784
|
type: 'gate:policy-removed',
|
|
2571
2785
|
configPath: this.configPath,
|
|
2572
2786
|
policyName: name,
|
|
2573
|
-
timestamp: Date.now(),
|
|
2574
2787
|
});
|
|
2575
2788
|
return true;
|
|
2576
2789
|
}
|
|
@@ -2585,6 +2798,15 @@ export class EventGate {
|
|
|
2585
2798
|
const current = read.state === 'loaded' ? read.config : this.config;
|
|
2586
2799
|
return current.policies.map((p) => p.name);
|
|
2587
2800
|
}
|
|
2801
|
+
/** The rules in force, in order (copies): what the last decision was made by. */
|
|
2802
|
+
policiesInForce() {
|
|
2803
|
+
return this.config.policies.map((p) => structuredClone(p));
|
|
2804
|
+
}
|
|
2805
|
+
/** The rule in force by this name (a copy), or undefined. */
|
|
2806
|
+
policyInForce(name) {
|
|
2807
|
+
const p = this.config.policies.find((x) => x.name === name);
|
|
2808
|
+
return p ? structuredClone(p) : undefined;
|
|
2809
|
+
}
|
|
2588
2810
|
/** @internal For `wake_add_rule`'s and `wake_remove_rule`'s refusals; not a host API. */
|
|
2589
2811
|
ruleNamesForChange() {
|
|
2590
2812
|
const edit = this.rulesForChange();
|
|
@@ -2763,7 +2985,6 @@ export class EventGate {
|
|
|
2763
2985
|
matchedPolicy: 'asleep',
|
|
2764
2986
|
trigger: false,
|
|
2765
2987
|
behavior: 'skip',
|
|
2766
|
-
timestamp: this.now(),
|
|
2767
2988
|
});
|
|
2768
2989
|
return { trigger: false, policyName: 'asleep', behavior: 'skip' };
|
|
2769
2990
|
}
|
|
@@ -2786,6 +3007,12 @@ export class EventGate {
|
|
|
2786
3007
|
}
|
|
2787
3008
|
else {
|
|
2788
3009
|
for (const compiled of this.compiledPolicies) {
|
|
3010
|
+
// A suppress rule doesn't decide a wake for an event suppression was
|
|
3011
|
+
// asked about: one it takes was suppressed before the gate was asked,
|
|
3012
|
+
// and one that reaches here is one it let by. Where suppression wasn't
|
|
3013
|
+
// asked (no view to hide it from), it defers, as its behavior says.
|
|
3014
|
+
if (compiled.policy.suppress && this.passesOverSuppress(info))
|
|
3015
|
+
continue;
|
|
2789
3016
|
if (!this.compiledMatches(compiled, info))
|
|
2790
3017
|
continue;
|
|
2791
3018
|
const candidate = this.computeDecision(compiled.policy, info);
|
|
@@ -2822,7 +3049,6 @@ export class EventGate {
|
|
|
2822
3049
|
trigger: decision.trigger,
|
|
2823
3050
|
behavior: formatBehavior(decision.behavior),
|
|
2824
3051
|
...(observed.length > 0 ? { observed } : {}),
|
|
2825
|
-
timestamp: this.now(),
|
|
2826
3052
|
});
|
|
2827
3053
|
return decision;
|
|
2828
3054
|
}
|
|
@@ -2843,7 +3069,6 @@ export class EventGate {
|
|
|
2843
3069
|
behavior: formatBehavior(policy.behavior),
|
|
2844
3070
|
eventType: info.eventType,
|
|
2845
3071
|
source: info.serverId || undefined,
|
|
2846
|
-
timestamp: this.now(),
|
|
2847
3072
|
});
|
|
2848
3073
|
if (policy.behavior === 'defer' || policy.behavior === 'skip') {
|
|
2849
3074
|
// Report the configured name faithfully ('defer' or legacy 'skip').
|
|
@@ -2975,6 +3200,7 @@ export class EventGate {
|
|
|
2975
3200
|
const debounce = this.debounceTimers.get(name);
|
|
2976
3201
|
if (debounce)
|
|
2977
3202
|
clearTimeout(debounce.timer);
|
|
3203
|
+
this.suppressScripts.forget(name);
|
|
2978
3204
|
this.rateLimitBuckets.delete(name);
|
|
2979
3205
|
this.passiveSampleCounters.delete(name);
|
|
2980
3206
|
this.rateLimitDenied.delete(name);
|
|
@@ -3032,6 +3258,8 @@ export class EventGate {
|
|
|
3032
3258
|
*/
|
|
3033
3259
|
probe(info) {
|
|
3034
3260
|
for (const compiled of this.compiledPolicies) {
|
|
3261
|
+
if (compiled.policy.suppress && this.passesOverSuppress(info))
|
|
3262
|
+
continue; // as evaluate does
|
|
3035
3263
|
if (!this.compiledMatches(compiled, info))
|
|
3036
3264
|
continue;
|
|
3037
3265
|
const trigger = this.wouldTrigger(compiled.policy, info);
|
|
@@ -3042,6 +3270,140 @@ export class EventGate {
|
|
|
3042
3270
|
const trigger = (this.config.default ?? 'always') === 'always';
|
|
3043
3271
|
return { trigger, policyName: null, behavior: this.config.default ?? 'always' };
|
|
3044
3272
|
}
|
|
3273
|
+
/**
|
|
3274
|
+
* The suppress rule that takes this event, or null when none does.
|
|
3275
|
+
* Side-effect-free, like probe: no counter moves, no batch forms, nothing
|
|
3276
|
+
* is traced, and a rule's script runs on a copy of its state, which is
|
|
3277
|
+
* then dropped. (A closure a script builds for itself is its own, and a
|
|
3278
|
+
* 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
|
|
3279
|
+
* framework asks decideSuppression instead, once per event.
|
|
3280
|
+
*
|
|
3281
|
+
* It reads the rules as evaluate does, first match deciding: a rule ahead
|
|
3282
|
+
* of the suppress rule that matches the event decides it instead (it isn't
|
|
3283
|
+
* suppressed), unless it's a passthrough rule that wouldn't fire. A rule
|
|
3284
|
+
* with a script (`suppress.script`) takes an event its match takes only
|
|
3285
|
+
* when the script says to hide it; otherwise the event goes on to the rules
|
|
3286
|
+
* after it, as one the rule doesn't match does. Sleep and gate.js aren't
|
|
3287
|
+
* consulted: they decide wakes, and suppression decides what's rendered.
|
|
3288
|
+
*
|
|
3289
|
+
* Never suppressed, whatever the rules say: an event addressed to the
|
|
3290
|
+
* agent (tagged `chat:addressed`: a mention, a reply, a DM) and the
|
|
3291
|
+
* heartbeat's (the agent's own clock). A suppress rule lets them by to the
|
|
3292
|
+
* rules after it, and its script isn't asked. The framework itself never
|
|
3293
|
+
* asks about console or API input.
|
|
3294
|
+
*/
|
|
3295
|
+
suppressionFor(info) {
|
|
3296
|
+
return this.walkSuppression(info, false);
|
|
3297
|
+
}
|
|
3298
|
+
/**
|
|
3299
|
+
* suppressionFor, for an event that is arriving now: each script asked
|
|
3300
|
+
* keeps the state it leaves, whatever later rules do with the event, and a
|
|
3301
|
+
* suppressed event counts in the passthrough rules ahead of its rule. Ask
|
|
3302
|
+
* once per event.
|
|
3303
|
+
*/
|
|
3304
|
+
decideSuppression(info) {
|
|
3305
|
+
return this.walkSuppression(info, true);
|
|
3306
|
+
}
|
|
3307
|
+
walkSuppression(info, commit) {
|
|
3308
|
+
this.reloadIfChanged();
|
|
3309
|
+
this.syncSuppressScripts();
|
|
3310
|
+
if (info.serverId === 'heartbeat')
|
|
3311
|
+
return null;
|
|
3312
|
+
const addressed = Array.isArray(info.tags) && info.tags.includes('chat:addressed');
|
|
3313
|
+
let event;
|
|
3314
|
+
const observers = [];
|
|
3315
|
+
for (const compiled of this.compiledPolicies) {
|
|
3316
|
+
if (!this.compiledMatches(compiled, info))
|
|
3317
|
+
continue;
|
|
3318
|
+
const suppress = compiled.policy.suppress;
|
|
3319
|
+
if (suppress) {
|
|
3320
|
+
if (addressed)
|
|
3321
|
+
continue;
|
|
3322
|
+
if (suppress.script !== undefined) {
|
|
3323
|
+
event ??= this.suppressScriptEvent(info);
|
|
3324
|
+
const ran = this.suppressScripts.run(compiled.policy.name, event, commit);
|
|
3325
|
+
// A script that failed shows the event: it goes on to the rules after.
|
|
3326
|
+
if (!ran || !('hide' in ran) || !ran.hide)
|
|
3327
|
+
continue;
|
|
3328
|
+
}
|
|
3329
|
+
// A suppressed event never reaches evaluate, so the passthrough rules
|
|
3330
|
+
// ahead of this one count it here, as evaluate would have: none of
|
|
3331
|
+
// them fires for it, and each fires on its schedule. A dry run counts
|
|
3332
|
+
// nothing.
|
|
3333
|
+
if (commit)
|
|
3334
|
+
for (const observer of observers)
|
|
3335
|
+
this.computeDecision(observer, info);
|
|
3336
|
+
return compiled.policy.name;
|
|
3337
|
+
}
|
|
3338
|
+
if (compiled.policy.passthrough && !this.wouldTrigger(compiled.policy, info)) {
|
|
3339
|
+
observers.push(compiled.policy);
|
|
3340
|
+
continue;
|
|
3341
|
+
}
|
|
3342
|
+
return null;
|
|
3343
|
+
}
|
|
3344
|
+
return null;
|
|
3345
|
+
}
|
|
3346
|
+
/** 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. */
|
|
3347
|
+
passesOverSuppress(info) {
|
|
3348
|
+
return info.suppressionAsked === true || info.serverId === 'heartbeat'
|
|
3349
|
+
|| (Array.isArray(info.tags) && info.tags.includes('chat:addressed'));
|
|
3350
|
+
}
|
|
3351
|
+
/** The scripts in step with the rules in force (SuppressScripts.sync: only changed ones are compiled). */
|
|
3352
|
+
syncSuppressScripts() {
|
|
3353
|
+
this.suppressScripts.sync(this.config.policies.map((p) => ({ name: p.name, ...(p.suppress?.script !== undefined ? { script: p.suppress.script } : {}) })));
|
|
3354
|
+
}
|
|
3355
|
+
/** What a suppress script is given for an event: plain JSON, its metadata included where it can be. */
|
|
3356
|
+
suppressScriptEvent(info) {
|
|
3357
|
+
let metadata = {};
|
|
3358
|
+
try {
|
|
3359
|
+
metadata = JSON.parse(JSON.stringify(info.metadata ?? {}));
|
|
3360
|
+
}
|
|
3361
|
+
catch {
|
|
3362
|
+
metadata = {};
|
|
3363
|
+
}
|
|
3364
|
+
const channel = this.routeChannelFor(info) ?? (info.channelId || undefined);
|
|
3365
|
+
const author = this.extractAuthorId(info.metadata);
|
|
3366
|
+
return {
|
|
3367
|
+
eventType: info.eventType,
|
|
3368
|
+
source: info.serverId,
|
|
3369
|
+
...(channel ? { channel } : {}),
|
|
3370
|
+
...(author ? { author } : {}),
|
|
3371
|
+
tags: [...(info.tags ?? [])],
|
|
3372
|
+
text: info.content,
|
|
3373
|
+
at: this.now(),
|
|
3374
|
+
metadata,
|
|
3375
|
+
};
|
|
3376
|
+
}
|
|
3377
|
+
/**
|
|
3378
|
+
* Whether a suppress script compiles to a function, on the scripts' worker:
|
|
3379
|
+
* an error's text, or null. wake_add_rule asks before it saves a rule.
|
|
3380
|
+
*/
|
|
3381
|
+
checkSuppressScript(script) {
|
|
3382
|
+
return this.suppressScripts.check(script);
|
|
3383
|
+
}
|
|
3384
|
+
/** A suppress rule's script state as it stands (a copy), for a host or a test. */
|
|
3385
|
+
suppressScriptState(name) {
|
|
3386
|
+
this.syncSuppressScripts();
|
|
3387
|
+
return this.suppressScripts.stateOf(name);
|
|
3388
|
+
}
|
|
3389
|
+
/**
|
|
3390
|
+
* Count a row the framework suppressed by `name` (suppressionFor's
|
|
3391
|
+
* answer) once it's written. The rule's stats count it as a match, so
|
|
3392
|
+
* gate_status shows how many events each suppress rule took since start.
|
|
3393
|
+
*/
|
|
3394
|
+
noteSuppressed(name, info) {
|
|
3395
|
+
const policyStats = this.stats.get(name) ?? { matchCount: 0, lastMatchTimestamp: null };
|
|
3396
|
+
policyStats.matchCount++;
|
|
3397
|
+
policyStats.lastMatchTimestamp = this.now();
|
|
3398
|
+
this.stats.set(name, policyStats);
|
|
3399
|
+
this.emitTrace({
|
|
3400
|
+
type: 'gate:suppressed',
|
|
3401
|
+
policyName: name,
|
|
3402
|
+
eventType: info.eventType,
|
|
3403
|
+
...(info.serverId ? { serverId: info.serverId } : {}),
|
|
3404
|
+
...(info.channelId ? { channelId: info.channelId } : {}),
|
|
3405
|
+
});
|
|
3406
|
+
}
|
|
3045
3407
|
/** The canonical synthetic events the probe table runs: chat shapes as MCPL delivers them, and a heartbeat. */
|
|
3046
3408
|
probeEvents() {
|
|
3047
3409
|
const chat = (label, eventType, metadata, tags) => ({
|
|
@@ -3100,7 +3462,12 @@ export class EventGate {
|
|
|
3100
3462
|
*/
|
|
3101
3463
|
probeTable() {
|
|
3102
3464
|
return this.probeEvents().map(({ label, info }) => {
|
|
3103
|
-
const
|
|
3465
|
+
const suppressedBy = this.suppressionFor(info);
|
|
3466
|
+
if (suppressedBy !== null) {
|
|
3467
|
+
return { probe: label, eventType: info.eventType, policy: suppressedBy, behavior: 'suppress', wouldWake: false };
|
|
3468
|
+
}
|
|
3469
|
+
// As admission asks: suppression first, so a script that says show lets the probe on.
|
|
3470
|
+
const d = this.probe({ ...info, suppressionAsked: true });
|
|
3104
3471
|
return {
|
|
3105
3472
|
probe: label,
|
|
3106
3473
|
eventType: info.eventType,
|
|
@@ -3147,7 +3514,6 @@ export class EventGate {
|
|
|
3147
3514
|
type: 'gate:shadow-warnings',
|
|
3148
3515
|
reason,
|
|
3149
3516
|
warnings: warnings.map(formatShadowWarning),
|
|
3150
|
-
timestamp: this.now(),
|
|
3151
3517
|
});
|
|
3152
3518
|
}
|
|
3153
3519
|
}
|
|
@@ -3201,9 +3567,11 @@ export class EventGate {
|
|
|
3201
3567
|
authorId: this.extractAuthorId(info.metadata) ?? undefined,
|
|
3202
3568
|
serverId: info.serverId || undefined,
|
|
3203
3569
|
addressed: Array.isArray(info.tags) && info.tags.includes('chat:addressed'),
|
|
3570
|
+
...(isSystemNotice(info.tags) && !info.folds ? { systemNotice: true } : {}),
|
|
3204
3571
|
routeChannelId: this.routeChannelFor(info),
|
|
3205
3572
|
...(typeof info.metadata?.messageId === 'string' ? { messageId: info.metadata.messageId } : {}),
|
|
3206
3573
|
...(typeof info.metadata?.eventId === 'string' ? { eventId: info.metadata.eventId } : {}),
|
|
3574
|
+
...(info.recipients !== undefined ? { recipients: [...info.recipients] } : {}),
|
|
3207
3575
|
};
|
|
3208
3576
|
const existing = this.debounceTimers.get(policy.name);
|
|
3209
3577
|
// A judge's maxWaitMs caps how far new events push the batch back: it's
|
|
@@ -3258,15 +3626,14 @@ export class EventGate {
|
|
|
3258
3626
|
}
|
|
3259
3627
|
catch (err) {
|
|
3260
3628
|
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`);
|
|
3629
|
+
console.error(`[gate] Rule ${quotedValue(policyName)}: its pending batch of ${events.length} ${one ? 'event' : 'events'} `
|
|
3630
|
+
+ (deliveredInPart(err) ?? `couldn't be delivered (${errorText(err)}), so the batch is dropped`));
|
|
3263
3631
|
return;
|
|
3264
3632
|
}
|
|
3265
3633
|
this.emitTrace({
|
|
3266
3634
|
type: 'gate:debounce-delivered',
|
|
3267
3635
|
policyName,
|
|
3268
3636
|
eventCount: events.length,
|
|
3269
|
-
timestamp: Date.now(),
|
|
3270
3637
|
});
|
|
3271
3638
|
}
|
|
3272
3639
|
// =========================================================================
|
|
@@ -3286,11 +3653,37 @@ export class EventGate {
|
|
|
3286
3653
|
type: 'gate:held-dropped',
|
|
3287
3654
|
dropped: batched.length - events.length,
|
|
3288
3655
|
channels: [...new Set(batched.filter((e) => !events.includes(e)).map(heldAt))],
|
|
3289
|
-
timestamp: this.now(),
|
|
3290
3656
|
});
|
|
3291
3657
|
}
|
|
3292
3658
|
if (events.length === 0)
|
|
3293
3659
|
return;
|
|
3660
|
+
// Who each delivery woke, so that a throw after a wake is told as a
|
|
3661
|
+
// delivery in part (PartialDelivery), the batch dropped only for those
|
|
3662
|
+
// it didn't wake.
|
|
3663
|
+
const woke = [];
|
|
3664
|
+
if (events.every((e) => e.recipients === undefined)) {
|
|
3665
|
+
const names = this.getAgentNamesFn();
|
|
3666
|
+
deliveredTracking(woke, names, () => this.deliverBatch(events, names, undefined, woke));
|
|
3667
|
+
return;
|
|
3668
|
+
}
|
|
3669
|
+
const woken = new Set(events.flatMap((e) => e.recipients ?? []));
|
|
3670
|
+
if (events.some((e) => e.recipients === undefined))
|
|
3671
|
+
for (const name of this.getAgentNamesFn())
|
|
3672
|
+
woken.add(name);
|
|
3673
|
+
const groups = new Map();
|
|
3674
|
+
for (const agentName of woken) {
|
|
3675
|
+
const seen = events.filter((e) => e.recipients === undefined || e.recipients.includes(agentName));
|
|
3676
|
+
const key = seen.map((e) => events.indexOf(e)).join(',');
|
|
3677
|
+
const group = groups.get(key) ?? { agents: [], events: seen };
|
|
3678
|
+
group.agents.push(agentName);
|
|
3679
|
+
groups.set(key, group);
|
|
3680
|
+
}
|
|
3681
|
+
deliveredTracking(woke, [...woken], () => {
|
|
3682
|
+
for (const group of groups.values())
|
|
3683
|
+
this.deliverBatch(group.events, group.agents, group.agents, woke);
|
|
3684
|
+
});
|
|
3685
|
+
}
|
|
3686
|
+
deliverBatch(events, woken, audience, woke) {
|
|
3294
3687
|
const policyNames = [...new Set(events.map(e => e.policyName))];
|
|
3295
3688
|
// Events whose content the framework already stored at arrival (MCPL
|
|
3296
3689
|
// channel-incoming / push-event) get a compact REFERENCE — re-quoting
|
|
@@ -3346,7 +3739,12 @@ export class EventGate {
|
|
|
3346
3739
|
const text = `[${NOTICE_TAG.gate}: ${events.length} event${events.length > 1 ? 's' : ''} matched]\n\n${lines.join('\n')}`;
|
|
3347
3740
|
const only = events.length === 1 && events[0].inContext ? events[0] : undefined;
|
|
3348
3741
|
let line;
|
|
3349
|
-
|
|
3742
|
+
// Where a line stored now landed, for the delivery agent's wake, as the
|
|
3743
|
+
// self-wake's notice places its own (readsRowFor): a live turn that
|
|
3744
|
+
// carried and answered the line drops the wake. Other agents' wakes stay
|
|
3745
|
+
// plain by readsRowFor's rule.
|
|
3746
|
+
const placement = {};
|
|
3747
|
+
if (only && this.newestEntry(only, audience)) {
|
|
3350
3748
|
const shownChannel = only.routeChannelId ?? only.channelId;
|
|
3351
3749
|
line = {
|
|
3352
3750
|
entry: entryRef(only),
|
|
@@ -3360,29 +3758,31 @@ export class EventGate {
|
|
|
3360
3758
|
this.addMessageFn('user', [{ type: 'text', text }], {
|
|
3361
3759
|
source: 'gate:debounce',
|
|
3362
3760
|
policies: policyNames,
|
|
3363
|
-
});
|
|
3761
|
+
}, undefined, placement, audience);
|
|
3364
3762
|
}
|
|
3365
3763
|
// Provenance of the batched wake (who/where woke the agent): the newest
|
|
3366
3764
|
// ADDRESSED event first, else the newest event naming a channel or an
|
|
3367
|
-
// author; channel + author + addressed from
|
|
3368
|
-
// event addressed the agent and its registered
|
|
3369
|
-
// `routeChannelId` names the turn's speech locus too —
|
|
3370
|
-
// back to whoever addressed the agent, not to whichever
|
|
3371
|
-
// any traffic. Ambient-only batches set no locus
|
|
3372
|
-
//
|
|
3373
|
-
//
|
|
3765
|
+
// author, `chat:system` notices aside; channel + author + addressed from
|
|
3766
|
+
// that ONE event. When that event addressed the agent and its registered
|
|
3767
|
+
// channel is known, `routeChannelId` names the turn's speech locus too —
|
|
3768
|
+
// the reply goes back to whoever addressed the agent, not to whichever
|
|
3769
|
+
// channel last saw any traffic. Ambient-only batches set no locus, and
|
|
3770
|
+
// name the channels they came from (`batchChannels`). A batch whose line
|
|
3771
|
+
// the gate left to the turn's start carries it too (`line`), one object
|
|
3772
|
+
// shared by every agent woken with these messages, so it is written once.
|
|
3374
3773
|
const chosen = wakeProvenance(events);
|
|
3375
3774
|
const provenance = line ? { ...chosen, line } : chosen;
|
|
3376
|
-
for (const agentName of
|
|
3377
|
-
this.requestInferenceFn(agentName, 'gate:debounce', 'gate', provenance);
|
|
3775
|
+
for (const agentName of woken) {
|
|
3776
|
+
this.requestInferenceFn(agentName, 'gate:debounce', 'gate', provenance, line ? undefined : placement);
|
|
3777
|
+
woke?.push(agentName);
|
|
3378
3778
|
}
|
|
3379
3779
|
}
|
|
3380
3780
|
/** isNewestEntry for one event; a callback fault keeps the line. */
|
|
3381
|
-
newestEntry(event) {
|
|
3781
|
+
newestEntry(event, recipients) {
|
|
3382
3782
|
if (!this.isNewestEntryFn)
|
|
3383
3783
|
return false;
|
|
3384
3784
|
try {
|
|
3385
|
-
return this.isNewestEntryFn(entryRef(event)) === true;
|
|
3785
|
+
return this.isNewestEntryFn(entryRef(event), recipients) === true;
|
|
3386
3786
|
}
|
|
3387
3787
|
catch {
|
|
3388
3788
|
return false;
|
|
@@ -3405,7 +3805,7 @@ export class EventGate {
|
|
|
3405
3805
|
return;
|
|
3406
3806
|
const deciding = this.judging.get(policyName);
|
|
3407
3807
|
if (deciding) {
|
|
3408
|
-
deciding.queued
|
|
3808
|
+
appendAll(deciding.queued, events);
|
|
3409
3809
|
this.capJudgeEvents(deciding.queued);
|
|
3410
3810
|
return;
|
|
3411
3811
|
}
|
|
@@ -3424,8 +3824,8 @@ export class EventGate {
|
|
|
3424
3824
|
this.judging.set(policyName, state);
|
|
3425
3825
|
const timedOut = new Promise((_, reject) => {
|
|
3426
3826
|
state.timer = setTimeout(() => {
|
|
3827
|
+
reject(new JudgeFailure(`the judge took more than ${this.judgeTimeoutMs} ms`));
|
|
3427
3828
|
abort.abort();
|
|
3428
|
-
reject(new Error(`the judge took more than ${this.judgeTimeoutMs} ms`));
|
|
3429
3829
|
}, this.judgeTimeoutMs);
|
|
3430
3830
|
// Never keep the process alive on the judge's account.
|
|
3431
3831
|
state.timer.unref?.();
|
|
@@ -3456,7 +3856,7 @@ export class EventGate {
|
|
|
3456
3856
|
holdForJudge(policyName, events, judge) {
|
|
3457
3857
|
const held = this.judgeHeld.get(policyName);
|
|
3458
3858
|
if (held) {
|
|
3459
|
-
held.events
|
|
3859
|
+
appendAll(held.events, events);
|
|
3460
3860
|
held.judge = judge;
|
|
3461
3861
|
this.capJudgeEvents(held.events);
|
|
3462
3862
|
}
|
|
@@ -3496,7 +3896,8 @@ export class EventGate {
|
|
|
3496
3896
|
* the judge to weigh what waiting costs: each agent's pending self-wake
|
|
3497
3897
|
* (skip_reply's wake_in_seconds), the end of a sleep, and the pending
|
|
3498
3898
|
* 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).
|
|
3899
|
+
* when it fires; a judge rule's goes to the judge, so it only may). A batch,
|
|
3900
|
+
* and a sleep with no agent named, reach the gate's recipients alone.
|
|
3500
3901
|
*/
|
|
3501
3902
|
upcomingWakes(agentNames, exceptPolicy) {
|
|
3502
3903
|
const wakes = [];
|
|
@@ -3504,20 +3905,22 @@ export class EventGate {
|
|
|
3504
3905
|
for (const name of agentNames) {
|
|
3505
3906
|
const due = this.selfWakeDue.get(name);
|
|
3506
3907
|
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` });
|
|
3908
|
+
wakes.push({ at: due.at, certain: true, kind: 'self-wake', what: `${agentNames.length > 1 ? `${name}'s` : 'its'} own ${due.source} timer` });
|
|
3508
3909
|
}
|
|
3509
3910
|
}
|
|
3510
|
-
|
|
3511
|
-
|
|
3911
|
+
const recipients = this.getAgentNamesFn();
|
|
3912
|
+
const recipient = agentNames.some((name) => recipients.includes(name));
|
|
3913
|
+
if (this.sleepUntil > now && (this.sleepAgent === undefined ? recipient : agentNames.includes(this.sleepAgent))) {
|
|
3914
|
+
wakes.push({ at: this.sleepUntil, certain: true, kind: 'sleep-end', what: 'the end of its sleep' });
|
|
3512
3915
|
}
|
|
3513
3916
|
for (const [name, state] of this.debounceTimers) {
|
|
3514
|
-
if (name === exceptPolicy || state.events.length === 0)
|
|
3917
|
+
if (!recipient || name === exceptPolicy || state.events.length === 0)
|
|
3515
3918
|
continue;
|
|
3516
3919
|
const n = state.events.length;
|
|
3517
3920
|
const batch = `${n} ${n === 1 ? 'event' : 'events'}`;
|
|
3518
3921
|
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}` });
|
|
3922
|
+
? { at: state.dueAt, certain: false, kind: 'judge-batch', what: `rule ${quotedValue(name)}'s batch of ${batch} goes to the judge` }
|
|
3923
|
+
: { at: state.dueAt, certain: true, kind: 'debounce', what: `rule ${quotedValue(name)}'s debounced batch of ${batch}` });
|
|
3521
3924
|
}
|
|
3522
3925
|
return wakes.sort((a, b) => (a.at ?? 0) - (b.at ?? 0));
|
|
3523
3926
|
}
|
|
@@ -3579,7 +3982,7 @@ export class EventGate {
|
|
|
3579
3982
|
}
|
|
3580
3983
|
const outcome = failed ? 'error' : verdict.reply ? 'reply' : 'wait';
|
|
3581
3984
|
const reason = failed
|
|
3582
|
-
? (err !== undefined ?
|
|
3985
|
+
? (err !== undefined ? judgeFailureWords(err) : 'the judge gave no verdict')
|
|
3583
3986
|
: typeof verdict.reason === 'string' ? verdict.reason : '';
|
|
3584
3987
|
const record = this.judgeRecords.get(policyName) ?? { verdicts: { reply: 0, wait: 0, error: 0 } };
|
|
3585
3988
|
record.verdicts[outcome]++;
|
|
@@ -3602,7 +4005,7 @@ export class EventGate {
|
|
|
3602
4005
|
}
|
|
3603
4006
|
const wake = outcome === 'reply' || (outcome === 'error' && this.judgeOnError === 'wake');
|
|
3604
4007
|
if (failed) {
|
|
3605
|
-
console.error(`[gate] Rule ${quotedValue(policyName)}: the reply judge failed (${reason}), so its batch of ${state.events.length} `
|
|
4008
|
+
console.error(`[gate] Rule ${quotedValue(policyName)}: the reply judge failed (${err !== undefined ? errorText(err) : reason}), so its batch of ${state.events.length} `
|
|
3606
4009
|
+ `${state.events.length === 1 ? 'event' : 'events'} ${wake ? 'wakes the agent' : "doesn't wake the agent"} (judge onError "${this.judgeOnError}")`);
|
|
3607
4010
|
}
|
|
3608
4011
|
this.emitTrace({
|
|
@@ -3616,7 +4019,6 @@ export class EventGate {
|
|
|
3616
4019
|
...(!failed && typeof verdict.inputTokens === 'number' ? { inputTokens: verdict.inputTokens } : {}),
|
|
3617
4020
|
...(!failed && typeof verdict.cacheReadTokens === 'number' ? { cacheReadTokens: verdict.cacheReadTokens } : {}),
|
|
3618
4021
|
...(!failed && typeof verdict.outputTokens === 'number' ? { outputTokens: verdict.outputTokens } : {}),
|
|
3619
|
-
timestamp: Date.now(),
|
|
3620
4022
|
});
|
|
3621
4023
|
if (this.disposed)
|
|
3622
4024
|
return;
|
|
@@ -3643,8 +4045,8 @@ export class EventGate {
|
|
|
3643
4045
|
}
|
|
3644
4046
|
catch (err) {
|
|
3645
4047
|
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`);
|
|
4048
|
+
console.error(`[gate] a judged batch of ${events.length} ${one ? 'event' : 'events'} `
|
|
4049
|
+
+ (deliveredInPart(err) ?? `couldn't be delivered (${errorText(err)}), so the batch is dropped`));
|
|
3648
4050
|
}
|
|
3649
4051
|
}
|
|
3650
4052
|
changesJudgedContext(agentName) {
|
|
@@ -3713,8 +4115,8 @@ export class EventGate {
|
|
|
3713
4115
|
* buffer through the same path a turn's end uses. Self-wake and sleep-expiry
|
|
3714
4116
|
* timers deliberately do NOT route through this — their inference requests
|
|
3715
4117
|
* are per-agent targeted pushes that the framework's quiesce wake gate parks
|
|
3716
|
-
* and releases at resume, whereas the buffer's deliverEvents
|
|
3717
|
-
*
|
|
4118
|
+
* and releases at resume, whereas the buffer's deliverEvents wakes every
|
|
4119
|
+
* agent a batch is for.
|
|
3718
4120
|
*/
|
|
3719
4121
|
setQuiesced(quiesced) {
|
|
3720
4122
|
if (this.quiesced === quiesced)
|
|
@@ -3745,8 +4147,12 @@ export class EventGate {
|
|
|
3745
4147
|
}
|
|
3746
4148
|
catch (err) {
|
|
3747
4149
|
const one = events.length === 1;
|
|
3748
|
-
|
|
3749
|
-
|
|
4150
|
+
const held = `${events.length} ${one ? 'event' : 'events'}`;
|
|
4151
|
+
const partly = deliveredInPart(err);
|
|
4152
|
+
console.error(partly
|
|
4153
|
+
? `[gate] a batch of ${held}, held for an agent's turn or the host's quiesce, ${partly}`
|
|
4154
|
+
: `[gate] ${held} held for an agent's turn or the host's quiesce couldn't be delivered (${errorText(err)}), `
|
|
4155
|
+
+ `so ${one ? "it's" : "they're"} dropped`);
|
|
3750
4156
|
}
|
|
3751
4157
|
});
|
|
3752
4158
|
}
|
|
@@ -3815,7 +4221,6 @@ export class EventGate {
|
|
|
3815
4221
|
matchedPolicy: 'asleep',
|
|
3816
4222
|
trigger: false,
|
|
3817
4223
|
behavior: 'skip',
|
|
3818
|
-
timestamp: this.now(),
|
|
3819
4224
|
});
|
|
3820
4225
|
return { until: this.sleepUntil };
|
|
3821
4226
|
}
|
|
@@ -3876,13 +4281,13 @@ export class EventGate {
|
|
|
3876
4281
|
matchedPolicy: 'self-wake',
|
|
3877
4282
|
trigger: true,
|
|
3878
4283
|
behavior: 'always',
|
|
3879
|
-
timestamp: this.now(),
|
|
3880
4284
|
});
|
|
3881
4285
|
// Compact in-window notice: without it the wake turn contains nothing
|
|
3882
4286
|
// new, so the agent can't tell WHAT woke it (its own timer vs. a
|
|
3883
4287
|
// heartbeat) or WHEN. One line, timestamped to the second. Added
|
|
3884
|
-
// before the inference request so it rides the wake turn
|
|
3885
|
-
//
|
|
4288
|
+
// before the inference request so it rides the wake turn: it is stored
|
|
4289
|
+
// at once, and while a turn of the agent's is alive it is kept out of
|
|
4290
|
+
// the window until the agent's next presentation point.
|
|
3886
4291
|
const nowIso = new Date(this.now()).toISOString().slice(0, 19) + 'Z';
|
|
3887
4292
|
// Deliver the notice into the WAKING agent's window — a self-wake
|
|
3888
4293
|
// armed by a non-primary agent (e.g. a subconscious cadence) must not
|
|
@@ -3905,7 +4310,6 @@ export class EventGate {
|
|
|
3905
4310
|
matchedPolicy: 'self-wake',
|
|
3906
4311
|
trigger: false,
|
|
3907
4312
|
behavior: 'skip',
|
|
3908
|
-
timestamp: this.now(),
|
|
3909
4313
|
});
|
|
3910
4314
|
return { inMs: ms, armed: true };
|
|
3911
4315
|
}
|
|
@@ -3953,7 +4357,6 @@ export class EventGate {
|
|
|
3953
4357
|
matchedPolicy: 'asleep',
|
|
3954
4358
|
trigger: true,
|
|
3955
4359
|
behavior: 'always',
|
|
3956
|
-
timestamp: this.now(),
|
|
3957
4360
|
});
|
|
3958
4361
|
const detail = note ? `${reason}: ${note}` : reason;
|
|
3959
4362
|
const targets = agent ? [agent] : this.getAgentNamesFn();
|
|
@@ -4088,6 +4491,7 @@ export class EventGate {
|
|
|
4088
4491
|
return { success: true, data: status };
|
|
4089
4492
|
}
|
|
4090
4493
|
getStatus() {
|
|
4494
|
+
this.syncSuppressScripts();
|
|
4091
4495
|
const policies = this.config.policies.map(p => {
|
|
4092
4496
|
const stats = this.stats.get(p.name);
|
|
4093
4497
|
const debounce = this.debounceTimers.get(p.name);
|
|
@@ -4096,6 +4500,9 @@ export class EventGate {
|
|
|
4096
4500
|
behavior: p.behavior,
|
|
4097
4501
|
matchCount: stats?.matchCount ?? 0,
|
|
4098
4502
|
lastMatchTimestamp: stats?.lastMatchTimestamp ?? null,
|
|
4503
|
+
...(p.suppress
|
|
4504
|
+
? { suppress: { ...p.suppress, ...(p.suppress.script !== undefined && this.suppressScripts.status(p.name) ? { scriptStatus: this.suppressScripts.status(p.name) } : {}) } }
|
|
4505
|
+
: {}),
|
|
4099
4506
|
};
|
|
4100
4507
|
if (typeof p.behavior === 'object' && 'debounce' in p.behavior) {
|
|
4101
4508
|
result.debounceState = {
|
|
@@ -4157,6 +4564,13 @@ export class EventGate {
|
|
|
4157
4564
|
// Fresh, not the cache: the sources the host knows (eventTypesFrom)
|
|
4158
4565
|
// change as servers connect, with no change to the rules.
|
|
4159
4566
|
shadowWarnings: this.lintShadows().map(formatShadowWarning),
|
|
4567
|
+
...(() => {
|
|
4568
|
+
const rule = firstCatchAll(this.config.policies, { lettingBy: true });
|
|
4569
|
+
return rule ? { catchAll: {
|
|
4570
|
+
rule,
|
|
4571
|
+
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.`,
|
|
4572
|
+
} } : {};
|
|
4573
|
+
})(),
|
|
4160
4574
|
totalEvaluations: this.totalEvaluations,
|
|
4161
4575
|
defaultDecisions: {
|
|
4162
4576
|
triggered: this.defaultTriggered,
|
|
@@ -4192,6 +4606,7 @@ export class EventGate {
|
|
|
4192
4606
|
// =========================================================================
|
|
4193
4607
|
dispose() {
|
|
4194
4608
|
this.gateScript.dispose();
|
|
4609
|
+
this.suppressScripts.dispose();
|
|
4195
4610
|
for (const state of this.debounceTimers.values()) {
|
|
4196
4611
|
clearTimeout(state.timer);
|
|
4197
4612
|
}
|