@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
package/src/gate/event-gate.ts
CHANGED
|
@@ -16,16 +16,23 @@ import { cutToFit, quotedJson, quotedValue, suppliedValue } from '../supplied-te
|
|
|
16
16
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
17
17
|
import { basename, dirname, join } from 'node:path';
|
|
18
18
|
import { GateScript } from './gate-script.js';
|
|
19
|
+
import { SuppressScripts, type SuppressScriptEvent } from './suppress-scripts.js';
|
|
20
|
+
import { firstCatchAll, namesSomething } from './rule-scope.js';
|
|
19
21
|
import { canonical, duplicateName, lastRecipePath, noChanges, reconcileLine, reconcileWithRecipe, ruleName, type WrittenConfig } from './recipe-reconcile.js';
|
|
20
22
|
import { closestNames, isNearMiss } from '../closest-names.js';
|
|
21
23
|
import { MAX_TIMER_MS } from '../deadline-timer.js';
|
|
22
24
|
import { safeSlice } from '../safe-slice.js';
|
|
25
|
+
import { failureWords } from '../failure-text.js';
|
|
26
|
+
import { JudgeFailure } from './reply-judge.js';
|
|
27
|
+
import { isSystemNotice } from '../mcpl/tags.js';
|
|
23
28
|
|
|
24
29
|
import type { ToolDefinition } from '../types/events.js';
|
|
30
|
+
import type { TraceEventInput } from '../types/trace.js';
|
|
25
31
|
import type {
|
|
26
32
|
GateConfig,
|
|
27
33
|
GatePolicy,
|
|
28
34
|
GatePolicyMatch,
|
|
35
|
+
SuppressConfig,
|
|
29
36
|
GateBehavior,
|
|
30
37
|
GateDecision,
|
|
31
38
|
GateEntryRef,
|
|
@@ -41,6 +48,7 @@ import type {
|
|
|
41
48
|
JudgeVerdict,
|
|
42
49
|
UpcomingWake,
|
|
43
50
|
} from './types.js';
|
|
51
|
+
import { appendAll } from '../append-all.js';
|
|
44
52
|
|
|
45
53
|
// Re-export types consumers need
|
|
46
54
|
export type {
|
|
@@ -128,7 +136,9 @@ interface PendingEvent {
|
|
|
128
136
|
* composite channel id. */
|
|
129
137
|
channelLabel?: string;
|
|
130
138
|
/** Author id of the event (discord snowflake etc.), when the metadata carries
|
|
131
|
-
* one — a debounced wake
|
|
139
|
+
* one — a debounced wake's counterparty is the author of the event
|
|
140
|
+
* wakeProvenance picks; when it picks none and gives no
|
|
141
|
+
* `batchChannels`, the author of the batch's newest notice that has one. */
|
|
132
142
|
authorId?: string;
|
|
133
143
|
/** MCPL server id the event came from — the namespace for the counterparty
|
|
134
144
|
* (`<serverId>:user:<id>`), same as the direct channel-incoming path. */
|
|
@@ -137,6 +147,7 @@ interface PendingEvent {
|
|
|
137
147
|
* newer ambient chatter when a batched wake picks its provenance — the same
|
|
138
148
|
* rule the framework applies to the turn's speech locus. */
|
|
139
149
|
addressed: boolean;
|
|
150
|
+
systemNotice?: true;
|
|
140
151
|
/** The REGISTERED (composite) channel id the event arrived on, resolved by
|
|
141
152
|
* the host at enqueue time. Differs from `channelId` for push events,
|
|
142
153
|
* whose `channelId` is the adapter's raw id (a bare Discord snowflake for a
|
|
@@ -146,15 +157,20 @@ interface PendingEvent {
|
|
|
146
157
|
* a channel message's id, a push event's event id (isNewestEntry). */
|
|
147
158
|
messageId?: string;
|
|
148
159
|
eventId?: string;
|
|
160
|
+
/** GateEventInfo.recipients. */
|
|
161
|
+
recipients?: readonly string[];
|
|
149
162
|
}
|
|
150
163
|
|
|
151
164
|
/** Where the host put a message the gate wrote: the agent whose window it
|
|
152
|
-
* went to
|
|
153
|
-
*
|
|
165
|
+
* went to; its stored id, once stored (absent while a surgery's hold, or
|
|
166
|
+
* its agent's recovery, keeps the write in memory); and `presented: false`
|
|
167
|
+
* when it wasn't presented to the agent: kept in memory, or stored while a
|
|
168
|
+
* turn of the agent's was alive, or with a presentation at rest that
|
|
169
|
+
* failed (#291's presentation index). The host marks the wake that goes with it from these. */
|
|
154
170
|
export interface WritePlacement {
|
|
155
171
|
agent?: string;
|
|
156
|
-
deferredId?: string;
|
|
157
172
|
messageId?: string;
|
|
173
|
+
presented?: false;
|
|
158
174
|
}
|
|
159
175
|
|
|
160
176
|
/**
|
|
@@ -170,7 +186,13 @@ export interface WakeProvenance {
|
|
|
170
186
|
* channel); the framework derives the composite id on the direct path. */
|
|
171
187
|
channelId?: string;
|
|
172
188
|
/** Server-namespaced author id: `<serverId>:user:<id>` (falls back to the
|
|
173
|
-
* channel id's first segment only when the event carried no server id).
|
|
189
|
+
* channel id's first segment only when the event carried no server id).
|
|
190
|
+
* A `chat:system` notice, unless it's a coalesced render's latest
|
|
191
|
+
* occurrence (GateEventInfo.folds), is never chosen for a batch's route,
|
|
192
|
+
* channel or `addressed` flag. In a batch in which no other event names a
|
|
193
|
+
* channel or an author, and which has no `batchChannels`, the chosen
|
|
194
|
+
* event is its newest notice that has an author, of which the wake names
|
|
195
|
+
* only the author and its time (`at`). */
|
|
174
196
|
counterparty?: string;
|
|
175
197
|
/** True when the chosen event was `chat:addressed`. */
|
|
176
198
|
addressed?: boolean;
|
|
@@ -187,6 +209,19 @@ export interface WakeProvenance {
|
|
|
187
209
|
* event named its server: channel ids are unique only within one
|
|
188
210
|
* server's connection (MCPL §14.2). */
|
|
189
211
|
routeServerId?: string;
|
|
212
|
+
/** The channels a batch came from, each its registered id and server, in
|
|
213
|
+
* the order they first appear, when no event of it addressed the agent:
|
|
214
|
+
* those the host resolved, one or more. A channel some events gave with
|
|
215
|
+
* its server and others without is listed once each way, since only the
|
|
216
|
+
* host can tell they're one. A `chat:system` notice counts
|
|
217
|
+
* neither as addressing the agent nor as a channel, except as a
|
|
218
|
+
* coalesced render's latest occurrence, which the gate takes as a
|
|
219
|
+
* message (GateEventInfo.folds). The wake has no route, and the turn's
|
|
220
|
+
* no-locus notice names them, one given without its server only if its id
|
|
221
|
+
* still names one registered channel when the wake is admitted, unless the
|
|
222
|
+
* turn's own wake named a channel that resolves to none, or the turn is an
|
|
223
|
+
* ephemeral run's, which writes none. */
|
|
224
|
+
batchChannels?: Array<{ channelId: string; serverId?: string }>;
|
|
190
225
|
/** Timestamp (ms) of the chosen event, so a consumer coalescing several
|
|
191
226
|
* requests can order by event recency, not by flush order. */
|
|
192
227
|
at?: number;
|
|
@@ -259,15 +294,6 @@ interface PolicyStats {
|
|
|
259
294
|
lastMatchTimestamp: number | null;
|
|
260
295
|
}
|
|
261
296
|
|
|
262
|
-
// ---------------------------------------------------------------------------
|
|
263
|
-
// Trace event shape (subset — avoids importing the full TraceEvent union)
|
|
264
|
-
// ---------------------------------------------------------------------------
|
|
265
|
-
|
|
266
|
-
interface TraceEventLike {
|
|
267
|
-
type: string;
|
|
268
|
-
[key: string]: unknown;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
297
|
// ---------------------------------------------------------------------------
|
|
272
298
|
// Glob matching (simple * wildcards)
|
|
273
299
|
// ---------------------------------------------------------------------------
|
|
@@ -296,15 +322,25 @@ function isMetadataTruthy(value: unknown): boolean {
|
|
|
296
322
|
}
|
|
297
323
|
|
|
298
324
|
/**
|
|
299
|
-
* Provenance of a batched wake: the newest ADDRESSED
|
|
300
|
-
* wins (mention / reply / DM outranks newer ambient
|
|
301
|
-
* own locus rule); else the newest
|
|
302
|
-
*
|
|
325
|
+
* Provenance of a batched wake: the newest ADDRESSED event that names a
|
|
326
|
+
* channel or an author wins (mention / reply / DM outranks newer ambient
|
|
327
|
+
* chatter — the framework's own locus rule); else the newest event that
|
|
328
|
+
* names either. An addressed event's resolved route counts as its channel;
|
|
329
|
+
* an ambient one's doesn't. Channel, author and addressed all come from
|
|
330
|
+
* that one event, so they can never disagree. A `chat:system` notice is
|
|
331
|
+
* never chosen for the batch's route, channel or addressed flag, even one
|
|
332
|
+
* addressed by its place (a pin in a DM), since a notice never decides where
|
|
333
|
+
* the agent's speech goes; a coalesced render whose latest occurrence is a
|
|
334
|
+
* notice isn't taken as one (GateEventInfo.folds). A batch with no such event
|
|
335
|
+
* carries only its `batchChannels` when it has them; else its chosen event is
|
|
336
|
+
* its newest notice that has an author, of which it takes only the author
|
|
337
|
+
* and time.
|
|
303
338
|
*/
|
|
304
339
|
export function wakeProvenance(events: PendingEvent[]): WakeProvenance | undefined {
|
|
340
|
+
const said = events.filter((e) => !e.systemNotice);
|
|
305
341
|
let newest: PendingEvent | undefined;
|
|
306
342
|
let newestAddressed: PendingEvent | undefined;
|
|
307
|
-
for (const e of
|
|
343
|
+
for (const e of said) {
|
|
308
344
|
// A resolved route counts as naming a channel only for an ADDRESSED
|
|
309
345
|
// event: a push whose origin declares only `mcplChannelId` (no raw
|
|
310
346
|
// channel id, no author) is still a routable addressed message, and
|
|
@@ -317,22 +353,42 @@ export function wakeProvenance(events: PendingEvent[]): WakeProvenance | undefin
|
|
|
317
353
|
if (e.addressed && (!newestAddressed || e.timestamp >= newestAddressed.timestamp)) newestAddressed = e;
|
|
318
354
|
}
|
|
319
355
|
const pick = newestAddressed ?? newest;
|
|
320
|
-
|
|
356
|
+
const channels = said.some((e) => e.addressed) ? [] : batchRoutes(said);
|
|
357
|
+
if (!pick && channels.length === 0) {
|
|
358
|
+
const notice = events.reduce<PendingEvent | undefined>(
|
|
359
|
+
(n, e) => (e.systemNotice && e.authorId && (!n || e.timestamp >= n.timestamp) ? e : n), undefined);
|
|
360
|
+
return notice ? { counterparty: counterpartyOf(notice), at: notice.timestamp } : undefined;
|
|
361
|
+
}
|
|
321
362
|
// push-event channel ids are the adapter's raw ids (a Discord snowflake):
|
|
322
363
|
// not a composite channel id, so not reported as one
|
|
323
|
-
const channelId = pick
|
|
324
|
-
const ns = pick.serverId || (pick.channelId ? pick.channelId.split(':')[0] : '') || 'channel';
|
|
364
|
+
const channelId = pick?.channelId && pick.eventType !== 'mcpl:push-event' ? pick.channelId : undefined;
|
|
325
365
|
return {
|
|
326
366
|
...(channelId ? { channelId } : {}),
|
|
327
|
-
...(pick
|
|
328
|
-
...(pick
|
|
329
|
-
...(pick
|
|
367
|
+
...(pick?.authorId ? { counterparty: counterpartyOf(pick) } : {}),
|
|
368
|
+
...(pick?.addressed ? { addressed: true } : {}),
|
|
369
|
+
...(pick?.addressed && pick.routeChannelId
|
|
330
370
|
? { routeChannelId: pick.routeChannelId, ...(pick.serverId ? { routeServerId: pick.serverId } : {}) }
|
|
331
371
|
: {}),
|
|
332
|
-
|
|
372
|
+
...(channels.length > 0 ? { batchChannels: channels } : {}),
|
|
373
|
+
...(pick ? { at: pick.timestamp } : {}),
|
|
333
374
|
};
|
|
334
375
|
}
|
|
335
376
|
|
|
377
|
+
function counterpartyOf(e: PendingEvent): string {
|
|
378
|
+
const ns = e.serverId || (e.channelId ? e.channelId.split(':')[0] : '') || 'channel';
|
|
379
|
+
return `${ns}:user:${e.authorId}`;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function batchRoutes(events: PendingEvent[]): Array<{ channelId: string; serverId?: string }> {
|
|
383
|
+
const routes: Array<{ channelId: string; serverId?: string }> = [];
|
|
384
|
+
for (const e of events) {
|
|
385
|
+
if (!e.routeChannelId) continue;
|
|
386
|
+
if (routes.some((r) => r.channelId === e.routeChannelId && r.serverId === e.serverId)) continue;
|
|
387
|
+
routes.push({ channelId: e.routeChannelId, ...(e.serverId ? { serverId: e.serverId } : {}) });
|
|
388
|
+
}
|
|
389
|
+
return routes;
|
|
390
|
+
}
|
|
391
|
+
|
|
336
392
|
/** A `[Gate: …]` line's reference to messages already in the window: how
|
|
337
393
|
* many, in which channel, and how long ago the oldest arrived, as of `now`. */
|
|
338
394
|
function referenceLine(count: number, channel: string, oldest: number, now: number): string {
|
|
@@ -447,7 +503,7 @@ function behaviorFingerprint(b: import('./types.js').GateBehavior): string {
|
|
|
447
503
|
// default), it now loads without it.
|
|
448
504
|
|
|
449
505
|
const CONFIG_FIELDS = ['default', 'policies'];
|
|
450
|
-
const POLICY_FIELDS = ['name', 'match', 'behavior', 'resets', 'passthrough'];
|
|
506
|
+
const POLICY_FIELDS = ['name', 'match', 'behavior', 'resets', 'passthrough', 'suppress'];
|
|
451
507
|
const MATCH_FIELDS = [
|
|
452
508
|
'scope', 'source', 'channel', 'mount', 'pathGlob', 'tagsAny', 'tagsAll', 'tagsNone', 'metadataTrue', 'filter',
|
|
453
509
|
];
|
|
@@ -456,6 +512,7 @@ const BEHAVIOR_KINDS = ['debounce', 'rate_limit', 'passive_sample', 'judge'] as
|
|
|
456
512
|
const RATE_LIMIT_FIELDS = ['tokens', 'refillIntervalMs', 'keyBy'];
|
|
457
513
|
const PASSIVE_SAMPLE_FIELDS = ['every', 'keyBy'];
|
|
458
514
|
const JUDGE_FIELDS = ['debounceMs', 'maxWaitMs', 'instructions'];
|
|
515
|
+
const SUPPRESS_FIELDS = ['description', 'script'];
|
|
459
516
|
|
|
460
517
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
461
518
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -937,6 +994,7 @@ function readRule(raw: unknown, unnamed: string, file = 'gate.json', named: (pat
|
|
|
937
994
|
const match = readMatch(r, obj.match);
|
|
938
995
|
const resets = readResets(r, obj.resets);
|
|
939
996
|
const passthrough = readPassthrough(r, obj.passthrough, kind);
|
|
997
|
+
const suppress = readSuppress(r, obj.suppress, obj.behavior, match, obj.match);
|
|
940
998
|
const reading: RuleReading = {
|
|
941
999
|
where: r.where, problems: r.problems, notes: r.notes, quiet: kind === 'skip' || kind === 'defer' || r.noTokens === true,
|
|
942
1000
|
};
|
|
@@ -944,6 +1002,7 @@ function readRule(raw: unknown, unnamed: string, file = 'gate.json', named: (pat
|
|
|
944
1002
|
const policy: GatePolicy = { name, match, behavior };
|
|
945
1003
|
if (resets) policy.resets = resets;
|
|
946
1004
|
if (passthrough) policy.passthrough = true;
|
|
1005
|
+
if (suppress) policy.suppress = suppress;
|
|
947
1006
|
reading.policy = policy;
|
|
948
1007
|
if (r.matchesAll) reading.matchesAll = true;
|
|
949
1008
|
}
|
|
@@ -1333,8 +1392,7 @@ function readMatch(r: Reader, value: unknown): GatePolicyMatch {
|
|
|
1333
1392
|
// Left with nothing that narrows it once something in it was named, it
|
|
1334
1393
|
// matches every event. Empty lists and nulls alone always meant no
|
|
1335
1394
|
// constraint, and name nothing.
|
|
1336
|
-
|
|
1337
|
-
if (!narrows && r.problems.length + r.notes.length > named) r.matchesAll = true;
|
|
1395
|
+
if (!matchNarrows(match) && r.problems.length + r.notes.length > named) r.matchesAll = true;
|
|
1338
1396
|
return match;
|
|
1339
1397
|
}
|
|
1340
1398
|
|
|
@@ -1381,6 +1439,133 @@ function readPassthrough(r: Reader, value: unknown, kind: BehaviorKind | undefin
|
|
|
1381
1439
|
return false;
|
|
1382
1440
|
}
|
|
1383
1441
|
|
|
1442
|
+
/** Whether a match narrows what it matches: anything but an empty scope. */
|
|
1443
|
+
export function matchNarrows(match: GatePolicyMatch): boolean {
|
|
1444
|
+
return Object.entries(match).some(([field, v]) => !(field === 'scope' && Array.isArray(v) && v.length === 0));
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/**
|
|
1448
|
+
* Whether a suppress rule's match names what it hides: an event type, or a
|
|
1449
|
+
* source, channel, mount, path or tag pattern that isn't only wildcards
|
|
1450
|
+
* (namesSomething), a metadata flag, or a content filter. A pattern of
|
|
1451
|
+
* wildcards alone, such as `source: "*"`, or only `tagsNone`, takes every
|
|
1452
|
+
* event, which a suppress rule may not.
|
|
1453
|
+
*/
|
|
1454
|
+
function suppressMatchNarrows(match: GatePolicyMatch): boolean {
|
|
1455
|
+
if ((match.scope ?? []).length > 0 || (match.metadataTrue ?? []).length > 0 || match.filter !== undefined) return true;
|
|
1456
|
+
if ([match.source, match.channel, match.mount, match.pathGlob].some(namesSomething)) return true;
|
|
1457
|
+
return [...(match.tagsAny ?? []), ...(match.tagsAll ?? [])].some(namesSomething);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
/** Longest suppress description, in characters: the review sends it with every rule it judges. */
|
|
1461
|
+
export const MAX_SUPPRESS_DESCRIPTION = 2_000;
|
|
1462
|
+
/** Longest suppress script, in characters. */
|
|
1463
|
+
export const MAX_SUPPRESS_SCRIPT = 20_000;
|
|
1464
|
+
const SUPPRESS_EXAMPLE = '{"description": "the eye going on and off"}';
|
|
1465
|
+
|
|
1466
|
+
/**
|
|
1467
|
+
* A rule's `suppress`. Every problem with it is read as a rule that doesn't
|
|
1468
|
+
* suppress, which is how a release without suppression reads the rule:
|
|
1469
|
+
* its behavior then decides the event, and the event is rendered. The
|
|
1470
|
+
* behavior has to be written out as "defer" (or "skip"), not left to
|
|
1471
|
+
* default, because such a release reads a rule with no behavior as
|
|
1472
|
+
* "always" and would wake the agent on every event the rule was meant to
|
|
1473
|
+
* hide.
|
|
1474
|
+
*/
|
|
1475
|
+
function readSuppress(r: Reader, value: unknown, behavior: unknown, match: GatePolicyMatch, rawMatch: unknown): SuppressConfig | undefined {
|
|
1476
|
+
if (value === undefined || value === false) return undefined;
|
|
1477
|
+
const without = "so the rule doesn't suppress, and its behavior decides its events as for any other rule";
|
|
1478
|
+
if (!isPlainObject(value)) {
|
|
1479
|
+
r.problems.push({ text: `suppress is ${shownValue(value)}, where an object such as ${SUPPRESS_EXAMPLE} is needed`, read: without });
|
|
1480
|
+
return undefined;
|
|
1481
|
+
}
|
|
1482
|
+
const before = r.problems.length;
|
|
1483
|
+
// A script given as null or "" is a script that's missing, not one left
|
|
1484
|
+
// out: read as left out, the rule would hide every event its match takes.
|
|
1485
|
+
const { script: _givenScript, ...rest } = value;
|
|
1486
|
+
const emptyScript = Object.hasOwn(value, 'script') && (value.script === null || value.script === '');
|
|
1487
|
+
if (emptyScript) {
|
|
1488
|
+
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 });
|
|
1489
|
+
}
|
|
1490
|
+
const given = withoutEmpty(`${r.where}: suppress`, emptyScript ? rest : value, r.notes);
|
|
1491
|
+
const reported = r.problems.length;
|
|
1492
|
+
readFieldNames(r, 'suppress', given, SUPPRESS_FIELDS);
|
|
1493
|
+
// A field it doesn't read can't be left aside here, as elsewhere in a
|
|
1494
|
+
// rule: one meant as a condition, left out, would hide every event the
|
|
1495
|
+
// match takes.
|
|
1496
|
+
const unread = Object.keys(given).filter((key) => !SUPPRESS_FIELDS.includes(key));
|
|
1497
|
+
if (unread.length > 0 && r.problems.length === reported) {
|
|
1498
|
+
r.problems.push({ text: `suppress has ${quotedList(unread)}, and a suppress rule reads only "description" and "script"`, read: without });
|
|
1499
|
+
}
|
|
1500
|
+
const config: SuppressConfig = { description: '' };
|
|
1501
|
+
if (typeof given.description !== 'string' || given.description.trim() === '') {
|
|
1502
|
+
r.problems.push({
|
|
1503
|
+
text: `suppress.description is ${given.description === undefined ? 'missing' : shownValue(given.description)}, where the rule's `
|
|
1504
|
+
+ 'purpose in a few words is needed: the review checks what the rule hides against it',
|
|
1505
|
+
read: without,
|
|
1506
|
+
});
|
|
1507
|
+
} else if ([...given.description].length > MAX_SUPPRESS_DESCRIPTION) {
|
|
1508
|
+
r.problems.push({
|
|
1509
|
+
text: `suppress.description is ${[...given.description].length.toLocaleString('en-US')} characters, over the `
|
|
1510
|
+
+ `${MAX_SUPPRESS_DESCRIPTION.toLocaleString('en-US')} allowed`,
|
|
1511
|
+
read: without,
|
|
1512
|
+
});
|
|
1513
|
+
} else {
|
|
1514
|
+
config.description = given.description.trim();
|
|
1515
|
+
}
|
|
1516
|
+
if (given.script !== undefined) {
|
|
1517
|
+
if (typeof given.script !== 'string' || given.script.trim() === '') {
|
|
1518
|
+
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 });
|
|
1519
|
+
} else if ([...given.script].length > MAX_SUPPRESS_SCRIPT) {
|
|
1520
|
+
r.problems.push({
|
|
1521
|
+
text: `suppress.script is ${[...given.script].length.toLocaleString('en-US')} characters, over the ${MAX_SUPPRESS_SCRIPT.toLocaleString('en-US')} allowed`,
|
|
1522
|
+
read: without,
|
|
1523
|
+
});
|
|
1524
|
+
} else {
|
|
1525
|
+
// Parsed only, never run here: the script runs on its worker.
|
|
1526
|
+
try {
|
|
1527
|
+
new Function(`"use strict";\nreturn (${given.script}\n);`);
|
|
1528
|
+
config.script = given.script;
|
|
1529
|
+
} catch (e) {
|
|
1530
|
+
r.problems.push({ text: `suppress.script doesn't parse (${quotedValue(errorText(e))})`, read: without });
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
if (behavior !== 'defer' && behavior !== 'skip') {
|
|
1535
|
+
r.problems.push({
|
|
1536
|
+
text: `suppress is given, and a rule that suppresses needs "behavior": "defer" written out, so a release that `
|
|
1537
|
+
+ `doesn't know "suppress" defers its events instead of waking you on them; it has `
|
|
1538
|
+
+ (behavior === undefined ? 'no behavior' : `behavior ${shownValue(behavior)}`),
|
|
1539
|
+
read: without,
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
// A match field the gate doesn't read would, left aside, hide more than was
|
|
1543
|
+
// written: the condition it was meant to add is gone.
|
|
1544
|
+
if (isPlainObject(rawMatch)) {
|
|
1545
|
+
const rawFilter = isPlainObject(rawMatch.filter) ? Object.keys(rawMatch.filter).filter((key) => !FILTER_FIELDS.includes(key)).map((key) => `filter.${key}`) : [];
|
|
1546
|
+
const unreadMatch = [...Object.keys(rawMatch).filter((key) => !MATCH_FIELDS.includes(key)), ...rawFilter];
|
|
1547
|
+
if (unreadMatch.length > 0) {
|
|
1548
|
+
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 });
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
if (!suppressMatchNarrows(match)) {
|
|
1552
|
+
r.problems.push({
|
|
1553
|
+
text: 'suppress is given, and the rule\'s match names nothing it hides: a suppress rule needs a match that names '
|
|
1554
|
+
+ 'a scope, a source, a channel, tags, a mount or path, metadata or a filter (not a pattern of wildcards alone, or tagsNone alone), '
|
|
1555
|
+
+ 'so it can\'t hide everything',
|
|
1556
|
+
read: without,
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
// Only a rule read without any problem suppresses: one read past a problem
|
|
1560
|
+
// (a slip of a field, a match read without a part of it) matches more, or
|
|
1561
|
+
// decides otherwise, than its author wrote, and hiding is what can't be
|
|
1562
|
+
// seen to go wrong. Its behavior decides its events instead.
|
|
1563
|
+
if (before > 0) {
|
|
1564
|
+
r.problems.push({ text: 'suppress is given on a rule with problems, and a rule suppresses only when it has none', read: without });
|
|
1565
|
+
}
|
|
1566
|
+
return r.problems.length === 0 ? config : undefined;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1384
1569
|
/** What a load or a start does with a rule. */
|
|
1385
1570
|
type RuleOutcome = 'as-written' | 'read' | 'left-out';
|
|
1386
1571
|
|
|
@@ -1560,7 +1745,7 @@ function readConfig(
|
|
|
1560
1745
|
};
|
|
1561
1746
|
}
|
|
1562
1747
|
}
|
|
1563
|
-
for (const rule of rules) sentences
|
|
1748
|
+
for (const rule of rules) appendAll(sentences, readingSentences(rule.reading, rule.outcome));
|
|
1564
1749
|
// A list with no rules, or none the gate reads, where no sentence above said it has none.
|
|
1565
1750
|
if (notUsed !== undefined && !sentences.some((said) => said.includes(notUsed))) {
|
|
1566
1751
|
sentences.push(`${File} has no rules${rules.length > 0 ? ' but ones the gate leaves out' : ''}, so ${notUsedWhy}.`);
|
|
@@ -1962,7 +2147,9 @@ export function findShadowedPolicies(policies: GatePolicy[], eventTypesFrom?: Ev
|
|
|
1962
2147
|
const out: ShadowWarning[] = [];
|
|
1963
2148
|
for (let i = 0; i < policies.length; i++) {
|
|
1964
2149
|
const earlier = policies[i];
|
|
1965
|
-
|
|
2150
|
+
// A suppress rule lets addressed events and the heartbeat's by, so it
|
|
2151
|
+
// can't be said to take every event of a later rule.
|
|
2152
|
+
if (earlier.passthrough || earlier.suppress) continue;
|
|
1966
2153
|
for (let j = i + 1; j < policies.length; j++) {
|
|
1967
2154
|
const found = subsumedEventTypes(earlier, policies[j], eventTypesFrom);
|
|
1968
2155
|
if (found) {
|
|
@@ -1995,15 +2182,48 @@ export function formatShadowWarning(w: ShadowWarning): string {
|
|
|
1995
2182
|
+ `A scope on ${later} naming the event types it's for clears this note.`;
|
|
1996
2183
|
}
|
|
1997
2184
|
|
|
2185
|
+
function judgeFailureWords(err: unknown): string {
|
|
2186
|
+
if (err instanceof JudgeFailure) return err.message;
|
|
2187
|
+
return err instanceof Error ? failureWords(err, 'the call failed') : 'the call failed';
|
|
2188
|
+
}
|
|
2189
|
+
|
|
1998
2190
|
/** The text of a thrown value, for a log line. It never throws itself, whatever was thrown. */
|
|
1999
2191
|
function errorText(err: unknown): string {
|
|
2000
2192
|
try {
|
|
2193
|
+
if (err instanceof JudgeFailure && err.detail !== undefined) return `${err.message} (${err.detail})`;
|
|
2001
2194
|
return err instanceof Error ? err.message : String(err);
|
|
2002
2195
|
} catch {
|
|
2003
2196
|
return 'a value that could not be printed';
|
|
2004
2197
|
}
|
|
2005
2198
|
}
|
|
2006
2199
|
|
|
2200
|
+
class PartialDelivery extends Error {
|
|
2201
|
+
constructor(readonly woke: readonly string[], readonly notWoken: readonly string[], cause: unknown) {
|
|
2202
|
+
super(errorText(cause), { cause });
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
/**
|
|
2207
|
+
* Runs a batch's delivery to `woken`, which pushes each agent it wakes to
|
|
2208
|
+
* `woke`: a throw once any is woken is a PartialDelivery naming them and the
|
|
2209
|
+
* rest; one before any wake is thrown as it is.
|
|
2210
|
+
*/
|
|
2211
|
+
function deliveredTracking(woke: string[], woken: readonly string[], deliver: () => void): void {
|
|
2212
|
+
try {
|
|
2213
|
+
deliver();
|
|
2214
|
+
} catch (err) {
|
|
2215
|
+
if (woke.length === 0) throw err;
|
|
2216
|
+
throw new PartialDelivery(woke, woken.filter((name) => !woke.includes(name)), err);
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
function deliveredInPart(err: unknown): string | undefined {
|
|
2221
|
+
if (!(err instanceof PartialDelivery)) return undefined;
|
|
2222
|
+
const one = err.notWoken.length === 1;
|
|
2223
|
+
return `woke ${joinedList(err.woke)}, and delivering it threw (${err.message}), `
|
|
2224
|
+
+ `so it's dropped for ${joinedList(err.notWoken)}, who ${one ? "isn't" : "aren't"} woken`;
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2007
2227
|
/**
|
|
2008
2228
|
* Run a caller's callback once the change it's told of is made and traced. A
|
|
2009
2229
|
* throw from it is logged, not thrown, so the call answers as the change it
|
|
@@ -2123,6 +2343,8 @@ export class EventGate {
|
|
|
2123
2343
|
|
|
2124
2344
|
// Per-policy stats
|
|
2125
2345
|
private stats = new Map<string, PolicyStats>();
|
|
2346
|
+
/** Suppress rules' scripts and their state (suppress-scripts.ts), kept beside gate.json. */
|
|
2347
|
+
private readonly suppressScripts: SuppressScripts;
|
|
2126
2348
|
|
|
2127
2349
|
// The shadow lint's last findings (see refreshShadowWarnings), recomputed
|
|
2128
2350
|
// on every config change and fingerprinted so an unchanged set doesn't
|
|
@@ -2172,7 +2394,7 @@ export class EventGate {
|
|
|
2172
2394
|
private lastPrivilegedCheck = 0;
|
|
2173
2395
|
|
|
2174
2396
|
// Dependency-injected callbacks
|
|
2175
|
-
private emitTrace: (event:
|
|
2397
|
+
private emitTrace: (event: TraceEventInput) => void;
|
|
2176
2398
|
private addMessageFn: (
|
|
2177
2399
|
participant: string,
|
|
2178
2400
|
content: Array<{ type: 'text'; text: string }>,
|
|
@@ -2181,6 +2403,10 @@ export class EventGate {
|
|
|
2181
2403
|
forAgent?: string,
|
|
2182
2404
|
/** Out-param: where the message landed, for the wake that goes with it. */
|
|
2183
2405
|
placement?: WritePlacement,
|
|
2406
|
+
/** The agents the row is for; absent: the host's default for the write.
|
|
2407
|
+
* The host stamps those of them that read where it stores the row, and
|
|
2408
|
+
* its default when none does. */
|
|
2409
|
+
audience?: readonly string[],
|
|
2184
2410
|
) => unknown;
|
|
2185
2411
|
private requestInferenceFn: (agentName: string, reason: string, source: string, provenance?: WakeProvenance, after?: WritePlacement) => void;
|
|
2186
2412
|
private getAgentNamesFn: () => string[];
|
|
@@ -2188,7 +2414,7 @@ export class EventGate {
|
|
|
2188
2414
|
private isHeldFn: (serverId: string | undefined, channelId: string | undefined) => boolean;
|
|
2189
2415
|
private resolveRouteChannelFn: (info: GateEventInfo) => string | undefined;
|
|
2190
2416
|
private channelShownFn?: (channel: { serverId: string; channelId: string }) => string;
|
|
2191
|
-
private isNewestEntryFn?: (entry: GateEntryRef) => boolean;
|
|
2417
|
+
private isNewestEntryFn?: (entry: GateEntryRef, recipients?: readonly string[]) => boolean;
|
|
2192
2418
|
/** Clock injection — keeps the new rate_limit / passive_sample paths
|
|
2193
2419
|
* testable without monkey-patching Date.now globally. */
|
|
2194
2420
|
private now: () => number;
|
|
@@ -2197,13 +2423,14 @@ export class EventGate {
|
|
|
2197
2423
|
configPath: string;
|
|
2198
2424
|
initialConfig?: GateConfig;
|
|
2199
2425
|
privilegedUsersPath?: string;
|
|
2200
|
-
emitTrace: (event:
|
|
2426
|
+
emitTrace: (event: TraceEventInput) => void;
|
|
2201
2427
|
addMessage: (
|
|
2202
2428
|
participant: string,
|
|
2203
2429
|
content: Array<{ type: 'text'; text: string }>,
|
|
2204
2430
|
metadata?: Record<string, unknown>,
|
|
2205
2431
|
forAgent?: string,
|
|
2206
2432
|
placement?: WritePlacement,
|
|
2433
|
+
audience?: readonly string[],
|
|
2207
2434
|
) => unknown;
|
|
2208
2435
|
/** `after`: the placement of the message the wake is for, when it has
|
|
2209
2436
|
* one, so the host can drop the wake once a request that carried the
|
|
@@ -2221,6 +2448,8 @@ export class EventGate {
|
|
|
2221
2448
|
now?: () => number;
|
|
2222
2449
|
/** Per-event timeout (ms) for the optional gate.js script. Default 50. */
|
|
2223
2450
|
scriptTimeoutMs?: number;
|
|
2451
|
+
/** How long a suppress rule's script may take for one event before the event is shown (ms). Default 50. */
|
|
2452
|
+
suppressScriptTimeoutMs?: number;
|
|
2224
2453
|
/** The reply judge `judge` rules ask (JudgeConfig). Absent: a judge
|
|
2225
2454
|
* rule's batch is delivered as a debounce's, and gate_status says so. */
|
|
2226
2455
|
judge?: JudgeFn;
|
|
@@ -2251,8 +2480,10 @@ export class EventGate {
|
|
|
2251
2480
|
* waiting to be written there. A batched wake for that one message
|
|
2252
2481
|
* doesn't write its line when it fires: it hands the line to the wake
|
|
2253
2482
|
* (WakeProvenance.line), for a fresh start of the wake's turn to decide
|
|
2254
|
-
* (WakeLine).
|
|
2255
|
-
|
|
2483
|
+
* (WakeLine). With `recipients`, the windows are those agents', each of
|
|
2484
|
+
* which the event must be newest in. Absent: every batched wake writes
|
|
2485
|
+
* its line, as before. */
|
|
2486
|
+
isNewestEntry?: (entry: GateEntryRef, recipients?: readonly string[]) => boolean;
|
|
2256
2487
|
/** The event types the gate can see from a source, when the host knows
|
|
2257
2488
|
* them (an MCPL server's id: MCPL_GATE_EVENT_TYPES), else undefined.
|
|
2258
2489
|
* The rule-order notes (findShadowedPolicies) leave out types a rule's
|
|
@@ -2260,6 +2491,11 @@ export class EventGate {
|
|
|
2260
2491
|
eventTypesFrom?: EventTypesFrom;
|
|
2261
2492
|
}) {
|
|
2262
2493
|
this.configPath = opts.configPath;
|
|
2494
|
+
// Before the rules are read: a start's merge can clear a rule's state, its script's included.
|
|
2495
|
+
this.suppressScripts = new SuppressScripts({
|
|
2496
|
+
statePath: join(dirname(opts.configPath), 'suppress-state.json'),
|
|
2497
|
+
...(opts.suppressScriptTimeoutMs !== undefined ? { timeoutMs: opts.suppressScriptTimeoutMs } : {}),
|
|
2498
|
+
});
|
|
2263
2499
|
this.privilegedUsersPath = opts.privilegedUsersPath;
|
|
2264
2500
|
this.emitTrace = opts.emitTrace;
|
|
2265
2501
|
this.addMessageFn = opts.addMessage;
|
|
@@ -2541,7 +2777,6 @@ export class EventGate {
|
|
|
2541
2777
|
...changes,
|
|
2542
2778
|
// Every name the start added to gate.json, as an append's trace gave.
|
|
2543
2779
|
addedPolicies: names,
|
|
2544
|
-
timestamp: Date.now(),
|
|
2545
2780
|
});
|
|
2546
2781
|
}
|
|
2547
2782
|
if (!appended && (!last.recipe || canonical({ rules: last.recipe.rules, default: last.recipe.default }) !== canonical({ rules: starting.rules.map((rule) => rule.raw), default: given.default }))) {
|
|
@@ -2652,7 +2887,6 @@ export class EventGate {
|
|
|
2652
2887
|
type: 'gate:config-error',
|
|
2653
2888
|
error: why,
|
|
2654
2889
|
configPath: this.configPath,
|
|
2655
|
-
timestamp: Date.now(),
|
|
2656
2890
|
});
|
|
2657
2891
|
}
|
|
2658
2892
|
this.configError = problem;
|
|
@@ -2798,7 +3032,11 @@ export class EventGate {
|
|
|
2798
3032
|
private clearStateFor(was: GateConfig, next: GateConfig): { removed: string[]; cleared: string[] } {
|
|
2799
3033
|
const behaviorsByName = (config: GateConfig): Map<string, string> => {
|
|
2800
3034
|
const byName = new Map<string, string[]>();
|
|
2801
|
-
for (const p of config.policies)
|
|
3035
|
+
for (const p of config.policies) {
|
|
3036
|
+
// A suppress rule's script is part of what it is: a changed script starts from a fresh state.
|
|
3037
|
+
const fingerprint = behaviorFingerprint(p.behavior) + (p.suppress ? ` suppress:${p.suppress.script ?? ''}` : '');
|
|
3038
|
+
byName.set(p.name, [...(byName.get(p.name) ?? []), fingerprint]);
|
|
3039
|
+
}
|
|
2802
3040
|
return new Map([...byName].map(([name, behaviors]) => [name, JSON.stringify(behaviors)]));
|
|
2803
3041
|
};
|
|
2804
3042
|
const before = behaviorsByName(was);
|
|
@@ -2851,7 +3089,6 @@ export class EventGate {
|
|
|
2851
3089
|
removedPolicies: removedPolicies.length > 0 ? removedPolicies : undefined,
|
|
2852
3090
|
stateClearedPolicies:
|
|
2853
3091
|
stateClearedPolicies.length > 0 ? stateClearedPolicies : undefined,
|
|
2854
|
-
timestamp: now,
|
|
2855
3092
|
});
|
|
2856
3093
|
} catch {
|
|
2857
3094
|
// The rules in force stay, and the next check tries again.
|
|
@@ -2977,7 +3214,6 @@ export class EventGate {
|
|
|
2977
3214
|
configPath: this.configPath,
|
|
2978
3215
|
policyName: policy.name,
|
|
2979
3216
|
replaced: idx >= 0,
|
|
2980
|
-
timestamp: Date.now(),
|
|
2981
3217
|
});
|
|
2982
3218
|
if (ignored.length > 0) {
|
|
2983
3219
|
// A caller that doesn't take the notes still gets them said, in the log.
|
|
@@ -3020,7 +3256,6 @@ export class EventGate {
|
|
|
3020
3256
|
type: 'gate:policy-removed',
|
|
3021
3257
|
configPath: this.configPath,
|
|
3022
3258
|
policyName: name,
|
|
3023
|
-
timestamp: Date.now(),
|
|
3024
3259
|
});
|
|
3025
3260
|
return true;
|
|
3026
3261
|
}
|
|
@@ -3037,6 +3272,17 @@ export class EventGate {
|
|
|
3037
3272
|
return current.policies.map((p) => p.name);
|
|
3038
3273
|
}
|
|
3039
3274
|
|
|
3275
|
+
/** The rules in force, in order (copies): what the last decision was made by. */
|
|
3276
|
+
policiesInForce(): GatePolicy[] {
|
|
3277
|
+
return this.config.policies.map((p) => structuredClone(p));
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3280
|
+
/** The rule in force by this name (a copy), or undefined. */
|
|
3281
|
+
policyInForce(name: string): GatePolicy | undefined {
|
|
3282
|
+
const p = this.config.policies.find((x) => x.name === name);
|
|
3283
|
+
return p ? structuredClone(p) : undefined;
|
|
3284
|
+
}
|
|
3285
|
+
|
|
3040
3286
|
/** @internal For `wake_add_rule`'s and `wake_remove_rule`'s refusals; not a host API. */
|
|
3041
3287
|
ruleNamesForChange(): { names: string[]; note: string } | { refused: string } {
|
|
3042
3288
|
const edit = this.rulesForChange();
|
|
@@ -3223,7 +3469,6 @@ export class EventGate {
|
|
|
3223
3469
|
matchedPolicy: 'asleep',
|
|
3224
3470
|
trigger: false,
|
|
3225
3471
|
behavior: 'skip',
|
|
3226
|
-
timestamp: this.now(),
|
|
3227
3472
|
});
|
|
3228
3473
|
return { trigger: false, policyName: 'asleep', behavior: 'skip' };
|
|
3229
3474
|
}
|
|
@@ -3246,6 +3491,11 @@ export class EventGate {
|
|
|
3246
3491
|
decision = this.computeDecision({ name: 'gate.js', match: {}, behavior: scripted }, info);
|
|
3247
3492
|
} else {
|
|
3248
3493
|
for (const compiled of this.compiledPolicies) {
|
|
3494
|
+
// A suppress rule doesn't decide a wake for an event suppression was
|
|
3495
|
+
// asked about: one it takes was suppressed before the gate was asked,
|
|
3496
|
+
// and one that reaches here is one it let by. Where suppression wasn't
|
|
3497
|
+
// asked (no view to hide it from), it defers, as its behavior says.
|
|
3498
|
+
if (compiled.policy.suppress && this.passesOverSuppress(info)) continue;
|
|
3249
3499
|
if (!this.compiledMatches(compiled, info)) continue;
|
|
3250
3500
|
const candidate = this.computeDecision(compiled.policy, info);
|
|
3251
3501
|
if (candidate.trigger || !compiled.policy.passthrough) {
|
|
@@ -3281,7 +3531,6 @@ export class EventGate {
|
|
|
3281
3531
|
trigger: decision.trigger,
|
|
3282
3532
|
behavior: formatBehavior(decision.behavior),
|
|
3283
3533
|
...(observed.length > 0 ? { observed } : {}),
|
|
3284
|
-
timestamp: this.now(),
|
|
3285
3534
|
});
|
|
3286
3535
|
|
|
3287
3536
|
return decision;
|
|
@@ -3306,7 +3555,6 @@ export class EventGate {
|
|
|
3306
3555
|
behavior: formatBehavior(policy.behavior),
|
|
3307
3556
|
eventType: info.eventType,
|
|
3308
3557
|
source: info.serverId || undefined,
|
|
3309
|
-
timestamp: this.now(),
|
|
3310
3558
|
});
|
|
3311
3559
|
|
|
3312
3560
|
if (policy.behavior === 'defer' || policy.behavior === 'skip') {
|
|
@@ -3452,6 +3700,7 @@ export class EventGate {
|
|
|
3452
3700
|
private clearPolicyState(name: string): void {
|
|
3453
3701
|
const debounce = this.debounceTimers.get(name);
|
|
3454
3702
|
if (debounce) clearTimeout(debounce.timer);
|
|
3703
|
+
this.suppressScripts.forget(name);
|
|
3455
3704
|
this.rateLimitBuckets.delete(name);
|
|
3456
3705
|
this.passiveSampleCounters.delete(name);
|
|
3457
3706
|
this.rateLimitDenied.delete(name);
|
|
@@ -3507,6 +3756,7 @@ export class EventGate {
|
|
|
3507
3756
|
*/
|
|
3508
3757
|
probe(info: GateEventInfo): GateDecision {
|
|
3509
3758
|
for (const compiled of this.compiledPolicies) {
|
|
3759
|
+
if (compiled.policy.suppress && this.passesOverSuppress(info)) continue; // as evaluate does
|
|
3510
3760
|
if (!this.compiledMatches(compiled, info)) continue;
|
|
3511
3761
|
const trigger = this.wouldTrigger(compiled.policy, info);
|
|
3512
3762
|
if (trigger || !compiled.policy.passthrough) {
|
|
@@ -3517,6 +3767,135 @@ export class EventGate {
|
|
|
3517
3767
|
return { trigger, policyName: null, behavior: this.config.default ?? 'always' };
|
|
3518
3768
|
}
|
|
3519
3769
|
|
|
3770
|
+
/**
|
|
3771
|
+
* The suppress rule that takes this event, or null when none does.
|
|
3772
|
+
* Side-effect-free, like probe: no counter moves, no batch forms, nothing
|
|
3773
|
+
* is traced, and a rule's script runs on a copy of its state, which is
|
|
3774
|
+
* then dropped. (A closure a script builds for itself is its own, and a
|
|
3775
|
+
* 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
|
|
3776
|
+
* framework asks decideSuppression instead, once per event.
|
|
3777
|
+
*
|
|
3778
|
+
* It reads the rules as evaluate does, first match deciding: a rule ahead
|
|
3779
|
+
* of the suppress rule that matches the event decides it instead (it isn't
|
|
3780
|
+
* suppressed), unless it's a passthrough rule that wouldn't fire. A rule
|
|
3781
|
+
* with a script (`suppress.script`) takes an event its match takes only
|
|
3782
|
+
* when the script says to hide it; otherwise the event goes on to the rules
|
|
3783
|
+
* after it, as one the rule doesn't match does. Sleep and gate.js aren't
|
|
3784
|
+
* consulted: they decide wakes, and suppression decides what's rendered.
|
|
3785
|
+
*
|
|
3786
|
+
* Never suppressed, whatever the rules say: an event addressed to the
|
|
3787
|
+
* agent (tagged `chat:addressed`: a mention, a reply, a DM) and the
|
|
3788
|
+
* heartbeat's (the agent's own clock). A suppress rule lets them by to the
|
|
3789
|
+
* rules after it, and its script isn't asked. The framework itself never
|
|
3790
|
+
* asks about console or API input.
|
|
3791
|
+
*/
|
|
3792
|
+
suppressionFor(info: GateEventInfo): string | null {
|
|
3793
|
+
return this.walkSuppression(info, false);
|
|
3794
|
+
}
|
|
3795
|
+
|
|
3796
|
+
/**
|
|
3797
|
+
* suppressionFor, for an event that is arriving now: each script asked
|
|
3798
|
+
* keeps the state it leaves, whatever later rules do with the event, and a
|
|
3799
|
+
* suppressed event counts in the passthrough rules ahead of its rule. Ask
|
|
3800
|
+
* once per event.
|
|
3801
|
+
*/
|
|
3802
|
+
decideSuppression(info: GateEventInfo): string | null {
|
|
3803
|
+
return this.walkSuppression(info, true);
|
|
3804
|
+
}
|
|
3805
|
+
|
|
3806
|
+
private walkSuppression(info: GateEventInfo, commit: boolean): string | null {
|
|
3807
|
+
this.reloadIfChanged();
|
|
3808
|
+
this.syncSuppressScripts();
|
|
3809
|
+
if (info.serverId === 'heartbeat') return null;
|
|
3810
|
+
const addressed = Array.isArray(info.tags) && info.tags.includes('chat:addressed');
|
|
3811
|
+
let event: SuppressScriptEvent | undefined;
|
|
3812
|
+
const observers: GatePolicy[] = [];
|
|
3813
|
+
for (const compiled of this.compiledPolicies) {
|
|
3814
|
+
if (!this.compiledMatches(compiled, info)) continue;
|
|
3815
|
+
const suppress = compiled.policy.suppress;
|
|
3816
|
+
if (suppress) {
|
|
3817
|
+
if (addressed) continue;
|
|
3818
|
+
if (suppress.script !== undefined) {
|
|
3819
|
+
event ??= this.suppressScriptEvent(info);
|
|
3820
|
+
const ran = this.suppressScripts.run(compiled.policy.name, event, commit);
|
|
3821
|
+
// A script that failed shows the event: it goes on to the rules after.
|
|
3822
|
+
if (!ran || !('hide' in ran) || !ran.hide) continue;
|
|
3823
|
+
}
|
|
3824
|
+
// A suppressed event never reaches evaluate, so the passthrough rules
|
|
3825
|
+
// ahead of this one count it here, as evaluate would have: none of
|
|
3826
|
+
// them fires for it, and each fires on its schedule. A dry run counts
|
|
3827
|
+
// nothing.
|
|
3828
|
+
if (commit) for (const observer of observers) this.computeDecision(observer, info);
|
|
3829
|
+
return compiled.policy.name;
|
|
3830
|
+
}
|
|
3831
|
+
if (compiled.policy.passthrough && !this.wouldTrigger(compiled.policy, info)) { observers.push(compiled.policy); continue; }
|
|
3832
|
+
return null;
|
|
3833
|
+
}
|
|
3834
|
+
return null;
|
|
3835
|
+
}
|
|
3836
|
+
|
|
3837
|
+
/** 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. */
|
|
3838
|
+
private passesOverSuppress(info: GateEventInfo): boolean {
|
|
3839
|
+
return info.suppressionAsked === true || info.serverId === 'heartbeat'
|
|
3840
|
+
|| (Array.isArray(info.tags) && info.tags.includes('chat:addressed'));
|
|
3841
|
+
}
|
|
3842
|
+
|
|
3843
|
+
/** The scripts in step with the rules in force (SuppressScripts.sync: only changed ones are compiled). */
|
|
3844
|
+
private syncSuppressScripts(): void {
|
|
3845
|
+
this.suppressScripts.sync(this.config.policies.map((p) => ({ name: p.name, ...(p.suppress?.script !== undefined ? { script: p.suppress.script } : {}) })));
|
|
3846
|
+
}
|
|
3847
|
+
|
|
3848
|
+
/** What a suppress script is given for an event: plain JSON, its metadata included where it can be. */
|
|
3849
|
+
private suppressScriptEvent(info: GateEventInfo): SuppressScriptEvent {
|
|
3850
|
+
let metadata: Record<string, unknown> = {};
|
|
3851
|
+
try { metadata = JSON.parse(JSON.stringify(info.metadata ?? {})) as Record<string, unknown>; } catch { metadata = {}; }
|
|
3852
|
+
const channel = this.routeChannelFor(info) ?? (info.channelId || undefined);
|
|
3853
|
+
const author = this.extractAuthorId(info.metadata);
|
|
3854
|
+
return {
|
|
3855
|
+
eventType: info.eventType,
|
|
3856
|
+
source: info.serverId,
|
|
3857
|
+
...(channel ? { channel } : {}),
|
|
3858
|
+
...(author ? { author } : {}),
|
|
3859
|
+
tags: [...(info.tags ?? [])],
|
|
3860
|
+
text: info.content,
|
|
3861
|
+
at: this.now(),
|
|
3862
|
+
metadata,
|
|
3863
|
+
};
|
|
3864
|
+
}
|
|
3865
|
+
|
|
3866
|
+
/**
|
|
3867
|
+
* Whether a suppress script compiles to a function, on the scripts' worker:
|
|
3868
|
+
* an error's text, or null. wake_add_rule asks before it saves a rule.
|
|
3869
|
+
*/
|
|
3870
|
+
checkSuppressScript(script: string): string | null {
|
|
3871
|
+
return this.suppressScripts.check(script);
|
|
3872
|
+
}
|
|
3873
|
+
|
|
3874
|
+
/** A suppress rule's script state as it stands (a copy), for a host or a test. */
|
|
3875
|
+
suppressScriptState(name: string): Record<string, unknown> | undefined {
|
|
3876
|
+
this.syncSuppressScripts();
|
|
3877
|
+
return this.suppressScripts.stateOf(name);
|
|
3878
|
+
}
|
|
3879
|
+
|
|
3880
|
+
/**
|
|
3881
|
+
* Count a row the framework suppressed by `name` (suppressionFor's
|
|
3882
|
+
* answer) once it's written. The rule's stats count it as a match, so
|
|
3883
|
+
* gate_status shows how many events each suppress rule took since start.
|
|
3884
|
+
*/
|
|
3885
|
+
noteSuppressed(name: string, info: Pick<GateEventInfo, 'eventType' | 'serverId' | 'channelId'>): void {
|
|
3886
|
+
const policyStats = this.stats.get(name) ?? { matchCount: 0, lastMatchTimestamp: null };
|
|
3887
|
+
policyStats.matchCount++;
|
|
3888
|
+
policyStats.lastMatchTimestamp = this.now();
|
|
3889
|
+
this.stats.set(name, policyStats);
|
|
3890
|
+
this.emitTrace({
|
|
3891
|
+
type: 'gate:suppressed',
|
|
3892
|
+
policyName: name,
|
|
3893
|
+
eventType: info.eventType,
|
|
3894
|
+
...(info.serverId ? { serverId: info.serverId } : {}),
|
|
3895
|
+
...(info.channelId ? { channelId: info.channelId } : {}),
|
|
3896
|
+
});
|
|
3897
|
+
}
|
|
3898
|
+
|
|
3520
3899
|
/** The canonical synthetic events the probe table runs: chat shapes as MCPL delivers them, and a heartbeat. */
|
|
3521
3900
|
private probeEvents(): Array<{ label: string; info: GateEventInfo }> {
|
|
3522
3901
|
const chat = (
|
|
@@ -3592,7 +3971,12 @@ export class EventGate {
|
|
|
3592
3971
|
*/
|
|
3593
3972
|
probeTable(): GateProbeRow[] {
|
|
3594
3973
|
return this.probeEvents().map(({ label, info }) => {
|
|
3595
|
-
const
|
|
3974
|
+
const suppressedBy = this.suppressionFor(info);
|
|
3975
|
+
if (suppressedBy !== null) {
|
|
3976
|
+
return { probe: label, eventType: info.eventType, policy: suppressedBy, behavior: 'suppress', wouldWake: false };
|
|
3977
|
+
}
|
|
3978
|
+
// As admission asks: suppression first, so a script that says show lets the probe on.
|
|
3979
|
+
const d = this.probe({ ...info, suppressionAsked: true });
|
|
3596
3980
|
return {
|
|
3597
3981
|
probe: label,
|
|
3598
3982
|
eventType: info.eventType,
|
|
@@ -3642,7 +4026,6 @@ export class EventGate {
|
|
|
3642
4026
|
type: 'gate:shadow-warnings',
|
|
3643
4027
|
reason,
|
|
3644
4028
|
warnings: warnings.map(formatShadowWarning),
|
|
3645
|
-
timestamp: this.now(),
|
|
3646
4029
|
});
|
|
3647
4030
|
}
|
|
3648
4031
|
}
|
|
@@ -3702,9 +4085,11 @@ export class EventGate {
|
|
|
3702
4085
|
authorId: this.extractAuthorId(info.metadata) ?? undefined,
|
|
3703
4086
|
serverId: info.serverId || undefined,
|
|
3704
4087
|
addressed: Array.isArray(info.tags) && info.tags.includes('chat:addressed'),
|
|
4088
|
+
...(isSystemNotice(info.tags) && !info.folds ? { systemNotice: true as const } : {}),
|
|
3705
4089
|
routeChannelId: this.routeChannelFor(info),
|
|
3706
4090
|
...(typeof info.metadata?.messageId === 'string' ? { messageId: info.metadata.messageId } : {}),
|
|
3707
4091
|
...(typeof info.metadata?.eventId === 'string' ? { eventId: info.metadata.eventId } : {}),
|
|
4092
|
+
...(info.recipients !== undefined ? { recipients: [...info.recipients] } : {}),
|
|
3708
4093
|
};
|
|
3709
4094
|
|
|
3710
4095
|
const existing = this.debounceTimers.get(policy.name);
|
|
@@ -3764,8 +4149,8 @@ export class EventGate {
|
|
|
3764
4149
|
} catch (err) {
|
|
3765
4150
|
const one = events.length === 1;
|
|
3766
4151
|
console.error(
|
|
3767
|
-
`[gate] Rule ${quotedValue(policyName)}: its pending batch of ${events.length} ${one ? 'event' : 'events'}
|
|
3768
|
-
+ `(${errorText(err)}), so the batch is dropped
|
|
4152
|
+
`[gate] Rule ${quotedValue(policyName)}: its pending batch of ${events.length} ${one ? 'event' : 'events'} `
|
|
4153
|
+
+ (deliveredInPart(err) ?? `couldn't be delivered (${errorText(err)}), so the batch is dropped`),
|
|
3769
4154
|
);
|
|
3770
4155
|
return;
|
|
3771
4156
|
}
|
|
@@ -3774,7 +4159,6 @@ export class EventGate {
|
|
|
3774
4159
|
type: 'gate:debounce-delivered',
|
|
3775
4160
|
policyName,
|
|
3776
4161
|
eventCount: events.length,
|
|
3777
|
-
timestamp: Date.now(),
|
|
3778
4162
|
});
|
|
3779
4163
|
}
|
|
3780
4164
|
|
|
@@ -3796,11 +4180,34 @@ export class EventGate {
|
|
|
3796
4180
|
type: 'gate:held-dropped',
|
|
3797
4181
|
dropped: batched.length - events.length,
|
|
3798
4182
|
channels: [...new Set(batched.filter((e) => !events.includes(e)).map(heldAt))],
|
|
3799
|
-
timestamp: this.now(),
|
|
3800
4183
|
});
|
|
3801
4184
|
}
|
|
3802
4185
|
if (events.length === 0) return;
|
|
4186
|
+
// Who each delivery woke, so that a throw after a wake is told as a
|
|
4187
|
+
// delivery in part (PartialDelivery), the batch dropped only for those
|
|
4188
|
+
// it didn't wake.
|
|
4189
|
+
const woke: string[] = [];
|
|
4190
|
+
if (events.every((e) => e.recipients === undefined)) {
|
|
4191
|
+
const names = this.getAgentNamesFn();
|
|
4192
|
+
deliveredTracking(woke, names, () => this.deliverBatch(events, names, undefined, woke));
|
|
4193
|
+
return;
|
|
4194
|
+
}
|
|
4195
|
+
const woken = new Set(events.flatMap((e) => e.recipients ?? []));
|
|
4196
|
+
if (events.some((e) => e.recipients === undefined)) for (const name of this.getAgentNamesFn()) woken.add(name);
|
|
4197
|
+
const groups = new Map<string, { agents: string[]; events: PendingEvent[] }>();
|
|
4198
|
+
for (const agentName of woken) {
|
|
4199
|
+
const seen = events.filter((e) => e.recipients === undefined || e.recipients.includes(agentName));
|
|
4200
|
+
const key = seen.map((e) => events.indexOf(e)).join(',');
|
|
4201
|
+
const group = groups.get(key) ?? { agents: [], events: seen };
|
|
4202
|
+
group.agents.push(agentName);
|
|
4203
|
+
groups.set(key, group);
|
|
4204
|
+
}
|
|
4205
|
+
deliveredTracking(woke, [...woken], () => {
|
|
4206
|
+
for (const group of groups.values()) this.deliverBatch(group.events, group.agents, group.agents, woke);
|
|
4207
|
+
});
|
|
4208
|
+
}
|
|
3803
4209
|
|
|
4210
|
+
private deliverBatch(events: PendingEvent[], woken: readonly string[], audience?: readonly string[], woke?: string[]): void {
|
|
3804
4211
|
const policyNames = [...new Set(events.map(e => e.policyName))];
|
|
3805
4212
|
|
|
3806
4213
|
// Events whose content the framework already stored at arrival (MCPL
|
|
@@ -3859,7 +4266,12 @@ export class EventGate {
|
|
|
3859
4266
|
const text = `[${NOTICE_TAG.gate}: ${events.length} event${events.length > 1 ? 's' : ''} matched]\n\n${lines.join('\n')}`;
|
|
3860
4267
|
const only = events.length === 1 && events[0].inContext ? events[0] : undefined;
|
|
3861
4268
|
let line: WakeLine | undefined;
|
|
3862
|
-
|
|
4269
|
+
// Where a line stored now landed, for the delivery agent's wake, as the
|
|
4270
|
+
// self-wake's notice places its own (readsRowFor): a live turn that
|
|
4271
|
+
// carried and answered the line drops the wake. Other agents' wakes stay
|
|
4272
|
+
// plain by readsRowFor's rule.
|
|
4273
|
+
const placement: WritePlacement = {};
|
|
4274
|
+
if (only && this.newestEntry(only, audience)) {
|
|
3863
4275
|
const shownChannel = only.routeChannelId ?? only.channelId;
|
|
3864
4276
|
line = {
|
|
3865
4277
|
entry: entryRef(only),
|
|
@@ -3872,30 +4284,32 @@ export class EventGate {
|
|
|
3872
4284
|
this.addMessageFn('user', [{ type: 'text', text }], {
|
|
3873
4285
|
source: 'gate:debounce',
|
|
3874
4286
|
policies: policyNames,
|
|
3875
|
-
});
|
|
4287
|
+
}, undefined, placement, audience);
|
|
3876
4288
|
}
|
|
3877
4289
|
|
|
3878
4290
|
// Provenance of the batched wake (who/where woke the agent): the newest
|
|
3879
4291
|
// ADDRESSED event first, else the newest event naming a channel or an
|
|
3880
|
-
// author; channel + author + addressed from
|
|
3881
|
-
// event addressed the agent and its registered
|
|
3882
|
-
// `routeChannelId` names the turn's speech locus too —
|
|
3883
|
-
// back to whoever addressed the agent, not to whichever
|
|
3884
|
-
// any traffic. Ambient-only batches set no locus
|
|
3885
|
-
//
|
|
3886
|
-
//
|
|
4292
|
+
// author, `chat:system` notices aside; channel + author + addressed from
|
|
4293
|
+
// that ONE event. When that event addressed the agent and its registered
|
|
4294
|
+
// channel is known, `routeChannelId` names the turn's speech locus too —
|
|
4295
|
+
// the reply goes back to whoever addressed the agent, not to whichever
|
|
4296
|
+
// channel last saw any traffic. Ambient-only batches set no locus, and
|
|
4297
|
+
// name the channels they came from (`batchChannels`). A batch whose line
|
|
4298
|
+
// the gate left to the turn's start carries it too (`line`), one object
|
|
4299
|
+
// shared by every agent woken with these messages, so it is written once.
|
|
3887
4300
|
const chosen = wakeProvenance(events);
|
|
3888
4301
|
const provenance = line ? { ...chosen, line } : chosen;
|
|
3889
|
-
for (const agentName of
|
|
3890
|
-
this.requestInferenceFn(agentName, 'gate:debounce', 'gate', provenance);
|
|
4302
|
+
for (const agentName of woken) {
|
|
4303
|
+
this.requestInferenceFn(agentName, 'gate:debounce', 'gate', provenance, line ? undefined : placement);
|
|
4304
|
+
woke?.push(agentName);
|
|
3891
4305
|
}
|
|
3892
4306
|
}
|
|
3893
4307
|
|
|
3894
4308
|
/** isNewestEntry for one event; a callback fault keeps the line. */
|
|
3895
|
-
private newestEntry(event: PendingEvent): boolean {
|
|
4309
|
+
private newestEntry(event: PendingEvent, recipients?: readonly string[]): boolean {
|
|
3896
4310
|
if (!this.isNewestEntryFn) return false;
|
|
3897
4311
|
try {
|
|
3898
|
-
return this.isNewestEntryFn(entryRef(event)) === true;
|
|
4312
|
+
return this.isNewestEntryFn(entryRef(event), recipients) === true;
|
|
3899
4313
|
} catch {
|
|
3900
4314
|
return false;
|
|
3901
4315
|
}
|
|
@@ -3918,7 +4332,7 @@ export class EventGate {
|
|
|
3918
4332
|
if (this.disposed) return;
|
|
3919
4333
|
const deciding = this.judging.get(policyName);
|
|
3920
4334
|
if (deciding) {
|
|
3921
|
-
deciding.queued
|
|
4335
|
+
appendAll(deciding.queued, events);
|
|
3922
4336
|
this.capJudgeEvents(deciding.queued);
|
|
3923
4337
|
return;
|
|
3924
4338
|
}
|
|
@@ -3937,8 +4351,8 @@ export class EventGate {
|
|
|
3937
4351
|
this.judging.set(policyName, state);
|
|
3938
4352
|
const timedOut = new Promise<never>((_, reject) => {
|
|
3939
4353
|
state.timer = setTimeout(() => {
|
|
4354
|
+
reject(new JudgeFailure(`the judge took more than ${this.judgeTimeoutMs} ms`));
|
|
3940
4355
|
abort.abort();
|
|
3941
|
-
reject(new Error(`the judge took more than ${this.judgeTimeoutMs} ms`));
|
|
3942
4356
|
}, this.judgeTimeoutMs);
|
|
3943
4357
|
// Never keep the process alive on the judge's account.
|
|
3944
4358
|
state.timer.unref?.();
|
|
@@ -3971,7 +4385,7 @@ export class EventGate {
|
|
|
3971
4385
|
private holdForJudge(policyName: string, events: PendingEvent[], judge: JudgeConfig): void {
|
|
3972
4386
|
const held = this.judgeHeld.get(policyName);
|
|
3973
4387
|
if (held) {
|
|
3974
|
-
held.events
|
|
4388
|
+
appendAll(held.events, events);
|
|
3975
4389
|
held.judge = judge;
|
|
3976
4390
|
this.capJudgeEvents(held.events);
|
|
3977
4391
|
} else {
|
|
@@ -4014,7 +4428,8 @@ export class EventGate {
|
|
|
4014
4428
|
* the judge to weigh what waiting costs: each agent's pending self-wake
|
|
4015
4429
|
* (skip_reply's wake_in_seconds), the end of a sleep, and the pending
|
|
4016
4430
|
* batches of rules other than `exceptPolicy` (a debounce's wakes the agent
|
|
4017
|
-
* when it fires; a judge rule's goes to the judge, so it only may).
|
|
4431
|
+
* when it fires; a judge rule's goes to the judge, so it only may). A batch,
|
|
4432
|
+
* and a sleep with no agent named, reach the gate's recipients alone.
|
|
4018
4433
|
*/
|
|
4019
4434
|
upcomingWakes(agentNames: string[], exceptPolicy?: string): UpcomingWake[] {
|
|
4020
4435
|
const wakes: UpcomingWake[] = [];
|
|
@@ -4022,19 +4437,21 @@ export class EventGate {
|
|
|
4022
4437
|
for (const name of agentNames) {
|
|
4023
4438
|
const due = this.selfWakeDue.get(name);
|
|
4024
4439
|
if (due && this.selfWakeTimers.has(name)) {
|
|
4025
|
-
wakes.push({ at: due.at, certain: true, what: `${agentNames.length > 1 ? `${name}'s` : 'its'} own ${due.source} timer` });
|
|
4440
|
+
wakes.push({ at: due.at, certain: true, kind: 'self-wake', what: `${agentNames.length > 1 ? `${name}'s` : 'its'} own ${due.source} timer` });
|
|
4026
4441
|
}
|
|
4027
4442
|
}
|
|
4028
|
-
|
|
4029
|
-
|
|
4443
|
+
const recipients = this.getAgentNamesFn();
|
|
4444
|
+
const recipient = agentNames.some((name) => recipients.includes(name));
|
|
4445
|
+
if (this.sleepUntil > now && (this.sleepAgent === undefined ? recipient : agentNames.includes(this.sleepAgent))) {
|
|
4446
|
+
wakes.push({ at: this.sleepUntil, certain: true, kind: 'sleep-end', what: 'the end of its sleep' });
|
|
4030
4447
|
}
|
|
4031
4448
|
for (const [name, state] of this.debounceTimers) {
|
|
4032
|
-
if (name === exceptPolicy || state.events.length === 0) continue;
|
|
4449
|
+
if (!recipient || name === exceptPolicy || state.events.length === 0) continue;
|
|
4033
4450
|
const n = state.events.length;
|
|
4034
4451
|
const batch = `${n} ${n === 1 ? 'event' : 'events'}`;
|
|
4035
4452
|
wakes.push(state.judge
|
|
4036
|
-
? { at: state.dueAt, certain: false, what: `rule ${quotedValue(name)}'s batch of ${batch} goes to the judge` }
|
|
4037
|
-
: { at: state.dueAt, certain: true, what: `rule ${quotedValue(name)}'s debounced batch of ${batch}` });
|
|
4453
|
+
? { at: state.dueAt, certain: false, kind: 'judge-batch', what: `rule ${quotedValue(name)}'s batch of ${batch} goes to the judge` }
|
|
4454
|
+
: { at: state.dueAt, certain: true, kind: 'debounce', what: `rule ${quotedValue(name)}'s debounced batch of ${batch}` });
|
|
4038
4455
|
}
|
|
4039
4456
|
return wakes.sort((a, b) => (a.at ?? 0) - (b.at ?? 0));
|
|
4040
4457
|
}
|
|
@@ -4094,7 +4511,7 @@ export class EventGate {
|
|
|
4094
4511
|
}
|
|
4095
4512
|
const outcome: 'reply' | 'wait' | 'error' = failed ? 'error' : verdict.reply ? 'reply' : 'wait';
|
|
4096
4513
|
const reason = failed
|
|
4097
|
-
? (err !== undefined ?
|
|
4514
|
+
? (err !== undefined ? judgeFailureWords(err) : 'the judge gave no verdict')
|
|
4098
4515
|
: typeof verdict.reason === 'string' ? verdict.reason : '';
|
|
4099
4516
|
const record = this.judgeRecords.get(policyName) ?? { verdicts: { reply: 0, wait: 0, error: 0 } };
|
|
4100
4517
|
record.verdicts[outcome]++;
|
|
@@ -4116,7 +4533,7 @@ export class EventGate {
|
|
|
4116
4533
|
const wake = outcome === 'reply' || (outcome === 'error' && this.judgeOnError === 'wake');
|
|
4117
4534
|
if (failed) {
|
|
4118
4535
|
console.error(
|
|
4119
|
-
`[gate] Rule ${quotedValue(policyName)}: the reply judge failed (${reason}), so its batch of ${state.events.length} `
|
|
4536
|
+
`[gate] Rule ${quotedValue(policyName)}: the reply judge failed (${err !== undefined ? errorText(err) : reason}), so its batch of ${state.events.length} `
|
|
4120
4537
|
+ `${state.events.length === 1 ? 'event' : 'events'} ${wake ? 'wakes the agent' : "doesn't wake the agent"} (judge onError "${this.judgeOnError}")`,
|
|
4121
4538
|
);
|
|
4122
4539
|
}
|
|
@@ -4131,7 +4548,6 @@ export class EventGate {
|
|
|
4131
4548
|
...(!failed && typeof verdict.inputTokens === 'number' ? { inputTokens: verdict.inputTokens } : {}),
|
|
4132
4549
|
...(!failed && typeof verdict.cacheReadTokens === 'number' ? { cacheReadTokens: verdict.cacheReadTokens } : {}),
|
|
4133
4550
|
...(!failed && typeof verdict.outputTokens === 'number' ? { outputTokens: verdict.outputTokens } : {}),
|
|
4134
|
-
timestamp: Date.now(),
|
|
4135
4551
|
});
|
|
4136
4552
|
if (this.disposed) return;
|
|
4137
4553
|
const judge = this.currentJudge(policyName, state.judge);
|
|
@@ -4156,8 +4572,8 @@ export class EventGate {
|
|
|
4156
4572
|
} catch (err) {
|
|
4157
4573
|
const one = events.length === 1;
|
|
4158
4574
|
console.error(
|
|
4159
|
-
`[gate] a judged batch of ${events.length} ${one ? 'event' : 'events'}
|
|
4160
|
-
+ `(${errorText(err)}), so the batch is dropped
|
|
4575
|
+
`[gate] a judged batch of ${events.length} ${one ? 'event' : 'events'} `
|
|
4576
|
+
+ (deliveredInPart(err) ?? `couldn't be delivered (${errorText(err)}), so the batch is dropped`),
|
|
4161
4577
|
);
|
|
4162
4578
|
}
|
|
4163
4579
|
}
|
|
@@ -4231,8 +4647,8 @@ export class EventGate {
|
|
|
4231
4647
|
* buffer through the same path a turn's end uses. Self-wake and sleep-expiry
|
|
4232
4648
|
* timers deliberately do NOT route through this — their inference requests
|
|
4233
4649
|
* are per-agent targeted pushes that the framework's quiesce wake gate parks
|
|
4234
|
-
* and releases at resume, whereas the buffer's deliverEvents
|
|
4235
|
-
*
|
|
4650
|
+
* and releases at resume, whereas the buffer's deliverEvents wakes every
|
|
4651
|
+
* agent a batch is for.
|
|
4236
4652
|
*/
|
|
4237
4653
|
setQuiesced(quiesced: boolean): void {
|
|
4238
4654
|
if (this.quiesced === quiesced) return;
|
|
@@ -4260,10 +4676,12 @@ export class EventGate {
|
|
|
4260
4676
|
this.deliverEvents(events);
|
|
4261
4677
|
} catch (err) {
|
|
4262
4678
|
const one = events.length === 1;
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4679
|
+
const held = `${events.length} ${one ? 'event' : 'events'}`;
|
|
4680
|
+
const partly = deliveredInPart(err);
|
|
4681
|
+
console.error(partly
|
|
4682
|
+
? `[gate] a batch of ${held}, held for an agent's turn or the host's quiesce, ${partly}`
|
|
4683
|
+
: `[gate] ${held} held for an agent's turn or the host's quiesce couldn't be delivered (${errorText(err)}), `
|
|
4684
|
+
+ `so ${one ? "it's" : "they're"} dropped`);
|
|
4267
4685
|
}
|
|
4268
4686
|
});
|
|
4269
4687
|
}
|
|
@@ -4347,7 +4765,6 @@ export class EventGate {
|
|
|
4347
4765
|
matchedPolicy: 'asleep',
|
|
4348
4766
|
trigger: false,
|
|
4349
4767
|
behavior: 'skip',
|
|
4350
|
-
timestamp: this.now(),
|
|
4351
4768
|
});
|
|
4352
4769
|
return { until: this.sleepUntil };
|
|
4353
4770
|
}
|
|
@@ -4405,13 +4822,13 @@ export class EventGate {
|
|
|
4405
4822
|
matchedPolicy: 'self-wake',
|
|
4406
4823
|
trigger: true,
|
|
4407
4824
|
behavior: 'always',
|
|
4408
|
-
timestamp: this.now(),
|
|
4409
4825
|
});
|
|
4410
4826
|
// Compact in-window notice: without it the wake turn contains nothing
|
|
4411
4827
|
// new, so the agent can't tell WHAT woke it (its own timer vs. a
|
|
4412
4828
|
// heartbeat) or WHEN. One line, timestamped to the second. Added
|
|
4413
|
-
// before the inference request so it rides the wake turn
|
|
4414
|
-
//
|
|
4829
|
+
// before the inference request so it rides the wake turn: it is stored
|
|
4830
|
+
// at once, and while a turn of the agent's is alive it is kept out of
|
|
4831
|
+
// the window until the agent's next presentation point.
|
|
4415
4832
|
const nowIso = new Date(this.now()).toISOString().slice(0, 19) + 'Z';
|
|
4416
4833
|
// Deliver the notice into the WAKING agent's window — a self-wake
|
|
4417
4834
|
// armed by a non-primary agent (e.g. a subconscious cadence) must not
|
|
@@ -4440,7 +4857,6 @@ export class EventGate {
|
|
|
4440
4857
|
matchedPolicy: 'self-wake',
|
|
4441
4858
|
trigger: false,
|
|
4442
4859
|
behavior: 'skip',
|
|
4443
|
-
timestamp: this.now(),
|
|
4444
4860
|
});
|
|
4445
4861
|
return { inMs: ms, armed: true };
|
|
4446
4862
|
}
|
|
@@ -4483,7 +4899,6 @@ export class EventGate {
|
|
|
4483
4899
|
matchedPolicy: 'asleep',
|
|
4484
4900
|
trigger: true,
|
|
4485
4901
|
behavior: 'always',
|
|
4486
|
-
timestamp: this.now(),
|
|
4487
4902
|
});
|
|
4488
4903
|
const detail = note ? `${reason}: ${note}` : reason;
|
|
4489
4904
|
const targets = agent ? [agent] : this.getAgentNamesFn();
|
|
@@ -4613,6 +5028,7 @@ export class EventGate {
|
|
|
4613
5028
|
}
|
|
4614
5029
|
|
|
4615
5030
|
getStatus(): GateStatus {
|
|
5031
|
+
this.syncSuppressScripts();
|
|
4616
5032
|
const policies: GatePolicyStats[] = this.config.policies.map(p => {
|
|
4617
5033
|
const stats = this.stats.get(p.name);
|
|
4618
5034
|
const debounce = this.debounceTimers.get(p.name);
|
|
@@ -4621,6 +5037,9 @@ export class EventGate {
|
|
|
4621
5037
|
behavior: p.behavior,
|
|
4622
5038
|
matchCount: stats?.matchCount ?? 0,
|
|
4623
5039
|
lastMatchTimestamp: stats?.lastMatchTimestamp ?? null,
|
|
5040
|
+
...(p.suppress
|
|
5041
|
+
? { suppress: { ...p.suppress, ...(p.suppress.script !== undefined && this.suppressScripts.status(p.name) ? { scriptStatus: this.suppressScripts.status(p.name)! } : {}) } }
|
|
5042
|
+
: {}),
|
|
4624
5043
|
};
|
|
4625
5044
|
if (typeof p.behavior === 'object' && 'debounce' in p.behavior) {
|
|
4626
5045
|
result.debounceState = {
|
|
@@ -4684,6 +5103,13 @@ export class EventGate {
|
|
|
4684
5103
|
// Fresh, not the cache: the sources the host knows (eventTypesFrom)
|
|
4685
5104
|
// change as servers connect, with no change to the rules.
|
|
4686
5105
|
shadowWarnings: this.lintShadows().map(formatShadowWarning),
|
|
5106
|
+
...((): Pick<GateStatus, 'catchAll'> => {
|
|
5107
|
+
const rule = firstCatchAll(this.config.policies, { lettingBy: true });
|
|
5108
|
+
return rule ? { catchAll: {
|
|
5109
|
+
rule,
|
|
5110
|
+
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.`,
|
|
5111
|
+
} } : {};
|
|
5112
|
+
})(),
|
|
4687
5113
|
totalEvaluations: this.totalEvaluations,
|
|
4688
5114
|
defaultDecisions: {
|
|
4689
5115
|
triggered: this.defaultTriggered,
|
|
@@ -4720,6 +5146,7 @@ export class EventGate {
|
|
|
4720
5146
|
|
|
4721
5147
|
dispose(): void {
|
|
4722
5148
|
this.gateScript.dispose();
|
|
5149
|
+
this.suppressScripts.dispose();
|
|
4723
5150
|
for (const state of this.debounceTimers.values()) {
|
|
4724
5151
|
clearTimeout(state.timer);
|
|
4725
5152
|
}
|