@animalabs/agent-framework 0.27.0 → 0.29.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 +145 -41
- package/dist/src/agent.d.ts +29 -2
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +73 -12
- 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 +3 -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 +11 -5
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/auto-continue.d.ts +5 -2
- package/dist/src/auto-continue.d.ts.map +1 -1
- package/dist/src/auto-continue.js +9 -5
- package/dist/src/auto-continue.js.map +1 -1
- package/dist/src/code-execution/isolate/runner.d.ts +7 -5
- package/dist/src/code-execution/isolate/runner.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/runner.js +7 -5
- package/dist/src/code-execution/isolate/runner.js.map +1 -1
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +3 -2
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/cut-off-round.d.ts +24 -0
- package/dist/src/cut-off-round.d.ts.map +1 -0
- package/dist/src/cut-off-round.js +87 -0
- package/dist/src/cut-off-round.js.map +1 -0
- package/dist/src/framework.d.ts +468 -125
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +3651 -1111
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +133 -13
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +654 -59
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/index.d.ts +3 -2
- package/dist/src/gate/index.d.ts.map +1 -1
- package/dist/src/gate/index.js +2 -1
- package/dist/src/gate/index.js.map +1 -1
- package/dist/src/gate/reply-judge.d.ts +116 -0
- package/dist/src/gate/reply-judge.d.ts.map +1 -0
- package/dist/src/gate/reply-judge.js +467 -0
- package/dist/src/gate/reply-judge.js.map +1 -0
- package/dist/src/gate/types.d.ts +173 -2
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/heartbeat/heartbeat.d.ts +202 -0
- package/dist/src/heartbeat/heartbeat.d.ts.map +1 -0
- package/dist/src/heartbeat/heartbeat.js +778 -0
- package/dist/src/heartbeat/heartbeat.js.map +1 -0
- package/dist/src/heartbeat/reminder-time.d.ts +5 -0
- package/dist/src/heartbeat/reminder-time.d.ts.map +1 -0
- package/dist/src/heartbeat/reminder-time.js +107 -0
- package/dist/src/heartbeat/reminder-time.js.map +1 -0
- package/dist/src/held-writes.d.ts +3 -3
- package/dist/src/held-writes.js +3 -3
- package/dist/src/host-presentation.d.ts +170 -0
- package/dist/src/host-presentation.d.ts.map +1 -0
- package/dist/src/host-presentation.js +561 -0
- package/dist/src/host-presentation.js.map +1 -0
- package/dist/src/index.d.ts +5 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/line-reader.d.ts +4 -0
- package/dist/src/line-reader.d.ts.map +1 -0
- package/dist/src/line-reader.js +59 -0
- package/dist/src/line-reader.js.map +1 -0
- package/dist/src/mcpl/capability-mask.d.ts.map +1 -1
- package/dist/src/mcpl/capability-mask.js +3 -2
- package/dist/src/mcpl/capability-mask.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +34 -6
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +133 -54
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +2 -1
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/convert-block.d.ts +1 -0
- package/dist/src/mcpl/convert-block.d.ts.map +1 -1
- package/dist/src/mcpl/convert-block.js +16 -4
- package/dist/src/mcpl/convert-block.js.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.js +4 -3
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +29 -11
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
- package/dist/src/mcpl/inbound-source.js +43 -70
- package/dist/src/mcpl/inbound-source.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +11 -7
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +3 -2
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +25 -3
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-handler.js +2 -2
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +12 -40
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +29 -23
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +18 -8
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +70 -30
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/skip-reply-wake.d.ts +25 -0
- package/dist/src/mcpl/skip-reply-wake.d.ts.map +1 -1
- package/dist/src/mcpl/skip-reply-wake.js +25 -0
- package/dist/src/mcpl/skip-reply-wake.js.map +1 -1
- package/dist/src/mcpl/tags.d.ts +9 -2
- package/dist/src/mcpl/tags.d.ts.map +1 -1
- package/dist/src/mcpl/tags.js +12 -2
- package/dist/src/mcpl/tags.js.map +1 -1
- package/dist/src/mcpl/tool-presence.d.ts +66 -14
- package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
- package/dist/src/mcpl/tool-presence.js +150 -80
- package/dist/src/mcpl/tool-presence.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts +10 -3
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
- package/dist/src/mcpl/tool-signature.js +60 -30
- package/dist/src/mcpl/tool-signature.js.map +1 -1
- package/dist/src/mcpl/toolset-host.d.ts +88 -55
- package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
- package/dist/src/mcpl/toolset-host.js +220 -111
- package/dist/src/mcpl/toolset-host.js.map +1 -1
- package/dist/src/mcpl/toolsets-module.d.ts +27 -35
- package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
- package/dist/src/mcpl/toolsets-module.js +313 -91
- package/dist/src/mcpl/toolsets-module.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +1 -1
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +16 -16
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +31 -13
- 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 +10 -8
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +15 -9
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/api/index.d.ts.map +1 -1
- package/dist/src/modules/api/index.js +4 -2
- package/dist/src/modules/api/index.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +33 -25
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/history/index.d.ts.map +1 -1
- package/dist/src/modules/history/index.js +30 -17
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/sender-name.d.ts +7 -0
- package/dist/src/modules/sender-name.d.ts.map +1 -0
- package/dist/src/modules/sender-name.js +10 -0
- package/dist/src/modules/sender-name.js.map +1 -0
- package/dist/src/modules/workspace/answers.d.ts +26 -0
- package/dist/src/modules/workspace/answers.d.ts.map +1 -0
- package/dist/src/modules/workspace/answers.js +138 -0
- package/dist/src/modules/workspace/answers.js.map +1 -0
- package/dist/src/modules/workspace/branch-intent.d.ts +5 -3
- package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
- package/dist/src/modules/workspace/branch-intent.js +6 -2
- package/dist/src/modules/workspace/branch-intent.js.map +1 -1
- package/dist/src/modules/workspace/direct-write.d.ts +3 -0
- package/dist/src/modules/workspace/direct-write.d.ts.map +1 -1
- package/dist/src/modules/workspace/direct-write.js +59 -24
- package/dist/src/modules/workspace/direct-write.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +33 -11
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +48 -17
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/edit-text.d.ts +2 -1
- package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
- package/dist/src/modules/workspace/edit-text.js +4 -2
- package/dist/src/modules/workspace/edit-text.js.map +1 -1
- package/dist/src/modules/workspace/effects.d.ts +4 -2
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +67 -50
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +63 -51
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +980 -547
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +125 -82
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +465 -241
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/read-page.d.ts.map +1 -1
- package/dist/src/modules/workspace/read-page.js +7 -1
- package/dist/src/modules/workspace/read-page.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +7 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +156 -50
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/result-text.d.ts +1 -0
- package/dist/src/modules/workspace/result-text.d.ts.map +1 -1
- package/dist/src/modules/workspace/result-text.js.map +1 -1
- package/dist/src/modules/workspace/sync.d.ts +9 -0
- package/dist/src/modules/workspace/sync.d.ts.map +1 -1
- package/dist/src/modules/workspace/sync.js +14 -0
- package/dist/src/modules/workspace/sync.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +96 -43
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +8 -3
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/notice-tags.d.ts +79 -0
- package/dist/src/notice-tags.d.ts.map +1 -0
- package/dist/src/notice-tags.js +83 -0
- package/dist/src/notice-tags.js.map +1 -0
- package/dist/src/prose-drafts.d.ts +4 -3
- package/dist/src/prose-drafts.d.ts.map +1 -1
- package/dist/src/prose-drafts.js +3 -2
- package/dist/src/prose-drafts.js.map +1 -1
- package/dist/src/reasoning-loss-notice.d.ts +23 -0
- package/dist/src/reasoning-loss-notice.d.ts.map +1 -0
- package/dist/src/reasoning-loss-notice.js +52 -0
- package/dist/src/reasoning-loss-notice.js.map +1 -0
- package/dist/src/reasoning-loss.d.ts +24 -0
- package/dist/src/reasoning-loss.d.ts.map +1 -0
- package/dist/src/reasoning-loss.js +102 -0
- package/dist/src/reasoning-loss.js.map +1 -0
- package/dist/src/send-tools.d.ts +3 -2
- package/dist/src/send-tools.d.ts.map +1 -1
- package/dist/src/send-tools.js +3 -2
- package/dist/src/send-tools.js.map +1 -1
- package/dist/src/sent-rows.js +1 -1
- package/dist/src/silent-heartbeat.d.ts +1 -53
- package/dist/src/silent-heartbeat.d.ts.map +1 -1
- package/dist/src/silent-heartbeat.js +56 -3
- package/dist/src/silent-heartbeat.js.map +1 -1
- package/dist/src/supplied-text.d.ts +35 -0
- package/dist/src/supplied-text.d.ts.map +1 -0
- package/dist/src/supplied-text.js +141 -0
- package/dist/src/supplied-text.js.map +1 -0
- package/dist/src/thinking-drops.d.ts +4 -11
- package/dist/src/thinking-drops.d.ts.map +1 -1
- package/dist/src/thinking-drops.js +7 -14
- package/dist/src/thinking-drops.js.map +1 -1
- package/dist/src/tool-args.d.ts +12 -0
- package/dist/src/tool-args.d.ts.map +1 -1
- package/dist/src/tool-args.js +39 -4
- package/dist/src/tool-args.js.map +1 -1
- package/dist/src/tool-image-ledger.d.ts.map +1 -1
- package/dist/src/tool-image-ledger.js +2 -1
- package/dist/src/tool-image-ledger.js.map +1 -1
- package/dist/src/tool-presentation.d.ts.map +1 -1
- package/dist/src/tool-presentation.js +5 -4
- package/dist/src/tool-presentation.js.map +1 -1
- package/dist/src/tool-result-guard.d.ts +8 -4
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +23 -11
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +3 -2
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +24 -26
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +4 -1
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/types/agent.d.ts +63 -18
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +18 -8
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +58 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +27 -13
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +34 -1
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agent.ts +76 -12
- package/src/api/mcp-server-core.ts +3 -14
- package/src/api/server.ts +11 -5
- package/src/auto-continue.ts +12 -5
- package/src/code-execution/isolate/runner.ts +7 -5
- package/src/code-execution/isolate/scripts.ts +3 -2
- package/src/cut-off-round.ts +98 -0
- package/src/framework.ts +3772 -1138
- package/src/gate/event-gate.ts +705 -60
- package/src/gate/index.ts +9 -1
- package/src/gate/reply-judge.ts +512 -0
- package/src/gate/types.ts +176 -3
- package/src/heartbeat/heartbeat.ts +843 -0
- package/src/heartbeat/reminder-time.ts +107 -0
- package/src/held-writes.ts +3 -3
- package/src/host-presentation.ts +605 -0
- package/src/index.ts +5 -1
- package/src/line-reader.ts +58 -0
- package/src/mcpl/capability-mask.ts +3 -2
- package/src/mcpl/channel-registry.ts +139 -53
- package/src/mcpl/conversation-router.ts +2 -1
- package/src/mcpl/convert-block.ts +17 -4
- package/src/mcpl/hook-orchestrator.ts +4 -3
- package/src/mcpl/inbound-source.ts +45 -70
- package/src/mcpl/modern-connection.ts +11 -7
- package/src/mcpl/protocol-family.ts +24 -3
- package/src/mcpl/push-handler.ts +2 -2
- package/src/mcpl/references.ts +31 -23
- package/src/mcpl/server-connection.ts +76 -34
- package/src/mcpl/skip-reply-wake.ts +35 -0
- package/src/mcpl/tags.ts +13 -2
- package/src/mcpl/tool-presence.ts +149 -82
- package/src/mcpl/tool-signature.ts +64 -32
- package/src/mcpl/toolset-host.ts +212 -110
- package/src/mcpl/toolsets-module.ts +277 -91
- package/src/mcpl/transport.ts +18 -14
- package/src/mcpl/types.ts +32 -13
- package/src/module-registry.ts +14 -9
- package/src/modules/api/index.ts +4 -2
- package/src/modules/discord/index.ts +37 -25
- package/src/modules/history/index.ts +30 -18
- package/src/modules/sender-name.ts +9 -0
- package/src/modules/workspace/answers.ts +151 -0
- package/src/modules/workspace/branch-intent.ts +10 -5
- package/src/modules/workspace/direct-write.ts +59 -23
- package/src/modules/workspace/disk-agreement.ts +52 -21
- package/src/modules/workspace/edit-text.ts +7 -5
- package/src/modules/workspace/effects.ts +69 -50
- package/src/modules/workspace/index.ts +966 -556
- package/src/modules/workspace/observe.ts +497 -242
- package/src/modules/workspace/read-page.ts +7 -1
- package/src/modules/workspace/reconcile.ts +184 -49
- package/src/modules/workspace/result-text.ts +2 -0
- package/src/modules/workspace/sync.ts +16 -0
- package/src/modules/workspace/types.ts +96 -43
- package/src/modules/workspace/watcher.ts +8 -3
- package/src/notice-tags.ts +84 -0
- package/src/prose-drafts.ts +4 -3
- package/src/reasoning-loss-notice.ts +64 -0
- package/src/reasoning-loss.ts +120 -0
- package/src/send-tools.ts +3 -2
- package/src/sent-rows.ts +1 -1
- package/src/silent-heartbeat.ts +4 -3
- package/src/supplied-text.ts +154 -0
- package/src/thinking-drops.ts +10 -16
- package/src/tool-args.ts +38 -4
- package/src/tool-image-ledger.ts +2 -1
- package/src/tool-presentation.ts +5 -4
- package/src/tool-result-guard.ts +26 -11
- package/src/tune-out/coordinator.ts +25 -27
- package/src/tune-out/tools.ts +4 -1
- package/src/types/agent.ts +63 -18
- package/src/types/events.ts +18 -8
- package/src/types/framework.ts +59 -0
- package/src/types/module.ts +28 -13
- package/src/types/trace.ts +36 -1
package/src/gate/event-gate.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* inference tokens, not whether the agent should know about the event.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { NOTICE_TAG } from '../notice-tags.js';
|
|
15
|
+
import { cutToFit, suppliedValue } from '../supplied-text.js';
|
|
14
16
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
15
17
|
import { basename, dirname, join } from 'node:path';
|
|
16
18
|
import { GateScript } from './gate-script.js';
|
|
@@ -33,10 +35,18 @@ import type {
|
|
|
33
35
|
GateStatus,
|
|
34
36
|
ShadowWarning,
|
|
35
37
|
GateProbeRow,
|
|
38
|
+
JudgeConfig,
|
|
39
|
+
JudgeEventSummary,
|
|
40
|
+
JudgeFn,
|
|
41
|
+
JudgeVerdict,
|
|
42
|
+
UpcomingWake,
|
|
36
43
|
} from './types.js';
|
|
37
44
|
|
|
38
45
|
// Re-export types consumers need
|
|
39
|
-
export type {
|
|
46
|
+
export type {
|
|
47
|
+
GateConfig, GateOptions, GateDecision, GateEntryRef, GateEventInfo, WakeLine,
|
|
48
|
+
JudgeConfig, JudgeEventSummary, JudgeFn, JudgeRequest, JudgeVerdict, ReplyJudgeOptions, UpcomingWake,
|
|
49
|
+
} from './types.js';
|
|
40
50
|
|
|
41
51
|
// ---------------------------------------------------------------------------
|
|
42
52
|
// Constants
|
|
@@ -63,6 +73,15 @@ function parseGateJson(text: string): unknown {
|
|
|
63
73
|
const MIN_DEBOUNCE_MS = 100;
|
|
64
74
|
/** Maximum debounce value (ms). */
|
|
65
75
|
const MAX_DEBOUNCE_MS = 300_000;
|
|
76
|
+
/** Maximum `judge.maxWaitMs` (ms): an hour. */
|
|
77
|
+
const MAX_JUDGE_WAIT_MS = 3_600_000;
|
|
78
|
+
/** Longest `judge.instructions` (characters) a rule may hold: they go to the
|
|
79
|
+
* judge with every batch, so they're kept short. */
|
|
80
|
+
const MAX_JUDGE_INSTRUCTIONS = 4_000;
|
|
81
|
+
/** How long a judge request may take by default (ms) before onError decides. */
|
|
82
|
+
const DEFAULT_JUDGE_TIMEOUT_MS = 20_000;
|
|
83
|
+
/** Judge failures in a row after which gate_status names the judge as failing. */
|
|
84
|
+
const JUDGE_FAILING_STREAK = 3;
|
|
66
85
|
|
|
67
86
|
/** Whether a check made at `last` is still fresh at `now`, less than the
|
|
68
87
|
* reload throttle ago. A clock stepped backwards since makes it stale, so
|
|
@@ -131,10 +150,11 @@ interface PendingEvent {
|
|
|
131
150
|
|
|
132
151
|
/** Where the host put a message the gate wrote: the agent whose window it
|
|
133
152
|
* went to and, when it was held for that agent's live turn, the id it waits
|
|
134
|
-
* under in the host's queue. */
|
|
153
|
+
* under in the host's queue, or else the id it was stored under. */
|
|
135
154
|
export interface WritePlacement {
|
|
136
155
|
agent?: string;
|
|
137
156
|
deferredId?: string;
|
|
157
|
+
messageId?: string;
|
|
138
158
|
}
|
|
139
159
|
|
|
140
160
|
/**
|
|
@@ -182,6 +202,33 @@ export interface WakeProvenance {
|
|
|
182
202
|
interface DebounceState {
|
|
183
203
|
timer: ReturnType<typeof setTimeout>;
|
|
184
204
|
events: PendingEvent[];
|
|
205
|
+
/** When the batch's first event arrived (ms), for a judge's maxWaitMs. */
|
|
206
|
+
firstAt: number;
|
|
207
|
+
/** When its timer fires (ms), as of its last re-arm (upcomingWakes). */
|
|
208
|
+
dueAt: number;
|
|
209
|
+
/** The judge the batch goes to when it fires, as its rule held it when the
|
|
210
|
+
* batch began: a rule removed or changed meanwhile has its batch judged
|
|
211
|
+
* as the rule it was asked. Absent for a debounce. */
|
|
212
|
+
judge?: JudgeConfig;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** A batch the reply judge is deciding now. */
|
|
216
|
+
interface JudgingState {
|
|
217
|
+
events: PendingEvent[];
|
|
218
|
+
judge: JudgeConfig;
|
|
219
|
+
started: number;
|
|
220
|
+
abort: AbortController;
|
|
221
|
+
/** The judge's time limit, cleared once it's decided or the gate disposed. */
|
|
222
|
+
timer?: ReturnType<typeof setTimeout>;
|
|
223
|
+
/** Batches of the same rule that fired meanwhile, judged once this one is
|
|
224
|
+
* decided: delivered with it on reply, judged afresh on wait. */
|
|
225
|
+
queued: PendingEvent[];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** A rule's verdict counts and newest verdict (gate_status judgeState). */
|
|
229
|
+
interface JudgeRecord {
|
|
230
|
+
verdicts: { reply: number; wait: number; error: number };
|
|
231
|
+
last?: NonNullable<GatePolicyStats['judgeState']>['last'];
|
|
185
232
|
}
|
|
186
233
|
|
|
187
234
|
/** Token bucket for one (policy, key) pair under a rate_limit behavior. */
|
|
@@ -298,7 +345,7 @@ function referenceLine(count: number, channel: string, oldest: number, now: numb
|
|
|
298
345
|
/** A handed batched-wake line's text (WakeLine), as it reads when written at
|
|
299
346
|
* `now`: the gate's words, its message's age counted to then. */
|
|
300
347
|
export function wakeLineText(line: WakeLine, now: number): string {
|
|
301
|
-
return `[
|
|
348
|
+
return `[${NOTICE_TAG.gate}: 1 event matched]\n\n${referenceLine(1, line.shown, line.at, now)}`;
|
|
302
349
|
}
|
|
303
350
|
|
|
304
351
|
/** The event's own stored entry, as the framework is asked about it. */
|
|
@@ -333,9 +380,21 @@ function formatBehavior(b: import('./types.js').GateBehavior): string {
|
|
|
333
380
|
const k = b.passive_sample.keyBy ? `,keyBy:${b.passive_sample.keyBy}` : '';
|
|
334
381
|
return `passive_sample:${b.passive_sample.every}${k}`;
|
|
335
382
|
}
|
|
383
|
+
if ('judge' in b) {
|
|
384
|
+
const w = b.judge.maxWaitMs !== undefined ? `,maxWait:${b.judge.maxWaitMs}` : '';
|
|
385
|
+
return `judge:${b.judge.debounceMs}${w}`;
|
|
386
|
+
}
|
|
336
387
|
return 'unknown';
|
|
337
388
|
}
|
|
338
389
|
|
|
390
|
+
/** The quiet period (ms) of a batching behavior: a debounce's, or a judge's. */
|
|
391
|
+
function batchQuietMs(b: GateBehavior): number | undefined {
|
|
392
|
+
if (typeof b !== 'object') return undefined;
|
|
393
|
+
if ('debounce' in b) return b.debounce;
|
|
394
|
+
if ('judge' in b) return b.judge.debounceMs;
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
|
|
339
398
|
/**
|
|
340
399
|
* Stable fingerprint for a GateBehavior, used during reload to detect
|
|
341
400
|
* config changes that invalidate per-policy state. JSON.stringify is
|
|
@@ -393,9 +452,10 @@ const MATCH_FIELDS = [
|
|
|
393
452
|
'scope', 'source', 'channel', 'mount', 'pathGlob', 'tagsAny', 'tagsAll', 'tagsNone', 'metadataTrue', 'filter',
|
|
394
453
|
];
|
|
395
454
|
const FILTER_FIELDS = ['type', 'pattern'];
|
|
396
|
-
const BEHAVIOR_KINDS = ['debounce', 'rate_limit', 'passive_sample'] as const;
|
|
455
|
+
const BEHAVIOR_KINDS = ['debounce', 'rate_limit', 'passive_sample', 'judge'] as const;
|
|
397
456
|
const RATE_LIMIT_FIELDS = ['tokens', 'refillIntervalMs', 'keyBy'];
|
|
398
457
|
const PASSIVE_SAMPLE_FIELDS = ['every', 'keyBy'];
|
|
458
|
+
const JUDGE_FIELDS = ['debounceMs', 'maxWaitMs', 'instructions'];
|
|
399
459
|
|
|
400
460
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
401
461
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -600,6 +660,7 @@ function fieldLevels(file: string): ReadonlyArray<{ name: string; fields: readon
|
|
|
600
660
|
{ name: '"behavior"', fields: BEHAVIOR_KINDS },
|
|
601
661
|
{ name: '"behavior.rate_limit"', fields: RATE_LIMIT_FIELDS },
|
|
602
662
|
{ name: '"behavior.passive_sample"', fields: PASSIVE_SAMPLE_FIELDS },
|
|
663
|
+
{ name: '"behavior.judge"', fields: JUDGE_FIELDS },
|
|
603
664
|
];
|
|
604
665
|
}
|
|
605
666
|
|
|
@@ -840,7 +901,7 @@ function readRule(raw: unknown, unnamed: string, file = 'gate.json'): RuleReadin
|
|
|
840
901
|
}, (m) => {
|
|
841
902
|
const of = (fields: readonly string[]) => m.keys.some((key) => fields.includes(key));
|
|
842
903
|
if (obj.match === undefined && (of(MATCH_FIELDS) || of(FILTER_FIELDS))) return 'so the rule matches every event';
|
|
843
|
-
if (obj.behavior === undefined && (of(BEHAVIOR_KINDS) || of(RATE_LIMIT_FIELDS) || of(PASSIVE_SAMPLE_FIELDS))) {
|
|
904
|
+
if (obj.behavior === undefined && (of(BEHAVIOR_KINDS) || of(RATE_LIMIT_FIELDS) || of(PASSIVE_SAMPLE_FIELDS) || of(JUDGE_FIELDS))) {
|
|
844
905
|
return 'so the rule\'s behavior is "always"';
|
|
845
906
|
}
|
|
846
907
|
return undefined;
|
|
@@ -869,7 +930,7 @@ function readRule(raw: unknown, unnamed: string, file = 'gate.json'): RuleReadin
|
|
|
869
930
|
|
|
870
931
|
type BehaviorKind = 'always' | 'defer' | 'skip' | (typeof BEHAVIOR_KINDS)[number];
|
|
871
932
|
|
|
872
|
-
const BEHAVIORS_NEEDED = '"always", "defer" or "skip", or an object holding one of "debounce", "rate_limit" and "
|
|
933
|
+
const BEHAVIORS_NEEDED = '"always", "defer" or "skip", or an object holding one of "debounce", "rate_limit", "passive_sample" and "judge"';
|
|
873
934
|
|
|
874
935
|
/**
|
|
875
936
|
* A rule's behavior: "always" when left out; one of the three names; or an
|
|
@@ -878,7 +939,8 @@ const BEHAVIORS_NEEDED = '"always", "defer" or "skip", or an object holding one
|
|
|
878
939
|
* and is read so; a list, or an object holding none of the kinds, was
|
|
879
940
|
* refused, and the rule is left out. Of
|
|
880
941
|
* two kinds or more, "debounce", then "rate_limit", then "passive_sample" was
|
|
881
|
-
* the one read, whatever their order, and is read so
|
|
942
|
+
* the one read, whatever their order, and is read so; "judge", newer than
|
|
943
|
+
* those, is read only where none of them is given. Each other kind is read
|
|
882
944
|
* on its own as well, so that a refusal names its problems too, and a load
|
|
883
945
|
* reads past them, since it isn't read either way. `kind` is the behavior's
|
|
884
946
|
* kind even where it can't be read, for passthrough's check.
|
|
@@ -899,7 +961,7 @@ function readBehavior(r: Reader, value: unknown): { behavior?: GateBehavior | un
|
|
|
899
961
|
const kinds = BEHAVIOR_KINDS.filter((k) => b[k] !== undefined);
|
|
900
962
|
const kind = kinds[0];
|
|
901
963
|
if (kind === undefined) {
|
|
902
|
-
r.problems.push({ text: 'behavior holds none of "debounce", "rate_limit" and "
|
|
964
|
+
r.problems.push({ text: 'behavior holds none of "debounce", "rate_limit", "passive_sample" and "judge"' });
|
|
903
965
|
return {};
|
|
904
966
|
}
|
|
905
967
|
if (kinds.length > 1) {
|
|
@@ -1022,11 +1084,99 @@ function readPassiveSample(r: Reader, value: unknown): GateBehavior | undefined
|
|
|
1022
1084
|
return { passive_sample: out };
|
|
1023
1085
|
}
|
|
1024
1086
|
|
|
1087
|
+
/**
|
|
1088
|
+
* A judge: a debounce's quiet period, read as `debounce` is, with at its
|
|
1089
|
+
* nearest bound one out of range; an optional longest wait, from that
|
|
1090
|
+
* quiet period to an hour, likewise; and the agent's optional instructions to
|
|
1091
|
+
* the judge, text of at most MAX_JUDGE_INSTRUCTIONS characters. Earlier
|
|
1092
|
+
* releases knew no judge, so nothing deployed has one: a rule whose judge
|
|
1093
|
+
* can't be read is left out, and its instructions, which go to a model with
|
|
1094
|
+
* every batch, are never cut to fit.
|
|
1095
|
+
*/
|
|
1096
|
+
function readJudge(r: Reader, value: unknown): GateBehavior | undefined {
|
|
1097
|
+
if (!isPlainObject(value)) {
|
|
1098
|
+
r.problems.push({
|
|
1099
|
+
text: `behavior.judge is ${shownValue(value)}, where an object is needed, such as {"debounceMs": 30000, "instructions": "..."}`,
|
|
1100
|
+
});
|
|
1101
|
+
return undefined;
|
|
1102
|
+
}
|
|
1103
|
+
const conf = withoutEmpty(`${r.where}: behavior.judge`, value, r.notes, {
|
|
1104
|
+
maxWaitMs: 'a batch is judged only after its quiet period',
|
|
1105
|
+
instructions: 'the judge decides without the agent\'s word',
|
|
1106
|
+
});
|
|
1107
|
+
readFieldNames(r, 'behavior.judge', conf, JUDGE_FIELDS);
|
|
1108
|
+
let ok = true;
|
|
1109
|
+
const ms = conf.debounceMs;
|
|
1110
|
+
let debounceMs: number | undefined;
|
|
1111
|
+
if (typeof ms !== 'number' || !Number.isFinite(ms)) {
|
|
1112
|
+
r.problems.push({ text: `behavior.judge.debounceMs is ${shownValue(ms)}, where a number of milliseconds is needed, such as 30000` });
|
|
1113
|
+
ok = false;
|
|
1114
|
+
} else if (ms < MIN_DEBOUNCE_MS) {
|
|
1115
|
+
r.problems.push({
|
|
1116
|
+
text: `behavior.judge.debounceMs is ${ms} ms, under the ${MIN_DEBOUNCE_MS} ms minimum`, read: `so it's read as ${MIN_DEBOUNCE_MS} ms`,
|
|
1117
|
+
});
|
|
1118
|
+
debounceMs = MIN_DEBOUNCE_MS;
|
|
1119
|
+
} else if (ms > MAX_DEBOUNCE_MS) {
|
|
1120
|
+
r.problems.push({
|
|
1121
|
+
text: `behavior.judge.debounceMs is ${ms} ms, over the ${MAX_DEBOUNCE_MS} ms maximum`, read: `so it's read as ${MAX_DEBOUNCE_MS} ms`,
|
|
1122
|
+
});
|
|
1123
|
+
debounceMs = MAX_DEBOUNCE_MS;
|
|
1124
|
+
} else {
|
|
1125
|
+
debounceMs = ms;
|
|
1126
|
+
}
|
|
1127
|
+
let maxWaitMs: number | undefined;
|
|
1128
|
+
const wait = conf.maxWaitMs;
|
|
1129
|
+
if (wait !== undefined) {
|
|
1130
|
+
if (typeof wait !== 'number' || !Number.isFinite(wait)) {
|
|
1131
|
+
r.problems.push({ text: `behavior.judge.maxWaitMs is ${shownValue(wait)}, where a number of milliseconds is needed, such as 120000` });
|
|
1132
|
+
ok = false;
|
|
1133
|
+
} else {
|
|
1134
|
+
const floor = debounceMs ?? MIN_DEBOUNCE_MS;
|
|
1135
|
+
if (wait < floor) {
|
|
1136
|
+
r.problems.push({
|
|
1137
|
+
text: `behavior.judge.maxWaitMs is ${wait} ms, under the rule's ${floor} ms quiet period`,
|
|
1138
|
+
read: `so it's read as ${floor} ms`,
|
|
1139
|
+
});
|
|
1140
|
+
maxWaitMs = floor;
|
|
1141
|
+
} else if (wait > MAX_JUDGE_WAIT_MS) {
|
|
1142
|
+
r.problems.push({
|
|
1143
|
+
text: `behavior.judge.maxWaitMs is ${wait} ms, over the ${MAX_JUDGE_WAIT_MS} ms maximum`,
|
|
1144
|
+
read: `so it's read as ${MAX_JUDGE_WAIT_MS} ms`,
|
|
1145
|
+
});
|
|
1146
|
+
maxWaitMs = MAX_JUDGE_WAIT_MS;
|
|
1147
|
+
} else {
|
|
1148
|
+
maxWaitMs = wait;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
const said = conf.instructions;
|
|
1153
|
+
let instructions: string | undefined;
|
|
1154
|
+
if (said !== undefined) {
|
|
1155
|
+
if (typeof said !== 'string') {
|
|
1156
|
+
r.problems.push({ text: `behavior.judge.instructions is ${shownValue(said)}, where text is needed` });
|
|
1157
|
+
ok = false;
|
|
1158
|
+
} else if (said.length > MAX_JUDGE_INSTRUCTIONS) {
|
|
1159
|
+
r.problems.push({
|
|
1160
|
+
text: `behavior.judge.instructions is ${said.length} characters long, over the ${MAX_JUDGE_INSTRUCTIONS}-character maximum`,
|
|
1161
|
+
});
|
|
1162
|
+
ok = false;
|
|
1163
|
+
} else {
|
|
1164
|
+
instructions = said;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
if (!ok || debounceMs === undefined) return undefined;
|
|
1168
|
+
const out: JudgeConfig = { debounceMs };
|
|
1169
|
+
if (maxWaitMs !== undefined) out.maxWaitMs = maxWaitMs;
|
|
1170
|
+
if (instructions !== undefined) out.instructions = instructions;
|
|
1171
|
+
return { judge: out };
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1025
1174
|
/** How each kind of behavior object is read. */
|
|
1026
1175
|
const READ_KIND: Readonly<Record<(typeof BEHAVIOR_KINDS)[number], (r: Reader, value: unknown) => GateBehavior | undefined>> = {
|
|
1027
1176
|
debounce: readDebounce,
|
|
1028
1177
|
rate_limit: readRateLimit,
|
|
1029
1178
|
passive_sample: readPassiveSample,
|
|
1179
|
+
judge: readJudge,
|
|
1030
1180
|
};
|
|
1031
1181
|
|
|
1032
1182
|
/** A timing kind's keyBy: a field name. One of another type was read as none. */
|
|
@@ -1677,30 +1827,72 @@ function globAtLeastAsBroad(earlier: string | undefined, later: string | undefin
|
|
|
1677
1827
|
return earlier === later;
|
|
1678
1828
|
}
|
|
1679
1829
|
|
|
1830
|
+
/** The event types the gate can see from an MCPL server: its pushes and its
|
|
1831
|
+
* channels' messages. */
|
|
1832
|
+
export const MCPL_GATE_EVENT_TYPES: readonly string[] = ['mcpl:push-event', 'mcpl:channel-incoming'];
|
|
1833
|
+
|
|
1834
|
+
/** The event types a host knows a source sends (EventGate's
|
|
1835
|
+
* `eventTypesFrom`), or undefined when it could send any. */
|
|
1836
|
+
export type EventTypesFrom = (source: string) => readonly string[] | undefined;
|
|
1837
|
+
|
|
1838
|
+
/** The event types `rule` can match: its scope's, narrowed to those its
|
|
1839
|
+
* source is known to send; 'all' when neither says. */
|
|
1840
|
+
function typesSeenBy(rule: GatePolicy, eventTypesFrom?: EventTypesFrom): { types: string[] | 'all'; known: boolean } {
|
|
1841
|
+
const scope = rule.match.scope && rule.match.scope.length > 0 ? rule.match.scope : undefined;
|
|
1842
|
+
const source = rule.match.source;
|
|
1843
|
+
let sent: readonly string[] | undefined;
|
|
1844
|
+
if (eventTypesFrom && source !== undefined && !source.includes('*')) {
|
|
1845
|
+
try {
|
|
1846
|
+
sent = eventTypesFrom(source);
|
|
1847
|
+
} catch {
|
|
1848
|
+
sent = undefined;
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
if (scope && sent) return { types: scope.filter((t) => sent!.includes(t)), known: true };
|
|
1852
|
+
if (scope) return { types: scope, known: true };
|
|
1853
|
+
if (sent) return { types: [...sent], known: true };
|
|
1854
|
+
return { types: 'all', known: false };
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1680
1857
|
/**
|
|
1681
|
-
*
|
|
1682
|
-
*
|
|
1683
|
-
*
|
|
1684
|
-
*
|
|
1685
|
-
*
|
|
1858
|
+
* Whether `earlier` provably matches events `later` would match, so that
|
|
1859
|
+
* `later` doesn't see them when `earlier` sits before it and consumes its
|
|
1860
|
+
* matches: the event types concerned, whether that's every event `later`
|
|
1861
|
+
* would match, and whether `later` is known to see those types. Returns null
|
|
1862
|
+
* when it can't be proven (which is not proof of safety — the lint is
|
|
1863
|
+
* deliberately one-sided so every note it emits is true).
|
|
1686
1864
|
*/
|
|
1687
|
-
function subsumedEventTypes(
|
|
1865
|
+
function subsumedEventTypes(
|
|
1866
|
+
earlier: GatePolicy,
|
|
1867
|
+
later: GatePolicy,
|
|
1868
|
+
eventTypesFrom?: EventTypesFrom,
|
|
1869
|
+
): Omit<ShadowWarning, 'earlier' | 'later'> | null {
|
|
1688
1870
|
const e = earlier.match;
|
|
1689
1871
|
const l = later.match;
|
|
1690
1872
|
|
|
1691
|
-
//
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1873
|
+
// Event types: which of those `later` can match does `earlier` cover?
|
|
1874
|
+
const seen = typesSeenBy(later, eventTypesFrom);
|
|
1875
|
+
const earlierTypes = e.scope && e.scope.length > 0 ? e.scope : 'all';
|
|
1876
|
+
let eventTypes: string[] | 'all';
|
|
1877
|
+
let whole: boolean;
|
|
1878
|
+
let known = seen.known;
|
|
1879
|
+
if (earlierTypes === 'all') {
|
|
1880
|
+
eventTypes = seen.types;
|
|
1881
|
+
whole = true;
|
|
1882
|
+
} else if (seen.types === 'all') {
|
|
1883
|
+
// `later` matches every type; `earlier` only consumes its own scope, so
|
|
1884
|
+
// the overlap is partial — exactly those types, which may never reach
|
|
1885
|
+
// `later`.
|
|
1886
|
+
eventTypes = earlierTypes;
|
|
1887
|
+
whole = false;
|
|
1699
1888
|
} else {
|
|
1700
|
-
const overlap =
|
|
1889
|
+
const overlap = seen.types.filter((t) => earlierTypes.includes(t));
|
|
1701
1890
|
if (overlap.length === 0) return null;
|
|
1702
|
-
|
|
1891
|
+
eventTypes = overlap;
|
|
1892
|
+
whole = overlap.length === seen.types.length;
|
|
1893
|
+
known = true;
|
|
1703
1894
|
}
|
|
1895
|
+
if (Array.isArray(eventTypes) && eventTypes.length === 0) return null;
|
|
1704
1896
|
|
|
1705
1897
|
// Every remaining discriminator on `earlier` must be at least as broad as
|
|
1706
1898
|
// `later`'s, otherwise some `later` events slip past `earlier`.
|
|
@@ -1728,38 +1920,55 @@ function subsumedEventTypes(earlier: GatePolicy, later: GatePolicy): string[] |
|
|
|
1728
1920
|
if (e.tagsAll && !subsetOf(e.tagsAll, l.tagsAll ?? [])) return null;
|
|
1729
1921
|
if (e.tagsNone && !subsetOf(e.tagsNone, l.tagsNone ?? [])) return null;
|
|
1730
1922
|
|
|
1731
|
-
return
|
|
1923
|
+
return { eventTypes, whole, known };
|
|
1732
1924
|
}
|
|
1733
1925
|
|
|
1734
1926
|
/**
|
|
1735
|
-
* Find rules
|
|
1927
|
+
* Find rules an earlier, broader rule keeps from applying (first match wins),
|
|
1928
|
+
* in whole or for some event types.
|
|
1736
1929
|
* The 2026-08-08 Mythos incident is the canonical catch: a prepended
|
|
1737
1930
|
* `{scope:[mcpl:channel-incoming]}` sampler silently made the
|
|
1738
1931
|
* direct-address and mention rules dead for the whole incoming lane.
|
|
1739
1932
|
* `passthrough` observers shadow nothing — non-firing matches fall through.
|
|
1933
|
+
* `eventTypesFrom` leaves out types a rule's source is known never to send:
|
|
1934
|
+
* a rule for an MCPL server's events isn't told about another rule that
|
|
1935
|
+
* takes every `external-message`.
|
|
1740
1936
|
*/
|
|
1741
|
-
export function findShadowedPolicies(policies: GatePolicy[]): ShadowWarning[] {
|
|
1937
|
+
export function findShadowedPolicies(policies: GatePolicy[], eventTypesFrom?: EventTypesFrom): ShadowWarning[] {
|
|
1742
1938
|
const out: ShadowWarning[] = [];
|
|
1743
1939
|
for (let i = 0; i < policies.length; i++) {
|
|
1744
1940
|
const earlier = policies[i];
|
|
1745
1941
|
if (earlier.passthrough) continue;
|
|
1746
1942
|
for (let j = i + 1; j < policies.length; j++) {
|
|
1747
|
-
const
|
|
1748
|
-
if (
|
|
1749
|
-
out.push({ earlier: earlier.name, later: policies[j].name,
|
|
1943
|
+
const found = subsumedEventTypes(earlier, policies[j], eventTypesFrom);
|
|
1944
|
+
if (found) {
|
|
1945
|
+
out.push({ earlier: earlier.name, later: policies[j].name, ...found });
|
|
1750
1946
|
}
|
|
1751
1947
|
}
|
|
1752
1948
|
}
|
|
1753
1949
|
return out;
|
|
1754
1950
|
}
|
|
1755
1951
|
|
|
1756
|
-
/**
|
|
1757
|
-
*
|
|
1758
|
-
*
|
|
1952
|
+
/** A ShadowWarning in words, used in tool results, traces, and gate_status
|
|
1953
|
+
* alike so the same note reads the same way everywhere. It says which of
|
|
1954
|
+
* three it is (the rule never applies; it misses some of its events; it
|
|
1955
|
+
* may not be meant for those events at all) and what to do. */
|
|
1759
1956
|
export function formatShadowWarning(w: ShadowWarning): string {
|
|
1760
|
-
const
|
|
1761
|
-
|
|
1762
|
-
|
|
1957
|
+
const later = `"${w.later}"`;
|
|
1958
|
+
const earlier = `"${w.earlier}"`;
|
|
1959
|
+
const move = `move ${later} above ${earlier} (wake_add_rule with the whole rule and insertBefore: "${w.earlier}")`;
|
|
1960
|
+
if (w.whole) {
|
|
1961
|
+
return `${later} never applies: ${earlier} comes first and matches every event ${later} would. `
|
|
1962
|
+
+ `If ${later} should apply, ${move}, or narrow ${earlier}.`;
|
|
1963
|
+
}
|
|
1964
|
+
const types = w.eventTypes === 'all' ? 'all' : w.eventTypes.join(', ');
|
|
1965
|
+
if (w.known) {
|
|
1966
|
+
return `${earlier} comes first and takes ${later}'s ${types} events, so ${later} applies only to its others. `
|
|
1967
|
+
+ `If it should apply to those too, ${move}.`;
|
|
1968
|
+
}
|
|
1969
|
+
return `${earlier} comes first and takes every ${types} event, so ${later} sees none. `
|
|
1970
|
+
+ `Nothing to do unless ${later} is meant for ${types} events; then ${move}. `
|
|
1971
|
+
+ `A scope on ${later} naming the event types it's for clears this note.`;
|
|
1763
1972
|
}
|
|
1764
1973
|
|
|
1765
1974
|
/** The text of a thrown value, for a log line. It never throws itself, whatever was thrown. */
|
|
@@ -1846,6 +2055,28 @@ export class EventGate {
|
|
|
1846
2055
|
// Debounce state
|
|
1847
2056
|
private debounceTimers = new Map<string, DebounceState>();
|
|
1848
2057
|
|
|
2058
|
+
// Reply judge state (`judge` rules). The judge itself, when the host wires
|
|
2059
|
+
// one; without it a judge rule's batch is delivered as a debounce's.
|
|
2060
|
+
private judgeFn: JudgeFn | undefined;
|
|
2061
|
+
private judgeTimeoutMs: number;
|
|
2062
|
+
private judgeOnError: 'wake' | 'skip';
|
|
2063
|
+
/** Per rule, the batch the judge is deciding now. */
|
|
2064
|
+
private judging = new Map<string, JudgingState>();
|
|
2065
|
+
/** Per rule, a batch that fired while a turn changing the judged context ran or the host was
|
|
2066
|
+
* quiesced, held to be judged once both are clear: the judge reads the
|
|
2067
|
+
* context the turn leaves, which may already answer it. */
|
|
2068
|
+
private judgeHeld = new Map<string, { events: PendingEvent[]; judge: JudgeConfig }>();
|
|
2069
|
+
private judgeRecords = new Map<string, JudgeRecord>();
|
|
2070
|
+
private changesJudgedContextFn: ((agentName: string) => boolean) | undefined;
|
|
2071
|
+
/** Lifetime count of events dropped from batches waiting on the judge, at
|
|
2072
|
+
* the inference buffer's cap (MAX_INFERENCE_BUFFER per rule). */
|
|
2073
|
+
private droppedJudgeEvents = 0;
|
|
2074
|
+
/** Judge requests failed in a row, and the newest failure's reason: a host
|
|
2075
|
+
* whose provider can't serve the judge's model fails every time, which
|
|
2076
|
+
* gate_status says once it's a streak (judgeFailingNotice). */
|
|
2077
|
+
private judgeFailStreak = 0;
|
|
2078
|
+
private judgeLastFailure = '';
|
|
2079
|
+
|
|
1849
2080
|
// Rate-limit state — policy name → key → bucket.
|
|
1850
2081
|
private rateLimitBuckets = new Map<string, Map<string, RateBucket>>();
|
|
1851
2082
|
// Passive-sample state — policy name → key → counter (count since last fire).
|
|
@@ -1865,10 +2096,12 @@ export class EventGate {
|
|
|
1865
2096
|
// Per-policy stats
|
|
1866
2097
|
private stats = new Map<string, PolicyStats>();
|
|
1867
2098
|
|
|
1868
|
-
//
|
|
1869
|
-
// config change
|
|
1870
|
-
|
|
2099
|
+
// The shadow lint's last findings (see refreshShadowWarnings), recomputed
|
|
2100
|
+
// on every config change and fingerprinted so an unchanged set doesn't
|
|
2101
|
+
// re-trace.
|
|
1871
2102
|
private shadowWarningsFingerprint = '[]';
|
|
2103
|
+
/** See the constructor's `eventTypesFrom`. */
|
|
2104
|
+
private eventTypesFromFn: EventTypesFrom | undefined;
|
|
1872
2105
|
|
|
1873
2106
|
// Observability counters for events that didn't match any policy
|
|
1874
2107
|
private totalEvaluations = 0;
|
|
@@ -1896,6 +2129,10 @@ export class EventGate {
|
|
|
1896
2129
|
// self-wake (an external wake supersedes it; the timer would otherwise
|
|
1897
2130
|
// land as a redundant empty wake right after the turn).
|
|
1898
2131
|
private selfWakeTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
2132
|
+
/** When each agent's pending self-wake fires, and what armed it
|
|
2133
|
+
* (upcomingWakes). Read only while its timer is in selfWakeTimers, so an
|
|
2134
|
+
* entry a fire or a cancel left behind says nothing. */
|
|
2135
|
+
private selfWakeDue = new Map<string, { at: number; source: string }>();
|
|
1899
2136
|
|
|
1900
2137
|
// Privileged users who may wake the agent through sleep. Hot-reloaded from
|
|
1901
2138
|
// privilegedUsersPath on change.
|
|
@@ -1947,6 +2184,18 @@ export class EventGate {
|
|
|
1947
2184
|
now?: () => number;
|
|
1948
2185
|
/** Per-event timeout (ms) for the optional gate.js script. Default 50. */
|
|
1949
2186
|
scriptTimeoutMs?: number;
|
|
2187
|
+
/** The reply judge `judge` rules ask (JudgeConfig). Absent: a judge
|
|
2188
|
+
* rule's batch is delivered as a debounce's, and gate_status says so. */
|
|
2189
|
+
judge?: JudgeFn;
|
|
2190
|
+
/** Whether a turn of this agent changes the context the judge reads. Such
|
|
2191
|
+
* turns, and only they, hold a judge rule's batch until they end, and one
|
|
2192
|
+
* that starts while the judge decides stops the judge, whose batch is
|
|
2193
|
+
* then judged after the turn. Absent: every agent's turns do. */
|
|
2194
|
+
changesJudgedContext?: (agentName: string) => boolean;
|
|
2195
|
+
/** How long a judge request may take (ms). Default 20,000. */
|
|
2196
|
+
judgeTimeoutMs?: number;
|
|
2197
|
+
/** What a judge failure does: wake (default) or skip. */
|
|
2198
|
+
judgeOnError?: 'wake' | 'skip';
|
|
1950
2199
|
/** Map an event to the registered channel it arrived on (the host knows
|
|
1951
2200
|
* how push-event origins compose into channel ids). The gate routes a
|
|
1952
2201
|
* batched wake by it, tests a policy's `match.channel` against it when
|
|
@@ -1967,6 +2216,11 @@ export class EventGate {
|
|
|
1967
2216
|
* (WakeProvenance.line), for a fresh start of the wake's turn to decide
|
|
1968
2217
|
* (WakeLine). Absent: every batched wake writes its line, as before. */
|
|
1969
2218
|
isNewestEntry?: (entry: GateEntryRef) => boolean;
|
|
2219
|
+
/** The event types the gate can see from a source, when the host knows
|
|
2220
|
+
* them (an MCPL server's id: MCPL_GATE_EVENT_TYPES), else undefined.
|
|
2221
|
+
* The rule-order notes (findShadowedPolicies) leave out types a rule's
|
|
2222
|
+
* source never sends. Absent: any source can send any type. */
|
|
2223
|
+
eventTypesFrom?: EventTypesFrom;
|
|
1970
2224
|
}) {
|
|
1971
2225
|
this.configPath = opts.configPath;
|
|
1972
2226
|
this.privilegedUsersPath = opts.privilegedUsersPath;
|
|
@@ -1977,7 +2231,14 @@ export class EventGate {
|
|
|
1977
2231
|
this.resolveRouteChannelFn = opts.resolveRouteChannel ?? defaultRouteChannel;
|
|
1978
2232
|
this.channelShownFn = opts.channelShown;
|
|
1979
2233
|
this.isNewestEntryFn = opts.isNewestEntry;
|
|
2234
|
+
this.eventTypesFromFn = opts.eventTypesFrom;
|
|
1980
2235
|
this.now = opts.now ?? (() => Date.now());
|
|
2236
|
+
this.judgeFn = opts.judge;
|
|
2237
|
+
this.changesJudgedContextFn = opts.changesJudgedContext;
|
|
2238
|
+
this.judgeTimeoutMs = opts.judgeTimeoutMs !== undefined && Number.isFinite(opts.judgeTimeoutMs) && opts.judgeTimeoutMs > 0
|
|
2239
|
+
? Math.min(opts.judgeTimeoutMs, MAX_TIMER_MS)
|
|
2240
|
+
: DEFAULT_JUDGE_TIMEOUT_MS;
|
|
2241
|
+
this.judgeOnError = opts.judgeOnError === 'skip' ? 'skip' : 'wake';
|
|
1981
2242
|
this.loadPrivileged();
|
|
1982
2243
|
|
|
1983
2244
|
// Seed or reconcile the on-disk config from the host's starting rules
|
|
@@ -2488,6 +2749,7 @@ export class EventGate {
|
|
|
2488
2749
|
for (const name of removed) {
|
|
2489
2750
|
this.clearPolicyState(name);
|
|
2490
2751
|
this.stats.delete(name);
|
|
2752
|
+
this.judgeRecords.delete(name);
|
|
2491
2753
|
}
|
|
2492
2754
|
const cleared = [...after.keys()].filter((name) => before.has(name) && before.get(name) !== after.get(name));
|
|
2493
2755
|
for (const name of cleared) this.clearPolicyState(name);
|
|
@@ -2963,7 +3225,7 @@ export class EventGate {
|
|
|
2963
3225
|
}
|
|
2964
3226
|
|
|
2965
3227
|
if (typeof policy.behavior === 'object') {
|
|
2966
|
-
if ('debounce' in policy.behavior) {
|
|
3228
|
+
if ('debounce' in policy.behavior || 'judge' in policy.behavior) {
|
|
2967
3229
|
this.handleDebounce(policy, info);
|
|
2968
3230
|
return { trigger: false, policyName: policy.name, behavior: policy.behavior };
|
|
2969
3231
|
}
|
|
@@ -3115,7 +3377,7 @@ export class EventGate {
|
|
|
3115
3377
|
if (b === 'always') return true;
|
|
3116
3378
|
if (b === 'defer' || b === 'skip') return false;
|
|
3117
3379
|
if (typeof b === 'object') {
|
|
3118
|
-
if ('debounce' in b) return false; // batches; never triggers inline
|
|
3380
|
+
if ('debounce' in b || 'judge' in b) return false; // batches; never triggers inline
|
|
3119
3381
|
if ('rate_limit' in b) {
|
|
3120
3382
|
const cfg = b.rate_limit;
|
|
3121
3383
|
const key = this.resolveKey(info, cfg.keyBy);
|
|
@@ -3255,22 +3517,32 @@ export class EventGate {
|
|
|
3255
3517
|
/** Current shadow-lint findings for the live policy order. Fresh compute —
|
|
3256
3518
|
* cheap (policy lists are tens of entries at most). */
|
|
3257
3519
|
lintShadows(): ShadowWarning[] {
|
|
3258
|
-
return findShadowedPolicies(this.config.policies);
|
|
3520
|
+
return findShadowedPolicies(this.config.policies, this.eventTypesFromFn);
|
|
3259
3521
|
}
|
|
3260
3522
|
|
|
3261
3523
|
/**
|
|
3262
|
-
*
|
|
3263
|
-
*
|
|
3264
|
-
*
|
|
3265
|
-
|
|
3524
|
+
* The sources the host knows changed (eventTypesFrom answers differently),
|
|
3525
|
+
* as when an MCPL server connects or is removed: the rule-order notes are
|
|
3526
|
+
* made again, and traced if they changed, as a change to the rules would.
|
|
3527
|
+
*/
|
|
3528
|
+
sourcesChanged(): void {
|
|
3529
|
+
this.refreshShadowWarnings('sources-changed');
|
|
3530
|
+
}
|
|
3531
|
+
|
|
3532
|
+
/**
|
|
3533
|
+
* Recompute shadow warnings and, when the set changed, emit a
|
|
3534
|
+
* `gate:shadow-warnings` trace so the hazard lands in ops observability at
|
|
3535
|
+
* the moment the order changed — not at the next missed DM — and, with an
|
|
3536
|
+
* empty list, the moment the last of them went. Called from writeConfig,
|
|
3537
|
+
* reloadIfChanged and sourcesChanged.
|
|
3266
3538
|
*/
|
|
3267
3539
|
private refreshShadowWarnings(reason: string): void {
|
|
3268
3540
|
const warnings = this.lintShadows();
|
|
3269
3541
|
const fingerprint = JSON.stringify(warnings);
|
|
3270
3542
|
if (fingerprint === this.shadowWarningsFingerprint) return;
|
|
3543
|
+
const hadSome = this.shadowWarningsFingerprint !== '[]';
|
|
3271
3544
|
this.shadowWarningsFingerprint = fingerprint;
|
|
3272
|
-
|
|
3273
|
-
if (warnings.length > 0) {
|
|
3545
|
+
if (warnings.length > 0 || hadSome) {
|
|
3274
3546
|
this.emitTrace({
|
|
3275
3547
|
type: 'gate:shadow-warnings',
|
|
3276
3548
|
reason,
|
|
@@ -3316,7 +3588,8 @@ export class EventGate {
|
|
|
3316
3588
|
}
|
|
3317
3589
|
|
|
3318
3590
|
private handleDebounce(policy: GatePolicy, info: GateEventInfo): void {
|
|
3319
|
-
const debounceMs = (policy.behavior
|
|
3591
|
+
const debounceMs = batchQuietMs(policy.behavior) ?? MIN_DEBOUNCE_MS;
|
|
3592
|
+
const judge = typeof policy.behavior === 'object' && 'judge' in policy.behavior ? policy.behavior.judge : undefined;
|
|
3320
3593
|
|
|
3321
3594
|
const event: PendingEvent = {
|
|
3322
3595
|
policyName: policy.name,
|
|
@@ -3340,13 +3613,23 @@ export class EventGate {
|
|
|
3340
3613
|
};
|
|
3341
3614
|
|
|
3342
3615
|
const existing = this.debounceTimers.get(policy.name);
|
|
3616
|
+
// A judge's maxWaitMs caps how far new events push the batch back: it's
|
|
3617
|
+
// judged by then, from its first event, quiet or not.
|
|
3618
|
+
const delay = (firstAt: number, batchJudge: JudgeConfig | undefined): number => {
|
|
3619
|
+
const cap = batchJudge?.maxWaitMs;
|
|
3620
|
+
return cap === undefined ? debounceMs : Math.max(0, Math.min(debounceMs, firstAt + cap - this.now()));
|
|
3621
|
+
};
|
|
3343
3622
|
if (existing) {
|
|
3344
3623
|
clearTimeout(existing.timer);
|
|
3345
3624
|
existing.events.push(event);
|
|
3346
|
-
|
|
3625
|
+
const ms = delay(existing.firstAt, existing.judge);
|
|
3626
|
+
existing.timer = setTimeout(() => this.fireDebounce(policy.name), ms);
|
|
3627
|
+
existing.dueAt = this.now() + ms;
|
|
3347
3628
|
} else {
|
|
3348
|
-
const
|
|
3349
|
-
|
|
3629
|
+
const firstAt = this.now();
|
|
3630
|
+
const ms = delay(firstAt, judge);
|
|
3631
|
+
const timer = setTimeout(() => this.fireDebounce(policy.name), ms);
|
|
3632
|
+
this.debounceTimers.set(policy.name, { timer, events: [event], firstAt, dueAt: firstAt + ms, ...(judge ? { judge } : {}) });
|
|
3350
3633
|
}
|
|
3351
3634
|
}
|
|
3352
3635
|
|
|
@@ -3360,6 +3643,14 @@ export class EventGate {
|
|
|
3360
3643
|
const events = state.events;
|
|
3361
3644
|
this.debounceTimers.delete(policyName);
|
|
3362
3645
|
|
|
3646
|
+
// A judge rule's batch goes to the reply judge, which delivers it, or
|
|
3647
|
+
// doesn't, once it has decided (judgeBatch). Without a judge wired it's
|
|
3648
|
+
// delivered below, as a debounce's batch is.
|
|
3649
|
+
if (state.judge && this.judgeFn) {
|
|
3650
|
+
this.judgeBatch(policyName, events, state.judge);
|
|
3651
|
+
return;
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3363
3654
|
// If any agent is currently inferring — or the host is quiesced — buffer
|
|
3364
3655
|
// for later (with cap). While quiesced, delivering would write a
|
|
3365
3656
|
// "[Gate: N events]" message into the very context being operated on
|
|
@@ -3409,7 +3700,7 @@ export class EventGate {
|
|
|
3409
3700
|
const quoted = events.filter(e => !e.inContext);
|
|
3410
3701
|
const referenced = events.filter(e => e.inContext);
|
|
3411
3702
|
|
|
3412
|
-
const lines: string[] = quoted.map(e => `- [${e.policyName}] (${e.eventType}): ${e.content}`);
|
|
3703
|
+
const lines: string[] = quoted.map(e => `- [${e.policyName}] (${e.eventType}): ${suppliedValue(e.content)}`);
|
|
3413
3704
|
// How each referenced channel is named in the line, in order.
|
|
3414
3705
|
const shown: string[] = [];
|
|
3415
3706
|
if (referenced.length > 0) {
|
|
@@ -3428,7 +3719,7 @@ export class EventGate {
|
|
|
3428
3719
|
? '(unknown channel)'
|
|
3429
3720
|
: e.routeChannelId !== undefined && e.serverId !== undefined && this.channelShownFn
|
|
3430
3721
|
? this.channelShownFn({ serverId: e.serverId, channelId: e.routeChannelId })
|
|
3431
|
-
: id;
|
|
3722
|
+
: suppliedValue(id);
|
|
3432
3723
|
const key = JSON.stringify([e.serverId ?? null, id ?? null]);
|
|
3433
3724
|
const entry = byChannel.get(key) ?? { name, count: 0, oldest: e.timestamp, label: e.channelLabel };
|
|
3434
3725
|
entry.count += 1;
|
|
@@ -3441,7 +3732,7 @@ export class EventGate {
|
|
|
3441
3732
|
// agent can still address tools that want the composite id. A name
|
|
3442
3733
|
// may carry its own `#`, so one is added only where the name lacks
|
|
3443
3734
|
// it, as at the other places that add a `#`.
|
|
3444
|
-
const channel = label ? `${label.startsWith('#') ?
|
|
3735
|
+
const channel = label ? `${label.startsWith('#') ? '' : '#'}${suppliedValue(label)} (${name})` : name;
|
|
3445
3736
|
shown.push(channel);
|
|
3446
3737
|
lines.push(referenceLine(count, channel, oldest, Date.now()));
|
|
3447
3738
|
}
|
|
@@ -3454,7 +3745,7 @@ export class EventGate {
|
|
|
3454
3745
|
// gate hands the line to the wake (WakeProvenance.line) and a fresh start
|
|
3455
3746
|
// of the wake's turn decides it (WakeLine). Several messages, one with
|
|
3456
3747
|
// anything after it, or content the line quotes, keep the line now.
|
|
3457
|
-
const text = `[
|
|
3748
|
+
const text = `[${NOTICE_TAG.gate}: ${events.length} event${events.length > 1 ? 's' : ''} matched]\n\n${lines.join('\n')}`;
|
|
3458
3749
|
const only = events.length === 1 && events[0].inContext ? events[0] : undefined;
|
|
3459
3750
|
let line: WakeLine | undefined;
|
|
3460
3751
|
if (only && this.newestEntry(only)) {
|
|
@@ -3499,12 +3790,303 @@ export class EventGate {
|
|
|
3499
3790
|
}
|
|
3500
3791
|
}
|
|
3501
3792
|
|
|
3793
|
+
// =========================================================================
|
|
3794
|
+
// Reply judge (`judge` rules)
|
|
3795
|
+
// =========================================================================
|
|
3796
|
+
|
|
3797
|
+
/**
|
|
3798
|
+
* Send one of a judge rule's batches to the reply judge. While a resident's
|
|
3799
|
+
* turn runs or the host is quiesced the batch is held instead, merged with
|
|
3800
|
+
* any the rule already holds, and judged once both are clear
|
|
3801
|
+
* (flushJudgeHeldIfClear): the judge then reads the context the turn left,
|
|
3802
|
+
* which may already have answered it. While the judge decides one of the
|
|
3803
|
+
* rule's batches, a later one waits for that verdict (finishJudging), so the
|
|
3804
|
+
* judge never decides two of a rule's batches at once.
|
|
3805
|
+
*/
|
|
3806
|
+
private judgeBatch(policyName: string, events: PendingEvent[], judge: JudgeConfig): void {
|
|
3807
|
+
if (this.disposed) return;
|
|
3808
|
+
const deciding = this.judging.get(policyName);
|
|
3809
|
+
if (deciding) {
|
|
3810
|
+
deciding.queued.push(...events);
|
|
3811
|
+
this.capJudgeEvents(deciding.queued);
|
|
3812
|
+
return;
|
|
3813
|
+
}
|
|
3814
|
+
if (this.judgedTurnRunning() || this.quiesced) {
|
|
3815
|
+
this.holdForJudge(policyName, events, judge);
|
|
3816
|
+
return;
|
|
3817
|
+
}
|
|
3818
|
+
const fn = this.judgeFn;
|
|
3819
|
+
if (!fn) {
|
|
3820
|
+
this.deliverOrBuffer(events);
|
|
3821
|
+
return;
|
|
3822
|
+
}
|
|
3823
|
+
const abort = new AbortController();
|
|
3824
|
+
const agentNames = this.getAgentNamesFn();
|
|
3825
|
+
const state: JudgingState = { events, judge, started: Date.now(), abort, queued: [] };
|
|
3826
|
+
this.judging.set(policyName, state);
|
|
3827
|
+
const timedOut = new Promise<never>((_, reject) => {
|
|
3828
|
+
state.timer = setTimeout(() => {
|
|
3829
|
+
abort.abort();
|
|
3830
|
+
reject(new Error(`the judge took more than ${this.judgeTimeoutMs} ms`));
|
|
3831
|
+
}, this.judgeTimeoutMs);
|
|
3832
|
+
// Never keep the process alive on the judge's account.
|
|
3833
|
+
state.timer.unref?.();
|
|
3834
|
+
});
|
|
3835
|
+
let asked: Promise<JudgeVerdict>;
|
|
3836
|
+
try {
|
|
3837
|
+
asked = fn({
|
|
3838
|
+
policyName,
|
|
3839
|
+
...(judge.instructions !== undefined ? { instructions: judge.instructions } : {}),
|
|
3840
|
+
events: events.map((e) => this.judgeEventSummary(e)),
|
|
3841
|
+
agentNames,
|
|
3842
|
+
upcomingWakes: this.upcomingWakes(agentNames, policyName),
|
|
3843
|
+
signal: abort.signal,
|
|
3844
|
+
});
|
|
3845
|
+
} catch (err) {
|
|
3846
|
+
asked = Promise.reject(err);
|
|
3847
|
+
}
|
|
3848
|
+
Promise.race([asked, timedOut]).then(
|
|
3849
|
+
(verdict) => this.finishJudging(policyName, state, verdict),
|
|
3850
|
+
(err: unknown) => this.finishJudging(policyName, state, undefined, err),
|
|
3851
|
+
).finally(() => {
|
|
3852
|
+
if (state.timer) clearTimeout(state.timer);
|
|
3853
|
+
}).catch((err: unknown) => {
|
|
3854
|
+
console.error(`[gate] Policy "${policyName}": the judge's verdict couldn't be applied (${errorText(err)})`);
|
|
3855
|
+
});
|
|
3856
|
+
}
|
|
3857
|
+
|
|
3858
|
+
/** Hold a batch for the judge until no turn changing the context it reads
|
|
3859
|
+
* runs and the host isn't quiesced, merged with what the rule already holds. */
|
|
3860
|
+
private holdForJudge(policyName: string, events: PendingEvent[], judge: JudgeConfig): void {
|
|
3861
|
+
const held = this.judgeHeld.get(policyName);
|
|
3862
|
+
if (held) {
|
|
3863
|
+
held.events.push(...events);
|
|
3864
|
+
held.judge = judge;
|
|
3865
|
+
this.capJudgeEvents(held.events);
|
|
3866
|
+
} else {
|
|
3867
|
+
const fresh = [...events];
|
|
3868
|
+
this.capJudgeEvents(fresh);
|
|
3869
|
+
this.judgeHeld.set(policyName, { events: fresh, judge });
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3873
|
+
/**
|
|
3874
|
+
* Keep a rule's events waiting on the judge to the inference buffer's cap,
|
|
3875
|
+
* dropping the oldest, counted and logged as bufferForInference counts its
|
|
3876
|
+
* drops: a turn that never ends (the wake-wedge) or a long quiesce on a busy
|
|
3877
|
+
* channel would otherwise grow them without bound. A queue beside a batch
|
|
3878
|
+
* the judge is deciding is capped on its own, so its newest events are never
|
|
3879
|
+
* the ones dropped for the batch ahead of it.
|
|
3880
|
+
*/
|
|
3881
|
+
private capJudgeEvents(events: PendingEvent[]): void {
|
|
3882
|
+
const drop = events.length - MAX_INFERENCE_BUFFER;
|
|
3883
|
+
if (drop <= 0) return;
|
|
3884
|
+
events.splice(0, drop);
|
|
3885
|
+
const before = this.droppedJudgeEvents;
|
|
3886
|
+
this.droppedJudgeEvents += drop;
|
|
3887
|
+
if (Math.floor(before / 50) !== Math.floor(this.droppedJudgeEvents / 50) || before === 0) {
|
|
3888
|
+
console.error(
|
|
3889
|
+
`[gate] events waiting on the reply judge reached the cap (${MAX_INFERENCE_BUFFER} per rule): dropped `
|
|
3890
|
+
+ `${this.droppedJudgeEvents} oldest so far${this.quiesced ? ' during quiesce' : ''}`,
|
|
3891
|
+
);
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
|
|
3895
|
+
/** The rule's judge as it is now, when the rule is still a judge rule; else the one the batch was asked with. */
|
|
3896
|
+
private currentJudge(policyName: string, asked: JudgeConfig): JudgeConfig {
|
|
3897
|
+
const now = this.config.policies.find((p) => p.name === policyName)?.behavior;
|
|
3898
|
+
return typeof now === 'object' && 'judge' in now ? now.judge : asked;
|
|
3899
|
+
}
|
|
3900
|
+
|
|
3901
|
+
/**
|
|
3902
|
+
* The gate's own timers that will wake `agentNames` with no new event, for
|
|
3903
|
+
* the judge to weigh what waiting costs: each agent's pending self-wake
|
|
3904
|
+
* (skip_reply's wake_in_seconds), the end of a sleep, and the pending
|
|
3905
|
+
* batches of rules other than `exceptPolicy` (a debounce's wakes the agent
|
|
3906
|
+
* when it fires; a judge rule's goes to the judge, so it only may).
|
|
3907
|
+
*/
|
|
3908
|
+
upcomingWakes(agentNames: string[], exceptPolicy?: string): UpcomingWake[] {
|
|
3909
|
+
const wakes: UpcomingWake[] = [];
|
|
3910
|
+
const now = this.now();
|
|
3911
|
+
for (const name of agentNames) {
|
|
3912
|
+
const due = this.selfWakeDue.get(name);
|
|
3913
|
+
if (due && this.selfWakeTimers.has(name)) {
|
|
3914
|
+
wakes.push({ at: due.at, certain: true, what: `${agentNames.length > 1 ? `${name}'s` : 'its'} own ${due.source} timer` });
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
if (this.sleepUntil > now && (this.sleepAgent === undefined || agentNames.includes(this.sleepAgent))) {
|
|
3918
|
+
wakes.push({ at: this.sleepUntil, certain: true, what: 'the end of its sleep' });
|
|
3919
|
+
}
|
|
3920
|
+
for (const [name, state] of this.debounceTimers) {
|
|
3921
|
+
if (name === exceptPolicy || state.events.length === 0) continue;
|
|
3922
|
+
const n = state.events.length;
|
|
3923
|
+
const batch = `${n} ${n === 1 ? 'event' : 'events'}`;
|
|
3924
|
+
wakes.push(state.judge
|
|
3925
|
+
? { at: state.dueAt, certain: false, what: `rule "${name}"'s batch of ${batch} goes to the judge` }
|
|
3926
|
+
: { at: state.dueAt, certain: true, what: `rule "${name}"'s debounced batch of ${batch}` });
|
|
3927
|
+
}
|
|
3928
|
+
return wakes.sort((a, b) => (a.at ?? 0) - (b.at ?? 0));
|
|
3929
|
+
}
|
|
3930
|
+
|
|
3931
|
+
/** A batch's event as the judge is shown it. */
|
|
3932
|
+
private judgeEventSummary(e: PendingEvent): JudgeEventSummary {
|
|
3933
|
+
const id = e.routeChannelId ?? e.channelId;
|
|
3934
|
+
let channel: string | undefined;
|
|
3935
|
+
if (id !== undefined) {
|
|
3936
|
+
let name = id;
|
|
3937
|
+
if (e.routeChannelId !== undefined && e.serverId !== undefined && this.channelShownFn) {
|
|
3938
|
+
try {
|
|
3939
|
+
name = this.channelShownFn({ serverId: e.serverId, channelId: e.routeChannelId });
|
|
3940
|
+
} catch {
|
|
3941
|
+
name = id;
|
|
3942
|
+
}
|
|
3943
|
+
}
|
|
3944
|
+
channel = e.channelLabel ? `${e.channelLabel.startsWith('#') ? e.channelLabel : `#${e.channelLabel}`} (${name})` : name;
|
|
3945
|
+
}
|
|
3946
|
+
return {
|
|
3947
|
+
eventType: e.eventType,
|
|
3948
|
+
...(channel !== undefined ? { channel } : {}),
|
|
3949
|
+
...(e.authorId !== undefined ? { authorId: e.authorId } : {}),
|
|
3950
|
+
addressed: e.addressed,
|
|
3951
|
+
content: e.content,
|
|
3952
|
+
at: e.timestamp,
|
|
3953
|
+
};
|
|
3954
|
+
}
|
|
3955
|
+
|
|
3956
|
+
/**
|
|
3957
|
+
* Apply the judge's verdict on a batch. Reply: the batch, with any of the
|
|
3958
|
+
* rule's batches that fired meanwhile, is delivered as a debounce's is
|
|
3959
|
+
* (held for a turn that started meanwhile, as fireDebounce holds one). Wait:
|
|
3960
|
+
* nothing is delivered, and batches that fired meanwhile go to the judge
|
|
3961
|
+
* afresh. A judge that failed, timed out or answered nothing usable decides
|
|
3962
|
+
* by onError: "wake" delivers, "skip" doesn't. Each verdict is traced and
|
|
3963
|
+
* counted for gate_status, and a failure is logged.
|
|
3964
|
+
*/
|
|
3965
|
+
private finishJudging(policyName: string, state: JudgingState, verdict: JudgeVerdict | undefined, err?: unknown): void {
|
|
3966
|
+
// A verdict for a batch the gate let go of (dispose() clears what the
|
|
3967
|
+
// judge was deciding) applies to nothing, even after a start() since.
|
|
3968
|
+
if (this.judging.get(policyName) !== state) return;
|
|
3969
|
+
this.judging.delete(policyName);
|
|
3970
|
+
if (state.timer) clearTimeout(state.timer);
|
|
3971
|
+
const durationMs = Date.now() - state.started;
|
|
3972
|
+
const failed = verdict == null || typeof verdict !== 'object' || typeof verdict.reply !== 'boolean';
|
|
3973
|
+
if (failed && (err as { providerAdmission?: unknown } | undefined)?.providerAdmission === 'deferred') {
|
|
3974
|
+
const wakes = this.judgeOnError === 'wake';
|
|
3975
|
+
console.error(
|
|
3976
|
+
`[gate] Policy "${policyName}": the reply judge made no call (${errorText(err)}), so its batch of ${state.events.length} `
|
|
3977
|
+
+ `${state.events.length === 1 ? 'event' : 'events'} ${wakes ? 'wakes the agent' : "doesn't wake the agent"} (judge onError "${this.judgeOnError}")`,
|
|
3978
|
+
);
|
|
3979
|
+
if (this.disposed) return;
|
|
3980
|
+
if (wakes) this.deliverOrBuffer([...state.events, ...state.queued]);
|
|
3981
|
+
else if (state.queued.length > 0) this.judgeBatch(policyName, state.queued, this.currentJudge(policyName, state.judge));
|
|
3982
|
+
return;
|
|
3983
|
+
}
|
|
3984
|
+
const outcome: 'reply' | 'wait' | 'error' = failed ? 'error' : verdict.reply ? 'reply' : 'wait';
|
|
3985
|
+
const reason = failed
|
|
3986
|
+
? (err !== undefined ? errorText(err) : 'the judge gave no verdict')
|
|
3987
|
+
: typeof verdict.reason === 'string' ? verdict.reason : '';
|
|
3988
|
+
const record = this.judgeRecords.get(policyName) ?? { verdicts: { reply: 0, wait: 0, error: 0 } };
|
|
3989
|
+
record.verdicts[outcome]++;
|
|
3990
|
+
record.last = {
|
|
3991
|
+
at: Date.now(),
|
|
3992
|
+
verdict: outcome,
|
|
3993
|
+
reason: cutToFit(suppliedValue(reason), 300),
|
|
3994
|
+
eventCount: state.events.length,
|
|
3995
|
+
durationMs,
|
|
3996
|
+
...(!failed && typeof verdict.inputTokens === 'number' ? { inputTokens: verdict.inputTokens } : {}),
|
|
3997
|
+
};
|
|
3998
|
+
if (this.config.policies.some((p) => p.name === policyName)) this.judgeRecords.set(policyName, record);
|
|
3999
|
+
if (failed) {
|
|
4000
|
+
this.judgeFailStreak++;
|
|
4001
|
+
this.judgeLastFailure = cutToFit(suppliedValue(reason), 200);
|
|
4002
|
+
} else {
|
|
4003
|
+
this.judgeFailStreak = 0;
|
|
4004
|
+
}
|
|
4005
|
+
const wake = outcome === 'reply' || (outcome === 'error' && this.judgeOnError === 'wake');
|
|
4006
|
+
if (failed) {
|
|
4007
|
+
console.error(
|
|
4008
|
+
`[gate] Policy "${policyName}": the reply judge failed (${reason}), so its batch of ${state.events.length} `
|
|
4009
|
+
+ `${state.events.length === 1 ? 'event' : 'events'} ${wake ? 'wakes the agent' : "doesn't wake the agent"} (judge onError "${this.judgeOnError}")`,
|
|
4010
|
+
);
|
|
4011
|
+
}
|
|
4012
|
+
this.emitTrace({
|
|
4013
|
+
type: 'gate:judge-verdict',
|
|
4014
|
+
policyName,
|
|
4015
|
+
verdict: outcome,
|
|
4016
|
+
woke: wake,
|
|
4017
|
+
eventCount: state.events.length,
|
|
4018
|
+
durationMs,
|
|
4019
|
+
...(!failed && verdict.model ? { model: verdict.model } : {}),
|
|
4020
|
+
...(!failed && typeof verdict.inputTokens === 'number' ? { inputTokens: verdict.inputTokens } : {}),
|
|
4021
|
+
...(!failed && typeof verdict.cacheReadTokens === 'number' ? { cacheReadTokens: verdict.cacheReadTokens } : {}),
|
|
4022
|
+
...(!failed && typeof verdict.outputTokens === 'number' ? { outputTokens: verdict.outputTokens } : {}),
|
|
4023
|
+
timestamp: Date.now(),
|
|
4024
|
+
});
|
|
4025
|
+
if (this.disposed) return;
|
|
4026
|
+
const judge = this.currentJudge(policyName, state.judge);
|
|
4027
|
+
if (wake) {
|
|
4028
|
+
this.deliverOrBuffer([...state.events, ...state.queued]);
|
|
4029
|
+
return;
|
|
4030
|
+
}
|
|
4031
|
+
if (state.queued.length > 0) this.judgeBatch(policyName, state.queued, judge);
|
|
4032
|
+
}
|
|
4033
|
+
|
|
4034
|
+
/** Deliver a batch now, or hold it for the agents' turns and the host's
|
|
4035
|
+
* quiesce to end, as fireDebounce does; a delivery that throws drops it,
|
|
4036
|
+
* named in the log. */
|
|
4037
|
+
private deliverOrBuffer(events: PendingEvent[]): void {
|
|
4038
|
+
if (events.length === 0) return;
|
|
4039
|
+
if (this.inferring.size > 0 || this.quiesced) {
|
|
4040
|
+
this.bufferForInference(events);
|
|
4041
|
+
return;
|
|
4042
|
+
}
|
|
4043
|
+
try {
|
|
4044
|
+
this.deliverEvents(events);
|
|
4045
|
+
} catch (err) {
|
|
4046
|
+
const one = events.length === 1;
|
|
4047
|
+
console.error(
|
|
4048
|
+
`[gate] a judged batch of ${events.length} ${one ? 'event' : 'events'} couldn't be delivered `
|
|
4049
|
+
+ `(${errorText(err)}), so the batch is dropped`,
|
|
4050
|
+
);
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
|
|
4054
|
+
private changesJudgedContext(agentName: string): boolean {
|
|
4055
|
+
return this.changesJudgedContextFn?.(agentName) ?? true;
|
|
4056
|
+
}
|
|
4057
|
+
|
|
4058
|
+
private judgedTurnRunning(): boolean {
|
|
4059
|
+
return [...this.inferring].some((name) => this.changesJudgedContext(name));
|
|
4060
|
+
}
|
|
4061
|
+
|
|
4062
|
+
private stopJudging(): void {
|
|
4063
|
+
for (const [policyName, state] of [...this.judging]) {
|
|
4064
|
+
this.judging.delete(policyName);
|
|
4065
|
+
if (state.timer) clearTimeout(state.timer);
|
|
4066
|
+
state.abort.abort();
|
|
4067
|
+
this.holdForJudge(policyName, [...state.events, ...state.queued], state.judge);
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
4070
|
+
|
|
4071
|
+
/** Send the batches held for the judge once no turn changing the context it reads runs and the host isn't quiesced. */
|
|
4072
|
+
private flushJudgeHeldIfClear(): void {
|
|
4073
|
+
if (this.quiesced || this.judgedTurnRunning() || this.judgeHeld.size === 0) return;
|
|
4074
|
+
const held = [...this.judgeHeld];
|
|
4075
|
+
this.judgeHeld.clear();
|
|
4076
|
+
// Deferred, as the inference buffer's flush is, to stay out of the
|
|
4077
|
+
// turn-end callback that called this.
|
|
4078
|
+
queueMicrotask(() => {
|
|
4079
|
+
for (const [policyName, { events, judge }] of held) this.judgeBatch(policyName, events, this.currentJudge(policyName, judge));
|
|
4080
|
+
});
|
|
4081
|
+
}
|
|
4082
|
+
|
|
3502
4083
|
// =========================================================================
|
|
3503
4084
|
// Inference lifecycle
|
|
3504
4085
|
// =========================================================================
|
|
3505
4086
|
|
|
3506
4087
|
onInferenceStarted(agentName: string): void {
|
|
3507
4088
|
this.inferring.add(agentName);
|
|
4089
|
+
if (this.changesJudgedContext(agentName)) this.stopJudging();
|
|
3508
4090
|
// A turn is starting — whatever caused it supersedes any pending
|
|
3509
4091
|
// self-wake. (Arming happens MID-turn via skip_reply, i.e. after this
|
|
3510
4092
|
// ran for the arming turn, so a fresh arm always survives its own turn.)
|
|
@@ -3524,11 +4106,13 @@ export class EventGate {
|
|
|
3524
4106
|
* still running. */
|
|
3525
4107
|
onInferenceResumed(agentName: string): void {
|
|
3526
4108
|
this.inferring.add(agentName);
|
|
4109
|
+
if (this.changesJudgedContext(agentName)) this.stopJudging();
|
|
3527
4110
|
}
|
|
3528
4111
|
|
|
3529
4112
|
onInferenceEnded(agentName: string): void {
|
|
3530
4113
|
this.inferring.delete(agentName);
|
|
3531
4114
|
this.flushInferenceBufferIfClear();
|
|
4115
|
+
this.flushJudgeHeldIfClear();
|
|
3532
4116
|
}
|
|
3533
4117
|
|
|
3534
4118
|
/**
|
|
@@ -3543,7 +4127,15 @@ export class EventGate {
|
|
|
3543
4127
|
setQuiesced(quiesced: boolean): void {
|
|
3544
4128
|
if (this.quiesced === quiesced) return;
|
|
3545
4129
|
this.quiesced = quiesced;
|
|
3546
|
-
if (
|
|
4130
|
+
if (quiesced) {
|
|
4131
|
+
// A judge reading the context now would read it mid-surgery, and call
|
|
4132
|
+
// its provider while the host holds still: it's stopped, and its batch
|
|
4133
|
+
// held to be judged at resume.
|
|
4134
|
+
this.stopJudging();
|
|
4135
|
+
} else {
|
|
4136
|
+
this.flushInferenceBufferIfClear();
|
|
4137
|
+
this.flushJudgeHeldIfClear();
|
|
4138
|
+
}
|
|
3547
4139
|
}
|
|
3548
4140
|
|
|
3549
4141
|
private flushInferenceBufferIfClear(): void {
|
|
@@ -3576,12 +4168,19 @@ export class EventGate {
|
|
|
3576
4168
|
bufferedEvents: number;
|
|
3577
4169
|
droppedBufferedEvents: number;
|
|
3578
4170
|
sleepUntil: number;
|
|
4171
|
+
/** Events held for the reply judge until the turn or quiesce ends, and the judge's own drops at the cap. */
|
|
4172
|
+
judgeHeldEvents: number;
|
|
4173
|
+
droppedJudgeEvents: number;
|
|
3579
4174
|
} {
|
|
4175
|
+
let judgeHeldEvents = 0;
|
|
4176
|
+
for (const held of this.judgeHeld.values()) judgeHeldEvents += held.events.length;
|
|
3580
4177
|
return {
|
|
3581
4178
|
inferring: [...this.inferring],
|
|
3582
4179
|
bufferedEvents: this.inferenceBuffer.length,
|
|
3583
4180
|
droppedBufferedEvents: this.droppedBufferedEvents,
|
|
3584
4181
|
sleepUntil: this.sleepUntil,
|
|
4182
|
+
judgeHeldEvents,
|
|
4183
|
+
droppedJudgeEvents: this.droppedJudgeEvents,
|
|
3585
4184
|
};
|
|
3586
4185
|
}
|
|
3587
4186
|
|
|
@@ -3726,7 +4325,7 @@ export class EventGate {
|
|
|
3726
4325
|
const placement: WritePlacement = {};
|
|
3727
4326
|
this.addMessageFn('user', [{
|
|
3728
4327
|
type: 'text',
|
|
3729
|
-
text: `[
|
|
4328
|
+
text: `[${NOTICE_TAG.selfWake}] your ${source} timer (${Math.round(ms / 1000)}s) elapsed — now ${nowIso}`,
|
|
3730
4329
|
}], { source: 'gate:self-wake' }, agentName, placement);
|
|
3731
4330
|
this.requestInferenceFn(
|
|
3732
4331
|
agentName,
|
|
@@ -3737,6 +4336,7 @@ export class EventGate {
|
|
|
3737
4336
|
);
|
|
3738
4337
|
}, ms);
|
|
3739
4338
|
this.selfWakeTimers.set(agentName, timer);
|
|
4339
|
+
this.selfWakeDue.set(agentName, { at: this.now() + ms, source });
|
|
3740
4340
|
this.emitTrace({
|
|
3741
4341
|
type: 'gate:decision',
|
|
3742
4342
|
eventType: 'selfwake:set',
|
|
@@ -3935,6 +4535,16 @@ export class EventGate {
|
|
|
3935
4535
|
fireCount: this.passiveSampleFires.get(p.name) ?? 0,
|
|
3936
4536
|
};
|
|
3937
4537
|
}
|
|
4538
|
+
if (typeof p.behavior === 'object' && 'judge' in p.behavior) {
|
|
4539
|
+
const record = this.judgeRecords.get(p.name);
|
|
4540
|
+
result.judgeState = {
|
|
4541
|
+
pendingCount: debounce?.events.length ?? 0,
|
|
4542
|
+
judging: this.judging.has(p.name),
|
|
4543
|
+
heldCount: this.judgeHeld.get(p.name)?.events.length ?? 0,
|
|
4544
|
+
verdicts: { ...(record?.verdicts ?? { reply: 0, wait: 0, error: 0 }) },
|
|
4545
|
+
...(record?.last ? { last: { ...record.last } } : {}),
|
|
4546
|
+
};
|
|
4547
|
+
}
|
|
3938
4548
|
return result;
|
|
3939
4549
|
});
|
|
3940
4550
|
|
|
@@ -3961,9 +4571,14 @@ export class EventGate {
|
|
|
3961
4571
|
default: this.config.default ?? 'always',
|
|
3962
4572
|
script: this.gateScript.status(),
|
|
3963
4573
|
policies,
|
|
3964
|
-
errors: [
|
|
4574
|
+
errors: [
|
|
4575
|
+
this.configErrorShown(), this.startNotice, this.startUnsaved, this.privilegedError,
|
|
4576
|
+
this.judgeUnwiredNotice(), this.judgeFailingNotice(),
|
|
4577
|
+
]
|
|
3965
4578
|
.filter((e): e is string => e !== null),
|
|
3966
|
-
|
|
4579
|
+
// Fresh, not the cache: the sources the host knows (eventTypesFrom)
|
|
4580
|
+
// change as servers connect, with no change to the rules.
|
|
4581
|
+
shadowWarnings: this.lintShadows().map(formatShadowWarning),
|
|
3967
4582
|
totalEvaluations: this.totalEvaluations,
|
|
3968
4583
|
defaultDecisions: {
|
|
3969
4584
|
triggered: this.defaultTriggered,
|
|
@@ -3973,6 +4588,27 @@ export class EventGate {
|
|
|
3973
4588
|
};
|
|
3974
4589
|
}
|
|
3975
4590
|
|
|
4591
|
+
/** gate_status's word on judge rules in force with no judge wired. */
|
|
4592
|
+
private judgeUnwiredNotice(): string | null {
|
|
4593
|
+
if (this.judgeFn) return null;
|
|
4594
|
+
const names = this.config.policies
|
|
4595
|
+
.filter((p) => typeof p.behavior === 'object' && 'judge' in p.behavior)
|
|
4596
|
+
.map((p) => p.name);
|
|
4597
|
+
if (names.length === 0) return null;
|
|
4598
|
+
return `${quotedList(names)} ${names.length === 1 ? 'is a judge rule' : 'are judge rules'}, and this host wires no reply `
|
|
4599
|
+
+ `judge, so ${names.length === 1 ? 'its batches wake' : 'their batches wake'} the agent as a debounce of the same quiet period would`;
|
|
4600
|
+
}
|
|
4601
|
+
|
|
4602
|
+
/** gate_status's word on a judge that keeps failing (JUDGE_FAILING_STREAK in a row). */
|
|
4603
|
+
private judgeFailingNotice(): string | null {
|
|
4604
|
+
if (this.judgeFailStreak < JUDGE_FAILING_STREAK) return null;
|
|
4605
|
+
return `the reply judge has failed its last ${this.judgeFailStreak} requests (the newest: ${this.judgeLastFailure}), `
|
|
4606
|
+
+ (this.judgeOnError === 'wake'
|
|
4607
|
+
? 'so judge rules wake the agent as debounces of the same quiet period would'
|
|
4608
|
+
: "so judge rules' batches don't wake the agent")
|
|
4609
|
+
+ "; the host's judge model or provider may not serve it (GateOptions.judge)";
|
|
4610
|
+
}
|
|
4611
|
+
|
|
3976
4612
|
// =========================================================================
|
|
3977
4613
|
// Cleanup
|
|
3978
4614
|
// =========================================================================
|
|
@@ -3983,10 +4619,19 @@ export class EventGate {
|
|
|
3983
4619
|
clearTimeout(state.timer);
|
|
3984
4620
|
}
|
|
3985
4621
|
this.debounceTimers.clear();
|
|
4622
|
+
// A judge deciding now is told to stop, and its verdict, when it comes,
|
|
4623
|
+
// delivers nothing (finishJudging); held batches go with the buffer.
|
|
4624
|
+
for (const state of this.judging.values()) {
|
|
4625
|
+
if (state.timer) clearTimeout(state.timer);
|
|
4626
|
+
state.abort.abort();
|
|
4627
|
+
}
|
|
4628
|
+
this.judging.clear();
|
|
4629
|
+
this.judgeHeld.clear();
|
|
3986
4630
|
for (const timer of this.selfWakeTimers.values()) {
|
|
3987
4631
|
clearTimeout(timer);
|
|
3988
4632
|
}
|
|
3989
4633
|
this.selfWakeTimers.clear();
|
|
4634
|
+
this.selfWakeDue.clear();
|
|
3990
4635
|
// The sleep stays, with its note and agent, and its timer goes: fired
|
|
3991
4636
|
// after a stop(), its wake reached a framework that had stopped, and the
|
|
3992
4637
|
// timer held the process until the sleep's end. The next start() arms it
|