@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
|
@@ -21,11 +21,13 @@ import { expandCoreTags, isMarkerTagged } from './tags.js';
|
|
|
21
21
|
import { coalesceRejectionReason, EmptyContentError, validateCoalescedContent } from './push-coalescer.js';
|
|
22
22
|
import { isVisiblyEmptyConversion } from './visible-content.js';
|
|
23
23
|
import { logDetail, logValue } from './log-value.js';
|
|
24
|
+
import { jsonKind } from '../jsonrpc-envelope.js';
|
|
24
25
|
import { RefusalLines } from './refusal-lines.js';
|
|
25
26
|
import { CapabilityGrant } from './capability-grant.js';
|
|
26
27
|
import { McplRequestError } from './server-connection.js';
|
|
27
28
|
import { readSkipReplyWake, skipReplyNote } from './skip-reply-wake.js';
|
|
28
29
|
import { isProseTargetToken } from './prose-grammar.js';
|
|
30
|
+
import { persistentReaderName, tuneOutHeldWords, tuneOutNoReaderAnswer, tuneOutReachWords } from '../tune-out/tools.js';
|
|
29
31
|
// ============================================================================
|
|
30
32
|
// Typing indicator interval (Discord typing lasts ~10s, so 7s keeps it alive)
|
|
31
33
|
// ============================================================================
|
|
@@ -146,8 +148,8 @@ function desiredStateRecord(item) {
|
|
|
146
148
|
}
|
|
147
149
|
/**
|
|
148
150
|
* The desired state `store`'s lifecycle log records for `channelId` on
|
|
149
|
-
* `serverId`, for a reader with no registry yet, as
|
|
150
|
-
*
|
|
151
|
+
* `serverId`, for a reader with no registry yet, as the boot's import of an
|
|
152
|
+
* earlier release's queued writes is, before initializeMcpl: the last decision wins, as
|
|
151
153
|
* the registry's replay reads it (getDesiredState answers once there is a
|
|
152
154
|
* registry). undefined when no decision names it.
|
|
153
155
|
*/
|
|
@@ -478,6 +480,32 @@ function withEmbeddedTextMarked(block) {
|
|
|
478
480
|
function channelOnServer(ref) {
|
|
479
481
|
return `${quotedValue(ref.channelId)} on server ${quotedValue(ref.serverId)}`;
|
|
480
482
|
}
|
|
483
|
+
/** A channel_close's lead where its channel's server no longer registers it. */
|
|
484
|
+
function noLongerRegistered(ref) {
|
|
485
|
+
return `Server ${quotedValue(ref.serverId)} no longer registers channel ${quotedValue(ref.channelId)}, so no close was recorded or sent.`;
|
|
486
|
+
}
|
|
487
|
+
/** What a channel_close's bare name reached besides the channel it closed
|
|
488
|
+
* (ChannelRegistry.focusNamedBare): each by its id and server, which the
|
|
489
|
+
* bare name leaves out, and the channelId and serverId a call closes it
|
|
490
|
+
* with: its own id, which needn't be the name the call gave (a focus on
|
|
491
|
+
* `b:room` beside server b's `room`), so naming the server alone, the
|
|
492
|
+
* name kept, is no channel there. */
|
|
493
|
+
function notClosedText(refs) {
|
|
494
|
+
const names = refs.map(channelOnServer);
|
|
495
|
+
const call = (ref) => `channelId ${quotedValue(ref.channelId)} and serverId ${quotedValue(ref.serverId)}`;
|
|
496
|
+
return refs.length === 1
|
|
497
|
+
? `Channel ${names[0]} was not closed: to close it, call again with ${call(refs[0])}.`
|
|
498
|
+
: `Channels ${names.slice(0, -1).join(', ')} and ${names.at(-1)} were not closed: to close one, call again with ` +
|
|
499
|
+
`${refs.slice(0, -1).map(call).join(', with ')}, or with ${call(refs.at(-1))}.`;
|
|
500
|
+
}
|
|
501
|
+
/** A tool result with a sentence added: a success's after its note (or as
|
|
502
|
+
* its note), a failure's after its error. */
|
|
503
|
+
function withSentence(result, sentence) {
|
|
504
|
+
if (!result.success)
|
|
505
|
+
return { ...result, error: result.error ? `${result.error} ${sentence}` : sentence };
|
|
506
|
+
const data = (result.data ?? {});
|
|
507
|
+
return { ...result, data: { ...data, note: typeof data.note === 'string' ? `${data.note} ${sentence}` : sentence } };
|
|
508
|
+
}
|
|
481
509
|
const CHANNEL_ON_SERVER_AT_END = /("(?:[^"\\]|\\.)*") on server ("(?:[^"\\]|\\.)*")$/;
|
|
482
510
|
/**
|
|
483
511
|
* A failed request's words, bounded, for a reason that a caller composes.
|
|
@@ -680,12 +708,17 @@ export class ChannelRegistry {
|
|
|
680
708
|
latestInboundLocus;
|
|
681
709
|
onToolClose;
|
|
682
710
|
focusChannelResolver;
|
|
711
|
+
tuneOutReaderFn;
|
|
683
712
|
onRouteFailure;
|
|
684
713
|
onChannelAutoOpened;
|
|
685
714
|
homeChannelResolver;
|
|
686
715
|
activeChannelResolver;
|
|
716
|
+
channellessTurn;
|
|
687
717
|
takesGlobalFallback;
|
|
688
718
|
serverConnecting;
|
|
719
|
+
unpromptedChannel;
|
|
720
|
+
unpromptedChannelWarned = false;
|
|
721
|
+
takesUnpromptedChannel;
|
|
689
722
|
store;
|
|
690
723
|
/**
|
|
691
724
|
* Registered channels, keyed by their server and id (destinationKey): a
|
|
@@ -861,6 +894,10 @@ export class ChannelRegistry {
|
|
|
861
894
|
return new ChannelLifecycleError(unreachableClause(serverId, reach), 'unreachable', reach);
|
|
862
895
|
return new ChannelLifecycleError(`this host isn't permitted to open or close the "${serverId}" server's channels`, 'not-permitted');
|
|
863
896
|
}
|
|
897
|
+
publishingServer(serverId) {
|
|
898
|
+
const server = this.serverRegistry.getServer(serverId);
|
|
899
|
+
return server && CapabilityGrant.of(server).has('channels.publish') ? server : undefined;
|
|
900
|
+
}
|
|
864
901
|
/**
|
|
865
902
|
* Why a send to this server's channels is refused before anything goes
|
|
866
903
|
* out, as one clause with what works from there, for a caller that found
|
|
@@ -934,17 +971,17 @@ export class ChannelRegistry {
|
|
|
934
971
|
(reach === 'gone' ? ' If you need the server back, ask whoever runs this host.' : '') +
|
|
935
972
|
(failure === undefined ? ` ${withStop(lifecycleClause(err))}` : '');
|
|
936
973
|
}
|
|
937
|
-
// A tune-out
|
|
974
|
+
// A tune-out holds the channel's messages for its reader, if any, and
|
|
938
975
|
// channel_open refuses a tuned-out channel.
|
|
939
976
|
if (action === 'open' ? now !== 'open' : now !== 'closed') {
|
|
940
977
|
if (action === 'open' && now === 'tuned-out') {
|
|
941
|
-
return `Channel ${channel} was tuned out meanwhile: its messages
|
|
978
|
+
return `Channel ${channel} was tuned out meanwhile: its messages ${this.tuneOutHeld()}, and channel_open doesn't ` +
|
|
942
979
|
`change that. ${this.tuneOutEndText(current)}`;
|
|
943
980
|
}
|
|
944
981
|
// channel_close refuses a tuned-out channel too, so calling it again
|
|
945
982
|
// isn't the way: the tune-out's cancel is, and it delivers the backlog.
|
|
946
983
|
if (action === 'close' && now === 'tuned-out') {
|
|
947
|
-
return `Channel ${channel} was tuned out meanwhile: its messages
|
|
984
|
+
return `Channel ${channel} was tuned out meanwhile: its messages ${this.tuneOutHeld()}, and channel_close doesn't ` +
|
|
948
985
|
`change that. ${this.tuneOutEndText(current, 'channel_close')}`;
|
|
949
986
|
}
|
|
950
987
|
// The later decision's own work settles the transport, and its call
|
|
@@ -1034,6 +1071,16 @@ export class ChannelRegistry {
|
|
|
1034
1071
|
this.handleCoalescedIncoming = options?.handleCoalescedIncoming;
|
|
1035
1072
|
this.acceptInbound = options?.acceptInbound;
|
|
1036
1073
|
this.channelTools = CHANNEL_TOOL_DEFINITIONS.map((tool) => {
|
|
1074
|
+
if (tool.name === 'channel_publish') {
|
|
1075
|
+
if (options?.unpromptedChannel === undefined || options.eventGatePresent !== true)
|
|
1076
|
+
return tool;
|
|
1077
|
+
return {
|
|
1078
|
+
...tool,
|
|
1079
|
+
description: tool.description
|
|
1080
|
+
.replace('otherwise, while a focus holds, the channel it keeps.', '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.')
|
|
1081
|
+
.replace('A turn with no channel of its own, such as', 'A turn with no channel by design, such as'),
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1037
1084
|
if (tool.name !== 'skip_reply' || options?.eventGatePresent === true)
|
|
1038
1085
|
return tool;
|
|
1039
1086
|
const schema = tool.inputSchema;
|
|
@@ -1053,11 +1100,15 @@ export class ChannelRegistry {
|
|
|
1053
1100
|
this.latestInboundLocus = options?.latestInboundLocus === true;
|
|
1054
1101
|
this.onToolClose = options?.onToolClose;
|
|
1055
1102
|
this.focusChannelResolver = options?.focusChannelResolver;
|
|
1103
|
+
this.tuneOutReaderFn = options?.tuneOutReader;
|
|
1056
1104
|
this.onRouteFailure = options?.onRouteFailure;
|
|
1057
1105
|
this.onChannelAutoOpened = options?.onChannelAutoOpened;
|
|
1058
1106
|
this.homeChannelResolver = options?.homeChannelResolver;
|
|
1059
1107
|
this.activeChannelResolver = options?.activeChannelResolver;
|
|
1108
|
+
this.channellessTurn = options?.channellessTurn;
|
|
1060
1109
|
this.takesGlobalFallback = options?.takesGlobalFallback;
|
|
1110
|
+
this.unpromptedChannel = options?.unpromptedChannel;
|
|
1111
|
+
this.takesUnpromptedChannel = options?.takesUnpromptedChannel;
|
|
1061
1112
|
this.serverConnecting = options?.serverConnecting;
|
|
1062
1113
|
this.store = options?.store;
|
|
1063
1114
|
this.initializeLifecycleStore();
|
|
@@ -1686,8 +1737,10 @@ export class ChannelRegistry {
|
|
|
1686
1737
|
}
|
|
1687
1738
|
}
|
|
1688
1739
|
/** The explicit 'stop' for a channel whose typing ended, while its
|
|
1689
|
-
* server still registers it. */
|
|
1740
|
+
* server still registers it and holds the grant its start needed. */
|
|
1690
1741
|
sendTypingStop(ref) {
|
|
1742
|
+
if (!this.typingGranted(ref))
|
|
1743
|
+
return;
|
|
1691
1744
|
if (this.sendTypingFn && this.entryAt(ref)) {
|
|
1692
1745
|
this.sendTypingFn(ref.serverId, ref.channelId, this.typingMetadata.get(destinationKey(ref)), 'stop');
|
|
1693
1746
|
}
|
|
@@ -1796,6 +1849,30 @@ export class ChannelRegistry {
|
|
|
1796
1849
|
getChannelTools() {
|
|
1797
1850
|
return this.channelTools;
|
|
1798
1851
|
}
|
|
1852
|
+
/**
|
|
1853
|
+
* The refusal of a channel tool's call that gives a parameter its schema
|
|
1854
|
+
* declares a string as something else, before anything changes; absent and
|
|
1855
|
+
* `null` pass, as each tool reads them. A number written past 2^53 has
|
|
1856
|
+
* usually lost its last digits by the time the call is parsed.
|
|
1857
|
+
*/
|
|
1858
|
+
nonStringRefusal(toolName, input) {
|
|
1859
|
+
if (input === null || typeof input !== 'object')
|
|
1860
|
+
return undefined;
|
|
1861
|
+
const properties = this.channelTools.find((tool) => tool.name === toolName)?.inputSchema.properties ?? {};
|
|
1862
|
+
for (const [field, value] of Object.entries(input)) {
|
|
1863
|
+
if (properties[field]?.type !== 'string')
|
|
1864
|
+
continue;
|
|
1865
|
+
if (value === undefined || value === null || typeof value === 'string')
|
|
1866
|
+
continue;
|
|
1867
|
+
return {
|
|
1868
|
+
success: false,
|
|
1869
|
+
error: `Nothing changed: ${toolName}'s ${field} must be a string, and this call gave ${jsonKind(value)}. Give it in quotes`
|
|
1870
|
+
+ (typeof value === 'number' ? ': a number can lose its last digits as the call is read.' : '.'),
|
|
1871
|
+
isError: true,
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
return undefined;
|
|
1875
|
+
}
|
|
1799
1876
|
/**
|
|
1800
1877
|
* Handle a call to one of the synthesized channel tools.
|
|
1801
1878
|
*
|
|
@@ -1812,6 +1889,9 @@ export class ChannelRegistry {
|
|
|
1812
1889
|
/** Filled with a channel_publish's outcome once its request is
|
|
1813
1890
|
* attempted, so the caller learns where it was delivered. */
|
|
1814
1891
|
seen) {
|
|
1892
|
+
const refused = this.nonStringRefusal(toolName, input);
|
|
1893
|
+
if (refused)
|
|
1894
|
+
return refused;
|
|
1815
1895
|
switch (toolName) {
|
|
1816
1896
|
case 'channel_list':
|
|
1817
1897
|
return this.handleToolList();
|
|
@@ -1858,7 +1938,9 @@ export class ChannelRegistry {
|
|
|
1858
1938
|
* its home.
|
|
1859
1939
|
*/
|
|
1860
1940
|
incomingFor(agentName) {
|
|
1861
|
-
|
|
1941
|
+
const channelless = agentName === undefined ? false
|
|
1942
|
+
: this.channellessTurn ? this.channellessTurn(agentName) : this.activeChannelResolver?.(agentName) === null;
|
|
1943
|
+
if (channelless && !this.homeChannelResolver?.(agentName)) {
|
|
1862
1944
|
return undefined;
|
|
1863
1945
|
}
|
|
1864
1946
|
// The latest delivered inbound itself, not the speech fallback
|
|
@@ -1885,7 +1967,9 @@ export class ChannelRegistry {
|
|
|
1885
1967
|
// declared it inbound-only, since speech isn't posted there): a conversation
|
|
1886
1968
|
// fork's home channel, else this turn's channel (single trunk agent — item-3
|
|
1887
1969
|
// redux: its locus, else its triggering channel), else the global default
|
|
1888
|
-
// (a no-trigger turn's, but not a heartbeat's, whose turn takes none)
|
|
1970
|
+
// (a no-trigger turn's, but not a heartbeat's, whose turn takes none), with
|
|
1971
|
+
// the configured unprompted channel in it where the turn takes it: the
|
|
1972
|
+
// channel channel_publish naming none publishes to, in every prose mode.
|
|
1889
1973
|
// Without this, a fork
|
|
1890
1974
|
// was advertised the global locus but published somewhere else; and a trunk
|
|
1891
1975
|
// agent was told the wrong channel under concurrency.
|
|
@@ -1896,9 +1980,11 @@ export class ChannelRegistry {
|
|
|
1896
1980
|
// An ephemeral run takes no global fallback (takesGlobalFallback): not the
|
|
1897
1981
|
// all-except hold's channel, not the latest inbound message's channel as
|
|
1898
1982
|
// its outgoing one, nor that message as the one it is replying to.
|
|
1899
|
-
const fallback = agentName !== undefined
|
|
1983
|
+
const fallback = () => agentName !== undefined
|
|
1984
|
+
? this.globalFallbackFor(agentName, () => this.configuredUnprompted(agentName))
|
|
1985
|
+
: this.sharedFallback();
|
|
1900
1986
|
// A turn that deliberately has no channel (null) takes no fallback.
|
|
1901
|
-
const outgoing = home ?? (active !== undefined ? active : fallback);
|
|
1987
|
+
const outgoing = home ?? (active !== undefined ? active : fallback());
|
|
1902
1988
|
const context = {};
|
|
1903
1989
|
// Incoming: the message the agent is replying to (incomingFor). The
|
|
1904
1990
|
// anchor is independent of the speech rule: under `speech.locus:
|
|
@@ -2466,7 +2552,7 @@ export class ChannelRegistry {
|
|
|
2466
2552
|
}
|
|
2467
2553
|
/**
|
|
2468
2554
|
* Make a tuned-out channel's transport match its tune-out, which keeps it
|
|
2469
|
-
* open so its traffic reaches
|
|
2555
|
+
* open so its traffic reaches its reader, or is held (TuneOutParams). The
|
|
2470
2556
|
* tune-out is recorded first (enterTuneOut), and this writes no decision,
|
|
2471
2557
|
* so the tune-out stands whatever becomes of the open. Lifecycle work
|
|
2472
2558
|
* queued for the channel goes first, until none is left, since each
|
|
@@ -2478,8 +2564,8 @@ export class ChannelRegistry {
|
|
|
2478
2564
|
* tune-out as open), and the answer waits for the work queued after it
|
|
2479
2565
|
* too, such as a registration that came back, and comes from the state
|
|
2480
2566
|
* that work leaves. Returns nothing where the transport is open, or, for
|
|
2481
|
-
* tune_out's answer, why the channel's messages don't or may not reach
|
|
2482
|
-
*
|
|
2567
|
+
* tune_out's answer, why the channel's messages don't or may not reach its
|
|
2568
|
+
* reader yet, or be received, and when they will, or that the tune-out isn't in
|
|
2483
2569
|
* effect any more. A call while this tune-out's open is in flight, as a
|
|
2484
2570
|
* repeated tune_out makes, gets that open's answer rather than send
|
|
2485
2571
|
* another.
|
|
@@ -2545,8 +2631,9 @@ export class ChannelRegistry {
|
|
|
2545
2631
|
// that took effect would have its traffic diverted. `again` where calling
|
|
2546
2632
|
// tune_out again tries the open now: the channel is registered here and
|
|
2547
2633
|
// its server up.
|
|
2548
|
-
|
|
2549
|
-
|
|
2634
|
+
// Where no reader runs, each answer is that alone (tuneOutNoReaderAnswer).
|
|
2635
|
+
const notYet = (later, why, again = false) => tuneOutNoReaderAnswer(this.tuneOutReaderNow()) ?? withStop(`The tune-out is recorded, but its messages ${this.tuneOutReach('may not reach')} until this host opens the ` +
|
|
2636
|
+
`channel, which it does ${later}${again ? ', or now if you call tune_out again' : ''}; ${why}`);
|
|
2550
2637
|
const nextRegistration = `when the "${serverId}" server next registers this channel, if this host may open and ` +
|
|
2551
2638
|
'close its channels then';
|
|
2552
2639
|
// The work queued for the channel as it stands: its tail, waited for,
|
|
@@ -2580,16 +2667,16 @@ export class ChannelRegistry {
|
|
|
2580
2667
|
// established.
|
|
2581
2668
|
const reach = this.serverReach(serverId);
|
|
2582
2669
|
if (reach !== 'up') {
|
|
2583
|
-
return withStop(`The tune-out is recorded, but its messages don't reach
|
|
2584
|
-
`connected and the channel is open; it can't be opened now: ${unreachableClause(serverId, reach)}` +
|
|
2670
|
+
return tuneOutNoReaderAnswer(this.tuneOutReaderNow()) ?? withStop(`The tune-out is recorded, but its messages ${this.tuneOutReach("don't reach")} until the "${serverId}" server ` +
|
|
2671
|
+
`is connected and the channel is open; it can't be opened now: ${unreachableClause(serverId, reach)}` +
|
|
2585
2672
|
(reach === 'gone' ? unreachableRemedy(reach) : ''));
|
|
2586
2673
|
}
|
|
2587
2674
|
const transportOpen = this.tuneOutReadsOpen(entry);
|
|
2588
2675
|
if (!this.directsLifecycle(serverId)) {
|
|
2589
2676
|
return transportOpen ? undefined
|
|
2590
|
-
: `The tune-out is recorded, but ${this.lifecycleRefusal(serverId).message}, so its messages
|
|
2591
|
-
'
|
|
2592
|
-
'this host to open it, ask whoever runs this host.';
|
|
2677
|
+
: tuneOutNoReaderAnswer(this.tuneOutReaderNow()) ?? `The tune-out is recorded, but ${this.lifecycleRefusal(serverId).message}, so its messages ` +
|
|
2678
|
+
`${this.tuneOutReach('reach')} only as that server sends them, and calling tune_out again won't change ` +
|
|
2679
|
+
'that. If you need this host to open it, ask whoever runs this host.';
|
|
2593
2680
|
}
|
|
2594
2681
|
if (!entry)
|
|
2595
2682
|
return missing;
|
|
@@ -3222,10 +3309,9 @@ export class ChannelRegistry {
|
|
|
3222
3309
|
* (typing timer lifecycle is still managed for when the callback is wired).
|
|
3223
3310
|
*/
|
|
3224
3311
|
sendTypingNotification(ref) {
|
|
3225
|
-
//
|
|
3226
|
-
//
|
|
3227
|
-
|
|
3228
|
-
if (!CapabilityGrant.of(this.serverRegistry.getServer(ref.serverId)).has('channels.typing'))
|
|
3312
|
+
// Silent skip — a typing indicator is cosmetic and per-7s, so a
|
|
3313
|
+
// diagnostic per tick would be noise.
|
|
3314
|
+
if (!this.typingGranted(ref))
|
|
3229
3315
|
return;
|
|
3230
3316
|
if (this.sendTypingFn) {
|
|
3231
3317
|
this.sendTypingFn(ref.serverId, ref.channelId, this.typingMetadata.get(destinationKey(ref)));
|
|
@@ -3233,6 +3319,10 @@ export class ChannelRegistry {
|
|
|
3233
3319
|
// TODO: When server-connection exposes a public sendNotification or
|
|
3234
3320
|
// sendTyping method, wire it here directly instead of using a callback.
|
|
3235
3321
|
}
|
|
3322
|
+
/** §14.1: channels/typing, start and stop alike, requires channels.typing in the grant. */
|
|
3323
|
+
typingGranted(ref) {
|
|
3324
|
+
return CapabilityGrant.of(this.serverRegistry.getServer(ref.serverId)).has('channels.typing');
|
|
3325
|
+
}
|
|
3236
3326
|
// ==========================================================================
|
|
3237
3327
|
// Private: Channel Lookup
|
|
3238
3328
|
// ==========================================================================
|
|
@@ -3305,6 +3395,18 @@ export class ChannelRegistry {
|
|
|
3305
3395
|
}
|
|
3306
3396
|
return channelOnServer(ref);
|
|
3307
3397
|
}
|
|
3398
|
+
/** The reader of a tuned-out channel's traffic, as the host says (tuneOutReader), or the default persistent reader. */
|
|
3399
|
+
tuneOutReaderNow() {
|
|
3400
|
+
return this.tuneOutReaderFn?.() ?? { kind: 'persistent', name: persistentReaderName({}) };
|
|
3401
|
+
}
|
|
3402
|
+
/** What a tuned-out channel's messages are, after "its messages" (tuneOutHeldWords). */
|
|
3403
|
+
tuneOutHeld() {
|
|
3404
|
+
return tuneOutHeldWords(this.tuneOutReaderNow());
|
|
3405
|
+
}
|
|
3406
|
+
/** Whether a tuned-out channel's messages get to its reader, after "its messages" (tuneOutReachWords). */
|
|
3407
|
+
tuneOutReach(form) {
|
|
3408
|
+
return tuneOutReachWords(this.tuneOutReaderNow(), form);
|
|
3409
|
+
}
|
|
3308
3410
|
/**
|
|
3309
3411
|
* A tool's refusal of a tuned-out channel, whose decision is the tune-out:
|
|
3310
3412
|
* its cancel is what ends it (tuneOutEndText), and a decision recorded over
|
|
@@ -3315,7 +3417,7 @@ export class ChannelRegistry {
|
|
|
3315
3417
|
return {
|
|
3316
3418
|
success: false,
|
|
3317
3419
|
isError: true,
|
|
3318
|
-
error: `Channel ${this.quotedShown(refOf(entry))} is tuned out: its messages
|
|
3420
|
+
error: `Channel ${this.quotedShown(refOf(entry))} is tuned out: its messages ${this.tuneOutHeld()}, and ${tool} ` +
|
|
3319
3421
|
`doesn't change that. ${this.tuneOutEndText(entry, tool)}`,
|
|
3320
3422
|
data: { refusal: 'tuned-out', channelId: entry.descriptor.id, serverId: entry.serverId },
|
|
3321
3423
|
};
|
|
@@ -3334,7 +3436,7 @@ export class ChannelRegistry {
|
|
|
3334
3436
|
const entry = this.entryAt(ref);
|
|
3335
3437
|
return entry ? this.tuneOutEndText(entry) : undefined;
|
|
3336
3438
|
}
|
|
3337
|
-
tuneOutEndText(entry, tool) {
|
|
3439
|
+
tuneOutEndText(entry, tool, closeWith) {
|
|
3338
3440
|
const close = tool === 'channel_close';
|
|
3339
3441
|
// A name is written as a literal tune_out's channelId takes back
|
|
3340
3442
|
// (quotedValue, main's #852); one no name picks alone, by its id and
|
|
@@ -3344,7 +3446,7 @@ export class ChannelRegistry {
|
|
|
3344
3446
|
? `channelId ${quotedValue(name)}`
|
|
3345
3447
|
: `channelId ${quotedValue(entry.descriptor.id)}, serverId ${JSON.stringify(entry.serverId)}`;
|
|
3346
3448
|
return `Call tune_out with ${names} and mode "cancel" for it to resume normal ` +
|
|
3347
|
-
`attention; cancelling delivers the diverted backlog${close ?
|
|
3449
|
+
`attention; cancelling delivers the diverted backlog${close ? `, and channel_close${closeWith ? ` with ${closeWith}` : ''} closes it after that.` : '.'}`;
|
|
3348
3450
|
}
|
|
3349
3451
|
/**
|
|
3350
3452
|
* A channel's name as nameShown gives it, quoted for a sentence: the name
|
|
@@ -4336,15 +4438,38 @@ export class ChannelRegistry {
|
|
|
4336
4438
|
}
|
|
4337
4439
|
}
|
|
4338
4440
|
async handleToolClose(input, origin) {
|
|
4339
|
-
const
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4441
|
+
const own = origin?.kind === 'agent' ? origin.own : undefined;
|
|
4442
|
+
// An agent's bare name for the channel the all-except hold excepts means
|
|
4443
|
+
// that channel (focusNamedBare), whether its server still registers it
|
|
4444
|
+
// or not: another server's channel with the id, which the name would
|
|
4445
|
+
// reach otherwise, isn't closed, and the answer says so. A module's
|
|
4446
|
+
// close resolves its name as any other: housekeeping doesn't end the
|
|
4447
|
+
// resident's focus.
|
|
4448
|
+
const focused = origin?.kind === 'module' ? undefined : this.focusNamedBare(input.channelId, input.serverId, own);
|
|
4449
|
+
if (focused && 'refusal' in focused)
|
|
4450
|
+
return { success: false, error: focused.refusal, isError: true };
|
|
4451
|
+
if (focused) {
|
|
4452
|
+
const entry = this.entryAt(focused.focus);
|
|
4453
|
+
const result = entry
|
|
4454
|
+
? await this.closeToolEntry(entry, input, origin)
|
|
4455
|
+
: this.unregisteredFocusClose(focused.focus) ?? { success: false, error: noLongerRegistered(focused.focus), isError: true };
|
|
4456
|
+
return focused.notClosed ? withSentence(result, focused.notClosed) : result;
|
|
4347
4457
|
}
|
|
4458
|
+
const resolved = this.resolveToolChannelEntry(input.channelId, input.serverId, own);
|
|
4459
|
+
if (resolved.entry)
|
|
4460
|
+
return this.closeToolEntry(resolved.entry, input, origin);
|
|
4461
|
+
// The name, with its serverId or as the turn's own channel, means the
|
|
4462
|
+
// channel the all-except hold excepts, which its server no longer
|
|
4463
|
+
// registers (unregisteredFocusNamed). A module's close of such a name is
|
|
4464
|
+
// refused as any name no channel answers to.
|
|
4465
|
+
const gone = origin?.kind === 'module' ? undefined : this.unregisteredFocusNamed(input.channelId, input.serverId, own);
|
|
4466
|
+
return (gone && this.unregisteredFocusClose(gone)) || { success: false, error: resolved.error, isError: true };
|
|
4467
|
+
}
|
|
4468
|
+
/**
|
|
4469
|
+
* A tool close of the channel `entry`, which its name resolved to
|
|
4470
|
+
* (handleToolClose).
|
|
4471
|
+
*/
|
|
4472
|
+
async closeToolEntry(entry, input, origin) {
|
|
4348
4473
|
// From here, the channel's own id, whichever name the call gave for it
|
|
4349
4474
|
// (a server-qualified one included).
|
|
4350
4475
|
input = { ...input, channelId: entry.descriptor.id };
|
|
@@ -4375,20 +4500,23 @@ export class ChannelRegistry {
|
|
|
4375
4500
|
// Closed already, perhaps only by default: the agent's close is still
|
|
4376
4501
|
// its decision, and is recorded as one (setDesiredState), so a later
|
|
4377
4502
|
// registration's lift of a default leaves the channel closed.
|
|
4378
|
-
const
|
|
4503
|
+
const hook = this.toolCloseHook(refOf(entry));
|
|
4504
|
+
if ('error' in hook)
|
|
4505
|
+
return { success: false, error: hook.error, isError: true };
|
|
4506
|
+
const ended = hook.text;
|
|
4379
4507
|
try {
|
|
4380
4508
|
this.setDesiredState(entry.serverId, input.channelId, 'closed', closeSource);
|
|
4381
4509
|
}
|
|
4382
4510
|
catch (err) {
|
|
4383
4511
|
return {
|
|
4384
4512
|
success: false,
|
|
4385
|
-
error: `Channel ${shown} is already closed, but recording the decision to close it failed: ${err.message}${ended ? ` ${ended}` : ''}`,
|
|
4513
|
+
error: `Channel ${shown} is already closed, but recording the decision to close it failed: ${err.message}${ended ? ` ${ended()}` : ''}`,
|
|
4386
4514
|
isError: true,
|
|
4387
4515
|
};
|
|
4388
4516
|
}
|
|
4389
4517
|
return {
|
|
4390
4518
|
success: true,
|
|
4391
|
-
data: { channelId: input.channelId, serverId: entry.serverId, status: 'already closed' },
|
|
4519
|
+
data: { channelId: input.channelId, serverId: entry.serverId, status: 'already closed', ...(ended ? { note: ended() } : {}) },
|
|
4392
4520
|
};
|
|
4393
4521
|
}
|
|
4394
4522
|
// Housekeeping must not override stated intent (issue #5: GC closes wore
|
|
@@ -4425,15 +4553,23 @@ export class ChannelRegistry {
|
|
|
4425
4553
|
// A hold around this channel — the all-except scope it is the excepted
|
|
4426
4554
|
// channel of — ends through the cancel flow: backlog released, end
|
|
4427
4555
|
// recorded, never by overwriting state (#232 gap 9).
|
|
4428
|
-
const
|
|
4556
|
+
const hook = this.toolCloseHook(refOf(entry));
|
|
4557
|
+
if ('error' in hook)
|
|
4558
|
+
return { success: false, error: hook.error, isError: true };
|
|
4559
|
+
const ended = hook.text;
|
|
4429
4560
|
// A decision the log refused changed nothing for the channel, and the
|
|
4430
|
-
// answer says so, and what the hook ended all the same.
|
|
4561
|
+
// answer says so, and what the hook ended all the same. Every answer
|
|
4562
|
+
// carries the hook's sentence, a success's as `note`: whether the close
|
|
4563
|
+
// went through says nothing of the focus, whose end may wait (an
|
|
4564
|
+
// operator's change to the store in progress). The sentence is read as
|
|
4565
|
+
// the answer is built, after the transport's round-trip, during which a
|
|
4566
|
+
// release that waited may have run.
|
|
4567
|
+
const withEnded = (text) => (ended ? `${text} ${ended()}` : text);
|
|
4431
4568
|
try {
|
|
4432
4569
|
this.setDesiredState(entry.serverId, input.channelId, 'closed', closeSource);
|
|
4433
4570
|
}
|
|
4434
4571
|
catch (err) {
|
|
4435
|
-
|
|
4436
|
-
return { success: false, error: ended ? `${text} ${ended}` : text, isError: true };
|
|
4572
|
+
return { success: false, error: withEnded(this.lifecycleFailureText('close', entry, unrecordedDecision(err))), isError: true };
|
|
4437
4573
|
}
|
|
4438
4574
|
// This channel's typing, not every channel with its id (stopTyping's
|
|
4439
4575
|
// fan-out of a bare name).
|
|
@@ -4441,7 +4577,7 @@ export class ChannelRegistry {
|
|
|
4441
4577
|
// Refused now, the close stays recorded for the server's next
|
|
4442
4578
|
// registration, and the text says so.
|
|
4443
4579
|
if (!this.directsLifecycle(entry.serverId)) {
|
|
4444
|
-
return { success: false, error: this.lifecycleFailureText('close', entry, this.lifecycleRefusal(entry.serverId)), isError: true };
|
|
4580
|
+
return { success: false, error: withEnded(this.lifecycleFailureText('close', entry, this.lifecycleRefusal(entry.serverId))), isError: true };
|
|
4445
4581
|
}
|
|
4446
4582
|
try {
|
|
4447
4583
|
const applied = await this.applyDesiredChannelState(entry.serverId, input.channelId);
|
|
@@ -4450,13 +4586,167 @@ export class ChannelRegistry {
|
|
|
4450
4586
|
throw new ChannelLifecycleError(supersededClause('close', now), 'superseded');
|
|
4451
4587
|
return {
|
|
4452
4588
|
success: true,
|
|
4453
|
-
data: { channelId: input.channelId, serverId: entry.serverId, status: 'closed' },
|
|
4589
|
+
data: { channelId: input.channelId, serverId: entry.serverId, status: 'closed', ...(ended ? { note: ended() } : {}) },
|
|
4454
4590
|
};
|
|
4455
4591
|
}
|
|
4456
4592
|
catch (err) {
|
|
4457
|
-
return { success: false, error: this.lifecycleFailureText('close', entry, err), isError: true };
|
|
4593
|
+
return { success: false, error: withEnded(this.lifecycleFailureText('close', entry, err)), isError: true };
|
|
4594
|
+
}
|
|
4595
|
+
}
|
|
4596
|
+
/**
|
|
4597
|
+
* The tool-close hook (onToolClose) for a channel, its throw taken as the
|
|
4598
|
+
* answer's text: a release of the hold that failed is the close's answer,
|
|
4599
|
+
* as the registry's other failures are, never a rejected call.
|
|
4600
|
+
*/
|
|
4601
|
+
toolCloseHook(ref, opts) {
|
|
4602
|
+
try {
|
|
4603
|
+
return { text: this.onToolClose?.(ref.serverId, ref.channelId, opts) };
|
|
4604
|
+
}
|
|
4605
|
+
catch (err) {
|
|
4606
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
4458
4607
|
}
|
|
4459
4608
|
}
|
|
4609
|
+
/**
|
|
4610
|
+
* A close naming the channel the all-except hold excepts, which its server
|
|
4611
|
+
* no longer registers: the close has nothing left to close, and the hold
|
|
4612
|
+
* nothing left to speak into, so it ends as a close of the registered
|
|
4613
|
+
* channel ends it, what it held released, and the answer says so, as a
|
|
4614
|
+
* failure, since no close ran. Undefined where no hold excepts the channel
|
|
4615
|
+
* (onToolClose answers nothing).
|
|
4616
|
+
*/
|
|
4617
|
+
unregisteredFocusClose(gone) {
|
|
4618
|
+
const lead = noLongerRegistered(gone);
|
|
4619
|
+
const ended = this.toolCloseHook(gone, { unregistered: true });
|
|
4620
|
+
if ('error' in ended)
|
|
4621
|
+
return { success: false, error: `${lead} ${ended.error}`, isError: true };
|
|
4622
|
+
if (ended.text)
|
|
4623
|
+
return { success: false, error: `${lead} ${ended.text()}`, isError: true };
|
|
4624
|
+
return undefined;
|
|
4625
|
+
}
|
|
4626
|
+
/**
|
|
4627
|
+
* The channel the all-except hold excepts (focusChannelResolver), where a
|
|
4628
|
+
* channel_close names it by its id or its server-qualified name alone: no
|
|
4629
|
+
* serverId, and no channel of the calling turn's own answering to the name
|
|
4630
|
+
* (ownChannelNamed, which a turn's name means first). Such a name means
|
|
4631
|
+
* the hold's channel, registered or not, and not another server's channel
|
|
4632
|
+
* with the id, as when the hold's channel's server stopped registering it
|
|
4633
|
+
* and another server registered the id: that one is closed only by a close
|
|
4634
|
+
* that names its server. With it, where other registered channels answer
|
|
4635
|
+
* to the name, the sentence saying they weren't closed (`notClosed`).
|
|
4636
|
+
*
|
|
4637
|
+
* Where the name is the hold channel's and a channel of the calling turn's
|
|
4638
|
+
* own, another channel, answers to it too, the name means neither: a
|
|
4639
|
+
* resident repeating a close that failed before its focus ended, in a turn
|
|
4640
|
+
* that has since opened another server's channel with the id, would
|
|
4641
|
+
* otherwise close that channel and leave the focus in force. That close is
|
|
4642
|
+
* refused, naming both (`refusal`, focusOrOwnRefusal), and a close naming
|
|
4643
|
+
* either's server reaches it. A turn's name for the hold's channel itself,
|
|
4644
|
+
* and a name the turn's channels on more than one server answer to
|
|
4645
|
+
* (ownNeitherRefusal), resolve as any other.
|
|
4646
|
+
*
|
|
4647
|
+
* Undefined where no hold is in force, where the name isn't the hold
|
|
4648
|
+
* channel's, and where the hold's record can't be read, which leaves the
|
|
4649
|
+
* name to resolve as any other (resolveToolChannelEntry).
|
|
4650
|
+
*
|
|
4651
|
+
* Another channel the name reaches that has a tune-out of its own is
|
|
4652
|
+
* refused by a close until that tune-out is cancelled (tunedOutRefusal),
|
|
4653
|
+
* so its sentence says it is tuned out, and names the cancel first, then
|
|
4654
|
+
* the close by its id and server.
|
|
4655
|
+
*/
|
|
4656
|
+
focusNamedBare(name, serverId, own) {
|
|
4657
|
+
if (serverId)
|
|
4658
|
+
return undefined;
|
|
4659
|
+
const mine = this.ownChannelNamed(name, own);
|
|
4660
|
+
if (mine && 'neither' in mine)
|
|
4661
|
+
return undefined;
|
|
4662
|
+
let focus;
|
|
4663
|
+
try {
|
|
4664
|
+
focus = this.focusChannelResolver?.();
|
|
4665
|
+
}
|
|
4666
|
+
catch {
|
|
4667
|
+
return undefined;
|
|
4668
|
+
}
|
|
4669
|
+
if (!focus || (name !== focus.channelId && name !== qualifiedChannelName(focus)))
|
|
4670
|
+
return undefined;
|
|
4671
|
+
if (mine)
|
|
4672
|
+
return sameChannel(mine.channel, focus) ? undefined : { refusal: this.focusOrOwnRefusal(name, focus, mine.channel, own) };
|
|
4673
|
+
const held = focus;
|
|
4674
|
+
const others = this.entriesNamed(name).filter((entry) => !sameChannel(refOf(entry), held));
|
|
4675
|
+
const tunedOut = others.filter((entry) => this.getDesiredState(entry.serverId, entry.descriptor.id) === 'tuned-out');
|
|
4676
|
+
const closable = others.filter((entry) => !tunedOut.includes(entry)).map(refOf);
|
|
4677
|
+
const sentences = [
|
|
4678
|
+
...(closable.length > 0 ? [notClosedText(closable)] : []),
|
|
4679
|
+
...tunedOut.map((entry) => {
|
|
4680
|
+
const ref = refOf(entry);
|
|
4681
|
+
return `Channel ${channelOnServer(ref)} was not closed: it is tuned out, and a close doesn't change that. ` +
|
|
4682
|
+
this.tuneOutEndText(entry, 'channel_close', `channelId ${quotedValue(ref.channelId)} and serverId ${quotedValue(ref.serverId)}`);
|
|
4683
|
+
}),
|
|
4684
|
+
];
|
|
4685
|
+
return { focus: held, ...(sentences.length > 0 ? { notClosed: sentences.join(' ') } : {}) };
|
|
4686
|
+
}
|
|
4687
|
+
/**
|
|
4688
|
+
* The refusal of a channel_close's bare name (focusNamedBare) that both the
|
|
4689
|
+
* channel the all-except hold excepts (`focus`) and another channel of the
|
|
4690
|
+
* calling turn's own (`mine`, ownChannelNamed) answer to, each named by
|
|
4691
|
+
* what reaches it alone. The hold's channel is reached by its
|
|
4692
|
+
* server-qualified name, registered or not (focusNamedBare), unless a
|
|
4693
|
+
* channel of the turn's answers to that too; the turn's channel as a
|
|
4694
|
+
* refusal of the turn's names shows it (ownCandidates), unless another
|
|
4695
|
+
* channel of the turn's has its qualified name for its id. A serverId goes
|
|
4696
|
+
* with the channel's own id, so it's offered only where the name is the id
|
|
4697
|
+
* of both, for the hold's server (registered or not, the close reaches its
|
|
4698
|
+
* channel: unregisteredFocusNamed) and for the turn's channel's where that
|
|
4699
|
+
* server still registers it. Where the name is the hold channel's
|
|
4700
|
+
* server-qualified name, which another of the turn's channels has for its
|
|
4701
|
+
* id, the hold's channel is reached by its id and server.
|
|
4702
|
+
*/
|
|
4703
|
+
focusOrOwnRefusal(name, focus, mine, own) {
|
|
4704
|
+
const qualified = qualifiedChannelName(focus);
|
|
4705
|
+
const focusReaches = this.ownChannelNamed(qualified, own) === undefined;
|
|
4706
|
+
const [theirs] = this.ownCandidates([mine]);
|
|
4707
|
+
// The turn's channel's name, as the focus's: a bare id answers to the
|
|
4708
|
+
// turn's own channels first, so one of them with that id catches it.
|
|
4709
|
+
const theirsReaches = theirs.reaches && this.ownChannelNamed(theirs.text, own) === undefined;
|
|
4710
|
+
const parts = [
|
|
4711
|
+
{ text: focusReaches ? quotedValue(qualified) : channelOnServer(focus), reaches: focusReaches },
|
|
4712
|
+
{ text: theirsReaches ? quotedValue(theirs.text) : theirs.reaches ? channelOnServer(mine) : theirs.text, reaches: theirsReaches },
|
|
4713
|
+
];
|
|
4714
|
+
const named = parts.filter((p) => p.reaches).map((p) => p.text);
|
|
4715
|
+
const byName = named.length === 2 ? 'channelId set to one of them' : named.length === 1 ? `channelId ${named[0]}` : '';
|
|
4716
|
+
const servers = name === focus.channelId ? [focus.serverId, ...(this.entryAt(mine) ? [mine.serverId] : [])] : [];
|
|
4717
|
+
const byServer = servers.length > 0 ? `serverId set to ${servers.map((id) => JSON.stringify(id)).join(' or ')}` : '';
|
|
4718
|
+
const how = [byName, byServer].filter((h) => h !== '').join(', or with ');
|
|
4719
|
+
const focusRoute = name !== focus.channelId && !focusReaches
|
|
4720
|
+
? ` For the channel the focus excepts, call again with channelId ${quotedValue(focus.channelId)} and serverId ${JSON.stringify(focus.serverId)}.`
|
|
4721
|
+
: '';
|
|
4722
|
+
return `Channel ${JSON.stringify(name)} names both the channel the all-except focus excepts (${parts[0].text}) ` +
|
|
4723
|
+
`and this turn's channel (${parts[1].text}), so nothing ran.${how ? ` Call again with ${how}.` : ''}${focusRoute}`;
|
|
4724
|
+
}
|
|
4725
|
+
/**
|
|
4726
|
+
* The channel the all-except hold excepts (focusChannelResolver), where a
|
|
4727
|
+
* channel tool's name with a serverId, or the calling turn's own channel's
|
|
4728
|
+
* name, means it though its server no longer registers it: with a
|
|
4729
|
+
* serverId, that server's channel with the id; inside the calling turn,
|
|
4730
|
+
* the turn's own channel the name answers to by its id (ownChannelNamed).
|
|
4731
|
+
* A name given alone is focusNamedBare's. Undefined where no hold is in
|
|
4732
|
+
* force, where its channel is still registered, and where the hold's
|
|
4733
|
+
* record can't be read, which leaves the name's refusal as it was.
|
|
4734
|
+
*/
|
|
4735
|
+
unregisteredFocusNamed(name, serverId, own) {
|
|
4736
|
+
let focus;
|
|
4737
|
+
try {
|
|
4738
|
+
focus = this.focusChannelResolver?.();
|
|
4739
|
+
}
|
|
4740
|
+
catch {
|
|
4741
|
+
return undefined;
|
|
4742
|
+
}
|
|
4743
|
+
if (!focus || this.entryAt(focus))
|
|
4744
|
+
return undefined;
|
|
4745
|
+
if (serverId)
|
|
4746
|
+
return sameChannel(focus, { serverId, channelId: name }) ? focus : undefined;
|
|
4747
|
+
const mine = this.ownChannelNamed(name, own);
|
|
4748
|
+
return mine && 'channel' in mine && sameChannel(mine.channel, focus) ? focus : undefined;
|
|
4749
|
+
}
|
|
4460
4750
|
async handleToolDecline(input, own) {
|
|
4461
4751
|
const resolved = this.resolveToolChannelEntry(input.channelId, input.serverId, own);
|
|
4462
4752
|
const entry = resolved.entry;
|
|
@@ -4651,8 +4941,8 @@ export class ChannelRegistry {
|
|
|
4651
4941
|
`diverted, and then call channel_close.${serverBack}`
|
|
4652
4942
|
: `. If you still want it closed, call channel_close.${serverBack}`;
|
|
4653
4943
|
closeError = decided === 'tuned-out'
|
|
4654
|
-
? `The decline is recorded, but the channel was tuned out meanwhile, and that decision stands: its messages
|
|
4655
|
-
|
|
4944
|
+
? `The decline is recorded, but the channel was tuned out meanwhile, and that decision stands: its messages ` +
|
|
4945
|
+
`${this.tuneOutHeld()}${step}`
|
|
4656
4946
|
: `The decline is recorded, but a later decision to open the channel replaced it, and that decision stands${step}`;
|
|
4657
4947
|
}
|
|
4658
4948
|
else if (closed) {
|
|
@@ -4886,7 +5176,8 @@ export class ChannelRegistry {
|
|
|
4886
5176
|
return CapabilityGrant.of(server).has('channels.streaming') ? server : null;
|
|
4887
5177
|
}
|
|
4888
5178
|
/** Resolve the outbound locus (fork HOME → this-turn's TRIGGERING channel →
|
|
4889
|
-
* process-global default
|
|
5179
|
+
* process-global default, with the configured unprompted channel in it
|
|
5180
|
+
* where the turn takes it), with its server. Public so a multi-segment
|
|
4890
5181
|
* caller can snapshot it ONCE and pin every segment to it via
|
|
4891
5182
|
* routeSpeech's locus. */
|
|
4892
5183
|
resolveLocus(conversationId) {
|
|
@@ -4895,7 +5186,32 @@ export class ChannelRegistry {
|
|
|
4895
5186
|
return home;
|
|
4896
5187
|
// A turn that deliberately has no channel (null) takes no fallback.
|
|
4897
5188
|
const active = this.activeChannelResolver?.(conversationId);
|
|
4898
|
-
return active !== undefined
|
|
5189
|
+
return active !== undefined
|
|
5190
|
+
? active
|
|
5191
|
+
: this.globalFallbackFor(conversationId, () => this.configuredUnprompted(conversationId));
|
|
5192
|
+
}
|
|
5193
|
+
configuredUnprompted(conversationId) {
|
|
5194
|
+
if (!this.unpromptedChannel)
|
|
5195
|
+
return null;
|
|
5196
|
+
if (conversationId !== undefined && this.takesUnpromptedChannel?.(conversationId) === false)
|
|
5197
|
+
return null;
|
|
5198
|
+
const named = this.entriesNamed(this.unpromptedChannel);
|
|
5199
|
+
if (named.length === 1 && !isInboundOnly(named[0])) {
|
|
5200
|
+
this.unpromptedChannelWarned = false;
|
|
5201
|
+
return refOf(named[0]);
|
|
5202
|
+
}
|
|
5203
|
+
if (!this.unpromptedChannelWarned) {
|
|
5204
|
+
this.unpromptedChannelWarned = true;
|
|
5205
|
+
const why = named.length === 0
|
|
5206
|
+
? 'names no registered channel'
|
|
5207
|
+
: named.length === 1
|
|
5208
|
+
? 'names a channel its server declared inbound-only'
|
|
5209
|
+
: `names ${named.length} registered channels (name one with its server, as "serverId:channelId")`;
|
|
5210
|
+
console.error(`[routing] unpromptedChannel ${logValue(this.unpromptedChannel)} ${why}, so a turn the agent's own timer starts `
|
|
5211
|
+
+ 'takes the fallback it takes without the setting until the setting names a channel it can speak in. '
|
|
5212
|
+
+ 'Not logged again until a turn has used it.');
|
|
5213
|
+
}
|
|
5214
|
+
return null;
|
|
4899
5215
|
}
|
|
4900
5216
|
/**
|
|
4901
5217
|
* The process-global fallback for this conversation, or null when it takes
|
|
@@ -4903,18 +5219,20 @@ export class ChannelRegistry {
|
|
|
4903
5219
|
* result, so neither the all-except hold's channel nor the latest inbound
|
|
4904
5220
|
* message's is ever its destination.
|
|
4905
5221
|
*/
|
|
4906
|
-
globalFallbackFor(conversationId) {
|
|
4907
|
-
return this.takesGlobalFallback?.(conversationId) === false ? null : this.sharedFallback();
|
|
5222
|
+
globalFallbackFor(conversationId, unprompted) {
|
|
5223
|
+
return this.takesGlobalFallback?.(conversationId) === false ? null : this.sharedFallback(unprompted);
|
|
4908
5224
|
}
|
|
4909
5225
|
/**
|
|
4910
5226
|
* What every conversation that takes the global fallback falls back to when
|
|
4911
5227
|
* neither a home channel nor its own delivery names one: the all-except
|
|
4912
|
-
* hold's channel while one is open, else
|
|
4913
|
-
* channel
|
|
4914
|
-
*
|
|
5228
|
+
* hold's channel while one is open, else, for a turn's locus, the
|
|
5229
|
+
* configured unprompted channel where the turn takes it (resolveLocus),
|
|
5230
|
+
* else the latest delivered inbound channel under the legacy locus rule,
|
|
5231
|
+
* else none — the prose stays private (architecture channel §3 carve-out,
|
|
5232
|
+
* 2026-10-06).
|
|
4915
5233
|
*/
|
|
4916
|
-
sharedFallback() {
|
|
4917
|
-
return this.focusChannelResolver?.() ?? this.fallbackLocus();
|
|
5234
|
+
sharedFallback(unprompted) {
|
|
5235
|
+
return this.focusChannelResolver?.() ?? unprompted?.() ?? this.fallbackLocus();
|
|
4918
5236
|
}
|
|
4919
5237
|
fallbackLocus() {
|
|
4920
5238
|
return this.latestInboundLocus ? this.defaultPublishChannel : null;
|
|
@@ -5048,12 +5366,12 @@ export class ChannelRegistry {
|
|
|
5048
5366
|
if (isInboundOnly(entry)) {
|
|
5049
5367
|
return { status: 'failed', destination, reason: inboundOnlyReason(entry), at: at() };
|
|
5050
5368
|
}
|
|
5051
|
-
const server = this.
|
|
5369
|
+
const server = this.publishingServer(entry.serverId);
|
|
5052
5370
|
// §14.1: channels/publish requires channels.publish in the grant. The
|
|
5053
5371
|
// host not sending is the enforcement, here and at the last check, and
|
|
5054
5372
|
// the server's state words the refusal (sendRefusal).
|
|
5055
5373
|
const ungranted = () => ({ status: 'failed', destination, reason: this.sendRefusal(entry.serverId), at: at() });
|
|
5056
|
-
if (!server
|
|
5374
|
+
if (!server)
|
|
5057
5375
|
return ungranted();
|
|
5058
5376
|
const prevented = (err) => ({
|
|
5059
5377
|
status: 'failed', destination, reason: lifecycleClause(err) + refusalRemedy(err), at: at(),
|
|
@@ -5215,11 +5533,14 @@ export class ChannelRegistry {
|
|
|
5215
5533
|
locus,
|
|
5216
5534
|
/** `heldOnFailure`: the caller holds a `failed` delivery's words as a
|
|
5217
5535
|
* draft with its own notice, so no failure marker is written for it.
|
|
5218
|
-
* An `unknown` outcome still gets its marker.
|
|
5536
|
+
* An `unknown` outcome still gets its marker. `cameFrom`: with no
|
|
5537
|
+
* locus, the channels the turn's batch came from, each with how the
|
|
5538
|
+
* agent is shown it; the failure's reason names them, and how to reply
|
|
5539
|
+
* in those registered that take speech. */
|
|
5219
5540
|
opts) {
|
|
5220
5541
|
// `logged` is what the log line and the trace say, where it isn't the
|
|
5221
|
-
// agent's reason (the
|
|
5222
|
-
const fail = (channel, reason, outcome, logged = reason) => {
|
|
5542
|
+
// agent's reason (the three no-locus reasons below).
|
|
5543
|
+
const fail = (channel, reason, outcome, logged = reason, byDesign = false) => {
|
|
5223
5544
|
const status = outcome.status === 'unknown' ? 'unknown' : 'failed';
|
|
5224
5545
|
const channelId = channel?.channelId ?? null;
|
|
5225
5546
|
const detail = outcome.detail !== undefined ? connectorText(outcome.detail) : undefined;
|
|
@@ -5239,16 +5560,18 @@ export class ChannelRegistry {
|
|
|
5239
5560
|
this.onRouteFailure?.({
|
|
5240
5561
|
conversationId, channelId, ...(channel ? { serverId: channel.serverId } : {}),
|
|
5241
5562
|
reason, textLen: text.length, outcome: status, ...(detail !== undefined ? { detail } : {}),
|
|
5563
|
+
...(byDesign ? { byDesign: true } : {}),
|
|
5242
5564
|
});
|
|
5243
5565
|
}
|
|
5244
5566
|
return outcome;
|
|
5245
5567
|
};
|
|
5246
|
-
// These
|
|
5568
|
+
// These three reasons are the only ones whose log line and trace keep the
|
|
5247
5569
|
// operator's words rather than the agent's: connectome-host's
|
|
5248
5570
|
// connectome-doctor reads the last [routeSpeech] line for "no locus". Keep
|
|
5249
|
-
// "no locus" in
|
|
5571
|
+
// "no locus" in each logged text. Each names no tool: whether one the
|
|
5250
5572
|
// agent may call could post the words isn't known here, and the notice
|
|
5251
|
-
// calls it a routing or configuration situation
|
|
5573
|
+
// calls it a routing or configuration situation, or for a turn whose
|
|
5574
|
+
// batch named the channels it came from a routing one (byDesign).
|
|
5252
5575
|
// Runtime backstop for JS callers the compiler can't see: an omitted
|
|
5253
5576
|
// locus is a routing bug at the call site, never something to paper over
|
|
5254
5577
|
// with a live re-resolution.
|
|
@@ -5267,6 +5590,34 @@ export class ChannelRegistry {
|
|
|
5267
5590
|
// global inbound ever seen) — the agent was told its prose stays in
|
|
5268
5591
|
// the archive; honor that.
|
|
5269
5592
|
// The locus froze at the turn's start, so this says what the turn had.
|
|
5593
|
+
const from = opts?.cameFrom ?? [];
|
|
5594
|
+
if (from.length > 0) {
|
|
5595
|
+
const list = (names) => names.length === 1 ? names[0] : `${names.slice(0, -1).join(', ')} and ${names.at(-1)}`;
|
|
5596
|
+
const closed = [];
|
|
5597
|
+
const open = [];
|
|
5598
|
+
for (const came of from) {
|
|
5599
|
+
const entry = this.entryAt(came.channel);
|
|
5600
|
+
if (!entry)
|
|
5601
|
+
closed.push(`${came.shown} isn't registered with this host now, so this host has no channel there to send to`);
|
|
5602
|
+
else if (isInboundOnly(entry))
|
|
5603
|
+
closed.push(`${came.shown} is ${inboundOnlyReason(entry)}`);
|
|
5604
|
+
else if (!this.publishingServer(entry.serverId)) {
|
|
5605
|
+
const reach = this.serverReach(entry.serverId);
|
|
5606
|
+
closed.push(reach === 'connecting' || reach === 'down'
|
|
5607
|
+
? `${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`
|
|
5608
|
+
: `${came.shown} can't take a send: ${this.sendRefusal(entry.serverId)}`);
|
|
5609
|
+
}
|
|
5610
|
+
else
|
|
5611
|
+
open.push(came);
|
|
5612
|
+
}
|
|
5613
|
+
const reply = open.length === 0 ? ''
|
|
5614
|
+
: open.length === from.length
|
|
5615
|
+
? `; to reply ${from.length === 1 ? 'there' : 'in one of them'}, send your reply to that channel explicitly`
|
|
5616
|
+
: `; to reply in ${open.length === 1 ? open[0].shown : `one of ${list(open.map((c) => c.shown))}`}, send your reply to that channel explicitly`;
|
|
5617
|
+
const reason = `this turn came from ${list(from.map((c) => c.shown))}, and no channel was chosen for its reply` +
|
|
5618
|
+
`${reply}${closed.map((c) => `; ${c}`).join('')}`;
|
|
5619
|
+
return fail(null, reason, { status: 'failed', reason, at: Date.now() }, "turn has no locus (no channel was chosen for its batch's reply)", true);
|
|
5620
|
+
}
|
|
5270
5621
|
const reason = 'this turn came from no channel, and it had no home or recent channel to reply in; if it should have ' +
|
|
5271
5622
|
'had one, ask whoever runs this host';
|
|
5272
5623
|
return fail(null, reason, { status: 'failed', reason, at: Date.now() }, 'turn has no locus (no home/trigger channel; nothing to deliver into)');
|
|
@@ -5293,8 +5644,8 @@ export class ChannelRegistry {
|
|
|
5293
5644
|
return outcome;
|
|
5294
5645
|
}
|
|
5295
5646
|
/** deliverSpeech, reduced to the confirmed delivery (null when there was none). */
|
|
5296
|
-
async routeSpeech(conversationId, text, locus) {
|
|
5297
|
-
const outcome = await this.deliverSpeech(conversationId, text, locus);
|
|
5647
|
+
async routeSpeech(conversationId, text, locus, opts) {
|
|
5648
|
+
const outcome = await this.deliverSpeech(conversationId, text, locus, opts);
|
|
5298
5649
|
if (outcome.status !== 'delivered')
|
|
5299
5650
|
return null;
|
|
5300
5651
|
return {
|
|
@@ -5358,8 +5709,13 @@ export class ChannelRegistry {
|
|
|
5358
5709
|
? 'Nothing was posted: name the channel with channelId. A subagent has no default channel, since its plain '
|
|
5359
5710
|
+ 'speech is returned to whoever called it, so channel_publish sends only to a channel it names.'
|
|
5360
5711
|
: caller !== undefined && inTurn && this.activeChannelResolver?.(caller) === null
|
|
5361
|
-
?
|
|
5362
|
-
|
|
5712
|
+
? (this.channellessTurn && !this.channellessTurn(caller)
|
|
5713
|
+
// The turn came from a channel, but its speech goes to none
|
|
5714
|
+
// (a locked turn held as drafts).
|
|
5715
|
+
? `Nothing was posted: name the channel with channelId. Your plain speech goes to no channel now, so `
|
|
5716
|
+
+ `channel_publish sends only to a channel it names. ${findIds}`
|
|
5717
|
+
: `Nothing was posted: name the channel with channelId. This turn has no channel of its own, so its plain speech `
|
|
5718
|
+
+ `goes to none and channel_publish sends only to a channel it names. ${findIds}`)
|
|
5363
5719
|
: `Nothing was posted: channel_publish needs a \`channelId\`, and there's no channel to default to yet. ${findIds}`,
|
|
5364
5720
|
isError: true,
|
|
5365
5721
|
};
|
|
@@ -5453,8 +5809,8 @@ export class ChannelRegistry {
|
|
|
5453
5809
|
}
|
|
5454
5810
|
// The same refusal, in the same words, as plain speech's (sendRefusal),
|
|
5455
5811
|
// whose server state is the reason's: nothing awaits between the two.
|
|
5456
|
-
const server = this.
|
|
5457
|
-
if (!server
|
|
5812
|
+
const server = this.publishingServer(entry.serverId);
|
|
5813
|
+
if (!server) {
|
|
5458
5814
|
if (seen) {
|
|
5459
5815
|
const reach = this.serverReach(entry.serverId);
|
|
5460
5816
|
seen.refusal = reach === 'up' ? 'not-permitted' : `server-${reach}`;
|