@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
|
@@ -43,12 +43,15 @@ import { expandCoreTags, isMarkerTagged } from './tags.js';
|
|
|
43
43
|
import { coalesceRejectionReason, EmptyContentError, validateCoalescedContent } from './push-coalescer.js';
|
|
44
44
|
import { isVisiblyEmptyConversion } from './visible-content.js';
|
|
45
45
|
import { logDetail, logValue } from './log-value.js';
|
|
46
|
+
import { jsonKind } from '../jsonrpc-envelope.js';
|
|
46
47
|
import { RefusalLines } from './refusal-lines.js';
|
|
47
48
|
import { CapabilityGrant } from './capability-grant.js';
|
|
48
49
|
import type { InboundSource } from './inbound-source.js';
|
|
49
50
|
import { McplRequestError, type McplServerConnection } from './server-connection.js';
|
|
50
51
|
import { readSkipReplyWake, skipReplyNote, type SkipReplyEarlierWake } from './skip-reply-wake.js';
|
|
51
52
|
import { isProseTargetToken } from './prose-grammar.js';
|
|
53
|
+
import { persistentReaderName, tuneOutHeldWords, tuneOutNoReaderAnswer, tuneOutReachWords, type TuneOutReader } from '../tune-out/tools.js';
|
|
54
|
+
import type { TraceEventInput } from '../types/trace.js';
|
|
52
55
|
|
|
53
56
|
// ============================================================================
|
|
54
57
|
// Typing indicator interval (Discord typing lasts ~10s, so 7s keeps it alive)
|
|
@@ -200,7 +203,7 @@ function substitutedChannel(
|
|
|
200
203
|
*/
|
|
201
204
|
const CHANNEL_LABEL_HISTORY_LOG_ID = 'mcpl/channel-label-history';
|
|
202
205
|
|
|
203
|
-
type DesiredChannelState = 'open' | 'closed' | 'tuned-out';
|
|
206
|
+
export type DesiredChannelState = 'open' | 'closed' | 'tuned-out';
|
|
204
207
|
|
|
205
208
|
/**
|
|
206
209
|
* Parameters of an active tune-out (issue #77), carried on the
|
|
@@ -275,8 +278,8 @@ function desiredStateRecord(item: unknown): {
|
|
|
275
278
|
|
|
276
279
|
/**
|
|
277
280
|
* The desired state `store`'s lifecycle log records for `channelId` on
|
|
278
|
-
* `serverId`, for a reader with no registry yet, as
|
|
279
|
-
*
|
|
281
|
+
* `serverId`, for a reader with no registry yet, as the boot's import of an
|
|
282
|
+
* earlier release's queued writes is, before initializeMcpl: the last decision wins, as
|
|
280
283
|
* the registry's replay reads it (getDesiredState answers once there is a
|
|
281
284
|
* registry). undefined when no decision names it.
|
|
282
285
|
*/
|
|
@@ -730,6 +733,34 @@ function channelOnServer(ref: ChannelRef): string {
|
|
|
730
733
|
return `${quotedValue(ref.channelId)} on server ${quotedValue(ref.serverId)}`;
|
|
731
734
|
}
|
|
732
735
|
|
|
736
|
+
/** A channel_close's lead where its channel's server no longer registers it. */
|
|
737
|
+
function noLongerRegistered(ref: ChannelRef): string {
|
|
738
|
+
return `Server ${quotedValue(ref.serverId)} no longer registers channel ${quotedValue(ref.channelId)}, so no close was recorded or sent.`;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/** What a channel_close's bare name reached besides the channel it closed
|
|
742
|
+
* (ChannelRegistry.focusNamedBare): each by its id and server, which the
|
|
743
|
+
* bare name leaves out, and the channelId and serverId a call closes it
|
|
744
|
+
* with: its own id, which needn't be the name the call gave (a focus on
|
|
745
|
+
* `b:room` beside server b's `room`), so naming the server alone, the
|
|
746
|
+
* name kept, is no channel there. */
|
|
747
|
+
function notClosedText(refs: readonly ChannelRef[]): string {
|
|
748
|
+
const names = refs.map(channelOnServer);
|
|
749
|
+
const call = (ref: ChannelRef) => `channelId ${quotedValue(ref.channelId)} and serverId ${quotedValue(ref.serverId)}`;
|
|
750
|
+
return refs.length === 1
|
|
751
|
+
? `Channel ${names[0]} was not closed: to close it, call again with ${call(refs[0]!)}.`
|
|
752
|
+
: `Channels ${names.slice(0, -1).join(', ')} and ${names.at(-1)} were not closed: to close one, call again with ` +
|
|
753
|
+
`${refs.slice(0, -1).map(call).join(', with ')}, or with ${call(refs.at(-1)!)}.`;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/** A tool result with a sentence added: a success's after its note (or as
|
|
757
|
+
* its note), a failure's after its error. */
|
|
758
|
+
function withSentence(result: ToolResult, sentence: string): ToolResult {
|
|
759
|
+
if (!result.success) return { ...result, error: result.error ? `${result.error} ${sentence}` : sentence };
|
|
760
|
+
const data = (result.data ?? {}) as Record<string, unknown>;
|
|
761
|
+
return { ...result, data: { ...data, note: typeof data.note === 'string' ? `${data.note} ${sentence}` : sentence } };
|
|
762
|
+
}
|
|
763
|
+
|
|
733
764
|
const CHANNEL_ON_SERVER_AT_END = /("(?:[^"\\]|\\.)*") on server ("(?:[^"\\]|\\.)*")$/;
|
|
734
765
|
|
|
735
766
|
/**
|
|
@@ -1011,16 +1042,31 @@ interface ChannelRegistryOptions {
|
|
|
1011
1042
|
* this is ends through the cancel flow (release the backlog, record the
|
|
1012
1043
|
* end) before the lifecycle state flips to closed. A
|
|
1013
1044
|
* tuned-out channel's close is refused before this (its cancel ends it).
|
|
1014
|
-
*
|
|
1015
|
-
* close
|
|
1045
|
+
* `unregistered`: the close names the channel after its server stopped
|
|
1046
|
+
* registering it, so no close is recorded or sent (handleToolClose).
|
|
1047
|
+
* Returns, where a hold excepts the channel, what became of that hold:
|
|
1048
|
+
* a function that says it in a sentence, read when the close's answer is
|
|
1049
|
+
* built, after its transport's round-trip, so the answer says what holds
|
|
1050
|
+
* when it arrives (a release that waited may have run by then). The
|
|
1051
|
+
* answer carries it whatever became of the close: as `note` on a success,
|
|
1052
|
+
* after the error on a failure. Throws, with the answer's text, when the
|
|
1053
|
+
* hold's release fails; one that ended the hold before it failed says so.
|
|
1016
1054
|
*/
|
|
1017
|
-
onToolClose?:
|
|
1055
|
+
onToolClose?: ToolCloseHook;
|
|
1018
1056
|
/**
|
|
1019
1057
|
* The channel unprefixed speech lands in while an all-except hold is
|
|
1020
1058
|
* active (its exception), else null. Consulted after a fork's home and
|
|
1021
1059
|
* the turn's own delivery, before the legacy fallback.
|
|
1022
1060
|
*/
|
|
1023
1061
|
focusChannelResolver?: () => ChannelRef | null;
|
|
1062
|
+
/**
|
|
1063
|
+
* Who reads a tuned-out channel's traffic (the framework's reader: the
|
|
1064
|
+
* persistent subconscious by its name, or reader forks), for the texts
|
|
1065
|
+
* that tell an agent where a tuned-out channel's messages go. Left out,
|
|
1066
|
+
* the texts name the persistent reader by its default name
|
|
1067
|
+
* (persistentReaderName): there is no hold without a reader (0.33.0).
|
|
1068
|
+
*/
|
|
1069
|
+
tuneOutReader?: () => TuneOutReader;
|
|
1024
1070
|
/**
|
|
1025
1071
|
* Called when a text-only turn's speech was not delivered to its
|
|
1026
1072
|
* conversational locus (`failed`), or may not have been (`unknown`), for
|
|
@@ -1048,6 +1094,9 @@ interface ChannelRegistryOptions {
|
|
|
1048
1094
|
outcome?: 'failed' | 'unknown';
|
|
1049
1095
|
/** Bounded display of the connector's structured error data, when present. */
|
|
1050
1096
|
detail?: string;
|
|
1097
|
+
/** Set when the turn had no channel and the reason names the channels
|
|
1098
|
+
* its batch came from. */
|
|
1099
|
+
byDesign?: true;
|
|
1051
1100
|
}) => void;
|
|
1052
1101
|
/**
|
|
1053
1102
|
* Announces either a durable subscription-policy admission (before its
|
|
@@ -1090,15 +1139,38 @@ interface ChannelRegistryOptions {
|
|
|
1090
1139
|
* message across ALL channels (markers aside) and misroutes a reply to
|
|
1091
1140
|
* whichever channel last spoke under concurrency (item-3 redux). The
|
|
1092
1141
|
* framework keeps both on the live turn's routing state and exposes them
|
|
1093
|
-
* here. For a
|
|
1094
|
-
*
|
|
1095
|
-
*
|
|
1096
|
-
*
|
|
1097
|
-
*
|
|
1098
|
-
*
|
|
1099
|
-
*
|
|
1142
|
+
* here. For a no-trigger turn it resolves to undefined until the turn's
|
|
1143
|
+
* locus is resolved, so that resolution takes the global fallback, which the
|
|
1144
|
+
* turn then keeps. Consulted AFTER `homeChannelResolver` (a fork's home
|
|
1145
|
+
* always wins), BEFORE the global fallback. Null where the turn deliberately
|
|
1146
|
+
* has no channel, as a heartbeat's turn, or one woken for a channel id two
|
|
1147
|
+
* servers register, given without its server: that takes no fallback
|
|
1148
|
+
* either.
|
|
1100
1149
|
*/
|
|
1101
1150
|
activeChannelResolver?: (agentName: string) => ChannelRef | null | undefined;
|
|
1151
|
+
/**
|
|
1152
|
+
* FrameworkConfig.unpromptedChannel: where a turn that takes it
|
|
1153
|
+
* (takesUnpromptedChannel) and has no home and no channel of its own is
|
|
1154
|
+
* placed, within the process-global fallback, after an all-except hold's
|
|
1155
|
+
* channel (sharedFallback). A name, as entriesNamed reads one.
|
|
1156
|
+
*/
|
|
1157
|
+
unpromptedChannel?: string;
|
|
1158
|
+
/**
|
|
1159
|
+
* Whether the conversation's current turn takes `unpromptedChannel` where it
|
|
1160
|
+
* has no channel of its own: the framework answers true only for a turn
|
|
1161
|
+
* the agent's own timer started, as FrameworkConfig.unpromptedChannel
|
|
1162
|
+
* says. Absent, every turn does.
|
|
1163
|
+
*/
|
|
1164
|
+
takesUnpromptedChannel?: (conversationId: string) => boolean;
|
|
1165
|
+
/**
|
|
1166
|
+
* Whether the agent's current turn has no channel of its own — a
|
|
1167
|
+
* heartbeat's, or one woken for a channel that names none — so it replies
|
|
1168
|
+
* to no incoming message (incomingFor). Left out, a null from
|
|
1169
|
+
* activeChannelResolver means that, as it always did; given, it separates
|
|
1170
|
+
* a turn with nowhere to speak (a locked turn pointed at drafts) from one
|
|
1171
|
+
* with nothing to answer.
|
|
1172
|
+
*/
|
|
1173
|
+
channellessTurn?: (agentName: string) => boolean;
|
|
1102
1174
|
/**
|
|
1103
1175
|
* Whether a conversation takes the process-global fallback, the channel of
|
|
1104
1176
|
* the most recent delivered inbound message: as where its plain speech lands
|
|
@@ -1127,6 +1199,33 @@ interface ChannelRegistryOptions {
|
|
|
1127
1199
|
// ChannelRegistry
|
|
1128
1200
|
// ============================================================================
|
|
1129
1201
|
|
|
1202
|
+
/** A channel_close call's input (ChannelRegistry.handleToolClose). */
|
|
1203
|
+
type ToolCloseInput = {
|
|
1204
|
+
channelId: string;
|
|
1205
|
+
serverId?: string;
|
|
1206
|
+
/** Machine callers name their decision source ('subscription-gc',
|
|
1207
|
+
* 'housekeeping') so the durable record carries honest provenance.
|
|
1208
|
+
* Honored ONLY for module-origin dispatch — tool input is untrusted,
|
|
1209
|
+
* so a model-origin call carrying this field is still recorded as
|
|
1210
|
+
* 'agent-tool' (the resident may close their own channel; they may
|
|
1211
|
+
* not attribute the act to housekeeping). A module-origin call
|
|
1212
|
+
* without it is the module's own close, MODULE_CLOSE_SOURCE. */
|
|
1213
|
+
source?: string;
|
|
1214
|
+
/** A machine close aimed at an explicitly-opened channel is refused
|
|
1215
|
+
* unless the caller certifies an explicit idle lease (a configured
|
|
1216
|
+
* per-channel budget is consent to close at that budget). A tuned-out
|
|
1217
|
+
* channel is refused even so, since its tune-out's cancel is what
|
|
1218
|
+
* ends it. Module origin only, like `source`. */
|
|
1219
|
+
overrideExplicitOpen?: boolean;
|
|
1220
|
+
};
|
|
1221
|
+
|
|
1222
|
+
/** ChannelRegistryOptions.onToolClose: a tool close's end of the all-except hold that excepts its channel. */
|
|
1223
|
+
export type ToolCloseHook = (
|
|
1224
|
+
serverId: string,
|
|
1225
|
+
channelId: string,
|
|
1226
|
+
opts?: { unregistered?: true },
|
|
1227
|
+
) => (() => string) | undefined;
|
|
1228
|
+
|
|
1130
1229
|
/**
|
|
1131
1230
|
* Who is actually making a channel tool call, as established by the
|
|
1132
1231
|
* framework's own dispatch — model-origin calls come through
|
|
@@ -1177,7 +1276,7 @@ export class ChannelRegistry {
|
|
|
1177
1276
|
private serverRegistry: McplServerRegistry;
|
|
1178
1277
|
private featureSetManager: FeatureSetManager;
|
|
1179
1278
|
private pushEventFn: (event: ProcessEvent) => void;
|
|
1180
|
-
private emitTraceFn: (event:
|
|
1279
|
+
private emitTraceFn: (event: TraceEventInput) => void;
|
|
1181
1280
|
private sendTypingFn?: (
|
|
1182
1281
|
serverId: string,
|
|
1183
1282
|
channelId: string,
|
|
@@ -1186,8 +1285,9 @@ export class ChannelRegistry {
|
|
|
1186
1285
|
) => void;
|
|
1187
1286
|
private shouldTriggerInference?: (content: string, metadata: Record<string, unknown>) => boolean;
|
|
1188
1287
|
private readonly latestInboundLocus: boolean;
|
|
1189
|
-
private onToolClose?:
|
|
1288
|
+
private onToolClose?: ToolCloseHook;
|
|
1190
1289
|
private focusChannelResolver?: () => ChannelRef | null;
|
|
1290
|
+
private readonly tuneOutReaderFn?: () => TuneOutReader;
|
|
1191
1291
|
private onRouteFailure?: ChannelRegistryOptions['onRouteFailure'];
|
|
1192
1292
|
private onChannelAutoOpened?: (info: {
|
|
1193
1293
|
conversationId?: string;
|
|
@@ -1197,8 +1297,12 @@ export class ChannelRegistry {
|
|
|
1197
1297
|
}) => void;
|
|
1198
1298
|
private homeChannelResolver?: (agentName: string) => ChannelRef | undefined;
|
|
1199
1299
|
private activeChannelResolver?: (agentName: string) => ChannelRef | null | undefined;
|
|
1300
|
+
private channellessTurn?: (agentName: string) => boolean;
|
|
1200
1301
|
private takesGlobalFallback?: (conversationId: string) => boolean;
|
|
1201
1302
|
private serverConnecting?: (serverId: string) => boolean;
|
|
1303
|
+
private unpromptedChannel?: string;
|
|
1304
|
+
private unpromptedChannelWarned = false;
|
|
1305
|
+
private takesUnpromptedChannel?: (conversationId: string) => boolean;
|
|
1202
1306
|
private store?: JsStore;
|
|
1203
1307
|
|
|
1204
1308
|
/**
|
|
@@ -1400,6 +1504,11 @@ export class ChannelRegistry {
|
|
|
1400
1504
|
return new ChannelLifecycleError(`this host isn't permitted to open or close the "${serverId}" server's channels`, 'not-permitted');
|
|
1401
1505
|
}
|
|
1402
1506
|
|
|
1507
|
+
private publishingServer(serverId: string): McplServerConnection | undefined {
|
|
1508
|
+
const server = this.serverRegistry.getServer(serverId);
|
|
1509
|
+
return server && CapabilityGrant.of(server).has('channels.publish') ? server : undefined;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1403
1512
|
/**
|
|
1404
1513
|
* Why a send to this server's channels is refused before anything goes
|
|
1405
1514
|
* out, as one clause with what works from there, for a caller that found
|
|
@@ -1478,17 +1587,17 @@ export class ChannelRegistry {
|
|
|
1478
1587
|
(reach === 'gone' ? ' If you need the server back, ask whoever runs this host.' : '') +
|
|
1479
1588
|
(failure === undefined ? ` ${withStop(lifecycleClause(err))}` : '');
|
|
1480
1589
|
}
|
|
1481
|
-
// A tune-out
|
|
1590
|
+
// A tune-out holds the channel's messages for its reader, if any, and
|
|
1482
1591
|
// channel_open refuses a tuned-out channel.
|
|
1483
1592
|
if (action === 'open' ? now !== 'open' : now !== 'closed') {
|
|
1484
1593
|
if (action === 'open' && now === 'tuned-out') {
|
|
1485
|
-
return `Channel ${channel} was tuned out meanwhile: its messages
|
|
1594
|
+
return `Channel ${channel} was tuned out meanwhile: its messages ${this.tuneOutHeld()}, and channel_open doesn't ` +
|
|
1486
1595
|
`change that. ${this.tuneOutEndText(current)}`;
|
|
1487
1596
|
}
|
|
1488
1597
|
// channel_close refuses a tuned-out channel too, so calling it again
|
|
1489
1598
|
// isn't the way: the tune-out's cancel is, and it delivers the backlog.
|
|
1490
1599
|
if (action === 'close' && now === 'tuned-out') {
|
|
1491
|
-
return `Channel ${channel} was tuned out meanwhile: its messages
|
|
1600
|
+
return `Channel ${channel} was tuned out meanwhile: its messages ${this.tuneOutHeld()}, and channel_close doesn't ` +
|
|
1492
1601
|
`change that. ${this.tuneOutEndText(current, 'channel_close')}`;
|
|
1493
1602
|
}
|
|
1494
1603
|
// The later decision's own work settles the transport, and its call
|
|
@@ -1581,7 +1690,7 @@ export class ChannelRegistry {
|
|
|
1581
1690
|
serverRegistry: McplServerRegistry,
|
|
1582
1691
|
featureSetManager: FeatureSetManager,
|
|
1583
1692
|
pushEventFn: (event: ProcessEvent) => void,
|
|
1584
|
-
emitTraceFn: (event:
|
|
1693
|
+
emitTraceFn: (event: TraceEventInput) => void,
|
|
1585
1694
|
options?: ChannelRegistryOptions & {
|
|
1586
1695
|
sendTypingFn?: (
|
|
1587
1696
|
serverId: string,
|
|
@@ -1594,6 +1703,18 @@ export class ChannelRegistry {
|
|
|
1594
1703
|
this.handleCoalescedIncoming = options?.handleCoalescedIncoming;
|
|
1595
1704
|
this.acceptInbound = options?.acceptInbound;
|
|
1596
1705
|
this.channelTools = CHANNEL_TOOL_DEFINITIONS.map((tool) => {
|
|
1706
|
+
if (tool.name === 'channel_publish') {
|
|
1707
|
+
if (options?.unpromptedChannel === undefined || options.eventGatePresent !== true) return tool;
|
|
1708
|
+
return {
|
|
1709
|
+
...tool,
|
|
1710
|
+
description: tool.description
|
|
1711
|
+
.replace(
|
|
1712
|
+
'otherwise, while a focus holds, the channel it keeps.',
|
|
1713
|
+
'otherwise, while a focus holds, the channel it keeps; otherwise, on a turn your own skip_reply wake or the end of your sleep started (or a turn continuing one, when your plain speech isn\'t routed), the channel this host names for such turns.',
|
|
1714
|
+
)
|
|
1715
|
+
.replace('A turn with no channel of its own, such as', 'A turn with no channel by design, such as'),
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1597
1718
|
if (tool.name !== 'skip_reply' || options?.eventGatePresent === true) return tool;
|
|
1598
1719
|
const schema = tool.inputSchema as {
|
|
1599
1720
|
type: 'object';
|
|
@@ -1619,11 +1740,15 @@ export class ChannelRegistry {
|
|
|
1619
1740
|
this.latestInboundLocus = options?.latestInboundLocus === true;
|
|
1620
1741
|
this.onToolClose = options?.onToolClose;
|
|
1621
1742
|
this.focusChannelResolver = options?.focusChannelResolver;
|
|
1743
|
+
this.tuneOutReaderFn = options?.tuneOutReader;
|
|
1622
1744
|
this.onRouteFailure = options?.onRouteFailure;
|
|
1623
1745
|
this.onChannelAutoOpened = options?.onChannelAutoOpened;
|
|
1624
1746
|
this.homeChannelResolver = options?.homeChannelResolver;
|
|
1625
1747
|
this.activeChannelResolver = options?.activeChannelResolver;
|
|
1748
|
+
this.channellessTurn = options?.channellessTurn;
|
|
1626
1749
|
this.takesGlobalFallback = options?.takesGlobalFallback;
|
|
1750
|
+
this.unpromptedChannel = options?.unpromptedChannel;
|
|
1751
|
+
this.takesUnpromptedChannel = options?.takesUnpromptedChannel;
|
|
1627
1752
|
this.serverConnecting = options?.serverConnecting;
|
|
1628
1753
|
this.store = options?.store;
|
|
1629
1754
|
this.initializeLifecycleStore();
|
|
@@ -2303,8 +2428,9 @@ export class ChannelRegistry {
|
|
|
2303
2428
|
}
|
|
2304
2429
|
|
|
2305
2430
|
/** The explicit 'stop' for a channel whose typing ended, while its
|
|
2306
|
-
* server still registers it. */
|
|
2431
|
+
* server still registers it and holds the grant its start needed. */
|
|
2307
2432
|
private sendTypingStop(ref: ChannelRef): void {
|
|
2433
|
+
if (!this.typingGranted(ref)) return;
|
|
2308
2434
|
if (this.sendTypingFn && this.entryAt(ref)) {
|
|
2309
2435
|
this.sendTypingFn(ref.serverId, ref.channelId, this.typingMetadata.get(destinationKey(ref)), 'stop');
|
|
2310
2436
|
}
|
|
@@ -2423,6 +2549,28 @@ export class ChannelRegistry {
|
|
|
2423
2549
|
return this.channelTools;
|
|
2424
2550
|
}
|
|
2425
2551
|
|
|
2552
|
+
/**
|
|
2553
|
+
* The refusal of a channel tool's call that gives a parameter its schema
|
|
2554
|
+
* declares a string as something else, before anything changes; absent and
|
|
2555
|
+
* `null` pass, as each tool reads them. A number written past 2^53 has
|
|
2556
|
+
* usually lost its last digits by the time the call is parsed.
|
|
2557
|
+
*/
|
|
2558
|
+
nonStringRefusal(toolName: string, input: unknown): ToolResult | undefined {
|
|
2559
|
+
if (input === null || typeof input !== 'object') return undefined;
|
|
2560
|
+
const properties = this.channelTools.find((tool) => tool.name === toolName)?.inputSchema.properties ?? {};
|
|
2561
|
+
for (const [field, value] of Object.entries(input)) {
|
|
2562
|
+
if ((properties[field] as { type?: unknown } | undefined)?.type !== 'string') continue;
|
|
2563
|
+
if (value === undefined || value === null || typeof value === 'string') continue;
|
|
2564
|
+
return {
|
|
2565
|
+
success: false,
|
|
2566
|
+
error: `Nothing changed: ${toolName}'s ${field} must be a string, and this call gave ${jsonKind(value)}. Give it in quotes`
|
|
2567
|
+
+ (typeof value === 'number' ? ': a number can lose its last digits as the call is read.' : '.'),
|
|
2568
|
+
isError: true,
|
|
2569
|
+
};
|
|
2570
|
+
}
|
|
2571
|
+
return undefined;
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2426
2574
|
/**
|
|
2427
2575
|
* Handle a call to one of the synthesized channel tools.
|
|
2428
2576
|
*
|
|
@@ -2443,6 +2591,8 @@ export class ChannelRegistry {
|
|
|
2443
2591
|
* attempted, so the caller learns where it was delivered. */
|
|
2444
2592
|
seen?: { outcome?: PublishOutcome },
|
|
2445
2593
|
): Promise<ToolResult> {
|
|
2594
|
+
const refused = this.nonStringRefusal(toolName, input);
|
|
2595
|
+
if (refused) return refused;
|
|
2446
2596
|
switch (toolName) {
|
|
2447
2597
|
case 'channel_list':
|
|
2448
2598
|
return this.handleToolList();
|
|
@@ -2456,15 +2606,7 @@ export class ChannelRegistry {
|
|
|
2456
2606
|
}, origin?.kind === 'agent' ? origin.own : undefined);
|
|
2457
2607
|
|
|
2458
2608
|
case 'channel_close':
|
|
2459
|
-
return this.handleToolClose(
|
|
2460
|
-
input as {
|
|
2461
|
-
channelId: string;
|
|
2462
|
-
serverId?: string;
|
|
2463
|
-
source?: string;
|
|
2464
|
-
overrideExplicitOpen?: boolean;
|
|
2465
|
-
},
|
|
2466
|
-
origin,
|
|
2467
|
-
);
|
|
2609
|
+
return this.handleToolClose(input as ToolCloseInput, origin);
|
|
2468
2610
|
|
|
2469
2611
|
case 'channel_decline':
|
|
2470
2612
|
return this.handleToolDecline(input as {
|
|
@@ -2520,7 +2662,9 @@ export class ChannelRegistry {
|
|
|
2520
2662
|
* its home.
|
|
2521
2663
|
*/
|
|
2522
2664
|
incomingFor(agentName?: string): { channel: ChannelRef; messageId: string; threadId?: string } | undefined {
|
|
2523
|
-
|
|
2665
|
+
const channelless = agentName === undefined ? false
|
|
2666
|
+
: this.channellessTurn ? this.channellessTurn(agentName) : this.activeChannelResolver?.(agentName) === null;
|
|
2667
|
+
if (channelless && !this.homeChannelResolver?.(agentName!)) {
|
|
2524
2668
|
return undefined;
|
|
2525
2669
|
}
|
|
2526
2670
|
// The latest delivered inbound itself, not the speech fallback
|
|
@@ -2547,7 +2691,9 @@ export class ChannelRegistry {
|
|
|
2547
2691
|
// declared it inbound-only, since speech isn't posted there): a conversation
|
|
2548
2692
|
// fork's home channel, else this turn's channel (single trunk agent — item-3
|
|
2549
2693
|
// redux: its locus, else its triggering channel), else the global default
|
|
2550
|
-
// (a no-trigger turn's, but not a heartbeat's, whose turn takes none)
|
|
2694
|
+
// (a no-trigger turn's, but not a heartbeat's, whose turn takes none), with
|
|
2695
|
+
// the configured unprompted channel in it where the turn takes it: the
|
|
2696
|
+
// channel channel_publish naming none publishes to, in every prose mode.
|
|
2551
2697
|
// Without this, a fork
|
|
2552
2698
|
// was advertised the global locus but published somewhere else; and a trunk
|
|
2553
2699
|
// agent was told the wrong channel under concurrency.
|
|
@@ -2558,9 +2704,11 @@ export class ChannelRegistry {
|
|
|
2558
2704
|
// An ephemeral run takes no global fallback (takesGlobalFallback): not the
|
|
2559
2705
|
// all-except hold's channel, not the latest inbound message's channel as
|
|
2560
2706
|
// its outgoing one, nor that message as the one it is replying to.
|
|
2561
|
-
const fallback =
|
|
2707
|
+
const fallback = (): ChannelRef | null => agentName !== undefined
|
|
2708
|
+
? this.globalFallbackFor(agentName, () => this.configuredUnprompted(agentName))
|
|
2709
|
+
: this.sharedFallback();
|
|
2562
2710
|
// A turn that deliberately has no channel (null) takes no fallback.
|
|
2563
|
-
const outgoing = home ?? (active !== undefined ? active : fallback);
|
|
2711
|
+
const outgoing = home ?? (active !== undefined ? active : fallback());
|
|
2564
2712
|
|
|
2565
2713
|
const context: ChannelContext = {};
|
|
2566
2714
|
|
|
@@ -3159,7 +3307,7 @@ export class ChannelRegistry {
|
|
|
3159
3307
|
|
|
3160
3308
|
/**
|
|
3161
3309
|
* Make a tuned-out channel's transport match its tune-out, which keeps it
|
|
3162
|
-
* open so its traffic reaches
|
|
3310
|
+
* open so its traffic reaches its reader, or is held (TuneOutParams). The
|
|
3163
3311
|
* tune-out is recorded first (enterTuneOut), and this writes no decision,
|
|
3164
3312
|
* so the tune-out stands whatever becomes of the open. Lifecycle work
|
|
3165
3313
|
* queued for the channel goes first, until none is left, since each
|
|
@@ -3171,8 +3319,8 @@ export class ChannelRegistry {
|
|
|
3171
3319
|
* tune-out as open), and the answer waits for the work queued after it
|
|
3172
3320
|
* too, such as a registration that came back, and comes from the state
|
|
3173
3321
|
* that work leaves. Returns nothing where the transport is open, or, for
|
|
3174
|
-
* tune_out's answer, why the channel's messages don't or may not reach
|
|
3175
|
-
*
|
|
3322
|
+
* tune_out's answer, why the channel's messages don't or may not reach its
|
|
3323
|
+
* reader yet, or be received, and when they will, or that the tune-out isn't in
|
|
3176
3324
|
* effect any more. A call while this tune-out's open is in flight, as a
|
|
3177
3325
|
* repeated tune_out makes, gets that open's answer rather than send
|
|
3178
3326
|
* another.
|
|
@@ -3245,9 +3393,10 @@ export class ChannelRegistry {
|
|
|
3245
3393
|
// that took effect would have its traffic diverted. `again` where calling
|
|
3246
3394
|
// tune_out again tries the open now: the channel is registered here and
|
|
3247
3395
|
// its server up.
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
`
|
|
3396
|
+
// Where no reader runs, each answer is that alone (tuneOutNoReaderAnswer).
|
|
3397
|
+
const notYet = (later: string, why: string, again = false): string => tuneOutNoReaderAnswer(this.tuneOutReaderNow()) ?? withStop(
|
|
3398
|
+
`The tune-out is recorded, but its messages ${this.tuneOutReach('may not reach')} until this host opens the ` +
|
|
3399
|
+
`channel, which it does ${later}${again ? ', or now if you call tune_out again' : ''}; ${why}`,
|
|
3251
3400
|
);
|
|
3252
3401
|
const nextRegistration = `when the "${serverId}" server next registers this channel, if this host may open and ` +
|
|
3253
3402
|
'close its channels then';
|
|
@@ -3279,18 +3428,18 @@ export class ChannelRegistry {
|
|
|
3279
3428
|
// established.
|
|
3280
3429
|
const reach = this.serverReach(serverId);
|
|
3281
3430
|
if (reach !== 'up') {
|
|
3282
|
-
return withStop(
|
|
3283
|
-
`The tune-out is recorded, but its messages don't reach
|
|
3284
|
-
`connected and the channel is open; it can't be opened now: ${unreachableClause(serverId, reach)}` +
|
|
3431
|
+
return tuneOutNoReaderAnswer(this.tuneOutReaderNow()) ?? withStop(
|
|
3432
|
+
`The tune-out is recorded, but its messages ${this.tuneOutReach("don't reach")} until the "${serverId}" server ` +
|
|
3433
|
+
`is connected and the channel is open; it can't be opened now: ${unreachableClause(serverId, reach)}` +
|
|
3285
3434
|
(reach === 'gone' ? unreachableRemedy(reach) : ''),
|
|
3286
3435
|
);
|
|
3287
3436
|
}
|
|
3288
3437
|
const transportOpen = this.tuneOutReadsOpen(entry);
|
|
3289
3438
|
if (!this.directsLifecycle(serverId)) {
|
|
3290
3439
|
return transportOpen ? undefined
|
|
3291
|
-
: `The tune-out is recorded, but ${this.lifecycleRefusal(serverId).message}, so its messages
|
|
3292
|
-
'
|
|
3293
|
-
'this host to open it, ask whoever runs this host.';
|
|
3440
|
+
: tuneOutNoReaderAnswer(this.tuneOutReaderNow()) ?? `The tune-out is recorded, but ${this.lifecycleRefusal(serverId).message}, so its messages ` +
|
|
3441
|
+
`${this.tuneOutReach('reach')} only as that server sends them, and calling tune_out again won't change ` +
|
|
3442
|
+
'that. If you need this host to open it, ask whoever runs this host.';
|
|
3294
3443
|
}
|
|
3295
3444
|
if (!entry) return missing;
|
|
3296
3445
|
// A flag a close that may have taken effect left gets an open.
|
|
@@ -3957,10 +4106,9 @@ export class ChannelRegistry {
|
|
|
3957
4106
|
* (typing timer lifecycle is still managed for when the callback is wired).
|
|
3958
4107
|
*/
|
|
3959
4108
|
private sendTypingNotification(ref: ChannelRef): void {
|
|
3960
|
-
//
|
|
3961
|
-
//
|
|
3962
|
-
|
|
3963
|
-
if (!CapabilityGrant.of(this.serverRegistry.getServer(ref.serverId)).has('channels.typing')) return;
|
|
4109
|
+
// Silent skip — a typing indicator is cosmetic and per-7s, so a
|
|
4110
|
+
// diagnostic per tick would be noise.
|
|
4111
|
+
if (!this.typingGranted(ref)) return;
|
|
3964
4112
|
if (this.sendTypingFn) {
|
|
3965
4113
|
this.sendTypingFn(ref.serverId, ref.channelId, this.typingMetadata.get(destinationKey(ref)));
|
|
3966
4114
|
}
|
|
@@ -3968,6 +4116,11 @@ export class ChannelRegistry {
|
|
|
3968
4116
|
// sendTyping method, wire it here directly instead of using a callback.
|
|
3969
4117
|
}
|
|
3970
4118
|
|
|
4119
|
+
/** §14.1: channels/typing, start and stop alike, requires channels.typing in the grant. */
|
|
4120
|
+
private typingGranted(ref: ChannelRef): boolean {
|
|
4121
|
+
return CapabilityGrant.of(this.serverRegistry.getServer(ref.serverId)).has('channels.typing');
|
|
4122
|
+
}
|
|
4123
|
+
|
|
3971
4124
|
// ==========================================================================
|
|
3972
4125
|
// Private: Channel Lookup
|
|
3973
4126
|
// ==========================================================================
|
|
@@ -4048,6 +4201,21 @@ export class ChannelRegistry {
|
|
|
4048
4201
|
return channelOnServer(ref);
|
|
4049
4202
|
}
|
|
4050
4203
|
|
|
4204
|
+
/** The reader of a tuned-out channel's traffic, as the host says (tuneOutReader), or the default persistent reader. */
|
|
4205
|
+
private tuneOutReaderNow(): TuneOutReader {
|
|
4206
|
+
return this.tuneOutReaderFn?.() ?? { kind: 'persistent', name: persistentReaderName({}) };
|
|
4207
|
+
}
|
|
4208
|
+
|
|
4209
|
+
/** What a tuned-out channel's messages are, after "its messages" (tuneOutHeldWords). */
|
|
4210
|
+
private tuneOutHeld(): string {
|
|
4211
|
+
return tuneOutHeldWords(this.tuneOutReaderNow());
|
|
4212
|
+
}
|
|
4213
|
+
|
|
4214
|
+
/** Whether a tuned-out channel's messages get to its reader, after "its messages" (tuneOutReachWords). */
|
|
4215
|
+
private tuneOutReach(form: Parameters<typeof tuneOutReachWords>[1]): string {
|
|
4216
|
+
return tuneOutReachWords(this.tuneOutReaderNow(), form);
|
|
4217
|
+
}
|
|
4218
|
+
|
|
4051
4219
|
/**
|
|
4052
4220
|
* A tool's refusal of a tuned-out channel, whose decision is the tune-out:
|
|
4053
4221
|
* its cancel is what ends it (tuneOutEndText), and a decision recorded over
|
|
@@ -4059,7 +4227,7 @@ export class ChannelRegistry {
|
|
|
4059
4227
|
success: false,
|
|
4060
4228
|
isError: true,
|
|
4061
4229
|
error:
|
|
4062
|
-
`Channel ${this.quotedShown(refOf(entry))} is tuned out: its messages
|
|
4230
|
+
`Channel ${this.quotedShown(refOf(entry))} is tuned out: its messages ${this.tuneOutHeld()}, and ${tool} ` +
|
|
4063
4231
|
`doesn't change that. ${this.tuneOutEndText(entry, tool)}`,
|
|
4064
4232
|
data: { refusal: 'tuned-out', channelId: entry.descriptor.id, serverId: entry.serverId },
|
|
4065
4233
|
};
|
|
@@ -4080,7 +4248,7 @@ export class ChannelRegistry {
|
|
|
4080
4248
|
return entry ? this.tuneOutEndText(entry) : undefined;
|
|
4081
4249
|
}
|
|
4082
4250
|
|
|
4083
|
-
private tuneOutEndText(entry: ChannelEntry, tool?: string): string {
|
|
4251
|
+
private tuneOutEndText(entry: ChannelEntry, tool?: string, closeWith?: string): string {
|
|
4084
4252
|
const close = tool === 'channel_close';
|
|
4085
4253
|
// A name is written as a literal tune_out's channelId takes back
|
|
4086
4254
|
// (quotedValue, main's #852); one no name picks alone, by its id and
|
|
@@ -4090,7 +4258,7 @@ export class ChannelRegistry {
|
|
|
4090
4258
|
? `channelId ${quotedValue(name)}`
|
|
4091
4259
|
: `channelId ${quotedValue(entry.descriptor.id)}, serverId ${JSON.stringify(entry.serverId)}`;
|
|
4092
4260
|
return `Call tune_out with ${names} and mode "cancel" for it to resume normal ` +
|
|
4093
|
-
`attention; cancelling delivers the diverted backlog${close ?
|
|
4261
|
+
`attention; cancelling delivers the diverted backlog${close ? `, and channel_close${closeWith ? ` with ${closeWith}` : ''} closes it after that.` : '.'}`;
|
|
4094
4262
|
}
|
|
4095
4263
|
|
|
4096
4264
|
/**
|
|
@@ -5170,36 +5338,38 @@ export class ChannelRegistry {
|
|
|
5170
5338
|
}
|
|
5171
5339
|
}
|
|
5172
5340
|
|
|
5173
|
-
private async handleToolClose(
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
* ends it. Module origin only, like `source`. */
|
|
5190
|
-
overrideExplicitOpen?: boolean;
|
|
5191
|
-
},
|
|
5192
|
-
origin?: ChannelToolOrigin,
|
|
5193
|
-
): Promise<ToolResult> {
|
|
5194
|
-
const resolved = this.resolveToolChannelEntry(input.channelId, input.serverId, origin?.kind === 'agent' ? origin.own : undefined);
|
|
5195
|
-
const entry = resolved.entry;
|
|
5196
|
-
if (!entry) {
|
|
5197
|
-
return {
|
|
5198
|
-
success: false,
|
|
5199
|
-
error: resolved.error,
|
|
5200
|
-
isError: true,
|
|
5201
|
-
};
|
|
5341
|
+
private async handleToolClose(input: ToolCloseInput, origin?: ChannelToolOrigin): Promise<ToolResult> {
|
|
5342
|
+
const own = origin?.kind === 'agent' ? origin.own : undefined;
|
|
5343
|
+
// An agent's bare name for the channel the all-except hold excepts means
|
|
5344
|
+
// that channel (focusNamedBare), whether its server still registers it
|
|
5345
|
+
// or not: another server's channel with the id, which the name would
|
|
5346
|
+
// reach otherwise, isn't closed, and the answer says so. A module's
|
|
5347
|
+
// close resolves its name as any other: housekeeping doesn't end the
|
|
5348
|
+
// resident's focus.
|
|
5349
|
+
const focused = origin?.kind === 'module' ? undefined : this.focusNamedBare(input.channelId, input.serverId, own);
|
|
5350
|
+
if (focused && 'refusal' in focused) return { success: false, error: focused.refusal, isError: true };
|
|
5351
|
+
if (focused) {
|
|
5352
|
+
const entry = this.entryAt(focused.focus);
|
|
5353
|
+
const result = entry
|
|
5354
|
+
? await this.closeToolEntry(entry, input, origin)
|
|
5355
|
+
: this.unregisteredFocusClose(focused.focus) ?? { success: false, error: noLongerRegistered(focused.focus), isError: true };
|
|
5356
|
+
return focused.notClosed ? withSentence(result, focused.notClosed) : result;
|
|
5202
5357
|
}
|
|
5358
|
+
const resolved = this.resolveToolChannelEntry(input.channelId, input.serverId, own);
|
|
5359
|
+
if (resolved.entry) return this.closeToolEntry(resolved.entry, input, origin);
|
|
5360
|
+
// The name, with its serverId or as the turn's own channel, means the
|
|
5361
|
+
// channel the all-except hold excepts, which its server no longer
|
|
5362
|
+
// registers (unregisteredFocusNamed). A module's close of such a name is
|
|
5363
|
+
// refused as any name no channel answers to.
|
|
5364
|
+
const gone = origin?.kind === 'module' ? undefined : this.unregisteredFocusNamed(input.channelId, input.serverId, own);
|
|
5365
|
+
return (gone && this.unregisteredFocusClose(gone)) || { success: false, error: resolved.error, isError: true };
|
|
5366
|
+
}
|
|
5367
|
+
|
|
5368
|
+
/**
|
|
5369
|
+
* A tool close of the channel `entry`, which its name resolved to
|
|
5370
|
+
* (handleToolClose).
|
|
5371
|
+
*/
|
|
5372
|
+
private async closeToolEntry(entry: ChannelEntry, input: ToolCloseInput, origin?: ChannelToolOrigin): Promise<ToolResult> {
|
|
5203
5373
|
// From here, the channel's own id, whichever name the call gave for it
|
|
5204
5374
|
// (a server-qualified one included).
|
|
5205
5375
|
input = { ...input, channelId: entry.descriptor.id };
|
|
@@ -5233,19 +5403,21 @@ export class ChannelRegistry {
|
|
|
5233
5403
|
// Closed already, perhaps only by default: the agent's close is still
|
|
5234
5404
|
// its decision, and is recorded as one (setDesiredState), so a later
|
|
5235
5405
|
// registration's lift of a default leaves the channel closed.
|
|
5236
|
-
const
|
|
5406
|
+
const hook = this.toolCloseHook(refOf(entry));
|
|
5407
|
+
if ('error' in hook) return { success: false, error: hook.error, isError: true };
|
|
5408
|
+
const ended = hook.text;
|
|
5237
5409
|
try {
|
|
5238
5410
|
this.setDesiredState(entry.serverId, input.channelId, 'closed', closeSource);
|
|
5239
5411
|
} catch (err) {
|
|
5240
5412
|
return {
|
|
5241
5413
|
success: false,
|
|
5242
|
-
error: `Channel ${shown} is already closed, but recording the decision to close it failed: ${(err as Error).message}${ended ? ` ${ended}` : ''}`,
|
|
5414
|
+
error: `Channel ${shown} is already closed, but recording the decision to close it failed: ${(err as Error).message}${ended ? ` ${ended()}` : ''}`,
|
|
5243
5415
|
isError: true,
|
|
5244
5416
|
};
|
|
5245
5417
|
}
|
|
5246
5418
|
return {
|
|
5247
5419
|
success: true,
|
|
5248
|
-
data: { channelId: input.channelId, serverId: entry.serverId, status: 'already closed' },
|
|
5420
|
+
data: { channelId: input.channelId, serverId: entry.serverId, status: 'already closed', ...(ended ? { note: ended() } : {}) },
|
|
5249
5421
|
};
|
|
5250
5422
|
}
|
|
5251
5423
|
|
|
@@ -5285,15 +5457,22 @@ export class ChannelRegistry {
|
|
|
5285
5457
|
// A hold around this channel — the all-except scope it is the excepted
|
|
5286
5458
|
// channel of — ends through the cancel flow: backlog released, end
|
|
5287
5459
|
// recorded, never by overwriting state (#232 gap 9).
|
|
5288
|
-
const
|
|
5460
|
+
const hook = this.toolCloseHook(refOf(entry));
|
|
5461
|
+
if ('error' in hook) return { success: false, error: hook.error, isError: true };
|
|
5462
|
+
const ended = hook.text;
|
|
5289
5463
|
|
|
5290
5464
|
// A decision the log refused changed nothing for the channel, and the
|
|
5291
|
-
// answer says so, and what the hook ended all the same.
|
|
5465
|
+
// answer says so, and what the hook ended all the same. Every answer
|
|
5466
|
+
// carries the hook's sentence, a success's as `note`: whether the close
|
|
5467
|
+
// went through says nothing of the focus, whose end may wait (an
|
|
5468
|
+
// operator's change to the store in progress). The sentence is read as
|
|
5469
|
+
// the answer is built, after the transport's round-trip, during which a
|
|
5470
|
+
// release that waited may have run.
|
|
5471
|
+
const withEnded = (text: string): string => (ended ? `${text} ${ended()}` : text);
|
|
5292
5472
|
try {
|
|
5293
5473
|
this.setDesiredState(entry.serverId, input.channelId, 'closed', closeSource);
|
|
5294
5474
|
} catch (err) {
|
|
5295
|
-
|
|
5296
|
-
return { success: false, error: ended ? `${text} ${ended}` : text, isError: true };
|
|
5475
|
+
return { success: false, error: withEnded(this.lifecycleFailureText('close', entry, unrecordedDecision(err))), isError: true };
|
|
5297
5476
|
}
|
|
5298
5477
|
// This channel's typing, not every channel with its id (stopTyping's
|
|
5299
5478
|
// fan-out of a bare name).
|
|
@@ -5302,7 +5481,7 @@ export class ChannelRegistry {
|
|
|
5302
5481
|
// Refused now, the close stays recorded for the server's next
|
|
5303
5482
|
// registration, and the text says so.
|
|
5304
5483
|
if (!this.directsLifecycle(entry.serverId)) {
|
|
5305
|
-
return { success: false, error: this.lifecycleFailureText('close', entry, this.lifecycleRefusal(entry.serverId)), isError: true };
|
|
5484
|
+
return { success: false, error: withEnded(this.lifecycleFailureText('close', entry, this.lifecycleRefusal(entry.serverId))), isError: true };
|
|
5306
5485
|
}
|
|
5307
5486
|
|
|
5308
5487
|
try {
|
|
@@ -5311,11 +5490,163 @@ export class ChannelRegistry {
|
|
|
5311
5490
|
if (applied.open || now !== 'closed') throw new ChannelLifecycleError(supersededClause('close', now), 'superseded');
|
|
5312
5491
|
return {
|
|
5313
5492
|
success: true,
|
|
5314
|
-
data: { channelId: input.channelId, serverId: entry.serverId, status: 'closed' },
|
|
5493
|
+
data: { channelId: input.channelId, serverId: entry.serverId, status: 'closed', ...(ended ? { note: ended() } : {}) },
|
|
5315
5494
|
};
|
|
5316
5495
|
} catch (err) {
|
|
5317
|
-
return { success: false, error: this.lifecycleFailureText('close', entry, err), isError: true };
|
|
5496
|
+
return { success: false, error: withEnded(this.lifecycleFailureText('close', entry, err)), isError: true };
|
|
5497
|
+
}
|
|
5498
|
+
}
|
|
5499
|
+
|
|
5500
|
+
/**
|
|
5501
|
+
* The tool-close hook (onToolClose) for a channel, its throw taken as the
|
|
5502
|
+
* answer's text: a release of the hold that failed is the close's answer,
|
|
5503
|
+
* as the registry's other failures are, never a rejected call.
|
|
5504
|
+
*/
|
|
5505
|
+
private toolCloseHook(ref: ChannelRef, opts?: { unregistered?: true }): { text?: () => string } | { error: string } {
|
|
5506
|
+
try {
|
|
5507
|
+
return { text: this.onToolClose?.(ref.serverId, ref.channelId, opts) };
|
|
5508
|
+
} catch (err) {
|
|
5509
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
5510
|
+
}
|
|
5511
|
+
}
|
|
5512
|
+
|
|
5513
|
+
/**
|
|
5514
|
+
* A close naming the channel the all-except hold excepts, which its server
|
|
5515
|
+
* no longer registers: the close has nothing left to close, and the hold
|
|
5516
|
+
* nothing left to speak into, so it ends as a close of the registered
|
|
5517
|
+
* channel ends it, what it held released, and the answer says so, as a
|
|
5518
|
+
* failure, since no close ran. Undefined where no hold excepts the channel
|
|
5519
|
+
* (onToolClose answers nothing).
|
|
5520
|
+
*/
|
|
5521
|
+
private unregisteredFocusClose(gone: ChannelRef): ToolResult | undefined {
|
|
5522
|
+
const lead = noLongerRegistered(gone);
|
|
5523
|
+
const ended = this.toolCloseHook(gone, { unregistered: true });
|
|
5524
|
+
if ('error' in ended) return { success: false, error: `${lead} ${ended.error}`, isError: true };
|
|
5525
|
+
if (ended.text) return { success: false, error: `${lead} ${ended.text()}`, isError: true };
|
|
5526
|
+
return undefined;
|
|
5527
|
+
}
|
|
5528
|
+
|
|
5529
|
+
/**
|
|
5530
|
+
* The channel the all-except hold excepts (focusChannelResolver), where a
|
|
5531
|
+
* channel_close names it by its id or its server-qualified name alone: no
|
|
5532
|
+
* serverId, and no channel of the calling turn's own answering to the name
|
|
5533
|
+
* (ownChannelNamed, which a turn's name means first). Such a name means
|
|
5534
|
+
* the hold's channel, registered or not, and not another server's channel
|
|
5535
|
+
* with the id, as when the hold's channel's server stopped registering it
|
|
5536
|
+
* and another server registered the id: that one is closed only by a close
|
|
5537
|
+
* that names its server. With it, where other registered channels answer
|
|
5538
|
+
* to the name, the sentence saying they weren't closed (`notClosed`).
|
|
5539
|
+
*
|
|
5540
|
+
* Where the name is the hold channel's and a channel of the calling turn's
|
|
5541
|
+
* own, another channel, answers to it too, the name means neither: a
|
|
5542
|
+
* resident repeating a close that failed before its focus ended, in a turn
|
|
5543
|
+
* that has since opened another server's channel with the id, would
|
|
5544
|
+
* otherwise close that channel and leave the focus in force. That close is
|
|
5545
|
+
* refused, naming both (`refusal`, focusOrOwnRefusal), and a close naming
|
|
5546
|
+
* either's server reaches it. A turn's name for the hold's channel itself,
|
|
5547
|
+
* and a name the turn's channels on more than one server answer to
|
|
5548
|
+
* (ownNeitherRefusal), resolve as any other.
|
|
5549
|
+
*
|
|
5550
|
+
* Undefined where no hold is in force, where the name isn't the hold
|
|
5551
|
+
* channel's, and where the hold's record can't be read, which leaves the
|
|
5552
|
+
* name to resolve as any other (resolveToolChannelEntry).
|
|
5553
|
+
*
|
|
5554
|
+
* Another channel the name reaches that has a tune-out of its own is
|
|
5555
|
+
* refused by a close until that tune-out is cancelled (tunedOutRefusal),
|
|
5556
|
+
* so its sentence says it is tuned out, and names the cancel first, then
|
|
5557
|
+
* the close by its id and server.
|
|
5558
|
+
*/
|
|
5559
|
+
private focusNamedBare(
|
|
5560
|
+
name: string,
|
|
5561
|
+
serverId: string | undefined,
|
|
5562
|
+
own: readonly ChannelRef[] | undefined,
|
|
5563
|
+
): { focus: ChannelRef; notClosed?: string } | { refusal: string } | undefined {
|
|
5564
|
+
if (serverId) return undefined;
|
|
5565
|
+
const mine = this.ownChannelNamed(name, own);
|
|
5566
|
+
if (mine && 'neither' in mine) return undefined;
|
|
5567
|
+
let focus: ChannelRef | null | undefined;
|
|
5568
|
+
try {
|
|
5569
|
+
focus = this.focusChannelResolver?.();
|
|
5570
|
+
} catch {
|
|
5571
|
+
return undefined;
|
|
5318
5572
|
}
|
|
5573
|
+
if (!focus || (name !== focus.channelId && name !== qualifiedChannelName(focus))) return undefined;
|
|
5574
|
+
if (mine) return sameChannel(mine.channel, focus) ? undefined : { refusal: this.focusOrOwnRefusal(name, focus, mine.channel, own) };
|
|
5575
|
+
const held = focus;
|
|
5576
|
+
const others = this.entriesNamed(name).filter((entry) => !sameChannel(refOf(entry), held));
|
|
5577
|
+
const tunedOut = others.filter((entry) => this.getDesiredState(entry.serverId, entry.descriptor.id) === 'tuned-out');
|
|
5578
|
+
const closable = others.filter((entry) => !tunedOut.includes(entry)).map(refOf);
|
|
5579
|
+
const sentences = [
|
|
5580
|
+
...(closable.length > 0 ? [notClosedText(closable)] : []),
|
|
5581
|
+
...tunedOut.map((entry) => {
|
|
5582
|
+
const ref = refOf(entry);
|
|
5583
|
+
return `Channel ${channelOnServer(ref)} was not closed: it is tuned out, and a close doesn't change that. ` +
|
|
5584
|
+
this.tuneOutEndText(entry, 'channel_close', `channelId ${quotedValue(ref.channelId)} and serverId ${quotedValue(ref.serverId)}`);
|
|
5585
|
+
}),
|
|
5586
|
+
];
|
|
5587
|
+
return { focus: held, ...(sentences.length > 0 ? { notClosed: sentences.join(' ') } : {}) };
|
|
5588
|
+
}
|
|
5589
|
+
|
|
5590
|
+
/**
|
|
5591
|
+
* The refusal of a channel_close's bare name (focusNamedBare) that both the
|
|
5592
|
+
* channel the all-except hold excepts (`focus`) and another channel of the
|
|
5593
|
+
* calling turn's own (`mine`, ownChannelNamed) answer to, each named by
|
|
5594
|
+
* what reaches it alone. The hold's channel is reached by its
|
|
5595
|
+
* server-qualified name, registered or not (focusNamedBare), unless a
|
|
5596
|
+
* channel of the turn's answers to that too; the turn's channel as a
|
|
5597
|
+
* refusal of the turn's names shows it (ownCandidates), unless another
|
|
5598
|
+
* channel of the turn's has its qualified name for its id. A serverId goes
|
|
5599
|
+
* with the channel's own id, so it's offered only where the name is the id
|
|
5600
|
+
* of both, for the hold's server (registered or not, the close reaches its
|
|
5601
|
+
* channel: unregisteredFocusNamed) and for the turn's channel's where that
|
|
5602
|
+
* server still registers it. Where the name is the hold channel's
|
|
5603
|
+
* server-qualified name, which another of the turn's channels has for its
|
|
5604
|
+
* id, the hold's channel is reached by its id and server.
|
|
5605
|
+
*/
|
|
5606
|
+
private focusOrOwnRefusal(name: string, focus: ChannelRef, mine: ChannelRef, own: readonly ChannelRef[] | undefined): string {
|
|
5607
|
+
const qualified = qualifiedChannelName(focus);
|
|
5608
|
+
const focusReaches = this.ownChannelNamed(qualified, own) === undefined;
|
|
5609
|
+
const [theirs] = this.ownCandidates([mine]);
|
|
5610
|
+
// The turn's channel's name, as the focus's: a bare id answers to the
|
|
5611
|
+
// turn's own channels first, so one of them with that id catches it.
|
|
5612
|
+
const theirsReaches = theirs!.reaches && this.ownChannelNamed(theirs!.text, own) === undefined;
|
|
5613
|
+
const parts = [
|
|
5614
|
+
{ text: focusReaches ? quotedValue(qualified) : channelOnServer(focus), reaches: focusReaches },
|
|
5615
|
+
{ text: theirsReaches ? quotedValue(theirs!.text) : theirs!.reaches ? channelOnServer(mine) : theirs!.text, reaches: theirsReaches },
|
|
5616
|
+
];
|
|
5617
|
+
const named = parts.filter((p) => p.reaches).map((p) => p.text);
|
|
5618
|
+
const byName = named.length === 2 ? 'channelId set to one of them' : named.length === 1 ? `channelId ${named[0]}` : '';
|
|
5619
|
+
const servers = name === focus.channelId ? [focus.serverId, ...(this.entryAt(mine) ? [mine.serverId] : [])] : [];
|
|
5620
|
+
const byServer = servers.length > 0 ? `serverId set to ${servers.map((id) => JSON.stringify(id)).join(' or ')}` : '';
|
|
5621
|
+
const how = [byName, byServer].filter((h) => h !== '').join(', or with ');
|
|
5622
|
+
const focusRoute = name !== focus.channelId && !focusReaches
|
|
5623
|
+
? ` For the channel the focus excepts, call again with channelId ${quotedValue(focus.channelId)} and serverId ${JSON.stringify(focus.serverId)}.`
|
|
5624
|
+
: '';
|
|
5625
|
+
return `Channel ${JSON.stringify(name)} names both the channel the all-except focus excepts (${parts[0]!.text}) ` +
|
|
5626
|
+
`and this turn's channel (${parts[1]!.text}), so nothing ran.${how ? ` Call again with ${how}.` : ''}${focusRoute}`;
|
|
5627
|
+
}
|
|
5628
|
+
|
|
5629
|
+
/**
|
|
5630
|
+
* The channel the all-except hold excepts (focusChannelResolver), where a
|
|
5631
|
+
* channel tool's name with a serverId, or the calling turn's own channel's
|
|
5632
|
+
* name, means it though its server no longer registers it: with a
|
|
5633
|
+
* serverId, that server's channel with the id; inside the calling turn,
|
|
5634
|
+
* the turn's own channel the name answers to by its id (ownChannelNamed).
|
|
5635
|
+
* A name given alone is focusNamedBare's. Undefined where no hold is in
|
|
5636
|
+
* force, where its channel is still registered, and where the hold's
|
|
5637
|
+
* record can't be read, which leaves the name's refusal as it was.
|
|
5638
|
+
*/
|
|
5639
|
+
private unregisteredFocusNamed(name: string, serverId: string | undefined, own: readonly ChannelRef[] | undefined): ChannelRef | undefined {
|
|
5640
|
+
let focus: ChannelRef | null | undefined;
|
|
5641
|
+
try {
|
|
5642
|
+
focus = this.focusChannelResolver?.();
|
|
5643
|
+
} catch {
|
|
5644
|
+
return undefined;
|
|
5645
|
+
}
|
|
5646
|
+
if (!focus || this.entryAt(focus)) return undefined;
|
|
5647
|
+
if (serverId) return sameChannel(focus, { serverId, channelId: name }) ? focus : undefined;
|
|
5648
|
+
const mine = this.ownChannelNamed(name, own);
|
|
5649
|
+
return mine && 'channel' in mine && sameChannel(mine.channel, focus) ? focus : undefined;
|
|
5319
5650
|
}
|
|
5320
5651
|
|
|
5321
5652
|
private async handleToolDecline(input: {
|
|
@@ -5513,8 +5844,8 @@ export class ChannelRegistry {
|
|
|
5513
5844
|
`diverted, and then call channel_close.${serverBack}`
|
|
5514
5845
|
: `. If you still want it closed, call channel_close.${serverBack}`;
|
|
5515
5846
|
closeError = decided === 'tuned-out'
|
|
5516
|
-
? `The decline is recorded, but the channel was tuned out meanwhile, and that decision stands: its messages
|
|
5517
|
-
|
|
5847
|
+
? `The decline is recorded, but the channel was tuned out meanwhile, and that decision stands: its messages ` +
|
|
5848
|
+
`${this.tuneOutHeld()}${step}`
|
|
5518
5849
|
: `The decline is recorded, but a later decision to open the channel replaced it, and that decision stands${step}`;
|
|
5519
5850
|
} else if (closed) {
|
|
5520
5851
|
// Closed for certain, whatever became of the decline's own close: one
|
|
@@ -5759,7 +6090,8 @@ export class ChannelRegistry {
|
|
|
5759
6090
|
}
|
|
5760
6091
|
|
|
5761
6092
|
/** Resolve the outbound locus (fork HOME → this-turn's TRIGGERING channel →
|
|
5762
|
-
* process-global default
|
|
6093
|
+
* process-global default, with the configured unprompted channel in it
|
|
6094
|
+
* where the turn takes it), with its server. Public so a multi-segment
|
|
5763
6095
|
* caller can snapshot it ONCE and pin every segment to it via
|
|
5764
6096
|
* routeSpeech's locus. */
|
|
5765
6097
|
resolveLocus(conversationId: string): ChannelRef | null {
|
|
@@ -5767,7 +6099,33 @@ export class ChannelRegistry {
|
|
|
5767
6099
|
if (home) return home;
|
|
5768
6100
|
// A turn that deliberately has no channel (null) takes no fallback.
|
|
5769
6101
|
const active = this.activeChannelResolver?.(conversationId);
|
|
5770
|
-
return active !== undefined
|
|
6102
|
+
return active !== undefined
|
|
6103
|
+
? active
|
|
6104
|
+
: this.globalFallbackFor(conversationId, () => this.configuredUnprompted(conversationId));
|
|
6105
|
+
}
|
|
6106
|
+
|
|
6107
|
+
private configuredUnprompted(conversationId: string | undefined): ChannelRef | null {
|
|
6108
|
+
if (!this.unpromptedChannel) return null;
|
|
6109
|
+
if (conversationId !== undefined && this.takesUnpromptedChannel?.(conversationId) === false) return null;
|
|
6110
|
+
const named = this.entriesNamed(this.unpromptedChannel);
|
|
6111
|
+
if (named.length === 1 && !isInboundOnly(named[0]!)) {
|
|
6112
|
+
this.unpromptedChannelWarned = false;
|
|
6113
|
+
return refOf(named[0]!);
|
|
6114
|
+
}
|
|
6115
|
+
if (!this.unpromptedChannelWarned) {
|
|
6116
|
+
this.unpromptedChannelWarned = true;
|
|
6117
|
+
const why = named.length === 0
|
|
6118
|
+
? 'names no registered channel'
|
|
6119
|
+
: named.length === 1
|
|
6120
|
+
? 'names a channel its server declared inbound-only'
|
|
6121
|
+
: `names ${named.length} registered channels (name one with its server, as "serverId:channelId")`;
|
|
6122
|
+
console.error(
|
|
6123
|
+
`[routing] unpromptedChannel ${logValue(this.unpromptedChannel)} ${why}, so a turn the agent's own timer starts `
|
|
6124
|
+
+ 'takes the fallback it takes without the setting until the setting names a channel it can speak in. '
|
|
6125
|
+
+ 'Not logged again until a turn has used it.',
|
|
6126
|
+
);
|
|
6127
|
+
}
|
|
6128
|
+
return null;
|
|
5771
6129
|
}
|
|
5772
6130
|
|
|
5773
6131
|
/**
|
|
@@ -5776,19 +6134,21 @@ export class ChannelRegistry {
|
|
|
5776
6134
|
* result, so neither the all-except hold's channel nor the latest inbound
|
|
5777
6135
|
* message's is ever its destination.
|
|
5778
6136
|
*/
|
|
5779
|
-
private globalFallbackFor(conversationId: string): ChannelRef | null {
|
|
5780
|
-
return this.takesGlobalFallback?.(conversationId) === false ? null : this.sharedFallback();
|
|
6137
|
+
private globalFallbackFor(conversationId: string, unprompted?: () => ChannelRef | null): ChannelRef | null {
|
|
6138
|
+
return this.takesGlobalFallback?.(conversationId) === false ? null : this.sharedFallback(unprompted);
|
|
5781
6139
|
}
|
|
5782
6140
|
|
|
5783
6141
|
/**
|
|
5784
6142
|
* What every conversation that takes the global fallback falls back to when
|
|
5785
6143
|
* neither a home channel nor its own delivery names one: the all-except
|
|
5786
|
-
* hold's channel while one is open, else
|
|
5787
|
-
* channel
|
|
5788
|
-
*
|
|
6144
|
+
* hold's channel while one is open, else, for a turn's locus, the
|
|
6145
|
+
* configured unprompted channel where the turn takes it (resolveLocus),
|
|
6146
|
+
* else the latest delivered inbound channel under the legacy locus rule,
|
|
6147
|
+
* else none — the prose stays private (architecture channel §3 carve-out,
|
|
6148
|
+
* 2026-10-06).
|
|
5789
6149
|
*/
|
|
5790
|
-
private sharedFallback(): ChannelRef | null {
|
|
5791
|
-
return this.focusChannelResolver?.() ?? this.fallbackLocus();
|
|
6150
|
+
private sharedFallback(unprompted?: () => ChannelRef | null): ChannelRef | null {
|
|
6151
|
+
return this.focusChannelResolver?.() ?? unprompted?.() ?? this.fallbackLocus();
|
|
5792
6152
|
}
|
|
5793
6153
|
|
|
5794
6154
|
private fallbackLocus(): ChannelRef | null {
|
|
@@ -5941,12 +6301,12 @@ export class ChannelRegistry {
|
|
|
5941
6301
|
if (isInboundOnly(entry)) {
|
|
5942
6302
|
return { status: 'failed', destination, reason: inboundOnlyReason(entry), at: at() };
|
|
5943
6303
|
}
|
|
5944
|
-
const server = this.
|
|
6304
|
+
const server = this.publishingServer(entry.serverId);
|
|
5945
6305
|
// §14.1: channels/publish requires channels.publish in the grant. The
|
|
5946
6306
|
// host not sending is the enforcement, here and at the last check, and
|
|
5947
6307
|
// the server's state words the refusal (sendRefusal).
|
|
5948
6308
|
const ungranted = (): PublishOutcome => ({ status: 'failed', destination, reason: this.sendRefusal(entry.serverId), at: at() });
|
|
5949
|
-
if (!server
|
|
6309
|
+
if (!server) return ungranted();
|
|
5950
6310
|
|
|
5951
6311
|
const prevented = (err: unknown): PublishOutcome => ({
|
|
5952
6312
|
status: 'failed', destination, reason: lifecycleClause(err) + refusalRemedy(err), at: at(),
|
|
@@ -6109,12 +6469,15 @@ export class ChannelRegistry {
|
|
|
6109
6469
|
locus: ChannelRef | null,
|
|
6110
6470
|
/** `heldOnFailure`: the caller holds a `failed` delivery's words as a
|
|
6111
6471
|
* draft with its own notice, so no failure marker is written for it.
|
|
6112
|
-
* An `unknown` outcome still gets its marker.
|
|
6113
|
-
|
|
6472
|
+
* An `unknown` outcome still gets its marker. `cameFrom`: with no
|
|
6473
|
+
* locus, the channels the turn's batch came from, each with how the
|
|
6474
|
+
* agent is shown it; the failure's reason names them, and how to reply
|
|
6475
|
+
* in those registered that take speech. */
|
|
6476
|
+
opts?: { heldOnFailure?: boolean; cameFrom?: ReadonlyArray<{ channel: ChannelRef; shown: string }> },
|
|
6114
6477
|
): Promise<PublishOutcome> {
|
|
6115
6478
|
// `logged` is what the log line and the trace say, where it isn't the
|
|
6116
|
-
// agent's reason (the
|
|
6117
|
-
const fail = (channel: ChannelRef | null, reason: string, outcome: PublishOutcome, logged = reason): PublishOutcome => {
|
|
6479
|
+
// agent's reason (the three no-locus reasons below).
|
|
6480
|
+
const fail = (channel: ChannelRef | null, reason: string, outcome: PublishOutcome, logged = reason, byDesign = false): PublishOutcome => {
|
|
6118
6481
|
const status = outcome.status === 'unknown' ? 'unknown' : 'failed';
|
|
6119
6482
|
const channelId = channel?.channelId ?? null;
|
|
6120
6483
|
const detail = outcome.detail !== undefined ? connectorText(outcome.detail) : undefined;
|
|
@@ -6134,17 +6497,19 @@ export class ChannelRegistry {
|
|
|
6134
6497
|
this.onRouteFailure?.({
|
|
6135
6498
|
conversationId, channelId, ...(channel ? { serverId: channel.serverId } : {}),
|
|
6136
6499
|
reason, textLen: text.length, outcome: status, ...(detail !== undefined ? { detail } : {}),
|
|
6500
|
+
...(byDesign ? { byDesign: true as const } : {}),
|
|
6137
6501
|
});
|
|
6138
6502
|
}
|
|
6139
6503
|
return outcome;
|
|
6140
6504
|
};
|
|
6141
6505
|
|
|
6142
|
-
// These
|
|
6506
|
+
// These three reasons are the only ones whose log line and trace keep the
|
|
6143
6507
|
// operator's words rather than the agent's: connectome-host's
|
|
6144
6508
|
// connectome-doctor reads the last [routeSpeech] line for "no locus". Keep
|
|
6145
|
-
// "no locus" in
|
|
6509
|
+
// "no locus" in each logged text. Each names no tool: whether one the
|
|
6146
6510
|
// agent may call could post the words isn't known here, and the notice
|
|
6147
|
-
// calls it a routing or configuration situation
|
|
6511
|
+
// calls it a routing or configuration situation, or for a turn whose
|
|
6512
|
+
// batch named the channels it came from a routing one (byDesign).
|
|
6148
6513
|
// Runtime backstop for JS callers the compiler can't see: an omitted
|
|
6149
6514
|
// locus is a routing bug at the call site, never something to paper over
|
|
6150
6515
|
// with a live re-resolution.
|
|
@@ -6164,6 +6529,32 @@ export class ChannelRegistry {
|
|
|
6164
6529
|
// global inbound ever seen) — the agent was told its prose stays in
|
|
6165
6530
|
// the archive; honor that.
|
|
6166
6531
|
// The locus froze at the turn's start, so this says what the turn had.
|
|
6532
|
+
const from = opts?.cameFrom ?? [];
|
|
6533
|
+
if (from.length > 0) {
|
|
6534
|
+
const list = (names: readonly string[]) =>
|
|
6535
|
+
names.length === 1 ? names[0] : `${names.slice(0, -1).join(', ')} and ${names.at(-1)}`;
|
|
6536
|
+
const closed: string[] = [];
|
|
6537
|
+
const open: Array<{ channel: ChannelRef; shown: string }> = [];
|
|
6538
|
+
for (const came of from) {
|
|
6539
|
+
const entry = this.entryAt(came.channel);
|
|
6540
|
+
if (!entry) closed.push(`${came.shown} isn't registered with this host now, so this host has no channel there to send to`);
|
|
6541
|
+
else if (isInboundOnly(entry)) closed.push(`${came.shown} is ${inboundOnlyReason(entry)}`);
|
|
6542
|
+
else if (!this.publishingServer(entry.serverId)) {
|
|
6543
|
+
const reach = this.serverReach(entry.serverId);
|
|
6544
|
+
closed.push(reach === 'connecting' || reach === 'down'
|
|
6545
|
+
? `${came.shown} can't take a send: ${unreachableClause(entry.serverId, reach)}, so to reply there, send your reply to that channel explicitly once it's back`
|
|
6546
|
+
: `${came.shown} can't take a send: ${this.sendRefusal(entry.serverId)}`);
|
|
6547
|
+
} else open.push(came);
|
|
6548
|
+
}
|
|
6549
|
+
const reply = open.length === 0 ? ''
|
|
6550
|
+
: open.length === from.length
|
|
6551
|
+
? `; to reply ${from.length === 1 ? 'there' : 'in one of them'}, send your reply to that channel explicitly`
|
|
6552
|
+
: `; to reply in ${open.length === 1 ? open[0].shown : `one of ${list(open.map((c) => c.shown))}`}, send your reply to that channel explicitly`;
|
|
6553
|
+
const reason = `this turn came from ${list(from.map((c) => c.shown))}, and no channel was chosen for its reply` +
|
|
6554
|
+
`${reply}${closed.map((c) => `; ${c}`).join('')}`;
|
|
6555
|
+
return fail(null, reason, { status: 'failed', reason, at: Date.now() },
|
|
6556
|
+
"turn has no locus (no channel was chosen for its batch's reply)", true);
|
|
6557
|
+
}
|
|
6167
6558
|
const reason = 'this turn came from no channel, and it had no home or recent channel to reply in; if it should have ' +
|
|
6168
6559
|
'had one, ask whoever runs this host';
|
|
6169
6560
|
return fail(null, reason, { status: 'failed', reason, at: Date.now() },
|
|
@@ -6198,8 +6589,9 @@ export class ChannelRegistry {
|
|
|
6198
6589
|
conversationId: string,
|
|
6199
6590
|
text: string,
|
|
6200
6591
|
locus: ChannelRef | null,
|
|
6592
|
+
opts?: { cameFrom?: ReadonlyArray<{ channel: ChannelRef; shown: string }> },
|
|
6201
6593
|
): Promise<{ delivered: boolean; serverId: string; channelId: string; messageId?: string } | null> {
|
|
6202
|
-
const outcome = await this.deliverSpeech(conversationId, text, locus);
|
|
6594
|
+
const outcome = await this.deliverSpeech(conversationId, text, locus, opts);
|
|
6203
6595
|
if (outcome.status !== 'delivered') return null;
|
|
6204
6596
|
return {
|
|
6205
6597
|
delivered: true,
|
|
@@ -6267,8 +6659,13 @@ export class ChannelRegistry {
|
|
|
6267
6659
|
? 'Nothing was posted: name the channel with channelId. A subagent has no default channel, since its plain '
|
|
6268
6660
|
+ 'speech is returned to whoever called it, so channel_publish sends only to a channel it names.'
|
|
6269
6661
|
: caller !== undefined && inTurn && this.activeChannelResolver?.(caller) === null
|
|
6270
|
-
?
|
|
6271
|
-
|
|
6662
|
+
? (this.channellessTurn && !this.channellessTurn(caller)
|
|
6663
|
+
// The turn came from a channel, but its speech goes to none
|
|
6664
|
+
// (a locked turn held as drafts).
|
|
6665
|
+
? `Nothing was posted: name the channel with channelId. Your plain speech goes to no channel now, so `
|
|
6666
|
+
+ `channel_publish sends only to a channel it names. ${findIds}`
|
|
6667
|
+
: `Nothing was posted: name the channel with channelId. This turn has no channel of its own, so its plain speech `
|
|
6668
|
+
+ `goes to none and channel_publish sends only to a channel it names. ${findIds}`)
|
|
6272
6669
|
: `Nothing was posted: channel_publish needs a \`channelId\`, and there's no channel to default to yet. ${findIds}`,
|
|
6273
6670
|
isError: true,
|
|
6274
6671
|
};
|
|
@@ -6353,8 +6750,8 @@ export class ChannelRegistry {
|
|
|
6353
6750
|
|
|
6354
6751
|
// The same refusal, in the same words, as plain speech's (sendRefusal),
|
|
6355
6752
|
// whose server state is the reason's: nothing awaits between the two.
|
|
6356
|
-
const server = this.
|
|
6357
|
-
if (!server
|
|
6753
|
+
const server = this.publishingServer(entry.serverId);
|
|
6754
|
+
if (!server) {
|
|
6358
6755
|
if (seen) {
|
|
6359
6756
|
const reach = this.serverReach(entry.serverId);
|
|
6360
6757
|
seen.refusal = reach === 'up' ? 'not-permitted' : `server-${reach}`;
|