@animalabs/agent-framework 0.22.2 → 0.23.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 +108 -4
- package/dist/src/agent.d.ts +108 -17
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +207 -67
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server-core.js +1 -1
- 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 +5 -1
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/api/types.d.ts +1 -1
- package/dist/src/api/types.d.ts.map +1 -1
- package/dist/src/branch-names.d.ts +14 -0
- package/dist/src/branch-names.d.ts.map +1 -0
- package/dist/src/branch-names.js +22 -0
- package/dist/src/branch-names.js.map +1 -0
- package/dist/src/closest-names.d.ts +28 -0
- package/dist/src/closest-names.d.ts.map +1 -0
- package/dist/src/closest-names.js +98 -0
- package/dist/src/closest-names.js.map +1 -0
- package/dist/src/code-execution/index.d.ts +1 -1
- package/dist/src/code-execution/index.d.ts.map +1 -1
- package/dist/src/code-execution/index.js.map +1 -1
- package/dist/src/code-execution/py-runner.d.ts +50 -7
- package/dist/src/code-execution/py-runner.d.ts.map +1 -1
- package/dist/src/code-execution/py-runner.js +258 -90
- package/dist/src/code-execution/py-runner.js.map +1 -1
- package/dist/src/code-execution/runtime-py.d.ts +3 -2
- package/dist/src/code-execution/runtime-py.d.ts.map +1 -1
- package/dist/src/code-execution/runtime-py.js +94 -8
- package/dist/src/code-execution/runtime-py.js.map +1 -1
- package/dist/src/code-execution/tool-definition.d.ts +17 -1
- package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
- package/dist/src/code-execution/tool-definition.js +72 -6
- package/dist/src/code-execution/tool-definition.js.map +1 -1
- package/dist/src/config-checks.d.ts +28 -0
- package/dist/src/config-checks.d.ts.map +1 -0
- package/dist/src/config-checks.js +56 -0
- package/dist/src/config-checks.js.map +1 -0
- package/dist/src/deadline-timer.d.ts +21 -0
- package/dist/src/deadline-timer.d.ts.map +1 -0
- package/dist/src/deadline-timer.js +43 -0
- package/dist/src/deadline-timer.js.map +1 -0
- package/dist/src/error-message.d.ts +30 -0
- package/dist/src/error-message.d.ts.map +1 -0
- package/dist/src/error-message.js +50 -0
- package/dist/src/error-message.js.map +1 -0
- package/dist/src/framework.d.ts +1529 -165
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +8264 -1939
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +8 -0
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +36 -5
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/held-writes.d.ts +16 -0
- package/dist/src/held-writes.d.ts.map +1 -0
- package/dist/src/held-writes.js +56 -0
- package/dist/src/held-writes.js.map +1 -0
- package/dist/src/index.d.ts +10 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +11 -5
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/capability-grant.d.ts +4 -0
- package/dist/src/mcpl/capability-grant.d.ts.map +1 -1
- package/dist/src/mcpl/capability-grant.js +1 -1
- package/dist/src/mcpl/capability-grant.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +233 -18
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +975 -386
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.d.ts +21 -1
- package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.js +91 -30
- package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts +19 -3
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +23 -8
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/convert-block.d.ts +20 -0
- package/dist/src/mcpl/convert-block.d.ts.map +1 -0
- package/dist/src/mcpl/convert-block.js +88 -0
- package/dist/src/mcpl/convert-block.js.map +1 -0
- package/dist/src/mcpl/errors.d.ts +13 -0
- package/dist/src/mcpl/errors.d.ts.map +1 -1
- package/dist/src/mcpl/errors.js +28 -0
- package/dist/src/mcpl/errors.js.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts +23 -0
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.js +83 -72
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +114 -0
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -0
- package/dist/src/mcpl/inbound-source.js +83 -0
- package/dist/src/mcpl/inbound-source.js.map +1 -0
- package/dist/src/mcpl/index.d.ts +7 -1
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js +5 -1
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/inference-router.d.ts +19 -13
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js +155 -87
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/log-value.d.ts +7 -0
- package/dist/src/mcpl/log-value.d.ts.map +1 -1
- package/dist/src/mcpl/log-value.js +9 -0
- package/dist/src/mcpl/log-value.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts +227 -0
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -0
- package/dist/src/mcpl/modern-connection.js +1088 -0
- package/dist/src/mcpl/modern-connection.js.map +1 -0
- package/dist/src/mcpl/prefix-rule.d.ts +18 -0
- package/dist/src/mcpl/prefix-rule.d.ts.map +1 -0
- package/dist/src/mcpl/prefix-rule.js +25 -0
- package/dist/src/mcpl/prefix-rule.js.map +1 -0
- package/dist/src/mcpl/protocol-family.d.ts +179 -0
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -0
- package/dist/src/mcpl/protocol-family.js +697 -0
- package/dist/src/mcpl/protocol-family.js.map +1 -0
- package/dist/src/mcpl/push-coalescer.d.ts +58 -17
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +93 -32
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts +16 -9
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +18 -83
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/reference-fetcher.d.ts +76 -13
- package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
- package/dist/src/mcpl/reference-fetcher.js +275 -66
- package/dist/src/mcpl/reference-fetcher.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +98 -18
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +170 -32
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +406 -31
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +1331 -272
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/server-registry.d.ts +35 -6
- package/dist/src/mcpl/server-registry.d.ts.map +1 -1
- package/dist/src/mcpl/server-registry.js +63 -13
- package/dist/src/mcpl/server-registry.js.map +1 -1
- package/dist/src/mcpl/skip-reply-wake.d.ts +36 -0
- package/dist/src/mcpl/skip-reply-wake.d.ts.map +1 -0
- package/dist/src/mcpl/skip-reply-wake.js +55 -0
- package/dist/src/mcpl/skip-reply-wake.js.map +1 -0
- package/dist/src/mcpl/tool-result-normalize.d.ts +54 -0
- package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -0
- package/dist/src/mcpl/tool-result-normalize.js +232 -0
- package/dist/src/mcpl/tool-result-normalize.js.map +1 -0
- package/dist/src/mcpl/transport.d.ts +104 -12
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +315 -85
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +76 -19
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/mcpl/visible-content.d.ts +16 -0
- package/dist/src/mcpl/visible-content.d.ts.map +1 -1
- package/dist/src/mcpl/visible-content.js +26 -0
- package/dist/src/mcpl/visible-content.js.map +1 -1
- package/dist/src/module-registry.d.ts +34 -6
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +119 -31
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/api/index.d.ts +1 -1
- package/dist/src/modules/api/index.d.ts.map +1 -1
- package/dist/src/modules/api/index.js +3 -2
- package/dist/src/modules/api/index.js.map +1 -1
- package/dist/src/modules/discord/discord-js-client.d.ts +43 -2
- package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
- package/dist/src/modules/discord/discord-js-client.js +286 -67
- package/dist/src/modules/discord/discord-js-client.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts +53 -5
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +712 -186
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/discord/refusal.d.ts +14 -0
- package/dist/src/modules/discord/refusal.d.ts.map +1 -0
- package/dist/src/modules/discord/refusal.js +14 -0
- package/dist/src/modules/discord/refusal.js.map +1 -0
- package/dist/src/modules/discord/types.d.ts +27 -4
- package/dist/src/modules/discord/types.d.ts.map +1 -1
- package/dist/src/modules/health/index.d.ts.map +1 -1
- package/dist/src/modules/health/index.js +8 -3
- package/dist/src/modules/health/index.js.map +1 -1
- package/dist/src/modules/history/index.d.ts +3 -1
- package/dist/src/modules/history/index.d.ts.map +1 -1
- package/dist/src/modules/history/index.js +359 -127
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/history/semantic.d.ts +28 -1
- package/dist/src/modules/history/semantic.d.ts.map +1 -1
- package/dist/src/modules/history/semantic.js +177 -32
- package/dist/src/modules/history/semantic.js.map +1 -1
- package/dist/src/modules/index.d.ts +1 -1
- package/dist/src/modules/index.d.ts.map +1 -1
- package/dist/src/modules/index.js +1 -1
- package/dist/src/modules/index.js.map +1 -1
- package/dist/src/modules/unknown-tool.d.ts +10 -0
- package/dist/src/modules/unknown-tool.d.ts.map +1 -0
- package/dist/src/modules/unknown-tool.js +17 -0
- package/dist/src/modules/unknown-tool.js.map +1 -0
- package/dist/src/modules/workspace/disk-agreement.d.ts +57 -4
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +75 -7
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/effects.d.ts +66 -7
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +303 -46
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +313 -45
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +1404 -304
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +315 -16
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +727 -37
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +58 -7
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +521 -56
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +28 -5
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts +49 -2
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +158 -29
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/prose-drafts.d.ts +240 -0
- package/dist/src/prose-drafts.d.ts.map +1 -0
- package/dist/src/prose-drafts.js +362 -0
- package/dist/src/prose-drafts.js.map +1 -0
- package/dist/src/prose-segments.d.ts +21 -2
- package/dist/src/prose-segments.d.ts.map +1 -1
- package/dist/src/prose-segments.js +46 -9
- package/dist/src/prose-segments.js.map +1 -1
- package/dist/src/recovery/offline-branch.d.ts +1 -0
- package/dist/src/recovery/offline-branch.d.ts.map +1 -1
- package/dist/src/recovery/offline-branch.js +3 -1
- package/dist/src/recovery/offline-branch.js.map +1 -1
- package/dist/src/safe-slice.d.ts +5 -1
- package/dist/src/safe-slice.d.ts.map +1 -1
- package/dist/src/safe-slice.js +9 -1
- package/dist/src/safe-slice.js.map +1 -1
- package/dist/src/silent-heartbeat.d.ts +112 -39
- package/dist/src/silent-heartbeat.d.ts.map +1 -1
- package/dist/src/silent-heartbeat.js +100 -29
- package/dist/src/silent-heartbeat.js.map +1 -1
- package/dist/src/tool-args.d.ts +40 -0
- package/dist/src/tool-args.d.ts.map +1 -0
- package/dist/src/tool-args.js +207 -0
- package/dist/src/tool-args.js.map +1 -0
- package/dist/src/tool-image-ledger.d.ts +87 -19
- package/dist/src/tool-image-ledger.d.ts.map +1 -1
- package/dist/src/tool-image-ledger.js +135 -32
- package/dist/src/tool-image-ledger.js.map +1 -1
- package/dist/src/tool-presentation.d.ts +9 -0
- package/dist/src/tool-presentation.d.ts.map +1 -1
- package/dist/src/tool-presentation.js +266 -62
- package/dist/src/tool-presentation.js.map +1 -1
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +7 -10
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tool-result-history.d.ts +32 -3
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +106 -19
- package/dist/src/tool-result-history.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +25 -1
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +260 -65
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/types/agent.d.ts +44 -22
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +25 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +38 -7
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/inference-log.d.ts +1 -1
- package/dist/src/types/inference-log.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +77 -14
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +97 -8
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/src/usage/usage-tracker.d.ts +5 -0
- package/dist/src/usage/usage-tracker.d.ts.map +1 -1
- package/dist/src/usage/usage-tracker.js +35 -8
- package/dist/src/usage/usage-tracker.js.map +1 -1
- package/package.json +5 -4
- package/src/agent.ts +257 -72
- package/src/api/mcp-server-core.ts +1 -1
- package/src/api/server.ts +4 -1
- package/src/api/types.ts +1 -1
- package/src/branch-names.ts +19 -0
- package/src/closest-names.ts +94 -0
- package/src/code-execution/index.ts +1 -0
- package/src/code-execution/py-runner.ts +304 -93
- package/src/code-execution/runtime-py.ts +94 -8
- package/src/code-execution/tool-definition.ts +85 -7
- package/src/config-checks.ts +50 -0
- package/src/deadline-timer.ts +44 -0
- package/src/error-message.ts +48 -0
- package/src/framework.ts +8625 -1984
- package/src/gate/event-gate.ts +38 -5
- package/src/held-writes.ts +59 -0
- package/src/index.ts +29 -5
- package/src/mcpl/capability-grant.ts +1 -1
- package/src/mcpl/channel-registry.ts +1095 -411
- package/src/mcpl/checkpoint-manager.ts +104 -29
- package/src/mcpl/conversation-router.ts +28 -8
- package/src/mcpl/convert-block.ts +105 -0
- package/src/mcpl/errors.ts +28 -0
- package/src/mcpl/hook-orchestrator.ts +84 -77
- package/src/mcpl/inbound-source.ts +156 -0
- package/src/mcpl/index.ts +23 -1
- package/src/mcpl/inference-router.ts +179 -103
- package/src/mcpl/log-value.ts +10 -0
- package/src/mcpl/modern-connection.ts +1182 -0
- package/src/mcpl/prefix-rule.ts +32 -0
- package/src/mcpl/protocol-family.ts +691 -0
- package/src/mcpl/push-coalescer.ts +110 -36
- package/src/mcpl/push-handler.ts +20 -90
- package/src/mcpl/reference-fetcher.ts +318 -67
- package/src/mcpl/references.ts +183 -34
- package/src/mcpl/server-connection.ts +1404 -280
- package/src/mcpl/server-registry.ts +63 -14
- package/src/mcpl/skip-reply-wake.ts +73 -0
- package/src/mcpl/tool-result-normalize.ts +294 -0
- package/src/mcpl/transport.ts +320 -76
- package/src/mcpl/types.ts +77 -19
- package/src/mcpl/visible-content.ts +28 -0
- package/src/module-registry.ts +126 -36
- package/src/modules/api/index.ts +3 -2
- package/src/modules/discord/discord-js-client.ts +306 -74
- package/src/modules/discord/index.ts +737 -201
- package/src/modules/discord/refusal.ts +13 -0
- package/src/modules/discord/types.ts +27 -6
- package/src/modules/health/index.ts +8 -3
- package/src/modules/history/index.ts +410 -120
- package/src/modules/history/semantic.ts +189 -32
- package/src/modules/index.ts +1 -1
- package/src/modules/unknown-tool.ts +16 -0
- package/src/modules/workspace/disk-agreement.ts +97 -8
- package/src/modules/workspace/effects.ts +291 -36
- package/src/modules/workspace/index.ts +1469 -321
- package/src/modules/workspace/observe.ts +784 -41
- package/src/modules/workspace/reconcile.ts +619 -64
- package/src/modules/workspace/types.ts +28 -5
- package/src/modules/workspace/watcher.ts +154 -30
- package/src/prose-drafts.ts +481 -0
- package/src/prose-segments.ts +51 -8
- package/src/recovery/offline-branch.ts +4 -1
- package/src/safe-slice.ts +9 -1
- package/src/silent-heartbeat.ts +184 -59
- package/src/tool-args.ts +213 -0
- package/src/tool-image-ledger.ts +153 -34
- package/src/tool-presentation.ts +233 -42
- package/src/tool-result-guard.ts +7 -8
- package/src/tool-result-history.ts +124 -20
- package/src/tune-out/coordinator.ts +259 -65
- package/src/types/agent.ts +44 -24
- package/src/types/events.ts +25 -0
- package/src/types/framework.ts +38 -7
- package/src/types/inference-log.ts +1 -1
- package/src/types/module.ts +79 -14
- package/src/types/trace.ts +103 -8
- package/src/usage/usage-tracker.ts +32 -8
- package/dist/test/activation-request-sanitize.test.d.ts +0 -2
- package/dist/test/activation-request-sanitize.test.d.ts.map +0 -1
- package/dist/test/activation-request-sanitize.test.js +0 -89
- package/dist/test/activation-request-sanitize.test.js.map +0 -1
- package/dist/test/agent-cache-ttl.test.d.ts +0 -2
- package/dist/test/agent-cache-ttl.test.d.ts.map +0 -1
- package/dist/test/agent-cache-ttl.test.js +0 -41
- package/dist/test/agent-cache-ttl.test.js.map +0 -1
- package/dist/test/agent-runtime-settings.test.d.ts +0 -2
- package/dist/test/agent-runtime-settings.test.d.ts.map +0 -1
- package/dist/test/agent-runtime-settings.test.js +0 -1598
- package/dist/test/agent-runtime-settings.test.js.map +0 -1
- package/dist/test/api-admin-gate.test.d.ts +0 -2
- package/dist/test/api-admin-gate.test.d.ts.map +0 -1
- package/dist/test/api-admin-gate.test.js +0 -116
- package/dist/test/api-admin-gate.test.js.map +0 -1
- package/dist/test/assistant-persist-refusal.test.d.ts +0 -2
- package/dist/test/assistant-persist-refusal.test.d.ts.map +0 -1
- package/dist/test/assistant-persist-refusal.test.js +0 -63
- package/dist/test/assistant-persist-refusal.test.js.map +0 -1
- package/dist/test/batched-wake-locus.test.d.ts +0 -2
- package/dist/test/batched-wake-locus.test.d.ts.map +0 -1
- package/dist/test/batched-wake-locus.test.js +0 -166
- package/dist/test/batched-wake-locus.test.js.map +0 -1
- package/dist/test/busy-agent-scheduler.test.d.ts +0 -2
- package/dist/test/busy-agent-scheduler.test.d.ts.map +0 -1
- package/dist/test/busy-agent-scheduler.test.js +0 -92
- package/dist/test/busy-agent-scheduler.test.js.map +0 -1
- package/dist/test/channel-incoming-targeting.test.d.ts +0 -2
- package/dist/test/channel-incoming-targeting.test.d.ts.map +0 -1
- package/dist/test/channel-incoming-targeting.test.js +0 -81
- package/dist/test/channel-incoming-targeting.test.js.map +0 -1
- package/dist/test/channel-label-history.test.d.ts +0 -2
- package/dist/test/channel-label-history.test.d.ts.map +0 -1
- package/dist/test/channel-label-history.test.js +0 -424
- package/dist/test/channel-label-history.test.js.map +0 -1
- package/dist/test/channel-lifecycle-provenance.test.d.ts +0 -2
- package/dist/test/channel-lifecycle-provenance.test.d.ts.map +0 -1
- package/dist/test/channel-lifecycle-provenance.test.js +0 -47
- package/dist/test/channel-lifecycle-provenance.test.js.map +0 -1
- package/dist/test/channel-registry-routing.test.d.ts +0 -2
- package/dist/test/channel-registry-routing.test.d.ts.map +0 -1
- package/dist/test/channel-registry-routing.test.js +0 -446
- package/dist/test/channel-registry-routing.test.js.map +0 -1
- package/dist/test/channel-registry-typing.test.d.ts +0 -2
- package/dist/test/channel-registry-typing.test.d.ts.map +0 -1
- package/dist/test/channel-registry-typing.test.js +0 -91
- package/dist/test/channel-registry-typing.test.js.map +0 -1
- package/dist/test/channels-changed-respond-first.test.d.ts +0 -2
- package/dist/test/channels-changed-respond-first.test.d.ts.map +0 -1
- package/dist/test/channels-changed-respond-first.test.js +0 -74
- package/dist/test/channels-changed-respond-first.test.js.map +0 -1
- package/dist/test/checkpoint-attribution.test.d.ts +0 -2
- package/dist/test/checkpoint-attribution.test.d.ts.map +0 -1
- package/dist/test/checkpoint-attribution.test.js +0 -200
- package/dist/test/checkpoint-attribution.test.js.map +0 -1
- package/dist/test/code-execution.test.d.ts +0 -11
- package/dist/test/code-execution.test.d.ts.map +0 -1
- package/dist/test/code-execution.test.js +0 -1272
- package/dist/test/code-execution.test.js.map +0 -1
- package/dist/test/component-defaults.test.d.ts +0 -2
- package/dist/test/component-defaults.test.d.ts.map +0 -1
- package/dist/test/component-defaults.test.js +0 -54
- package/dist/test/component-defaults.test.js.map +0 -1
- package/dist/test/conversation-fork-injection-scoping.test.d.ts +0 -2
- package/dist/test/conversation-fork-injection-scoping.test.d.ts.map +0 -1
- package/dist/test/conversation-fork-injection-scoping.test.js +0 -200
- package/dist/test/conversation-fork-injection-scoping.test.js.map +0 -1
- package/dist/test/conversation-router.test.d.ts +0 -2
- package/dist/test/conversation-router.test.d.ts.map +0 -1
- package/dist/test/conversation-router.test.js +0 -174
- package/dist/test/conversation-router.test.js.map +0 -1
- package/dist/test/conversation-routing.test.d.ts +0 -7
- package/dist/test/conversation-routing.test.d.ts.map +0 -1
- package/dist/test/conversation-routing.test.js +0 -621
- package/dist/test/conversation-routing.test.js.map +0 -1
- package/dist/test/deferred-wake-integrity.test.d.ts +0 -29
- package/dist/test/deferred-wake-integrity.test.d.ts.map +0 -1
- package/dist/test/deferred-wake-integrity.test.js +0 -320
- package/dist/test/deferred-wake-integrity.test.js.map +0 -1
- package/dist/test/discord-awareness-outbox.test.d.ts +0 -2
- package/dist/test/discord-awareness-outbox.test.d.ts.map +0 -1
- package/dist/test/discord-awareness-outbox.test.js +0 -371
- package/dist/test/discord-awareness-outbox.test.js.map +0 -1
- package/dist/test/discord-history-delete-window.test.d.ts +0 -2
- package/dist/test/discord-history-delete-window.test.d.ts.map +0 -1
- package/dist/test/discord-history-delete-window.test.js +0 -81
- package/dist/test/discord-history-delete-window.test.js.map +0 -1
- package/dist/test/discord-history-sync.test.d.ts +0 -2
- package/dist/test/discord-history-sync.test.d.ts.map +0 -1
- package/dist/test/discord-history-sync.test.js +0 -56
- package/dist/test/discord-history-sync.test.js.map +0 -1
- package/dist/test/discord-typing-keepalive.test.d.ts +0 -2
- package/dist/test/discord-typing-keepalive.test.d.ts.map +0 -1
- package/dist/test/discord-typing-keepalive.test.js +0 -77
- package/dist/test/discord-typing-keepalive.test.js.map +0 -1
- package/dist/test/ephemeral-settle.test.d.ts +0 -2
- package/dist/test/ephemeral-settle.test.d.ts.map +0 -1
- package/dist/test/ephemeral-settle.test.js +0 -354
- package/dist/test/ephemeral-settle.test.js.map +0 -1
- package/dist/test/event-gate-passthrough.test.d.ts +0 -15
- package/dist/test/event-gate-passthrough.test.d.ts.map +0 -1
- package/dist/test/event-gate-passthrough.test.js +0 -289
- package/dist/test/event-gate-passthrough.test.js.map +0 -1
- package/dist/test/event-gate-policy-mutation.test.d.ts +0 -2
- package/dist/test/event-gate-policy-mutation.test.d.ts.map +0 -1
- package/dist/test/event-gate-policy-mutation.test.js +0 -137
- package/dist/test/event-gate-policy-mutation.test.js.map +0 -1
- package/dist/test/event-gate-rate-limit.test.d.ts +0 -2
- package/dist/test/event-gate-rate-limit.test.d.ts.map +0 -1
- package/dist/test/event-gate-rate-limit.test.js +0 -627
- package/dist/test/event-gate-rate-limit.test.js.map +0 -1
- package/dist/test/event-gate-script.test.d.ts +0 -2
- package/dist/test/event-gate-script.test.d.ts.map +0 -1
- package/dist/test/event-gate-script.test.js +0 -95
- package/dist/test/event-gate-script.test.js.map +0 -1
- package/dist/test/event-gate-selfwake.test.d.ts +0 -15
- package/dist/test/event-gate-selfwake.test.d.ts.map +0 -1
- package/dist/test/event-gate-selfwake.test.js +0 -125
- package/dist/test/event-gate-selfwake.test.js.map +0 -1
- package/dist/test/event-gate-tags.test.d.ts +0 -2
- package/dist/test/event-gate-tags.test.d.ts.map +0 -1
- package/dist/test/event-gate-tags.test.js +0 -76
- package/dist/test/event-gate-tags.test.js.map +0 -1
- package/dist/test/event-gate.test.d.ts +0 -2
- package/dist/test/event-gate.test.d.ts.map +0 -1
- package/dist/test/event-gate.test.js +0 -919
- package/dist/test/event-gate.test.js.map +0 -1
- package/dist/test/explicit-prose-routing.test.d.ts +0 -11
- package/dist/test/explicit-prose-routing.test.d.ts.map +0 -1
- package/dist/test/explicit-prose-routing.test.js +0 -1715
- package/dist/test/explicit-prose-routing.test.js.map +0 -1
- package/dist/test/framework.test.d.ts +0 -5
- package/dist/test/framework.test.d.ts.map +0 -1
- package/dist/test/framework.test.js +0 -1048
- package/dist/test/framework.test.js.map +0 -1
- package/dist/test/gather-context-timeout.test.d.ts +0 -2
- package/dist/test/gather-context-timeout.test.d.ts.map +0 -1
- package/dist/test/gather-context-timeout.test.js +0 -120
- package/dist/test/gather-context-timeout.test.js.map +0 -1
- package/dist/test/health-snapshot.test.d.ts +0 -2
- package/dist/test/health-snapshot.test.d.ts.map +0 -1
- package/dist/test/health-snapshot.test.js +0 -74
- package/dist/test/health-snapshot.test.js.map +0 -1
- package/dist/test/helpers/coalescing-fixture.d.ts +0 -74
- package/dist/test/helpers/coalescing-fixture.d.ts.map +0 -1
- package/dist/test/helpers/coalescing-fixture.js +0 -152
- package/dist/test/helpers/coalescing-fixture.js.map +0 -1
- package/dist/test/helpers/coalescing-kill-child.d.ts +0 -2
- package/dist/test/helpers/coalescing-kill-child.d.ts.map +0 -1
- package/dist/test/helpers/coalescing-kill-child.js +0 -24
- package/dist/test/helpers/coalescing-kill-child.js.map +0 -1
- package/dist/test/helpers/mock-membrane.d.ts +0 -53
- package/dist/test/helpers/mock-membrane.d.ts.map +0 -1
- package/dist/test/helpers/mock-membrane.js +0 -168
- package/dist/test/helpers/mock-membrane.js.map +0 -1
- package/dist/test/helpers/pytc-stop-rows.d.ts +0 -11
- package/dist/test/helpers/pytc-stop-rows.d.ts.map +0 -1
- package/dist/test/helpers/pytc-stop-rows.js +0 -143
- package/dist/test/helpers/pytc-stop-rows.js.map +0 -1
- package/dist/test/helpers/quiesce-crash-child.d.ts +0 -2
- package/dist/test/helpers/quiesce-crash-child.d.ts.map +0 -1
- package/dist/test/helpers/quiesce-crash-child.js +0 -156
- package/dist/test/helpers/quiesce-crash-child.js.map +0 -1
- package/dist/test/hide-then-turn.test.d.ts +0 -2
- package/dist/test/hide-then-turn.test.d.ts.map +0 -1
- package/dist/test/hide-then-turn.test.js +0 -101
- package/dist/test/hide-then-turn.test.js.map +0 -1
- package/dist/test/history-module-overview.test.d.ts +0 -2
- package/dist/test/history-module-overview.test.d.ts.map +0 -1
- package/dist/test/history-module-overview.test.js +0 -711
- package/dist/test/history-module-overview.test.js.map +0 -1
- package/dist/test/history-module-realstore.test.d.ts +0 -2
- package/dist/test/history-module-realstore.test.d.ts.map +0 -1
- package/dist/test/history-module-realstore.test.js +0 -363
- package/dist/test/history-module-realstore.test.js.map +0 -1
- package/dist/test/history-module-semantic.test.d.ts +0 -2
- package/dist/test/history-module-semantic.test.d.ts.map +0 -1
- package/dist/test/history-module-semantic.test.js +0 -213
- package/dist/test/history-module-semantic.test.js.map +0 -1
- package/dist/test/history-module-ux.test.d.ts +0 -2
- package/dist/test/history-module-ux.test.d.ts.map +0 -1
- package/dist/test/history-module-ux.test.js +0 -304
- package/dist/test/history-module-ux.test.js.map +0 -1
- package/dist/test/history-module.test.d.ts +0 -2
- package/dist/test/history-module.test.d.ts.map +0 -1
- package/dist/test/history-module.test.js +0 -619
- package/dist/test/history-module.test.js.map +0 -1
- package/dist/test/history-semantic-review-findings.test.d.ts +0 -2
- package/dist/test/history-semantic-review-findings.test.d.ts.map +0 -1
- package/dist/test/history-semantic-review-findings.test.js +0 -338
- package/dist/test/history-semantic-review-findings.test.js.map +0 -1
- package/dist/test/history-semantic-sync-converges.test.d.ts +0 -2
- package/dist/test/history-semantic-sync-converges.test.d.ts.map +0 -1
- package/dist/test/history-semantic-sync-converges.test.js +0 -109
- package/dist/test/history-semantic-sync-converges.test.js.map +0 -1
- package/dist/test/host-quiesce.test.d.ts +0 -2
- package/dist/test/host-quiesce.test.d.ts.map +0 -1
- package/dist/test/host-quiesce.test.js +0 -988
- package/dist/test/host-quiesce.test.js.map +0 -1
- package/dist/test/inference-exhaustion-rewind.test.d.ts +0 -2
- package/dist/test/inference-exhaustion-rewind.test.d.ts.map +0 -1
- package/dist/test/inference-exhaustion-rewind.test.js +0 -387
- package/dist/test/inference-exhaustion-rewind.test.js.map +0 -1
- package/dist/test/inference-failure-bounds.test.d.ts +0 -2
- package/dist/test/inference-failure-bounds.test.d.ts.map +0 -1
- package/dist/test/inference-failure-bounds.test.js +0 -1202
- package/dist/test/inference-failure-bounds.test.js.map +0 -1
- package/dist/test/inference-failure-observability.test.d.ts +0 -2
- package/dist/test/inference-failure-observability.test.d.ts.map +0 -1
- package/dist/test/inference-failure-observability.test.js +0 -84
- package/dist/test/inference-failure-observability.test.js.map +0 -1
- package/dist/test/journal-tool.test.d.ts +0 -2
- package/dist/test/journal-tool.test.d.ts.map +0 -1
- package/dist/test/journal-tool.test.js +0 -33
- package/dist/test/journal-tool.test.js.map +0 -1
- package/dist/test/kv-unified-open-flight-retry.test.d.ts +0 -2
- package/dist/test/kv-unified-open-flight-retry.test.d.ts.map +0 -1
- package/dist/test/kv-unified-open-flight-retry.test.js +0 -189
- package/dist/test/kv-unified-open-flight-retry.test.js.map +0 -1
- package/dist/test/kv-unified-wiring.test.d.ts +0 -2
- package/dist/test/kv-unified-wiring.test.d.ts.map +0 -1
- package/dist/test/kv-unified-wiring.test.js +0 -95
- package/dist/test/kv-unified-wiring.test.js.map +0 -1
- package/dist/test/late-events-after-stop.test.d.ts +0 -2
- package/dist/test/late-events-after-stop.test.d.ts.map +0 -1
- package/dist/test/late-events-after-stop.test.js +0 -122
- package/dist/test/late-events-after-stop.test.js.map +0 -1
- package/dist/test/liveness-watchdog.test.d.ts +0 -2
- package/dist/test/liveness-watchdog.test.d.ts.map +0 -1
- package/dist/test/liveness-watchdog.test.js +0 -94
- package/dist/test/liveness-watchdog.test.js.map +0 -1
- package/dist/test/machine-close-provenance.test.d.ts +0 -2
- package/dist/test/machine-close-provenance.test.d.ts.map +0 -1
- package/dist/test/machine-close-provenance.test.js +0 -116
- package/dist/test/machine-close-provenance.test.js.map +0 -1
- package/dist/test/maintenance-timer.test.d.ts +0 -2
- package/dist/test/maintenance-timer.test.d.ts.map +0 -1
- package/dist/test/maintenance-timer.test.js +0 -1827
- package/dist/test/maintenance-timer.test.js.map +0 -1
- package/dist/test/mcp-server-bin.test.d.ts +0 -2
- package/dist/test/mcp-server-bin.test.d.ts.map +0 -1
- package/dist/test/mcp-server-bin.test.js +0 -142
- package/dist/test/mcp-server-bin.test.js.map +0 -1
- package/dist/test/mcpl-access-provider.test.d.ts +0 -2
- package/dist/test/mcpl-access-provider.test.d.ts.map +0 -1
- package/dist/test/mcpl-access-provider.test.js +0 -20
- package/dist/test/mcpl-access-provider.test.js.map +0 -1
- package/dist/test/mcpl-awareness-barrier.test.d.ts +0 -2
- package/dist/test/mcpl-awareness-barrier.test.d.ts.map +0 -1
- package/dist/test/mcpl-awareness-barrier.test.js +0 -769
- package/dist/test/mcpl-awareness-barrier.test.js.map +0 -1
- package/dist/test/mcpl-capability-grant.test.d.ts +0 -10
- package/dist/test/mcpl-capability-grant.test.d.ts.map +0 -1
- package/dist/test/mcpl-capability-grant.test.js +0 -111
- package/dist/test/mcpl-capability-grant.test.js.map +0 -1
- package/dist/test/mcpl-capability-scoping.test.d.ts +0 -2
- package/dist/test/mcpl-capability-scoping.test.d.ts.map +0 -1
- package/dist/test/mcpl-capability-scoping.test.js +0 -266
- package/dist/test/mcpl-capability-scoping.test.js.map +0 -1
- package/dist/test/mcpl-child-env.test.d.ts +0 -2
- package/dist/test/mcpl-child-env.test.d.ts.map +0 -1
- package/dist/test/mcpl-child-env.test.js +0 -158
- package/dist/test/mcpl-child-env.test.js.map +0 -1
- package/dist/test/mcpl-coalesced-content-parity.test.d.ts +0 -2
- package/dist/test/mcpl-coalesced-content-parity.test.d.ts.map +0 -1
- package/dist/test/mcpl-coalesced-content-parity.test.js +0 -532
- package/dist/test/mcpl-coalesced-content-parity.test.js.map +0 -1
- package/dist/test/mcpl-coalescing.test.d.ts +0 -2
- package/dist/test/mcpl-coalescing.test.d.ts.map +0 -1
- package/dist/test/mcpl-coalescing.test.js +0 -1125
- package/dist/test/mcpl-coalescing.test.js.map +0 -1
- package/dist/test/mcpl-config-diagnostics.test.d.ts +0 -2
- package/dist/test/mcpl-config-diagnostics.test.d.ts.map +0 -1
- package/dist/test/mcpl-config-diagnostics.test.js +0 -452
- package/dist/test/mcpl-config-diagnostics.test.js.map +0 -1
- package/dist/test/mcpl-empty-content.test.d.ts +0 -2
- package/dist/test/mcpl-empty-content.test.d.ts.map +0 -1
- package/dist/test/mcpl-empty-content.test.js +0 -368
- package/dist/test/mcpl-empty-content.test.js.map +0 -1
- package/dist/test/mcpl-gate-roundtrip.test.d.ts +0 -2
- package/dist/test/mcpl-gate-roundtrip.test.d.ts.map +0 -1
- package/dist/test/mcpl-gate-roundtrip.test.js +0 -285
- package/dist/test/mcpl-gate-roundtrip.test.js.map +0 -1
- package/dist/test/mcpl-list-status.test.d.ts +0 -2
- package/dist/test/mcpl-list-status.test.d.ts.map +0 -1
- package/dist/test/mcpl-list-status.test.js +0 -82
- package/dist/test/mcpl-list-status.test.js.map +0 -1
- package/dist/test/mcpl-manifest-changed.test.d.ts +0 -2
- package/dist/test/mcpl-manifest-changed.test.d.ts.map +0 -1
- package/dist/test/mcpl-manifest-changed.test.js +0 -178
- package/dist/test/mcpl-manifest-changed.test.js.map +0 -1
- package/dist/test/mcpl-push-convert.test.d.ts +0 -2
- package/dist/test/mcpl-push-convert.test.d.ts.map +0 -1
- package/dist/test/mcpl-push-convert.test.js +0 -42
- package/dist/test/mcpl-push-convert.test.js.map +0 -1
- package/dist/test/mcpl-reconnect-events.test.d.ts +0 -2
- package/dist/test/mcpl-reconnect-events.test.d.ts.map +0 -1
- package/dist/test/mcpl-reconnect-events.test.js +0 -144
- package/dist/test/mcpl-reconnect-events.test.js.map +0 -1
- package/dist/test/mcpl-reconnect-reregistration.test.d.ts +0 -2
- package/dist/test/mcpl-reconnect-reregistration.test.d.ts.map +0 -1
- package/dist/test/mcpl-reconnect-reregistration.test.js +0 -238
- package/dist/test/mcpl-reconnect-reregistration.test.js.map +0 -1
- package/dist/test/mcpl-reference-fetcher.test.d.ts +0 -2
- package/dist/test/mcpl-reference-fetcher.test.d.ts.map +0 -1
- package/dist/test/mcpl-reference-fetcher.test.js +0 -427
- package/dist/test/mcpl-reference-fetcher.test.js.map +0 -1
- package/dist/test/mcpl-reference-fields.test.d.ts +0 -2
- package/dist/test/mcpl-reference-fields.test.d.ts.map +0 -1
- package/dist/test/mcpl-reference-fields.test.js +0 -149
- package/dist/test/mcpl-reference-fields.test.js.map +0 -1
- package/dist/test/mcpl-rejection-log-lines.test.d.ts +0 -2
- package/dist/test/mcpl-rejection-log-lines.test.d.ts.map +0 -1
- package/dist/test/mcpl-rejection-log-lines.test.js +0 -349
- package/dist/test/mcpl-rejection-log-lines.test.js.map +0 -1
- package/dist/test/mcpl-request-timeout.test.d.ts +0 -2
- package/dist/test/mcpl-request-timeout.test.d.ts.map +0 -1
- package/dist/test/mcpl-request-timeout.test.js +0 -138
- package/dist/test/mcpl-request-timeout.test.js.map +0 -1
- package/dist/test/mcpl-runtime-lifecycle.test.d.ts +0 -7
- package/dist/test/mcpl-runtime-lifecycle.test.d.ts.map +0 -1
- package/dist/test/mcpl-runtime-lifecycle.test.js +0 -142
- package/dist/test/mcpl-runtime-lifecycle.test.js.map +0 -1
- package/dist/test/mcpl-stderr-tail.test.d.ts +0 -2
- package/dist/test/mcpl-stderr-tail.test.d.ts.map +0 -1
- package/dist/test/mcpl-stderr-tail.test.js +0 -210
- package/dist/test/mcpl-stderr-tail.test.js.map +0 -1
- package/dist/test/mcpl-tool-class-listing.test.d.ts +0 -2
- package/dist/test/mcpl-tool-class-listing.test.d.ts.map +0 -1
- package/dist/test/mcpl-tool-class-listing.test.js +0 -147
- package/dist/test/mcpl-tool-class-listing.test.js.map +0 -1
- package/dist/test/mcpl-tool-classes.test.d.ts +0 -7
- package/dist/test/mcpl-tool-classes.test.d.ts.map +0 -1
- package/dist/test/mcpl-tool-classes.test.js +0 -80
- package/dist/test/mcpl-tool-classes.test.js.map +0 -1
- package/dist/test/mcpl-tool-lifecycle-e2e.test.d.ts +0 -2
- package/dist/test/mcpl-tool-lifecycle-e2e.test.d.ts.map +0 -1
- package/dist/test/mcpl-tool-lifecycle-e2e.test.js +0 -237
- package/dist/test/mcpl-tool-lifecycle-e2e.test.js.map +0 -1
- package/dist/test/mcpl-tool-lifecycle-emitter.test.d.ts +0 -11
- package/dist/test/mcpl-tool-lifecycle-emitter.test.d.ts.map +0 -1
- package/dist/test/mcpl-tool-lifecycle-emitter.test.js +0 -412
- package/dist/test/mcpl-tool-lifecycle-emitter.test.js.map +0 -1
- package/dist/test/mcpl-tool-lifecycle-script.test.d.ts +0 -2
- package/dist/test/mcpl-tool-lifecycle-script.test.d.ts.map +0 -1
- package/dist/test/mcpl-tool-lifecycle-script.test.js +0 -288
- package/dist/test/mcpl-tool-lifecycle-script.test.js.map +0 -1
- package/dist/test/mcpl-tool-lifecycle.test.d.ts +0 -11
- package/dist/test/mcpl-tool-lifecycle.test.d.ts.map +0 -1
- package/dist/test/mcpl-tool-lifecycle.test.js +0 -270
- package/dist/test/mcpl-tool-lifecycle.test.js.map +0 -1
- package/dist/test/mcpl-transport-close.test.d.ts +0 -2
- package/dist/test/mcpl-transport-close.test.d.ts.map +0 -1
- package/dist/test/mcpl-transport-close.test.js +0 -373
- package/dist/test/mcpl-transport-close.test.js.map +0 -1
- package/dist/test/mcpl-transport-spawn.test.d.ts +0 -2
- package/dist/test/mcpl-transport-spawn.test.d.ts.map +0 -1
- package/dist/test/mcpl-transport-spawn.test.js +0 -58
- package/dist/test/mcpl-transport-spawn.test.js.map +0 -1
- package/dist/test/mcpl-ws-transport.test.d.ts +0 -2
- package/dist/test/mcpl-ws-transport.test.d.ts.map +0 -1
- package/dist/test/mcpl-ws-transport.test.js +0 -259
- package/dist/test/mcpl-ws-transport.test.js.map +0 -1
- package/dist/test/module-context-integration.test.d.ts +0 -18
- package/dist/test/module-context-integration.test.d.ts.map +0 -1
- package/dist/test/module-context-integration.test.js +0 -181
- package/dist/test/module-context-integration.test.js.map +0 -1
- package/dist/test/offline-recovery-branch.test.d.ts +0 -2
- package/dist/test/offline-recovery-branch.test.d.ts.map +0 -1
- package/dist/test/offline-recovery-branch.test.js +0 -570
- package/dist/test/offline-recovery-branch.test.js.map +0 -1
- package/dist/test/operator-surgery.test.d.ts +0 -2
- package/dist/test/operator-surgery.test.d.ts.map +0 -1
- package/dist/test/operator-surgery.test.js +0 -595
- package/dist/test/operator-surgery.test.js.map +0 -1
- package/dist/test/ops-alert.test.d.ts +0 -2
- package/dist/test/ops-alert.test.d.ts.map +0 -1
- package/dist/test/ops-alert.test.js +0 -153
- package/dist/test/ops-alert.test.js.map +0 -1
- package/dist/test/overbudget-drain-breaker.test.d.ts +0 -2
- package/dist/test/overbudget-drain-breaker.test.d.ts.map +0 -1
- package/dist/test/overbudget-drain-breaker.test.js +0 -198
- package/dist/test/overbudget-drain-breaker.test.js.map +0 -1
- package/dist/test/pack-installs-alone.test.d.ts +0 -2
- package/dist/test/pack-installs-alone.test.d.ts.map +0 -1
- package/dist/test/pack-installs-alone.test.js +0 -284
- package/dist/test/pack-installs-alone.test.js.map +0 -1
- package/dist/test/per-agent-delivery.test.d.ts +0 -2
- package/dist/test/per-agent-delivery.test.d.ts.map +0 -1
- package/dist/test/per-agent-delivery.test.js +0 -108
- package/dist/test/per-agent-delivery.test.js.map +0 -1
- package/dist/test/physical-window-restart.test.d.ts +0 -16
- package/dist/test/physical-window-restart.test.d.ts.map +0 -1
- package/dist/test/physical-window-restart.test.js +0 -487
- package/dist/test/physical-window-restart.test.js.map +0 -1
- package/dist/test/placed-reaction-baseline.test.d.ts +0 -2
- package/dist/test/placed-reaction-baseline.test.d.ts.map +0 -1
- package/dist/test/placed-reaction-baseline.test.js +0 -312
- package/dist/test/placed-reaction-baseline.test.js.map +0 -1
- package/dist/test/present-while-acting.test.d.ts +0 -19
- package/dist/test/present-while-acting.test.d.ts.map +0 -1
- package/dist/test/present-while-acting.test.js +0 -1649
- package/dist/test/present-while-acting.test.js.map +0 -1
- package/dist/test/prose-segments.test.d.ts +0 -2
- package/dist/test/prose-segments.test.d.ts.map +0 -1
- package/dist/test/prose-segments.test.js +0 -52
- package/dist/test/prose-segments.test.js.map +0 -1
- package/dist/test/prose-stream-router.test.d.ts +0 -2
- package/dist/test/prose-stream-router.test.d.ts.map +0 -1
- package/dist/test/prose-stream-router.test.js +0 -171
- package/dist/test/prose-stream-router.test.js.map +0 -1
- package/dist/test/provider-acceleration-cooldown.test.d.ts +0 -2
- package/dist/test/provider-acceleration-cooldown.test.d.ts.map +0 -1
- package/dist/test/provider-acceleration-cooldown.test.js +0 -435
- package/dist/test/provider-acceleration-cooldown.test.js.map +0 -1
- package/dist/test/provider-waits.test.d.ts +0 -2
- package/dist/test/provider-waits.test.d.ts.map +0 -1
- package/dist/test/provider-waits.test.js +0 -2259
- package/dist/test/provider-waits.test.js.map +0 -1
- package/dist/test/puppet-tool-call.test.d.ts +0 -2
- package/dist/test/puppet-tool-call.test.d.ts.map +0 -1
- package/dist/test/puppet-tool-call.test.js +0 -226
- package/dist/test/puppet-tool-call.test.js.map +0 -1
- package/dist/test/puppet-turn-reservation.test.d.ts +0 -2
- package/dist/test/puppet-turn-reservation.test.d.ts.map +0 -1
- package/dist/test/puppet-turn-reservation.test.js +0 -139
- package/dist/test/puppet-turn-reservation.test.js.map +0 -1
- package/dist/test/reaction-baseline-platform.test.d.ts +0 -2
- package/dist/test/reaction-baseline-platform.test.d.ts.map +0 -1
- package/dist/test/reaction-baseline-platform.test.js +0 -124
- package/dist/test/reaction-baseline-platform.test.js.map +0 -1
- package/dist/test/record-journal.test.d.ts +0 -6
- package/dist/test/record-journal.test.d.ts.map +0 -1
- package/dist/test/record-journal.test.js +0 -269
- package/dist/test/record-journal.test.js.map +0 -1
- package/dist/test/refusal-reaction-baseline.test.d.ts +0 -2
- package/dist/test/refusal-reaction-baseline.test.d.ts.map +0 -1
- package/dist/test/refusal-reaction-baseline.test.js +0 -81
- package/dist/test/refusal-reaction-baseline.test.js.map +0 -1
- package/dist/test/refusal-rewind.test.d.ts +0 -2
- package/dist/test/refusal-rewind.test.d.ts.map +0 -1
- package/dist/test/refusal-rewind.test.js +0 -117
- package/dist/test/refusal-rewind.test.js.map +0 -1
- package/dist/test/refusal-single-call.test.d.ts +0 -2
- package/dist/test/refusal-single-call.test.d.ts.map +0 -1
- package/dist/test/refusal-single-call.test.js +0 -177
- package/dist/test/refusal-single-call.test.js.map +0 -1
- package/dist/test/release-bump.test.d.ts +0 -2
- package/dist/test/release-bump.test.d.ts.map +0 -1
- package/dist/test/release-bump.test.js +0 -248
- package/dist/test/release-bump.test.js.map +0 -1
- package/dist/test/release-changelog.test.d.ts +0 -2
- package/dist/test/release-changelog.test.d.ts.map +0 -1
- package/dist/test/release-changelog.test.js +0 -159
- package/dist/test/release-changelog.test.js.map +0 -1
- package/dist/test/release-holds.test.d.ts +0 -2
- package/dist/test/release-holds.test.d.ts.map +0 -1
- package/dist/test/release-holds.test.js +0 -456
- package/dist/test/release-holds.test.js.map +0 -1
- package/dist/test/same-round-think-policy.test.d.ts +0 -2
- package/dist/test/same-round-think-policy.test.d.ts.map +0 -1
- package/dist/test/same-round-think-policy.test.js +0 -853
- package/dist/test/same-round-think-policy.test.js.map +0 -1
- package/dist/test/save-recent-image.test.d.ts +0 -26
- package/dist/test/save-recent-image.test.d.ts.map +0 -1
- package/dist/test/save-recent-image.test.js +0 -974
- package/dist/test/save-recent-image.test.js.map +0 -1
- package/dist/test/shutdown-errors.test.d.ts +0 -2
- package/dist/test/shutdown-errors.test.d.ts.map +0 -1
- package/dist/test/shutdown-errors.test.js +0 -662
- package/dist/test/shutdown-errors.test.js.map +0 -1
- package/dist/test/silent-heartbeat-rows.test.d.ts +0 -2
- package/dist/test/silent-heartbeat-rows.test.d.ts.map +0 -1
- package/dist/test/silent-heartbeat-rows.test.js +0 -451
- package/dist/test/silent-heartbeat-rows.test.js.map +0 -1
- package/dist/test/silent-heartbeat.test.d.ts +0 -2
- package/dist/test/silent-heartbeat.test.d.ts.map +0 -1
- package/dist/test/silent-heartbeat.test.js +0 -165
- package/dist/test/silent-heartbeat.test.js.map +0 -1
- package/dist/test/state-scaling-e2e.test.d.ts +0 -22
- package/dist/test/state-scaling-e2e.test.d.ts.map +0 -1
- package/dist/test/state-scaling-e2e.test.js +0 -206
- package/dist/test/state-scaling-e2e.test.js.map +0 -1
- package/dist/test/state-scaling.test.d.ts +0 -12
- package/dist/test/state-scaling.test.d.ts.map +0 -1
- package/dist/test/state-scaling.test.js +0 -251
- package/dist/test/state-scaling.test.js.map +0 -1
- package/dist/test/stop-with-host-operations.test.d.ts +0 -20
- package/dist/test/stop-with-host-operations.test.d.ts.map +0 -1
- package/dist/test/stop-with-host-operations.test.js +0 -1174
- package/dist/test/stop-with-host-operations.test.js.map +0 -1
- package/dist/test/stop-with-work-in-flight.test.d.ts +0 -23
- package/dist/test/stop-with-work-in-flight.test.d.ts.map +0 -1
- package/dist/test/stop-with-work-in-flight.test.js +0 -784
- package/dist/test/stop-with-work-in-flight.test.js.map +0 -1
- package/dist/test/surgery-marks-receipt.test.d.ts +0 -2
- package/dist/test/surgery-marks-receipt.test.d.ts.map +0 -1
- package/dist/test/surgery-marks-receipt.test.js +0 -540
- package/dist/test/surgery-marks-receipt.test.js.map +0 -1
- package/dist/test/thinking-token-stamp.test.d.ts +0 -12
- package/dist/test/thinking-token-stamp.test.d.ts.map +0 -1
- package/dist/test/thinking-token-stamp.test.js +0 -129
- package/dist/test/thinking-token-stamp.test.js.map +0 -1
- package/dist/test/timezone.test.d.ts +0 -2
- package/dist/test/timezone.test.d.ts.map +0 -1
- package/dist/test/timezone.test.js +0 -18
- package/dist/test/timezone.test.js.map +0 -1
- package/dist/test/tool-batch-hook.test.d.ts +0 -8
- package/dist/test/tool-batch-hook.test.d.ts.map +0 -1
- package/dist/test/tool-batch-hook.test.js +0 -275
- package/dist/test/tool-batch-hook.test.js.map +0 -1
- package/dist/test/tool-policy.test.d.ts +0 -2
- package/dist/test/tool-policy.test.d.ts.map +0 -1
- package/dist/test/tool-policy.test.js +0 -51
- package/dist/test/tool-policy.test.js.map +0 -1
- package/dist/test/tool-presentation.test.d.ts +0 -2
- package/dist/test/tool-presentation.test.d.ts.map +0 -1
- package/dist/test/tool-presentation.test.js +0 -268
- package/dist/test/tool-presentation.test.js.map +0 -1
- package/dist/test/tool-result-content.test.d.ts +0 -2
- package/dist/test/tool-result-content.test.d.ts.map +0 -1
- package/dist/test/tool-result-content.test.js +0 -37
- package/dist/test/tool-result-content.test.js.map +0 -1
- package/dist/test/tool-result-guard.test.d.ts +0 -2
- package/dist/test/tool-result-guard.test.d.ts.map +0 -1
- package/dist/test/tool-result-guard.test.js +0 -3729
- package/dist/test/tool-result-guard.test.js.map +0 -1
- package/dist/test/tool-result-history.test.d.ts +0 -7
- package/dist/test/tool-result-history.test.d.ts.map +0 -1
- package/dist/test/tool-result-history.test.js +0 -156
- package/dist/test/tool-result-history.test.js.map +0 -1
- package/dist/test/tool-result-spill.test.d.ts +0 -13
- package/dist/test/tool-result-spill.test.d.ts.map +0 -1
- package/dist/test/tool-result-spill.test.js +0 -586
- package/dist/test/tool-result-spill.test.js.map +0 -1
- package/dist/test/tool-wrapper-prose-guard-integration.test.d.ts +0 -2
- package/dist/test/tool-wrapper-prose-guard-integration.test.d.ts.map +0 -1
- package/dist/test/tool-wrapper-prose-guard-integration.test.js +0 -485
- package/dist/test/tool-wrapper-prose-guard-integration.test.js.map +0 -1
- package/dist/test/tool-wrapper-prose-guard.test.d.ts +0 -2
- package/dist/test/tool-wrapper-prose-guard.test.d.ts.map +0 -1
- package/dist/test/tool-wrapper-prose-guard.test.js +0 -34
- package/dist/test/tool-wrapper-prose-guard.test.js.map +0 -1
- package/dist/test/tools-observe-parser-conformance.test.d.ts +0 -2
- package/dist/test/tools-observe-parser-conformance.test.d.ts.map +0 -1
- package/dist/test/tools-observe-parser-conformance.test.js +0 -129
- package/dist/test/tools-observe-parser-conformance.test.js.map +0 -1
- package/dist/test/trunk-channel-routing.test.d.ts +0 -19
- package/dist/test/trunk-channel-routing.test.d.ts.map +0 -1
- package/dist/test/trunk-channel-routing.test.js +0 -652
- package/dist/test/trunk-channel-routing.test.js.map +0 -1
- package/dist/test/tune-out-e2e.test.d.ts +0 -2
- package/dist/test/tune-out-e2e.test.d.ts.map +0 -1
- package/dist/test/tune-out-e2e.test.js +0 -142
- package/dist/test/tune-out-e2e.test.js.map +0 -1
- package/dist/test/tune-out-gate-composition.test.d.ts +0 -2
- package/dist/test/tune-out-gate-composition.test.d.ts.map +0 -1
- package/dist/test/tune-out-gate-composition.test.js +0 -221
- package/dist/test/tune-out-gate-composition.test.js.map +0 -1
- package/dist/test/tune-out-lifecycle.test.d.ts +0 -2
- package/dist/test/tune-out-lifecycle.test.d.ts.map +0 -1
- package/dist/test/tune-out-lifecycle.test.js +0 -121
- package/dist/test/tune-out-lifecycle.test.js.map +0 -1
- package/dist/test/turn-trigger-provenance.test.d.ts +0 -2
- package/dist/test/turn-trigger-provenance.test.d.ts.map +0 -1
- package/dist/test/turn-trigger-provenance.test.js +0 -137
- package/dist/test/turn-trigger-provenance.test.js.map +0 -1
- package/dist/test/unconvertible-tool-result.test.d.ts +0 -2
- package/dist/test/unconvertible-tool-result.test.d.ts.map +0 -1
- package/dist/test/unconvertible-tool-result.test.js +0 -358
- package/dist/test/unconvertible-tool-result.test.js.map +0 -1
- package/dist/test/unsent-compiles.test.d.ts +0 -2
- package/dist/test/unsent-compiles.test.d.ts.map +0 -1
- package/dist/test/unsent-compiles.test.js +0 -139
- package/dist/test/unsent-compiles.test.js.map +0 -1
- package/dist/test/usage-accounting.test.d.ts +0 -2
- package/dist/test/usage-accounting.test.d.ts.map +0 -1
- package/dist/test/usage-accounting.test.js +0 -13
- package/dist/test/usage-accounting.test.js.map +0 -1
- package/dist/test/usage-tracker.test.d.ts +0 -2
- package/dist/test/usage-tracker.test.d.ts.map +0 -1
- package/dist/test/usage-tracker.test.js +0 -129
- package/dist/test/usage-tracker.test.js.map +0 -1
- package/dist/test/utils-surface.test.d.ts +0 -2
- package/dist/test/utils-surface.test.d.ts.map +0 -1
- package/dist/test/utils-surface.test.js +0 -145
- package/dist/test/utils-surface.test.js.map +0 -1
- package/dist/test/workspace-branch-continuation.test.d.ts +0 -15
- package/dist/test/workspace-branch-continuation.test.d.ts.map +0 -1
- package/dist/test/workspace-branch-continuation.test.js +0 -183
- package/dist/test/workspace-branch-continuation.test.js.map +0 -1
- package/dist/test/workspace-materialize-freshness.test.d.ts +0 -16
- package/dist/test/workspace-materialize-freshness.test.d.ts.map +0 -1
- package/dist/test/workspace-materialize-freshness.test.js +0 -252
- package/dist/test/workspace-materialize-freshness.test.js.map +0 -1
- package/dist/test/workspace-path-containment.test.d.ts +0 -19
- package/dist/test/workspace-path-containment.test.d.ts.map +0 -1
- package/dist/test/workspace-path-containment.test.js +0 -91
- package/dist/test/workspace-path-containment.test.js.map +0 -1
- package/dist/test/workspace-read-file-from-disk.test.d.ts +0 -17
- package/dist/test/workspace-read-file-from-disk.test.d.ts.map +0 -1
- package/dist/test/workspace-read-file-from-disk.test.js +0 -174
- package/dist/test/workspace-read-file-from-disk.test.js.map +0 -1
- package/dist/test/workspace-read-image.test.d.ts +0 -2
- package/dist/test/workspace-read-image.test.d.ts.map +0 -1
- package/dist/test/workspace-read-image.test.js +0 -597
- package/dist/test/workspace-read-image.test.js.map +0 -1
- package/dist/test/workspace-read-text.test.d.ts +0 -2
- package/dist/test/workspace-read-text.test.d.ts.map +0 -1
- package/dist/test/workspace-read-text.test.js +0 -101
- package/dist/test/workspace-read-text.test.js.map +0 -1
- package/dist/test/workspace-restore-order.test.d.ts +0 -16
- package/dist/test/workspace-restore-order.test.d.ts.map +0 -1
- package/dist/test/workspace-restore-order.test.js +0 -140
- package/dist/test/workspace-restore-order.test.js.map +0 -1
- package/dist/test/workspace-three-way.test.d.ts +0 -12
- package/dist/test/workspace-three-way.test.d.ts.map +0 -1
- package/dist/test/workspace-three-way.test.js +0 -4094
- package/dist/test/workspace-three-way.test.js.map +0 -1
- package/dist/test/workspace-watcher.test.d.ts +0 -2
- package/dist/test/workspace-watcher.test.d.ts.map +0 -1
- package/dist/test/workspace-watcher.test.js +0 -281
- package/dist/test/workspace-watcher.test.js.map +0 -1
- package/dist/test/xml-round-persist.test.d.ts +0 -19
- package/dist/test/xml-round-persist.test.d.ts.map +0 -1
- package/dist/test/xml-round-persist.test.js +0 -235
- package/dist/test/xml-round-persist.test.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -14,8 +14,10 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import type { ContentBlock } from '@animalabs/membrane';
|
|
17
|
-
import {
|
|
17
|
+
import { convertBlock } from './convert-block.js';
|
|
18
18
|
import type { JsStore } from '@animalabs/chronicle';
|
|
19
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
20
|
+
import { ensureStateRegistered } from '../module-registry.js';
|
|
19
21
|
|
|
20
22
|
import type {
|
|
21
23
|
ChannelDescriptor,
|
|
@@ -30,7 +32,6 @@ import type {
|
|
|
30
32
|
ChannelsPublishParams,
|
|
31
33
|
ChannelsOpenResult,
|
|
32
34
|
ChannelHistoryRequest,
|
|
33
|
-
McplContentBlock,
|
|
34
35
|
} from './types.js';
|
|
35
36
|
|
|
36
37
|
import type { McplServerRegistry } from './server-registry.js';
|
|
@@ -38,18 +39,55 @@ import type { FeatureSetManager } from './feature-set-manager.js';
|
|
|
38
39
|
import type { ToolDefinition, ToolResult, ProcessEvent } from '../types/index.js';
|
|
39
40
|
import { expandCoreTags } from './tags.js';
|
|
40
41
|
import { coalesceRejectionReason, EmptyContentError, validateCoalescedContent } from './push-coalescer.js';
|
|
41
|
-
import {
|
|
42
|
+
import { isVisiblyEmptyConversion } from './visible-content.js';
|
|
42
43
|
import { logDetail, logValue } from './log-value.js';
|
|
43
44
|
import { RefusalLines } from './refusal-lines.js';
|
|
44
45
|
import { CapabilityGrant } from './capability-grant.js';
|
|
46
|
+
import type { InboundSource } from './inbound-source.js';
|
|
47
|
+
import { McplRequestError } from './server-connection.js';
|
|
48
|
+
import { readSkipReplyWake } from './skip-reply-wake.js';
|
|
45
49
|
|
|
46
50
|
// ============================================================================
|
|
47
51
|
// Typing indicator interval (Discord typing lasts ~10s, so 7s keeps it alive)
|
|
48
52
|
// ============================================================================
|
|
49
53
|
|
|
50
54
|
const TYPING_INTERVAL_MS = 7_000;
|
|
55
|
+
const MAX_CHANNEL_LIFECYCLE_ATTEMPTS = 5;
|
|
56
|
+
|
|
57
|
+
/** A successful RPC can still fail to establish the latest channel target. */
|
|
58
|
+
class ChannelLifecycleConvergenceError extends Error {}
|
|
59
|
+
/** A channels/open answered for a channel other than the one it named. */
|
|
60
|
+
class ChannelOpenSubstitutionError extends Error {}
|
|
61
|
+
/** An operation queued for a server's registrations that were removed. */
|
|
62
|
+
class ChannelLifecycleCancelledError extends Error {}
|
|
51
63
|
const CHANNEL_LIFECYCLE_LOG_ID = 'mcpl/channel-lifecycle';
|
|
52
64
|
|
|
65
|
+
/**
|
|
66
|
+
* What a channels/open answer says about which channel the server opened:
|
|
67
|
+
* its `channel.id`, or else the `channelId` of a history item. Each is
|
|
68
|
+
* evidence. A string other than the requested id names another channel (ids
|
|
69
|
+
* compare exactly, as the server's traffic is matched). Null or absence names
|
|
70
|
+
* no channel, and gives undefined, as does an answer that names none at all.
|
|
71
|
+
* Any other value is malformed: it names nothing the host could register.
|
|
72
|
+
*/
|
|
73
|
+
function substitutedChannel(
|
|
74
|
+
result: ChannelsOpenResult | undefined,
|
|
75
|
+
requested: string,
|
|
76
|
+
): { kind: 'other'; id: string } | { kind: 'malformed'; value: unknown } | undefined {
|
|
77
|
+
const named = (value: unknown) => {
|
|
78
|
+
if (value === undefined || value === null || value === requested) return undefined;
|
|
79
|
+
return typeof value === 'string' ? { kind: 'other' as const, id: value } : { kind: 'malformed' as const, value };
|
|
80
|
+
};
|
|
81
|
+
const answered = named(result?.channel?.id);
|
|
82
|
+
if (answered) return answered;
|
|
83
|
+
const history: unknown[] = Array.isArray(result?.history) ? result.history : [];
|
|
84
|
+
for (const item of history) {
|
|
85
|
+
const from = named((item as { channelId?: unknown } | null)?.channelId);
|
|
86
|
+
if (from) return from;
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
53
91
|
/**
|
|
54
92
|
* Durable "which label did this channelId have, each time we saw it" log.
|
|
55
93
|
* `resolveProseTarget()` resolves a label to an id only for channels
|
|
@@ -210,9 +248,14 @@ function shallowEqualRecord(
|
|
|
210
248
|
interface ChannelEntry {
|
|
211
249
|
serverId: string;
|
|
212
250
|
descriptor: ChannelDescriptor;
|
|
251
|
+
/** Whether the current target is confirmed open; false can be unconfirmed. */
|
|
213
252
|
open: boolean;
|
|
214
253
|
}
|
|
215
254
|
|
|
255
|
+
function sameChannelTarget(a: ChannelDescriptor, b: ChannelDescriptor): boolean {
|
|
256
|
+
return a.type === b.type && isDeepStrictEqual(a.address, b.address);
|
|
257
|
+
}
|
|
258
|
+
|
|
216
259
|
/** Minimal responder interface for sending JSON-RPC results back. */
|
|
217
260
|
interface Responder {
|
|
218
261
|
respond(result: unknown): void;
|
|
@@ -239,94 +282,30 @@ interface McplChannelIncomingEvent {
|
|
|
239
282
|
tags?: string[];
|
|
240
283
|
triggerInference?: boolean;
|
|
241
284
|
targetAgents?: string[];
|
|
285
|
+
/** Host acceptance time (epoch ms), stamped where the message is admitted. */
|
|
286
|
+
acceptedAt?: number;
|
|
287
|
+
/** Source envelope frozen at admission (mcpl/inbound-source.ts). */
|
|
288
|
+
inboundSource?: InboundSource;
|
|
242
289
|
}
|
|
243
290
|
|
|
244
291
|
// ============================================================================
|
|
245
|
-
//
|
|
292
|
+
// Channel Tool Definitions
|
|
246
293
|
// ============================================================================
|
|
247
294
|
|
|
248
295
|
/**
|
|
249
|
-
*
|
|
296
|
+
* `journal`'s description. It tells the model to call `skip_reply` to end the
|
|
297
|
+
* turn only where the agent may call it: the framework builds each agent's
|
|
298
|
+
* surface with that agent's permission, and a refused `skip_reply` leaves the
|
|
299
|
+
* turn going on.
|
|
250
300
|
*/
|
|
251
|
-
function
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
case 'image':
|
|
259
|
-
if (isInlineContradiction(block)) {
|
|
260
|
-
// RFC-005 vector 2: inline data claiming bulk disposition — fail
|
|
261
|
-
// closed, withhold the data (checked BEFORE the data branch).
|
|
262
|
-
return { type: 'text', text: INLINE_WITHHELD_TEXT };
|
|
263
|
-
}
|
|
264
|
-
if (isFilledString(block.uri) && block.disposition) {
|
|
265
|
-
// RFC-005 uri-form media with a disposition claim: stub, do not
|
|
266
|
-
// hand the URI to the provider or inline it.
|
|
267
|
-
return { type: 'text', text: referenceStubOrNull(block, 'attachment on channel message') ?? '[reference]' };
|
|
268
|
-
}
|
|
269
|
-
if (isFilledString(block.data) && isFilledString(block.mimeType)) {
|
|
270
|
-
return {
|
|
271
|
-
type: 'image',
|
|
272
|
-
source: { type: 'base64', data: block.data, mediaType: block.mimeType },
|
|
273
|
-
} as ContentBlock;
|
|
274
|
-
}
|
|
275
|
-
if (isFilledString(block.uri)) {
|
|
276
|
-
return {
|
|
277
|
-
type: 'image',
|
|
278
|
-
source: { type: 'url', url: block.uri },
|
|
279
|
-
} as ContentBlock;
|
|
280
|
-
}
|
|
281
|
-
return { type: 'text', text: isFilledString(block.data) ? '[Image: no media type]' : '[Image: no data]' };
|
|
282
|
-
|
|
283
|
-
case 'audio':
|
|
284
|
-
if (isInlineContradiction(block)) {
|
|
285
|
-
// RFC-005 vector 2: inline data claiming bulk disposition — fail
|
|
286
|
-
// closed, withhold the data (checked BEFORE the data branch).
|
|
287
|
-
return { type: 'text', text: INLINE_WITHHELD_TEXT };
|
|
288
|
-
}
|
|
289
|
-
if (isFilledString(block.uri) && block.disposition) {
|
|
290
|
-
// RFC-005 uri-form media with a disposition claim: stub, do not
|
|
291
|
-
// hand the URI to the provider or inline it.
|
|
292
|
-
return { type: 'text', text: referenceStubOrNull(block, 'attachment on channel message') ?? '[reference]' };
|
|
293
|
-
}
|
|
294
|
-
if (isFilledString(block.data) && isFilledString(block.mimeType)) {
|
|
295
|
-
return {
|
|
296
|
-
type: 'audio',
|
|
297
|
-
source: { type: 'base64', data: block.data, mediaType: block.mimeType },
|
|
298
|
-
} as ContentBlock;
|
|
299
|
-
}
|
|
300
|
-
return { type: 'text', text: isFilledString(block.data) ? '[Audio: no media type]' : '[Audio: no data]' };
|
|
301
|
-
|
|
302
|
-
case 'resource':
|
|
303
|
-
// RFC-005: reference blocks become bounded stubs — never raw URIs
|
|
304
|
-
// (a signed URL is a bearer credential that looks like a location).
|
|
305
|
-
return { type: 'text', text: referenceStubOrNull(block, 'attachment on channel message') ?? '[reference]' };
|
|
306
|
-
|
|
307
|
-
default: {
|
|
308
|
-
// Unknown wire block types previously fell off the exhaustive switch
|
|
309
|
-
// and propagated `undefined` into ContentBlock[]. Fail visibly. A type
|
|
310
|
-
// that isn't a string is no type, and is never converted to one: an
|
|
311
|
-
// object can throw when it is.
|
|
312
|
-
const type: unknown = (block as { type?: unknown }).type;
|
|
313
|
-
return { type: 'text', text: `[unrecognized content block: ${typeof type === 'string' ? type : 'untyped'}]` };
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/** A string with something in it: the only data, mimeType or uri a block's
|
|
319
|
-
* source is built from, and the only uri that makes it a reference. Anything
|
|
320
|
-
* else a sender puts there leaves the block without it, so conversion never
|
|
321
|
-
* throws and never copies a non-string. */
|
|
322
|
-
function isFilledString(value: unknown): value is string {
|
|
323
|
-
return typeof value === 'string' && value !== '';
|
|
301
|
+
export function journalToolDescription(canSkipReply: boolean): string {
|
|
302
|
+
return 'Write an entry in your private journal. The entry stays in your own context and ' +
|
|
303
|
+
'memory and is NOT sent to any channel, surface or person. Use it for anything longer ' +
|
|
304
|
+
'than a line that you want to keep for yourself — reflections, what you decided and ' +
|
|
305
|
+
'why, notes for later. It does not end your turn and does not affect where ordinary ' +
|
|
306
|
+
'text is routed' + (canSkipReply ? '; to end the turn without replying, call skip_reply afterwards.' : '.');
|
|
324
307
|
}
|
|
325
308
|
|
|
326
|
-
// ============================================================================
|
|
327
|
-
// Channel Tool Definitions
|
|
328
|
-
// ============================================================================
|
|
329
|
-
|
|
330
309
|
const CHANNEL_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
331
310
|
{
|
|
332
311
|
name: 'channel_list',
|
|
@@ -336,9 +315,8 @@ const CHANNEL_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
336
315
|
{
|
|
337
316
|
name: 'channel_open',
|
|
338
317
|
description:
|
|
339
|
-
'Open a channel
|
|
340
|
-
'
|
|
341
|
-
'history preceding the message that invited you into the channel.',
|
|
318
|
+
'Open a channel. What an open channel delivers to you depends on its MCPL server. ' +
|
|
319
|
+
'Optionally request history preceding the message that invited you into the channel.',
|
|
342
320
|
inputSchema: {
|
|
343
321
|
type: 'object' as const,
|
|
344
322
|
properties: {
|
|
@@ -378,7 +356,9 @@ const CHANNEL_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
378
356
|
},
|
|
379
357
|
{
|
|
380
358
|
name: 'channel_close',
|
|
381
|
-
description:
|
|
359
|
+
description:
|
|
360
|
+
'Close a channel. Messages that address you still reach you; what else stops reaching you ' +
|
|
361
|
+
'depends on its server.',
|
|
382
362
|
inputSchema: {
|
|
383
363
|
type: 'object' as const,
|
|
384
364
|
properties: {
|
|
@@ -390,11 +370,11 @@ const CHANNEL_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
390
370
|
},
|
|
391
371
|
{
|
|
392
372
|
name: 'channel_publish',
|
|
393
|
-
description: 'Publish a message to a channel. If channelId is omitted, publishes to the most recent incoming channel.',
|
|
373
|
+
description: 'Publish a message to a channel. If channelId is omitted, publishes to the channel this conversation is bound to, if any, else to the most recent incoming channel. A subagent has neither, and must name one.',
|
|
394
374
|
inputSchema: {
|
|
395
375
|
type: 'object' as const,
|
|
396
376
|
properties: {
|
|
397
|
-
channelId: { type: 'string', description: 'ID of the channel to publish to (defaults to the most recent incoming channel)' },
|
|
377
|
+
channelId: { type: 'string', description: 'ID of the channel to publish to (defaults to the conversation\'s bound channel, else the most recent incoming channel; required of a subagent)' },
|
|
398
378
|
content: { type: 'string', description: 'Text content to publish' },
|
|
399
379
|
text: { type: 'string', description: 'Alias for content' },
|
|
400
380
|
},
|
|
@@ -403,6 +383,9 @@ const CHANNEL_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
403
383
|
},
|
|
404
384
|
{
|
|
405
385
|
name: 'think',
|
|
386
|
+
// No agent's surface shows this description: getToolsForAgent gives every
|
|
387
|
+
// agent its own think (buildThinkTool), which names skip_reply only where
|
|
388
|
+
// that agent may call it.
|
|
406
389
|
description:
|
|
407
390
|
'Reason privately. The content stays in your own context and is NOT sent to any ' +
|
|
408
391
|
'channel or surface. Same-round routing of ordinary text beside think() depends on ' +
|
|
@@ -422,12 +405,7 @@ const CHANNEL_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
422
405
|
},
|
|
423
406
|
{
|
|
424
407
|
name: 'journal',
|
|
425
|
-
description:
|
|
426
|
-
'Write an entry in your private journal. The entry stays in your own context and ' +
|
|
427
|
-
'memory and is NOT sent to any channel, surface or person. Use it for anything longer ' +
|
|
428
|
-
'than a line that you want to keep for yourself — reflections, what you decided and ' +
|
|
429
|
-
'why, notes for later. It does not end your turn and does not affect where ordinary ' +
|
|
430
|
-
'text is routed; to end the turn without replying, call skip_reply afterwards.',
|
|
408
|
+
description: journalToolDescription(true),
|
|
431
409
|
inputSchema: {
|
|
432
410
|
type: 'object' as const,
|
|
433
411
|
properties: {
|
|
@@ -457,8 +435,9 @@ const CHANNEL_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
457
435
|
type: 'number',
|
|
458
436
|
description:
|
|
459
437
|
'Optional self-wake: if nothing else wakes you first, you wake again after this ' +
|
|
460
|
-
'many seconds
|
|
461
|
-
'wake before then cancels it. Omit to stay idle until the
|
|
438
|
+
'many seconds, from 1 to 3600; a larger number waits 3600 (1 = go again almost ' +
|
|
439
|
+
'immediately). Any other wake before then cancels it. Omit to stay idle until the ' +
|
|
440
|
+
'next external wake.',
|
|
462
441
|
},
|
|
463
442
|
},
|
|
464
443
|
required: [],
|
|
@@ -470,7 +449,130 @@ const CHANNEL_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
470
449
|
// Constructor Options
|
|
471
450
|
// ============================================================================
|
|
472
451
|
|
|
452
|
+
/** Bound connector values for notices, logs and an outcome's reason. A
|
|
453
|
+
* PublishOutcome keeps the connector's structured data (`detail`) whole. */
|
|
454
|
+
export function connectorText(value: unknown): string {
|
|
455
|
+
let text: string;
|
|
456
|
+
try {
|
|
457
|
+
text = typeof value === 'string' ? value : JSON.stringify(value) ?? String(value);
|
|
458
|
+
} catch {
|
|
459
|
+
return '[unserializable connector value]';
|
|
460
|
+
}
|
|
461
|
+
const limit = 2000;
|
|
462
|
+
const suffix = '… [truncated]';
|
|
463
|
+
return text.length > limit ? text.slice(0, limit - suffix.length) + suffix : text;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Where a publish was resolved to go — named by the registry, never by
|
|
467
|
+
* the caller's spelling. */
|
|
468
|
+
export interface PublishDestination {
|
|
469
|
+
serverId: string;
|
|
470
|
+
channelId: string;
|
|
471
|
+
/** The channel's registered label when the attempt was made. */
|
|
472
|
+
label?: string;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* What one channels/publish attempt established (ChannelRegistry.publish).
|
|
477
|
+
* - `delivered`: the connector returned `delivered: true`. A historical
|
|
478
|
+
* receipt (confirmed at `at`, to `destination`, message `messageId` when
|
|
479
|
+
* the connector named one), not a claim the message still exists.
|
|
480
|
+
* - `failed`: nothing was posted: the request never left the host
|
|
481
|
+
* (unresolvable or shared channel id, missing grant, failed open, closed
|
|
482
|
+
* connection), or the connector answered `delivered: false` and named no
|
|
483
|
+
* message.
|
|
484
|
+
* - `unknown`: the request was dispatched and no valid receipt came back (an
|
|
485
|
+
* error response, a timeout, a lost connection, or a missing, malformed or
|
|
486
|
+
* contradictory receipt, such as `delivered: false` beside a message id).
|
|
487
|
+
* It may or may not have been posted.
|
|
488
|
+
*/
|
|
489
|
+
export interface PublishOutcome {
|
|
490
|
+
status: 'delivered' | 'failed' | 'unknown';
|
|
491
|
+
/** Absent only when no destination could be resolved. */
|
|
492
|
+
destination?: PublishDestination;
|
|
493
|
+
messageId?: string;
|
|
494
|
+
/** Why it failed, or why its outcome is unknown — for a connector error,
|
|
495
|
+
* its message, bounded (which may say what was already posted). */
|
|
496
|
+
reason?: string;
|
|
497
|
+
/** The connector error's structured `data`, verbatim, when it sent one. */
|
|
498
|
+
detail?: unknown;
|
|
499
|
+
/** When the outcome was established (epoch ms). */
|
|
500
|
+
at: number;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* What a channels/publish request that threw establishes. Only a request
|
|
505
|
+
* that never left the host proves nothing was posted. An error response may
|
|
506
|
+
* follow a partial post (a multi-part send whose later part failed), so it
|
|
507
|
+
* is unknown, with the connector's own words, bounded, and its data kept for
|
|
508
|
+
* the resident to judge. The publish executor and channel_publish both read
|
|
509
|
+
* a failed request this way.
|
|
510
|
+
*/
|
|
511
|
+
function publishErrorOutcome(err: unknown, destination: PublishDestination, at: number): PublishOutcome {
|
|
512
|
+
// This host's own words for a request that wasn't sent or wasn't answered
|
|
513
|
+
// are sentences, and every composition of the reason adds its own ending,
|
|
514
|
+
// so one final period comes off them. A server's error answer keeps its
|
|
515
|
+
// words as they came.
|
|
516
|
+
const text = connectorText(err instanceof Error ? err.message : String(err));
|
|
517
|
+
const message = err instanceof McplRequestError && err.outcome !== 'error-response' ? text.replace(/(?<!\.)\.$/, '') : text;
|
|
518
|
+
const notSent = err instanceof McplRequestError && err.outcome === 'not-sent';
|
|
519
|
+
const detail = err instanceof McplRequestError ? err.data : undefined;
|
|
520
|
+
return {
|
|
521
|
+
status: notSent ? 'failed' : 'unknown',
|
|
522
|
+
destination,
|
|
523
|
+
reason: notSent ? message : `${message} (delivery uncertain)`,
|
|
524
|
+
...(detail !== undefined ? { detail } : {}),
|
|
525
|
+
at,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* What a channels/publish answer establishes. A final publish is a request,
|
|
531
|
+
* so success requires an explicit receipt; a missing or malformed one leaves
|
|
532
|
+
* delivery uncertain (anima-research/agent-framework#163's rule).
|
|
533
|
+
* `delivered: false` proves nothing went out only when the receipt names no
|
|
534
|
+
* message at all: a message id beside it, valid or malformed, contradicts it,
|
|
535
|
+
* since something may have been posted. The publish executor and
|
|
536
|
+
* channel_publish both read an answer this way.
|
|
537
|
+
*/
|
|
538
|
+
function publishReceiptOutcome(result: unknown, destination: PublishDestination, at: number): PublishOutcome {
|
|
539
|
+
const receipt = (result ?? {}) as { delivered?: unknown; messageId?: unknown };
|
|
540
|
+
const messageId = typeof receipt.messageId === 'string' && receipt.messageId ? receipt.messageId : undefined;
|
|
541
|
+
if (receipt.delivered === true) {
|
|
542
|
+
return { status: 'delivered', destination, ...(messageId ? { messageId } : {}), at };
|
|
543
|
+
}
|
|
544
|
+
if (receipt.delivered === false) {
|
|
545
|
+
if (receipt.messageId === undefined || receipt.messageId === null) {
|
|
546
|
+
return { status: 'failed', destination, reason: `server "${destination.serverId}" reported delivered:false`, at };
|
|
547
|
+
}
|
|
548
|
+
return {
|
|
549
|
+
status: 'unknown',
|
|
550
|
+
destination,
|
|
551
|
+
...(messageId ? { messageId } : {}),
|
|
552
|
+
reason: messageId
|
|
553
|
+
? `server "${destination.serverId}" reported delivered:false but named posted message ${connectorText(messageId)} (delivery uncertain)`
|
|
554
|
+
: `server "${destination.serverId}" reported delivered:false beside a malformed message id ` +
|
|
555
|
+
`${connectorText(receipt.messageId)} (contradictory receipt; delivery uncertain)`,
|
|
556
|
+
at,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
status: 'unknown',
|
|
561
|
+
destination,
|
|
562
|
+
...(messageId ? { messageId } : {}),
|
|
563
|
+
reason: `server "${destination.serverId}" returned no valid delivery receipt (delivery uncertain)`,
|
|
564
|
+
at,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
473
568
|
interface ChannelRegistryOptions {
|
|
569
|
+
/**
|
|
570
|
+
* An ordinary (uncoalesced) `channels/incoming` message has just been
|
|
571
|
+
* admitted, before it is queued or acknowledged: return its source
|
|
572
|
+
* envelope, frozen now (mcpl/inbound-source.ts). The framework observes the
|
|
573
|
+
* acceptance here; coalesced work is stamped and observed by its own path.
|
|
574
|
+
*/
|
|
575
|
+
acceptInbound?: (event: McplChannelIncomingEvent) => InboundSource | undefined;
|
|
474
576
|
/**
|
|
475
577
|
* RFC-006: an admitted `channels/incoming` message carrying `coalesce`, with
|
|
476
578
|
* the event the ordinary path would have queued. The handler decides
|
|
@@ -489,8 +591,10 @@ interface ChannelRegistryOptions {
|
|
|
489
591
|
/**
|
|
490
592
|
* Called when a text-only turn's speech could NOT be delivered to its
|
|
491
593
|
* conversational locus — no locus, an unregistered channel, a missing
|
|
492
|
-
* server, or the server reporting `delivered: false`.
|
|
493
|
-
*
|
|
594
|
+
* server, or the server reporting `delivered: false`. Not called for an
|
|
595
|
+
* ephemeral run's speech with no locus, which is its caller's result
|
|
596
|
+
* (`takesGlobalFallback`). The host wires this to drop a
|
|
597
|
+
* `[discord-send-failed]` marker into chronicle so the failure is
|
|
494
598
|
* visible to the agent (and operator) rather than silently lost. Must not
|
|
495
599
|
* itself trigger inference (avoid wake loops).
|
|
496
600
|
*/
|
|
@@ -499,15 +603,22 @@ interface ChannelRegistryOptions {
|
|
|
499
603
|
channelId: string | null;
|
|
500
604
|
reason: string;
|
|
501
605
|
textLen: number;
|
|
606
|
+
/** `failed`: nothing was posted; `unknown`: the request was dispatched
|
|
607
|
+
* and no valid receipt came back, so it may or may not have been. */
|
|
608
|
+
outcome?: 'failed' | 'unknown';
|
|
609
|
+
/** Bounded display of the connector's structured error data, when present. */
|
|
610
|
+
detail?: string;
|
|
502
611
|
}) => void;
|
|
503
612
|
/**
|
|
504
|
-
*
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
*
|
|
510
|
-
*
|
|
613
|
+
* Announces either a durable subscription-policy admission (before its
|
|
614
|
+
* transport RPC; not an open confirmation), or a delivery that turned a
|
|
615
|
+
* closed channel open, once its transport is confirmed. Reconfirming the
|
|
616
|
+
* transport of a channel already decided open or tuned out isn't one.
|
|
617
|
+
* The host records a durable, non-waking notice. What an open channel
|
|
618
|
+
* delivers is the server's to say (#406).
|
|
619
|
+
* Policy admissions fire once per channel: the persisted desired-state
|
|
620
|
+
* decision prevents re-announcement on retry or reboot. channel_close
|
|
621
|
+
* supersedes policy; a fresh delivery into a closed locus engages it again.
|
|
511
622
|
*/
|
|
512
623
|
onChannelAutoOpened?: (info: {
|
|
513
624
|
/** Agent whose action caused the open (delivery); absent for policy opens. */
|
|
@@ -543,6 +654,20 @@ interface ChannelRegistryOptions {
|
|
|
543
654
|
* `defaultPublishChannel`.
|
|
544
655
|
*/
|
|
545
656
|
activeChannelResolver?: (agentName: string) => string | undefined;
|
|
657
|
+
/**
|
|
658
|
+
* Whether a conversation takes the process-global fallback, the channel of
|
|
659
|
+
* the most recent delivered inbound message: as where its plain speech lands
|
|
660
|
+
* when it has no home and no channel of its own this turn, as its channel
|
|
661
|
+
* context's `incoming` and default outgoing channel, and as
|
|
662
|
+
* `channel_publish`'s target when it names none. False for an ephemeral run
|
|
663
|
+
* (a subagent): its prose is its caller's result, so a channel that only the
|
|
664
|
+
* latest inbound message picked is never its destination, and its plain
|
|
665
|
+
* speech with no channel of its own is undelivered rather than a failed
|
|
666
|
+
* delivery. Absent, every conversation takes it.
|
|
667
|
+
*/
|
|
668
|
+
takesGlobalFallback?: (conversationId: string) => boolean;
|
|
669
|
+
/** Whether this framework has EventGate and can honor skip_reply self-wakes. */
|
|
670
|
+
eventGatePresent?: boolean;
|
|
546
671
|
}
|
|
547
672
|
|
|
548
673
|
// ============================================================================
|
|
@@ -576,12 +701,7 @@ export class ChannelRegistry {
|
|
|
576
701
|
op?: 'start' | 'stop',
|
|
577
702
|
) => void;
|
|
578
703
|
private shouldTriggerInference?: (content: string, metadata: Record<string, unknown>) => boolean;
|
|
579
|
-
private onRouteFailure?:
|
|
580
|
-
conversationId: string;
|
|
581
|
-
channelId: string | null;
|
|
582
|
-
reason: string;
|
|
583
|
-
textLen: number;
|
|
584
|
-
}) => void;
|
|
704
|
+
private onRouteFailure?: ChannelRegistryOptions['onRouteFailure'];
|
|
585
705
|
private onChannelAutoOpened?: (info: {
|
|
586
706
|
conversationId?: string;
|
|
587
707
|
serverId: string;
|
|
@@ -590,6 +710,7 @@ export class ChannelRegistry {
|
|
|
590
710
|
}) => void;
|
|
591
711
|
private homeChannelResolver?: (agentName: string) => string | undefined;
|
|
592
712
|
private activeChannelResolver?: (agentName: string) => string | undefined;
|
|
713
|
+
private takesGlobalFallback?: (conversationId: string) => boolean;
|
|
593
714
|
private store?: JsStore;
|
|
594
715
|
|
|
595
716
|
/** Registered channels, keyed by `{serverId}:{channelId}`. */
|
|
@@ -679,7 +800,49 @@ export class ChannelRegistry {
|
|
|
679
800
|
/** One-time migration inputs from the retired recipe auto-open policy. */
|
|
680
801
|
private legacyPolicies = new Map<string, 'auto' | 'manual' | string[]>();
|
|
681
802
|
private migratedLegacyPolicies = new Set<string>();
|
|
803
|
+
|
|
804
|
+
// Keep one transport operation in flight per logical channel, including
|
|
805
|
+
// across descriptor replacement. Settled tails release themselves; a
|
|
806
|
+
// failed operation must not poison later decisions.
|
|
807
|
+
private channelLifecycleTails = new Map<string, Promise<void>>();
|
|
808
|
+
// Bumped when a server's registrations are removed: an operation queued
|
|
809
|
+
// before that is cancelled when it would run, rather than acting on a
|
|
810
|
+
// closing connection or on a later re-registration's entries.
|
|
811
|
+
private serverRemovals = new Map<string, number>();
|
|
812
|
+
// Operations queued or running per channel that will still read its
|
|
813
|
+
// target before confirming anything. Unlike the tail, which settles a few
|
|
814
|
+
// microtasks after its operation's last check, this drops at that check.
|
|
815
|
+
private lifecycleOutstanding = new Map<string, number>();
|
|
816
|
+
// Losing an open receipt does not confirm closure. Keep that distinction
|
|
817
|
+
// private while preserving the public open:boolean availability projection.
|
|
818
|
+
private unconfirmedChannelTargets = new WeakSet<ChannelEntry>();
|
|
819
|
+
|
|
820
|
+
private invalidateTransportConfirmation(entry: ChannelEntry): void {
|
|
821
|
+
entry.open = false;
|
|
822
|
+
this.unconfirmedChannelTargets.add(entry);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/** Whether the host directs this server's channel lifecycle (§14.1: its
|
|
826
|
+
* `channels.lifecycle` grant). Where it doesn't, the server opens and
|
|
827
|
+
* closes its own channels, and sends no receipt the host could wait for. */
|
|
828
|
+
private directsLifecycle(serverId: string): boolean {
|
|
829
|
+
return CapabilityGrant.of(this.serverRegistry.getServer(serverId)).has('channels.lifecycle');
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/** A registration's entry: nothing about its transport is known until a
|
|
833
|
+
* receipt for its target confirms it open or closed. */
|
|
834
|
+
private registerEntry(key: string, serverId: string, descriptor: ChannelDescriptor): void {
|
|
835
|
+
const entry: ChannelEntry = { serverId, descriptor, open: false };
|
|
836
|
+
this.channels.set(key, entry);
|
|
837
|
+
this.unconfirmedChannelTargets.add(entry);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
private hasPendingLifecycle(entry: ChannelEntry): boolean {
|
|
841
|
+
return this.channelLifecycleTails.has(this.lifecycleKey(entry.serverId, entry.descriptor.id));
|
|
842
|
+
}
|
|
682
843
|
private handleCoalescedIncoming?: ChannelRegistryOptions['handleCoalescedIncoming'];
|
|
844
|
+
private acceptInbound?: ChannelRegistryOptions['acceptInbound'];
|
|
845
|
+
private channelTools: ToolDefinition[];
|
|
683
846
|
|
|
684
847
|
constructor(
|
|
685
848
|
serverRegistry: McplServerRegistry,
|
|
@@ -696,6 +859,24 @@ export class ChannelRegistry {
|
|
|
696
859
|
},
|
|
697
860
|
) {
|
|
698
861
|
this.handleCoalescedIncoming = options?.handleCoalescedIncoming;
|
|
862
|
+
this.acceptInbound = options?.acceptInbound;
|
|
863
|
+
this.channelTools = CHANNEL_TOOL_DEFINITIONS.map((tool) => {
|
|
864
|
+
if (tool.name !== 'skip_reply' || options?.eventGatePresent === true) return tool;
|
|
865
|
+
const schema = tool.inputSchema as {
|
|
866
|
+
type: 'object';
|
|
867
|
+
properties: NonNullable<ToolDefinition['inputSchema']['properties']>;
|
|
868
|
+
required?: string[];
|
|
869
|
+
};
|
|
870
|
+
const { wake_in_seconds: _wake, ...properties } = schema.properties;
|
|
871
|
+
return {
|
|
872
|
+
...tool,
|
|
873
|
+
description: tool.description.replace(
|
|
874
|
+
' To end this turn but come back on your own shortly, set wake_in_seconds.',
|
|
875
|
+
'',
|
|
876
|
+
),
|
|
877
|
+
inputSchema: { ...schema, properties },
|
|
878
|
+
};
|
|
879
|
+
});
|
|
699
880
|
this.serverRegistry = serverRegistry;
|
|
700
881
|
this.featureSetManager = featureSetManager;
|
|
701
882
|
this.pushEventFn = pushEventFn;
|
|
@@ -706,6 +887,7 @@ export class ChannelRegistry {
|
|
|
706
887
|
this.onChannelAutoOpened = options?.onChannelAutoOpened;
|
|
707
888
|
this.homeChannelResolver = options?.homeChannelResolver;
|
|
708
889
|
this.activeChannelResolver = options?.activeChannelResolver;
|
|
890
|
+
this.takesGlobalFallback = options?.takesGlobalFallback;
|
|
709
891
|
this.store = options?.store;
|
|
710
892
|
this.initializeLifecycleStore();
|
|
711
893
|
this.initializeLabelHistoryStore();
|
|
@@ -749,11 +931,7 @@ export class ChannelRegistry {
|
|
|
749
931
|
continue;
|
|
750
932
|
}
|
|
751
933
|
const key = `${serverId}:${channel.id}`;
|
|
752
|
-
this.
|
|
753
|
-
serverId,
|
|
754
|
-
descriptor: channel,
|
|
755
|
-
open: false,
|
|
756
|
-
});
|
|
934
|
+
this.registerEntry(key, serverId, channel);
|
|
757
935
|
this.appendLabelSighting(channel.id, channel.label, extractDmMeta(channel.metadata));
|
|
758
936
|
registeredIds.push(channel.id);
|
|
759
937
|
results.push({ id: channel.id, accepted: true });
|
|
@@ -794,6 +972,7 @@ export class ChannelRegistry {
|
|
|
794
972
|
// channels/register — a changed-set cannot smuggle in what register
|
|
795
973
|
// would have rejected (PR #79 review blocker 5).
|
|
796
974
|
const addedResults: Array<{ id: string; accepted: boolean; reason?: string }> = [];
|
|
975
|
+
const retargeted: ChannelDescriptor[] = [];
|
|
797
976
|
// Process removed channels — itemized truthfully: removal of a channel
|
|
798
977
|
// we never had is reported, not silently accepted.
|
|
799
978
|
if (params.removed) {
|
|
@@ -823,6 +1002,21 @@ export class ChannelRegistry {
|
|
|
823
1002
|
this.emitTraceFn({ type: 'mcpl:channel-descriptor-rejected', serverId, channelId: channel.id, reason: 'update for unregistered channel' });
|
|
824
1003
|
continue;
|
|
825
1004
|
}
|
|
1005
|
+
// A live subscription to the old target does not confirm the new
|
|
1006
|
+
// type/address. Label-only updates preserve that confirmation. A
|
|
1007
|
+
// channel decided open (or tuned out) is opened at its new target.
|
|
1008
|
+
if (!sameChannelTarget(existing.descriptor, channel)) {
|
|
1009
|
+
this.invalidateTransportConfirmation(existing);
|
|
1010
|
+
// An operation queued or running for this channel reads its target
|
|
1011
|
+
// again before it confirms anything, and after a failed RPC, so it
|
|
1012
|
+
// opens the new one. With none, a channel decided open (or tuned
|
|
1013
|
+
// out) is reconciled here.
|
|
1014
|
+
const desired = this.getDesiredState(serverId, channel.id);
|
|
1015
|
+
if ((desired === 'open' || desired === 'tuned-out') &&
|
|
1016
|
+
!this.lifecycleOutstanding.has(this.lifecycleKey(serverId, channel.id))) {
|
|
1017
|
+
retargeted.push(channel);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
826
1020
|
existing.descriptor = channel;
|
|
827
1021
|
this.appendLabelSighting(channel.id, channel.label, extractDmMeta(channel.metadata));
|
|
828
1022
|
addedResults.push({ id: channel.id, accepted: true });
|
|
@@ -840,11 +1034,7 @@ export class ChannelRegistry {
|
|
|
840
1034
|
continue;
|
|
841
1035
|
}
|
|
842
1036
|
const key = `${serverId}:${channel.id}`;
|
|
843
|
-
this.
|
|
844
|
-
serverId,
|
|
845
|
-
descriptor: channel,
|
|
846
|
-
open: false,
|
|
847
|
-
});
|
|
1037
|
+
this.registerEntry(key, serverId, channel);
|
|
848
1038
|
this.appendLabelSighting(channel.id, channel.label, extractDmMeta(channel.metadata));
|
|
849
1039
|
addedResults.push({ id: channel.id, accepted: true });
|
|
850
1040
|
accepted.push(channel);
|
|
@@ -859,7 +1049,8 @@ export class ChannelRegistry {
|
|
|
859
1049
|
// settled above, so nothing in the response depends on reconciling.
|
|
860
1050
|
responder?.respond({ results: addedResults });
|
|
861
1051
|
|
|
862
|
-
|
|
1052
|
+
const reconcile = [...accepted, ...retargeted];
|
|
1053
|
+
if (reconcile.length > 0) await this.reconcileChannels(serverId, reconcile);
|
|
863
1054
|
|
|
864
1055
|
this.emitTraceFn({
|
|
865
1056
|
type: 'mcpl:channels-changed',
|
|
@@ -1031,8 +1222,8 @@ export class ChannelRegistry {
|
|
|
1031
1222
|
}
|
|
1032
1223
|
}
|
|
1033
1224
|
if (message.tags) message.tags = expandCoreTags(message.tags);
|
|
1034
|
-
const convertedContent: ContentBlock[] = message.content.map(convertBlock);
|
|
1035
|
-
if (!emptyAllowed &&
|
|
1225
|
+
const convertedContent: ContentBlock[] = message.content.map((block) => convertBlock(block, 'attachment on channel message', serverId));
|
|
1226
|
+
if (!emptyAllowed && isVisiblyEmptyConversion(message.content, convertedContent)) {
|
|
1036
1227
|
rejectEmpty();
|
|
1037
1228
|
continue;
|
|
1038
1229
|
}
|
|
@@ -1043,15 +1234,31 @@ export class ChannelRegistry {
|
|
|
1043
1234
|
// exactly the authority a self-attested channel would be stealing).
|
|
1044
1235
|
// A coalesced item is "accepted" only once the coalescer admits it, so
|
|
1045
1236
|
// for those this runs after the hook (below).
|
|
1237
|
+
const incomingEntry = this.channels.get(incomingKey)!;
|
|
1046
1238
|
const markAccepted = () => {
|
|
1047
1239
|
this.defaultPublishChannel = message.channelId;
|
|
1048
1240
|
this.defaultPublishMessageId = message.messageId;
|
|
1049
1241
|
this.defaultPublishThreadId = message.threadId;
|
|
1050
|
-
//
|
|
1051
|
-
// the
|
|
1052
|
-
//
|
|
1053
|
-
//
|
|
1054
|
-
|
|
1242
|
+
// Where the host directs this server's lifecycle, only a receipt for
|
|
1243
|
+
// the current target confirms it: traffic shows the transport open
|
|
1244
|
+
// only for a confirmed target, and only for a channel still decided
|
|
1245
|
+
// open or tuned out. After a retarget, traffic for this id may still
|
|
1246
|
+
// come from the old subscription, and a close receipt that landed
|
|
1247
|
+
// first (or during the coalescer's await) stands. Where the host
|
|
1248
|
+
// doesn't direct it, the server opens its own channels and its
|
|
1249
|
+
// traffic is the only evidence the host gets, so it marks the
|
|
1250
|
+
// channel open, as it always has. The entry is the one the message
|
|
1251
|
+
// was accepted for: one a re-registration put in its place meanwhile
|
|
1252
|
+
// waits for its own evidence. Durable desired state still changes
|
|
1253
|
+
// exclusively through lifecycle operations.
|
|
1254
|
+
if (this.directsLifecycle(serverId)) {
|
|
1255
|
+
if (this.unconfirmedChannelTargets.has(incomingEntry)) return;
|
|
1256
|
+
const desired = this.getDesiredState(serverId, message.channelId);
|
|
1257
|
+
if (desired !== 'open' && desired !== 'tuned-out') return;
|
|
1258
|
+
} else {
|
|
1259
|
+
this.unconfirmedChannelTargets.delete(incomingEntry);
|
|
1260
|
+
}
|
|
1261
|
+
incomingEntry.open = true;
|
|
1055
1262
|
};
|
|
1056
1263
|
if (!coalesced) markAccepted();
|
|
1057
1264
|
|
|
@@ -1060,7 +1267,7 @@ export class ChannelRegistry {
|
|
|
1060
1267
|
if (this.shouldTriggerInference) {
|
|
1061
1268
|
// The text the agent's context gets, as the push lane's filter reads
|
|
1062
1269
|
// it: the converted text blocks, a placeholder or reference stub
|
|
1063
|
-
// among them,
|
|
1270
|
+
// among them, NON_STRING_TEXT_PLACEHOLDER included.
|
|
1064
1271
|
const textContent = convertedContent
|
|
1065
1272
|
.filter((b): b is ContentBlock & { type: 'text' } => b.type === 'text')
|
|
1066
1273
|
.map((b) => b.text)
|
|
@@ -1093,6 +1300,7 @@ export class ChannelRegistry {
|
|
|
1093
1300
|
metadata: message.metadata,
|
|
1094
1301
|
...(message.tags ? { tags: message.tags } : {}),
|
|
1095
1302
|
triggerInference,
|
|
1303
|
+
acceptedAt: Date.now(),
|
|
1096
1304
|
};
|
|
1097
1305
|
|
|
1098
1306
|
if (coalesced) {
|
|
@@ -1118,6 +1326,12 @@ export class ChannelRegistry {
|
|
|
1118
1326
|
continue;
|
|
1119
1327
|
}
|
|
1120
1328
|
|
|
1329
|
+
// The source envelope is frozen here, at admission, before the message
|
|
1330
|
+
// is queued or acknowledged: a rename or rebind while it waits in the
|
|
1331
|
+
// queue cannot change where it says it came from.
|
|
1332
|
+
const inboundSource = this.acceptInbound?.(event);
|
|
1333
|
+
if (inboundSource) event.inboundSource = inboundSource;
|
|
1334
|
+
|
|
1121
1335
|
// Push to the processing queue
|
|
1122
1336
|
// Cast through unknown because McplChannelIncomingEvent matches the
|
|
1123
1337
|
// CustomEvent `${string}:${string}` type pattern but lacks an index signature.
|
|
@@ -1171,20 +1385,34 @@ export class ChannelRegistry {
|
|
|
1171
1385
|
(existing.descriptor.metadata as { lazyRegistered?: boolean } | undefined)?.lazyRegistered &&
|
|
1172
1386
|
label && existing.descriptor.label === channelId
|
|
1173
1387
|
) {
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
}
|
|
1388
|
+
const metadata = extraMetadata
|
|
1389
|
+
? { ...existing.descriptor.metadata, ...extraMetadata }
|
|
1390
|
+
: existing.descriptor.metadata;
|
|
1178
1391
|
// The channel now has a real label, not just the bare id — durable.
|
|
1179
|
-
this
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1392
|
+
// Recorded before the live descriptor takes it, since this branch's
|
|
1393
|
+
// guard reads the descriptor: an append that throws leaves the
|
|
1394
|
+
// placeholder, so the next push tries again.
|
|
1395
|
+
this.appendLabelSighting(existing.descriptor.id, label, extractDmMeta(metadata));
|
|
1396
|
+
existing.descriptor.label = label;
|
|
1397
|
+
existing.descriptor.metadata = metadata;
|
|
1184
1398
|
}
|
|
1185
1399
|
return;
|
|
1186
1400
|
}
|
|
1187
1401
|
|
|
1402
|
+
// Only persist a REAL label durably. Falling back to the bare
|
|
1403
|
+
// channelId as a placeholder (no server-supplied label at all — e.g. a
|
|
1404
|
+
// Discord DM push missing both channelName and an author name,
|
|
1405
|
+
// framework.ts's derivePushEventChannel) must never overwrite a real
|
|
1406
|
+
// label already on file from a previous boot: after a restart the live
|
|
1407
|
+
// `channels` map starts empty, so THIS is the very branch a
|
|
1408
|
+
// label-less post-restart event takes — recording the placeholder here
|
|
1409
|
+
// would clobber the good label initializeLabelHistoryStore() just
|
|
1410
|
+
// replayed, defeating the entire point of the durable log. The live
|
|
1411
|
+
// descriptor below still carries the placeholder for in-process
|
|
1412
|
+
// routing; only the durable write is skipped when there's no real
|
|
1413
|
+
// label to record. A real label is recorded before the entry exists:
|
|
1414
|
+
// an append that throws registers nothing, so the next push tries again.
|
|
1415
|
+
if (label) this.appendLabelSighting(channelId, label, extractDmMeta(extraMetadata));
|
|
1188
1416
|
const key = `${serverId}:${channelId}`;
|
|
1189
1417
|
this.channels.set(key, {
|
|
1190
1418
|
serverId,
|
|
@@ -1197,19 +1425,6 @@ export class ChannelRegistry {
|
|
|
1197
1425
|
},
|
|
1198
1426
|
open: false,
|
|
1199
1427
|
});
|
|
1200
|
-
// Only persist a REAL label durably. Falling back to the bare
|
|
1201
|
-
// channelId as a placeholder (no server-supplied label at all — e.g. a
|
|
1202
|
-
// Discord DM push missing both channelName and an author name,
|
|
1203
|
-
// framework.ts's derivePushEventChannel) must never overwrite a real
|
|
1204
|
-
// label already on file from a previous boot: after a restart the live
|
|
1205
|
-
// `channels` map starts empty, so THIS is the very branch a
|
|
1206
|
-
// label-less post-restart event takes — recording the placeholder here
|
|
1207
|
-
// would clobber the good label initializeLabelHistoryStore() just
|
|
1208
|
-
// replayed, defeating the entire point of the durable log. The live
|
|
1209
|
-
// descriptor above still carries the placeholder for in-process
|
|
1210
|
-
// routing; only the durable write is skipped when there's no real
|
|
1211
|
-
// label to record.
|
|
1212
|
-
if (label) this.appendLabelSighting(channelId, label, extractDmMeta(extraMetadata));
|
|
1213
1428
|
this.emitTraceFn({
|
|
1214
1429
|
type: 'mcpl:channel-lazy-registered',
|
|
1215
1430
|
serverId,
|
|
@@ -1344,6 +1559,13 @@ export class ChannelRegistry {
|
|
|
1344
1559
|
return this.findChannelEntry(channelId)?.descriptor;
|
|
1345
1560
|
}
|
|
1346
1561
|
|
|
1562
|
+
/** The registered label of `channelId` on exactly `serverId` (no
|
|
1563
|
+
* cross-server first match), or undefined when it has none. */
|
|
1564
|
+
getChannelLabel(serverId: string, channelId: string): string | undefined {
|
|
1565
|
+
const label = this.channels.get(`${serverId}:${channelId}`)?.descriptor.label;
|
|
1566
|
+
return typeof label === 'string' && label.length > 0 ? label : undefined;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1347
1569
|
isChannelOpen(channelId: string): boolean {
|
|
1348
1570
|
return this.findChannelEntry(channelId)?.open === true;
|
|
1349
1571
|
}
|
|
@@ -1373,7 +1595,7 @@ export class ChannelRegistry {
|
|
|
1373
1595
|
* Get synthesized tool definitions for channel operations.
|
|
1374
1596
|
*/
|
|
1375
1597
|
getChannelTools(): ToolDefinition[] {
|
|
1376
|
-
return
|
|
1598
|
+
return this.channelTools;
|
|
1377
1599
|
}
|
|
1378
1600
|
|
|
1379
1601
|
/**
|
|
@@ -1425,7 +1647,7 @@ export class ChannelRegistry {
|
|
|
1425
1647
|
});
|
|
1426
1648
|
|
|
1427
1649
|
case 'channel_publish':
|
|
1428
|
-
return this.handleToolPublish(input as { channelId: string; content: string });
|
|
1650
|
+
return this.handleToolPublish(input as { channelId: string; content: string }, origin);
|
|
1429
1651
|
|
|
1430
1652
|
case 'think':
|
|
1431
1653
|
return this.handleToolThink(input as { content?: string });
|
|
@@ -1434,7 +1656,7 @@ export class ChannelRegistry {
|
|
|
1434
1656
|
return this.handleToolJournal(input as { content?: string });
|
|
1435
1657
|
|
|
1436
1658
|
case 'skip_reply':
|
|
1437
|
-
return this.handleToolSkipReply(input as { reason?: string; wake_in_seconds?:
|
|
1659
|
+
return this.handleToolSkipReply(input as { reason?: string; wake_in_seconds?: unknown });
|
|
1438
1660
|
|
|
1439
1661
|
default:
|
|
1440
1662
|
return { success: false, error: `Unknown channel tool: ${toolName}`, isError: true };
|
|
@@ -1460,9 +1682,15 @@ export class ChannelRegistry {
|
|
|
1460
1682
|
// (heartbeats). Without this, a fork
|
|
1461
1683
|
// was advertised the global locus but published somewhere else; and a trunk
|
|
1462
1684
|
// agent was told the wrong channel under concurrency.
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
const
|
|
1685
|
+
// Each lookup is keyed on whether a name was given, not its truthiness:
|
|
1686
|
+
// '' is a name an ephemeral run can have, and resolveLocus reads it too.
|
|
1687
|
+
const home = agentName !== undefined ? this.homeChannelResolver?.(agentName) : undefined;
|
|
1688
|
+
const active = agentName !== undefined ? this.activeChannelResolver?.(agentName) : undefined;
|
|
1689
|
+
// An ephemeral run takes no global fallback (takesGlobalFallback): neither
|
|
1690
|
+
// the latest inbound message's channel as its outgoing one, nor that
|
|
1691
|
+
// message as the one it is replying to.
|
|
1692
|
+
const fallback = agentName !== undefined ? this.globalFallbackFor(agentName) : this.defaultPublishChannel;
|
|
1693
|
+
const outgoing = home ?? active ?? fallback;
|
|
1466
1694
|
|
|
1467
1695
|
if (openChannels.length === 0 && !outgoing) {
|
|
1468
1696
|
return undefined;
|
|
@@ -1473,9 +1701,9 @@ export class ChannelRegistry {
|
|
|
1473
1701
|
// Incoming: the most-recent inbound message (what the agent is replying to).
|
|
1474
1702
|
// Left process-global — per-channel inbound tracking (the right messageId for
|
|
1475
1703
|
// a fork's own channel) is a separate concern from the outbound routing fix.
|
|
1476
|
-
if (
|
|
1704
|
+
if (fallback && this.defaultPublishMessageId) {
|
|
1477
1705
|
context.incoming = {
|
|
1478
|
-
channelId:
|
|
1706
|
+
channelId: fallback,
|
|
1479
1707
|
messageId: this.defaultPublishMessageId,
|
|
1480
1708
|
threadId: this.defaultPublishThreadId,
|
|
1481
1709
|
};
|
|
@@ -1506,6 +1734,10 @@ export class ChannelRegistry {
|
|
|
1506
1734
|
* server's channels don't linger and route speech into the void.
|
|
1507
1735
|
*/
|
|
1508
1736
|
removeServer(serverId: string): void {
|
|
1737
|
+
// Every lifecycle operation queued for this server is cancelled when it
|
|
1738
|
+
// would run (ChannelLifecycleCancelledError), so none acts on a closing
|
|
1739
|
+
// connection or on a later re-registration's entries.
|
|
1740
|
+
this.serverRemovals.set(serverId, (this.serverRemovals.get(serverId) ?? 0) + 1);
|
|
1509
1741
|
for (const [key, entry] of this.channels) {
|
|
1510
1742
|
if (entry.serverId !== serverId) continue;
|
|
1511
1743
|
this.channels.delete(key);
|
|
@@ -1549,13 +1781,7 @@ export class ChannelRegistry {
|
|
|
1549
1781
|
private initializeLifecycleStore(): void {
|
|
1550
1782
|
if (!this.store) return;
|
|
1551
1783
|
|
|
1552
|
-
|
|
1553
|
-
this.store.registerState({ id: CHANNEL_LIFECYCLE_LOG_ID, strategy: 'append_log' });
|
|
1554
|
-
} catch (error) {
|
|
1555
|
-
if (!(error instanceof Error) || !error.message.includes('State already exists')) {
|
|
1556
|
-
throw error;
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1784
|
+
ensureStateRegistered(this.store, { id: CHANNEL_LIFECYCLE_LOG_ID, strategy: 'append_log' });
|
|
1559
1785
|
|
|
1560
1786
|
const raw = this.store.getStateJson(CHANNEL_LIFECYCLE_LOG_ID);
|
|
1561
1787
|
if (!Array.isArray(raw)) return;
|
|
@@ -1598,6 +1824,15 @@ export class ChannelRegistry {
|
|
|
1598
1824
|
}
|
|
1599
1825
|
}
|
|
1600
1826
|
|
|
1827
|
+
/**
|
|
1828
|
+
* Each writer appends its record here before it changes memory for it, so
|
|
1829
|
+
* an append that throws, as on a full disk, fails the operation with that
|
|
1830
|
+
* change unmade, and memory and the log still agree. A retry then records
|
|
1831
|
+
* the change rather than finding it in memory already, where
|
|
1832
|
+
* setDesiredState's same-state return would record nothing, and a restart
|
|
1833
|
+
* replays what memory held. appendLabelSighting keeps the same order for
|
|
1834
|
+
* the label history.
|
|
1835
|
+
*/
|
|
1601
1836
|
private appendLifecycleEvent(event: ChannelLifecycleEvent): void {
|
|
1602
1837
|
this.store?.appendToStateJson(CHANNEL_LIFECYCLE_LOG_ID, event);
|
|
1603
1838
|
}
|
|
@@ -1616,13 +1851,7 @@ export class ChannelRegistry {
|
|
|
1616
1851
|
private initializeLabelHistoryStore(): void {
|
|
1617
1852
|
if (!this.store) return;
|
|
1618
1853
|
|
|
1619
|
-
|
|
1620
|
-
this.store.registerState({ id: CHANNEL_LABEL_HISTORY_LOG_ID, strategy: 'append_log' });
|
|
1621
|
-
} catch (error) {
|
|
1622
|
-
if (!(error instanceof Error) || !error.message.includes('State already exists')) {
|
|
1623
|
-
throw error;
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1854
|
+
ensureStateRegistered(this.store, { id: CHANNEL_LABEL_HISTORY_LOG_ID, strategy: 'append_log' });
|
|
1626
1855
|
|
|
1627
1856
|
const raw = this.store.getStateJson(CHANNEL_LABEL_HISTORY_LOG_ID);
|
|
1628
1857
|
if (!Array.isArray(raw)) return;
|
|
@@ -1703,6 +1932,16 @@ export class ChannelRegistry {
|
|
|
1703
1932
|
const recipientNameUnchanged = recipientName === undefined || recipientName === this.dmRecipientNames.get(channelId);
|
|
1704
1933
|
const isDmUnchanged = isDm === undefined || this.dmClassified.get(channelId) === true;
|
|
1705
1934
|
if (labelUnchanged && recipientIdUnchanged && recipientNameUnchanged && isDmUnchanged) return;
|
|
1935
|
+
// The log first, as for lifecycle events: an append that throws leaves
|
|
1936
|
+
// the history in memory as it was, so the next sighting records it.
|
|
1937
|
+
this.store?.appendToStateJson(CHANNEL_LABEL_HISTORY_LOG_ID, {
|
|
1938
|
+
channelId,
|
|
1939
|
+
label,
|
|
1940
|
+
ts: Date.now(),
|
|
1941
|
+
...(recipientId ? { recipientId } : {}),
|
|
1942
|
+
...(recipientName ? { recipientName } : {}),
|
|
1943
|
+
...(isDm ? { isDm } : {}),
|
|
1944
|
+
} satisfies ChannelLabelSightingEvent);
|
|
1706
1945
|
this.labelHistory.set(channelId, label);
|
|
1707
1946
|
let seen = this.allLabelsSeen.get(channelId);
|
|
1708
1947
|
if (!seen) {
|
|
@@ -1713,14 +1952,6 @@ export class ChannelRegistry {
|
|
|
1713
1952
|
if (recipientId) this.dmRecipientIds.set(channelId, recipientId);
|
|
1714
1953
|
if (recipientName) this.dmRecipientNames.set(channelId, recipientName);
|
|
1715
1954
|
if (isDm) this.dmClassified.set(channelId, true);
|
|
1716
|
-
this.store?.appendToStateJson(CHANNEL_LABEL_HISTORY_LOG_ID, {
|
|
1717
|
-
channelId,
|
|
1718
|
-
label,
|
|
1719
|
-
ts: Date.now(),
|
|
1720
|
-
...(recipientId ? { recipientId } : {}),
|
|
1721
|
-
...(recipientName ? { recipientName } : {}),
|
|
1722
|
-
...(isDm ? { isDm } : {}),
|
|
1723
|
-
} satisfies ChannelLabelSightingEvent);
|
|
1724
1955
|
}
|
|
1725
1956
|
|
|
1726
1957
|
/**
|
|
@@ -1945,7 +2176,6 @@ export class ChannelRegistry {
|
|
|
1945
2176
|
): void {
|
|
1946
2177
|
const key = this.lifecycleKey(serverId, channelId);
|
|
1947
2178
|
if (this.desiredStates.get(key)?.state === desired) return;
|
|
1948
|
-
this.desiredStates.set(key, { state: desired, source });
|
|
1949
2179
|
this.appendLifecycleEvent({
|
|
1950
2180
|
kind: 'desired-state',
|
|
1951
2181
|
serverId,
|
|
@@ -1954,6 +2184,7 @@ export class ChannelRegistry {
|
|
|
1954
2184
|
source,
|
|
1955
2185
|
timestamp: new Date().toISOString(),
|
|
1956
2186
|
});
|
|
2187
|
+
this.desiredStates.set(key, { state: desired, source });
|
|
1957
2188
|
}
|
|
1958
2189
|
|
|
1959
2190
|
// ==========================================================================
|
|
@@ -1973,12 +2204,6 @@ export class ChannelRegistry {
|
|
|
1973
2204
|
source: string,
|
|
1974
2205
|
): void {
|
|
1975
2206
|
const key = this.lifecycleKey(serverId, channelId);
|
|
1976
|
-
this.desiredStates.set(key, {
|
|
1977
|
-
state: 'tuned-out',
|
|
1978
|
-
source,
|
|
1979
|
-
tuneOut: params,
|
|
1980
|
-
wakeCount: 0,
|
|
1981
|
-
});
|
|
1982
2207
|
this.appendLifecycleEvent({
|
|
1983
2208
|
kind: 'desired-state',
|
|
1984
2209
|
serverId,
|
|
@@ -1988,6 +2213,12 @@ export class ChannelRegistry {
|
|
|
1988
2213
|
source,
|
|
1989
2214
|
timestamp: new Date().toISOString(),
|
|
1990
2215
|
});
|
|
2216
|
+
this.desiredStates.set(key, {
|
|
2217
|
+
state: 'tuned-out',
|
|
2218
|
+
source,
|
|
2219
|
+
tuneOut: params,
|
|
2220
|
+
wakeCount: 0,
|
|
2221
|
+
});
|
|
1991
2222
|
}
|
|
1992
2223
|
|
|
1993
2224
|
/**
|
|
@@ -2006,7 +2237,6 @@ export class ChannelRegistry {
|
|
|
2006
2237
|
const current = this.desiredStates.get(key);
|
|
2007
2238
|
if (current?.state !== 'tuned-out' || !current.tuneOut) return null;
|
|
2008
2239
|
const ended = { params: current.tuneOut, wakeCount: current.wakeCount ?? 0 };
|
|
2009
|
-
this.desiredStates.set(key, { state: nextState, source });
|
|
2010
2240
|
this.appendLifecycleEvent({
|
|
2011
2241
|
kind: 'desired-state',
|
|
2012
2242
|
serverId,
|
|
@@ -2015,6 +2245,7 @@ export class ChannelRegistry {
|
|
|
2015
2245
|
source,
|
|
2016
2246
|
timestamp: new Date().toISOString(),
|
|
2017
2247
|
});
|
|
2248
|
+
this.desiredStates.set(key, { state: nextState, source });
|
|
2018
2249
|
return ended;
|
|
2019
2250
|
}
|
|
2020
2251
|
|
|
@@ -2032,16 +2263,17 @@ export class ChannelRegistry {
|
|
|
2032
2263
|
const key = this.lifecycleKey(serverId, channelId);
|
|
2033
2264
|
const current = this.desiredStates.get(key);
|
|
2034
2265
|
if (current?.state !== 'tuned-out' || !current.tuneOut) return null;
|
|
2035
|
-
|
|
2266
|
+
const wakeCount = (current.wakeCount ?? 0) + 1;
|
|
2036
2267
|
this.appendLifecycleEvent({
|
|
2037
2268
|
kind: 'tune-out-wake',
|
|
2038
2269
|
serverId,
|
|
2039
2270
|
channelId,
|
|
2040
2271
|
epochId: current.tuneOut.epochId,
|
|
2041
|
-
wakeCount
|
|
2272
|
+
wakeCount,
|
|
2042
2273
|
timestamp: new Date().toISOString(),
|
|
2043
2274
|
});
|
|
2044
|
-
|
|
2275
|
+
current.wakeCount = wakeCount;
|
|
2276
|
+
return { params: current.tuneOut, wakeCount };
|
|
2045
2277
|
}
|
|
2046
2278
|
|
|
2047
2279
|
/** Active tune-out params + wake count for a channel, or null. */
|
|
@@ -2069,17 +2301,13 @@ export class ChannelRegistry {
|
|
|
2069
2301
|
*/
|
|
2070
2302
|
readCoordinatorState(stateId: string): unknown {
|
|
2071
2303
|
if (!this.store) return null;
|
|
2072
|
-
|
|
2073
|
-
this.store.registerState({ id: stateId, strategy: 'snapshot' });
|
|
2074
|
-
} catch { /* already registered */ }
|
|
2304
|
+
ensureStateRegistered(this.store, { id: stateId, strategy: 'snapshot' });
|
|
2075
2305
|
return this.store.getStateJson(stateId);
|
|
2076
2306
|
}
|
|
2077
2307
|
|
|
2078
2308
|
writeCoordinatorState(stateId: string, value: unknown): void {
|
|
2079
2309
|
if (!this.store) return;
|
|
2080
|
-
|
|
2081
|
-
this.store.registerState({ id: stateId, strategy: 'snapshot' });
|
|
2082
|
-
} catch { /* already registered */ }
|
|
2310
|
+
ensureStateRegistered(this.store, { id: stateId, strategy: 'snapshot' });
|
|
2083
2311
|
this.store.setStateJson(stateId, value);
|
|
2084
2312
|
}
|
|
2085
2313
|
|
|
@@ -2116,13 +2344,13 @@ export class ChannelRegistry {
|
|
|
2116
2344
|
this.setDesiredState(serverId, channel.id, desired, 'legacy-recipe-migration');
|
|
2117
2345
|
}
|
|
2118
2346
|
|
|
2119
|
-
this.migratedLegacyPolicies.add(serverId);
|
|
2120
2347
|
this.appendLifecycleEvent({
|
|
2121
2348
|
kind: 'legacy-policy-migrated',
|
|
2122
2349
|
serverId,
|
|
2123
2350
|
source: Array.isArray(policy) ? 'allow-list' : policy,
|
|
2124
2351
|
timestamp: new Date().toISOString(),
|
|
2125
2352
|
});
|
|
2353
|
+
this.migratedLegacyPolicies.add(serverId);
|
|
2126
2354
|
}
|
|
2127
2355
|
|
|
2128
2356
|
/**
|
|
@@ -2181,6 +2409,225 @@ export class ChannelRegistry {
|
|
|
2181
2409
|
return channel.initiallyOpen !== true && byPolicy;
|
|
2182
2410
|
}
|
|
2183
2411
|
|
|
2412
|
+
/**
|
|
2413
|
+
* Apply current durable intent, serializing the transport rather than the
|
|
2414
|
+
* server's registration ACK. Re-read after every await: registration,
|
|
2415
|
+
* descriptor updates, or a newer decision may have replaced our snapshot.
|
|
2416
|
+
*/
|
|
2417
|
+
private applyDesiredChannelState(
|
|
2418
|
+
serverId: string,
|
|
2419
|
+
channelId: string,
|
|
2420
|
+
options?: { history?: ChannelHistoryRequest },
|
|
2421
|
+
): Promise<{ open: boolean; result?: ChannelsOpenResult }> {
|
|
2422
|
+
const lifecycleKey = this.lifecycleKey(serverId, channelId);
|
|
2423
|
+
const removals = this.serverRemovals.get(serverId) ?? 0;
|
|
2424
|
+
const previous = this.channelLifecycleTails.get(lifecycleKey) ?? Promise.resolve();
|
|
2425
|
+
this.lifecycleOutstanding.set(lifecycleKey, (this.lifecycleOutstanding.get(lifecycleKey) ?? 0) + 1);
|
|
2426
|
+
const operation = previous.catch(() => {}).then(
|
|
2427
|
+
() => this.runLifecycleOperation(serverId, channelId, removals, options),
|
|
2428
|
+
);
|
|
2429
|
+
// Keep the rejection visible to existing joiners, while observing it
|
|
2430
|
+
// immediately and letting each queued successor start after either result.
|
|
2431
|
+
const tail = operation.then(() => {});
|
|
2432
|
+
this.channelLifecycleTails.set(lifecycleKey, tail);
|
|
2433
|
+
const cleanup = () => {
|
|
2434
|
+
if (this.channelLifecycleTails.get(lifecycleKey) === tail) {
|
|
2435
|
+
this.channelLifecycleTails.delete(lifecycleKey);
|
|
2436
|
+
}
|
|
2437
|
+
};
|
|
2438
|
+
void tail.then(cleanup, cleanup);
|
|
2439
|
+
return operation;
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
/** One queued lifecycle operation, run when its turn in the queue comes. */
|
|
2443
|
+
private async runLifecycleOperation(
|
|
2444
|
+
serverId: string,
|
|
2445
|
+
channelId: string,
|
|
2446
|
+
removals: number,
|
|
2447
|
+
options?: { history?: ChannelHistoryRequest },
|
|
2448
|
+
): Promise<{ open: boolean; result?: ChannelsOpenResult }> {
|
|
2449
|
+
const channelKey = `${serverId}:${channelId}`;
|
|
2450
|
+
const lifecycleKey = this.lifecycleKey(serverId, channelId);
|
|
2451
|
+
try {
|
|
2452
|
+
let attempts = 0;
|
|
2453
|
+
for (;;) {
|
|
2454
|
+
if ((this.serverRemovals.get(serverId) ?? 0) !== removals) {
|
|
2455
|
+
throw new ChannelLifecycleCancelledError(
|
|
2456
|
+
`Channel ${channelId}: its server's registrations were removed while this operation was queued`,
|
|
2457
|
+
);
|
|
2458
|
+
}
|
|
2459
|
+
const entry = this.channels.get(channelKey);
|
|
2460
|
+
if (!entry) throw new Error(`Channel is no longer registered: ${channelId}`);
|
|
2461
|
+
const server = this.serverRegistry.getServer(serverId);
|
|
2462
|
+
if (!server) throw new Error(`Server not found: ${serverId}`);
|
|
2463
|
+
if (!CapabilityGrant.of(server).has('channels.lifecycle')) {
|
|
2464
|
+
throw new Error(`channels.lifecycle not in "${serverId}"'s effective grant (§14.1)`);
|
|
2465
|
+
}
|
|
2466
|
+
const descriptor = entry.descriptor;
|
|
2467
|
+
const desired = this.getDesiredState(serverId, channelId);
|
|
2468
|
+
const open = desired === 'open' || desired === 'tuned-out';
|
|
2469
|
+
// Per-RPC timeouts do not bound a stream of promptly successful but
|
|
2470
|
+
// continually superseded receipts. Bound this whole operation, then
|
|
2471
|
+
// release its queue slot and surface the failure to delivery joiners.
|
|
2472
|
+
// Its open flag needs no change here: every retry follows a replaced
|
|
2473
|
+
// registration, a retarget (already invalidated, whether the old
|
|
2474
|
+
// target's RPC was answered or failed), or a receipt for this very
|
|
2475
|
+
// target.
|
|
2476
|
+
if (attempts >= MAX_CHANNEL_LIFECYCLE_ATTEMPTS) {
|
|
2477
|
+
const error = new ChannelLifecycleConvergenceError(
|
|
2478
|
+
`Channel ${channelId} did not converge after ${attempts} lifecycle attempts; its target or desired state kept changing`,
|
|
2479
|
+
);
|
|
2480
|
+
this.emitTraceFn({
|
|
2481
|
+
type: 'mcpl:channel-reconcile-failed',
|
|
2482
|
+
serverId, channelId, desired, attempts, error: error.message,
|
|
2483
|
+
});
|
|
2484
|
+
throw error;
|
|
2485
|
+
}
|
|
2486
|
+
attempts++;
|
|
2487
|
+
const sameTarget = () => this.serverRegistry.getServer(serverId) === server &&
|
|
2488
|
+
sameChannelTarget(entry.descriptor, descriptor);
|
|
2489
|
+
let result: ChannelsOpenResult | undefined;
|
|
2490
|
+
try {
|
|
2491
|
+
if (open) {
|
|
2492
|
+
result = await server.sendChannelsOpen({
|
|
2493
|
+
channelId, type: descriptor.type, address: descriptor.address,
|
|
2494
|
+
...(options?.history ? { history: options.history } : {}),
|
|
2495
|
+
});
|
|
2496
|
+
} else {
|
|
2497
|
+
await server.sendChannelsClose({ channelId });
|
|
2498
|
+
}
|
|
2499
|
+
} catch (error) {
|
|
2500
|
+
// An RPC can fail after the server acted, so a failure for the
|
|
2501
|
+
// current target leaves its outcome unknown: it is marked
|
|
2502
|
+
// unconfirmed with open unchanged, which keeps a confirmed-open
|
|
2503
|
+
// channel usable after a failed backscroll (the mark is read only
|
|
2504
|
+
// while not open). A failure from an older connection or target
|
|
2505
|
+
// also withdraws the open flag.
|
|
2506
|
+
if (this.channels.get(channelKey) === entry) {
|
|
2507
|
+
if (sameTarget()) this.unconfirmedChannelTargets.add(entry);
|
|
2508
|
+
else this.invalidateTransportConfirmation(entry);
|
|
2509
|
+
}
|
|
2510
|
+
// A retarget while this RPC was out queued nothing for the new
|
|
2511
|
+
// target, since handleChanged leaves that to this operation. So a
|
|
2512
|
+
// failure for the old target says nothing about the new one, and a
|
|
2513
|
+
// channel still decided open or tuned out tries its new target,
|
|
2514
|
+
// within the attempt limit.
|
|
2515
|
+
if (this.channels.get(channelKey) === entry && !sameChannelTarget(entry.descriptor, descriptor)) {
|
|
2516
|
+
const now = this.getDesiredState(serverId, channelId);
|
|
2517
|
+
if (now === 'open' || now === 'tuned-out') continue;
|
|
2518
|
+
}
|
|
2519
|
+
throw error;
|
|
2520
|
+
}
|
|
2521
|
+
// A server can answer for a channel other than the one named: one
|
|
2522
|
+
// that falls back to its first channel of the type for an id it no
|
|
2523
|
+
// longer knows opens that channel and returns its history. Whatever
|
|
2524
|
+
// becomes of this receipt, the server did that: say so, and undo the
|
|
2525
|
+
// open it made if the host wants that channel closed.
|
|
2526
|
+
const substitute = open ? substitutedChannel(result, channelId) : undefined;
|
|
2527
|
+
if (substitute !== undefined) {
|
|
2528
|
+
// Each id prints through logValue, as the rejection lines' ids do,
|
|
2529
|
+
// so an odd one can't add a line or a field.
|
|
2530
|
+
const answered = logValue(substitute.kind === 'other' ? substitute.id : substitute.value);
|
|
2531
|
+
console.error(
|
|
2532
|
+
`[channel-open-substituted] server=${serverId} requested=${logValue(channelId)} answered=${answered}`,
|
|
2533
|
+
);
|
|
2534
|
+
this.emitTraceFn({ type: 'mcpl:channel-open-substituted', serverId, channelId, answeredFor: answered });
|
|
2535
|
+
// A malformed id names nothing registered here, so there is
|
|
2536
|
+
// nothing to close.
|
|
2537
|
+
if (substitute.kind === 'other') this.closeSubstitutedChannel(serverId, substitute.id);
|
|
2538
|
+
}
|
|
2539
|
+
// Never apply an old receipt to the current target, even transiently:
|
|
2540
|
+
// a corrective open might fail before that stale bit is replaced.
|
|
2541
|
+
if (this.channels.get(channelKey) !== entry) continue;
|
|
2542
|
+
if (!sameTarget()) {
|
|
2543
|
+
this.invalidateTransportConfirmation(entry);
|
|
2544
|
+
continue;
|
|
2545
|
+
}
|
|
2546
|
+
// A current receipt for another channel confirms nothing about this
|
|
2547
|
+
// one, and its history is another channel's. So the open fails,
|
|
2548
|
+
// nothing in the answer is used, and this channel's open state is
|
|
2549
|
+
// unconfirmed: a server that doesn't resolve this id may not resolve
|
|
2550
|
+
// its sends either.
|
|
2551
|
+
if (substitute !== undefined) {
|
|
2552
|
+
this.invalidateTransportConfirmation(entry);
|
|
2553
|
+
throw new ChannelOpenSubstitutionError(
|
|
2554
|
+
`Server "${serverId}" answered channels/open for ${channelId} with ` +
|
|
2555
|
+
(substitute.kind === 'other' ? 'a different channel' : 'a channel id that is not a string') +
|
|
2556
|
+
`; ${channelId} is not marked open, and nothing in the answer is used`,
|
|
2557
|
+
);
|
|
2558
|
+
}
|
|
2559
|
+
entry.open = open;
|
|
2560
|
+
this.unconfirmedChannelTargets.delete(entry);
|
|
2561
|
+
const now = this.getDesiredState(serverId, channelId);
|
|
2562
|
+
if (open !== (now === 'open' || now === 'tuned-out')) continue;
|
|
2563
|
+
return { open, result };
|
|
2564
|
+
}
|
|
2565
|
+
} finally {
|
|
2566
|
+
// Synchronous with the operation's last look at its target: from here
|
|
2567
|
+
// on, nothing it does will open a new target.
|
|
2568
|
+
const left = (this.lifecycleOutstanding.get(lifecycleKey) ?? 1) - 1;
|
|
2569
|
+
if (left > 0) this.lifecycleOutstanding.set(lifecycleKey, left);
|
|
2570
|
+
else this.lifecycleOutstanding.delete(lifecycleKey);
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
/**
|
|
2575
|
+
* A server that answered an open with another channel really opened that
|
|
2576
|
+
* one. When it's registered here and the host wants it closed, close it
|
|
2577
|
+
* through its own lifecycle queue: otherwise its traffic would reach the
|
|
2578
|
+
* resident, marked open by its first incoming message. A channel the host
|
|
2579
|
+
* wants open is left alone, and a close can't be answered for another
|
|
2580
|
+
* channel, so this never starts another substitution. An id the host
|
|
2581
|
+
* doesn't know needs nothing: §14.5 refuses its traffic.
|
|
2582
|
+
*/
|
|
2583
|
+
private closeSubstitutedChannel(serverId: string, channelId: string): void {
|
|
2584
|
+
if (!this.channels.has(`${serverId}:${channelId}`)) return;
|
|
2585
|
+
if (this.getDesiredState(serverId, channelId) !== 'closed') return;
|
|
2586
|
+
this.applyDesiredChannelState(serverId, channelId).catch((err) => {
|
|
2587
|
+
// Removal while queued is cancellation, as in reconcileChannels, and
|
|
2588
|
+
// exhausted convergence emitted its own diagnostic.
|
|
2589
|
+
if (!this.channels.has(`${serverId}:${channelId}`) ||
|
|
2590
|
+
err instanceof ChannelLifecycleCancelledError ||
|
|
2591
|
+
err instanceof ChannelLifecycleConvergenceError) return;
|
|
2592
|
+
this.emitTraceFn({
|
|
2593
|
+
type: 'mcpl:channel-reconcile-failed',
|
|
2594
|
+
serverId, channelId, desired: 'closed', error: (err as Error).message,
|
|
2595
|
+
});
|
|
2596
|
+
});
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
/**
|
|
2600
|
+
* Join outstanding lifecycle work without recording new intent. Retargeting
|
|
2601
|
+
* may have invalidated open confirmation while the decision remains pending.
|
|
2602
|
+
* A close that settles during the wait remains authoritative; waiting for
|
|
2603
|
+
* it is not an instruction to open the channel again.
|
|
2604
|
+
*/
|
|
2605
|
+
private async waitForOpenChannel(entry: ChannelEntry): Promise<ChannelEntry> {
|
|
2606
|
+
const lifecycleKey = this.lifecycleKey(entry.serverId, entry.descriptor.id);
|
|
2607
|
+
for (;;) {
|
|
2608
|
+
const pending = this.channelLifecycleTails.get(lifecycleKey);
|
|
2609
|
+
if (!pending) break;
|
|
2610
|
+
try {
|
|
2611
|
+
await pending;
|
|
2612
|
+
} catch (error) {
|
|
2613
|
+
// A redundant backscroll failure can leave a known-open channel
|
|
2614
|
+
// usable. Exhausted convergence is different: it never established
|
|
2615
|
+
// the current target, so joined delivery must fail visibly.
|
|
2616
|
+
if (error instanceof ChannelLifecycleConvergenceError) throw error;
|
|
2617
|
+
}
|
|
2618
|
+
// A settled tail releases its own key. One still current after it
|
|
2619
|
+
// settled would be awaited again without ever yielding to the event
|
|
2620
|
+
// loop, so stop waiting rather than spin.
|
|
2621
|
+
if (this.channelLifecycleTails.get(lifecycleKey) === pending) break;
|
|
2622
|
+
}
|
|
2623
|
+
const current = this.channels.get(`${entry.serverId}:${entry.descriptor.id}`);
|
|
2624
|
+
if (!current) throw new Error('Channel registration was removed while delivery waited');
|
|
2625
|
+
if (!current.open || this.getDesiredState(current.serverId, current.descriptor.id) === 'closed') {
|
|
2626
|
+
throw new Error('Channel is not confirmed open for delivery, or was selected for closure while delivery waited');
|
|
2627
|
+
}
|
|
2628
|
+
return current;
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2184
2631
|
private async reconcileChannels(
|
|
2185
2632
|
serverId: string,
|
|
2186
2633
|
channels: ChannelDescriptor[],
|
|
@@ -2206,54 +2653,18 @@ export class ChannelRegistry {
|
|
|
2206
2653
|
// first boot on a new policy doesn't produce one notice per channel.
|
|
2207
2654
|
const policyOpened: Array<{ channelId: string; label?: string }> = [];
|
|
2208
2655
|
|
|
2656
|
+
// Record all admissions before waiting on any channel. A slow first RPC
|
|
2657
|
+
// must not delay intent for the rest of this registration batch.
|
|
2209
2658
|
for (const channel of channels) {
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
const key = `${serverId}:${channel.id}`;
|
|
2214
|
-
const desired = this.getDesiredState(serverId, channel.id);
|
|
2215
|
-
const entry = this.channels.get(key);
|
|
2216
|
-
// Tuned-out sits on the OPEN side of reconcile: traffic must keep
|
|
2217
|
-
// arriving (the subconscious reads it); only main's wake/visibility
|
|
2218
|
-
// is diverted, downstream at ingestion.
|
|
2219
|
-
if (desired !== 'open' && desired !== 'tuned-out') {
|
|
2220
|
-
try {
|
|
2221
|
-
await server.sendChannelsClose({ channelId: channel.id });
|
|
2222
|
-
if (entry) entry.open = false;
|
|
2223
|
-
} catch (err) {
|
|
2224
|
-
this.emitTraceFn({
|
|
2225
|
-
type: 'mcpl:channel-reconcile-failed',
|
|
2226
|
-
serverId,
|
|
2227
|
-
channelId: channel.id,
|
|
2228
|
-
desired,
|
|
2229
|
-
error: (err as Error).message,
|
|
2230
|
-
});
|
|
2231
|
-
}
|
|
2232
|
-
continue;
|
|
2233
|
-
}
|
|
2234
|
-
|
|
2235
|
-
try {
|
|
2236
|
-
await server.sendChannelsOpen({
|
|
2237
|
-
channelId: channel.id,
|
|
2238
|
-
type: channel.type,
|
|
2239
|
-
address: channel.address,
|
|
2240
|
-
});
|
|
2241
|
-
if (entry) entry.open = true;
|
|
2242
|
-
} catch (err) {
|
|
2243
|
-
if (entry) entry.open = false;
|
|
2244
|
-
this.emitTraceFn({
|
|
2245
|
-
type: 'mcpl:channel-reconcile-failed',
|
|
2246
|
-
serverId,
|
|
2247
|
-
channelId: channel.id,
|
|
2248
|
-
desired,
|
|
2249
|
-
error: (err as Error).message,
|
|
2250
|
-
});
|
|
2659
|
+
const entry = this.channels.get(`${serverId}:${channel.id}`);
|
|
2660
|
+
if (entry && this.ensureInitialDesiredState(serverId, entry.descriptor)) {
|
|
2661
|
+
policyOpened.push({ channelId: channel.id, label: entry.descriptor.label });
|
|
2251
2662
|
}
|
|
2252
2663
|
}
|
|
2253
2664
|
|
|
2254
|
-
// Announce
|
|
2255
|
-
//
|
|
2256
|
-
//
|
|
2665
|
+
// Announce the durable admission decision before awaiting transport.
|
|
2666
|
+
// This is intent, not confirmation that an open RPC succeeded. It stays
|
|
2667
|
+
// truthful if the first attempt fails, and a retry needs no new admission.
|
|
2257
2668
|
if (policyOpened.length > 0) {
|
|
2258
2669
|
try {
|
|
2259
2670
|
this.onChannelAutoOpened?.({
|
|
@@ -2265,6 +2676,30 @@ export class ChannelRegistry {
|
|
|
2265
2676
|
console.error('onChannelAutoOpened (policy) failed:', err);
|
|
2266
2677
|
}
|
|
2267
2678
|
}
|
|
2679
|
+
|
|
2680
|
+
// Each channel has its own lifecycle queue, so a held RPC for one (up to
|
|
2681
|
+
// its timeout, times its attempts) doesn't delay its siblings.
|
|
2682
|
+
await Promise.allSettled(channels.map(async (channel) => {
|
|
2683
|
+
if (!this.channels.has(`${serverId}:${channel.id}`)) return;
|
|
2684
|
+
try {
|
|
2685
|
+
await this.applyDesiredChannelState(serverId, channel.id);
|
|
2686
|
+
} catch (err) {
|
|
2687
|
+
// Removal while queued is cancellation, not a failed subscription.
|
|
2688
|
+
// Exhausted convergence and a substituted answer already emitted
|
|
2689
|
+
// their own diagnostics.
|
|
2690
|
+
if (!this.channels.has(`${serverId}:${channel.id}`) ||
|
|
2691
|
+
err instanceof ChannelLifecycleCancelledError ||
|
|
2692
|
+
err instanceof ChannelLifecycleConvergenceError ||
|
|
2693
|
+
err instanceof ChannelOpenSubstitutionError) return;
|
|
2694
|
+
this.emitTraceFn({
|
|
2695
|
+
type: 'mcpl:channel-reconcile-failed',
|
|
2696
|
+
serverId,
|
|
2697
|
+
channelId: channel.id,
|
|
2698
|
+
desired: this.getDesiredState(serverId, channel.id),
|
|
2699
|
+
error: (err as Error).message,
|
|
2700
|
+
});
|
|
2701
|
+
}
|
|
2702
|
+
}));
|
|
2268
2703
|
}
|
|
2269
2704
|
|
|
2270
2705
|
// ==========================================================================
|
|
@@ -2386,37 +2821,39 @@ export class ChannelRegistry {
|
|
|
2386
2821
|
}
|
|
2387
2822
|
|
|
2388
2823
|
/**
|
|
2389
|
-
*
|
|
2390
|
-
*
|
|
2391
|
-
*
|
|
2392
|
-
*
|
|
2393
|
-
*
|
|
2824
|
+
* Record durable desired-open intent, then queue the channel's lifecycle
|
|
2825
|
+
* work (applyDesiredChannelState), which tells the server to subscribe and
|
|
2826
|
+
* marks the live entry open on a receipt for its current target. Every
|
|
2827
|
+
* open the agent or its speech asks for — the channel_open tool, delivery
|
|
2828
|
+
* into a closed locus, an explicit send into a closed channel — comes
|
|
2829
|
+
* here. Reconciliation queues that work directly, as the closes do
|
|
2830
|
+
* (channel_close, a decline, and the close of a substituted channel).
|
|
2394
2831
|
* Desired state is recorded BEFORE the server round-trip: intent sticks
|
|
2395
2832
|
* even if the subscribe fails, and reconciliation retries later.
|
|
2833
|
+
* Automatic delivery needs open transport, not a change of attention:
|
|
2834
|
+
* preserve tune-out epochs both before and during the round-trip.
|
|
2835
|
+
*
|
|
2836
|
+
* `opened` says whether this call turned a closed channel open, which is
|
|
2837
|
+
* what the delivery and reply notices tell the resident. Reconfirming the
|
|
2838
|
+
* transport of a channel already decided open or tuned out isn't one.
|
|
2396
2839
|
*/
|
|
2397
2840
|
private async openChannelNow(
|
|
2398
2841
|
entry: ChannelEntry,
|
|
2399
|
-
source:
|
|
2842
|
+
source: 'agent-tool' | 'opened-by-reply' | 'opened-by-delivery',
|
|
2400
2843
|
history?: ChannelHistoryRequest,
|
|
2401
|
-
): Promise<ChannelsOpenResult> {
|
|
2402
|
-
|
|
2403
|
-
const
|
|
2404
|
-
|
|
2405
|
-
|
|
2844
|
+
): Promise<{ result: ChannelsOpenResult; opened: boolean }> {
|
|
2845
|
+
const transportOnly = source !== 'agent-tool';
|
|
2846
|
+
const before = this.getDesiredState(entry.serverId, entry.descriptor.id);
|
|
2847
|
+
const opened = before !== 'open' && before !== 'tuned-out';
|
|
2848
|
+
if (!transportOnly || before !== 'tuned-out') {
|
|
2849
|
+
this.setDesiredState(entry.serverId, entry.descriptor.id, 'open', source);
|
|
2406
2850
|
}
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
throw new Error(
|
|
2851
|
+
const applied = await this.applyDesiredChannelState(entry.serverId, entry.descriptor.id, { history });
|
|
2852
|
+
const desired = this.getDesiredState(entry.serverId, entry.descriptor.id);
|
|
2853
|
+
if (!applied.open || (desired !== 'open' && !(transportOnly && desired === 'tuned-out'))) {
|
|
2854
|
+
throw new Error('Channel open was superseded by a newer lifecycle decision');
|
|
2411
2855
|
}
|
|
2412
|
-
|
|
2413
|
-
channelId: entry.descriptor.id,
|
|
2414
|
-
type: entry.descriptor.type,
|
|
2415
|
-
address: entry.descriptor.address,
|
|
2416
|
-
...(history ? { history } : {}),
|
|
2417
|
-
});
|
|
2418
|
-
entry.open = true;
|
|
2419
|
-
return result;
|
|
2856
|
+
return { result: applied.result!, opened };
|
|
2420
2857
|
}
|
|
2421
2858
|
|
|
2422
2859
|
/**
|
|
@@ -2627,19 +3064,25 @@ export class ChannelRegistry {
|
|
|
2627
3064
|
}
|
|
2628
3065
|
if (matches.length === 0) return { status: 'unknown-channel' };
|
|
2629
3066
|
if (matches.length > 1) return { status: 'ambiguous' };
|
|
2630
|
-
|
|
2631
|
-
const resolved = { channelId: entry.descriptor.id, label: entry.descriptor.label };
|
|
2632
|
-
if (entry.open && this.getDesiredState(entry.serverId, entry.descriptor.id) === 'open') {
|
|
2633
|
-
return { status: 'already-open', ...resolved };
|
|
2634
|
-
}
|
|
3067
|
+
let entry = matches[0]!;
|
|
2635
3068
|
try {
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
3069
|
+
if (this.hasPendingLifecycle(entry)) {
|
|
3070
|
+
entry = await this.waitForOpenChannel(entry);
|
|
3071
|
+
}
|
|
3072
|
+
const desired = this.getDesiredState(entry.serverId, entry.descriptor.id);
|
|
3073
|
+
if (entry.open && (desired === 'open' || desired === 'tuned-out')) {
|
|
3074
|
+
return { status: 'already-open', channelId: entry.descriptor.id, label: entry.descriptor.label };
|
|
3075
|
+
}
|
|
3076
|
+
const { opened } = await this.openChannelNow(entry, 'opened-by-reply');
|
|
3077
|
+
entry = await this.waitForOpenChannel(entry);
|
|
3078
|
+
if (opened) {
|
|
3079
|
+
this.emitTraceFn({
|
|
3080
|
+
type: 'mcpl:channel-opened-by-send',
|
|
3081
|
+
serverId: entry.serverId,
|
|
3082
|
+
channelId: entry.descriptor.id,
|
|
3083
|
+
});
|
|
3084
|
+
}
|
|
3085
|
+
return { status: opened ? 'opened' : 'already-open', channelId: entry.descriptor.id, label: entry.descriptor.label };
|
|
2643
3086
|
} catch (err) {
|
|
2644
3087
|
this.emitTraceFn({
|
|
2645
3088
|
type: 'mcpl:channel-open-failed',
|
|
@@ -2647,7 +3090,9 @@ export class ChannelRegistry {
|
|
|
2647
3090
|
channelId: entry.descriptor.id,
|
|
2648
3091
|
error: (err as Error).message,
|
|
2649
3092
|
});
|
|
2650
|
-
|
|
3093
|
+
const current = this.channels.get(`${entry.serverId}:${entry.descriptor.id}`);
|
|
3094
|
+
return { status: 'open-failed', channelId: entry.descriptor.id,
|
|
3095
|
+
...(current ? { label: current.descriptor.label } : {}) };
|
|
2651
3096
|
}
|
|
2652
3097
|
}
|
|
2653
3098
|
|
|
@@ -2670,11 +3115,13 @@ export class ChannelRegistry {
|
|
|
2670
3115
|
// A tuned-out channel is transport-open but attention-diverted; a plain
|
|
2671
3116
|
// open would silently discard the epoch (and its pending backlog dump).
|
|
2672
3117
|
// Cancelling is the tune-out coordinator's flow, reached via its own
|
|
2673
|
-
// tool — refuse with the pointer rather than eat the state.
|
|
3118
|
+
// tool — refuse with the pointer rather than eat the state. As an error
|
|
3119
|
+
// result, the agent reads the pointer itself; `data` names the refusal
|
|
3120
|
+
// for an in-process caller.
|
|
2674
3121
|
if (this.getDesiredState(entry.serverId, input.channelId) === 'tuned-out') {
|
|
2675
3122
|
return {
|
|
2676
3123
|
success: false,
|
|
2677
|
-
isError:
|
|
3124
|
+
isError: true,
|
|
2678
3125
|
error:
|
|
2679
3126
|
`Channel ${input.channelId} is tuned out. Cancel the tune-out ` +
|
|
2680
3127
|
`(tune_out with mode: 'cancel') to resume normal attention — ` +
|
|
@@ -2684,7 +3131,7 @@ export class ChannelRegistry {
|
|
|
2684
3131
|
}
|
|
2685
3132
|
|
|
2686
3133
|
const alreadyDesiredOpen = this.getDesiredState(entry.serverId, input.channelId) === 'open';
|
|
2687
|
-
if (entry.open && alreadyDesiredOpen && !input.backscroll) {
|
|
3134
|
+
if (entry.open && alreadyDesiredOpen && !input.backscroll && !this.hasPendingLifecycle(entry)) {
|
|
2688
3135
|
return {
|
|
2689
3136
|
success: true,
|
|
2690
3137
|
data: { channelId: input.channelId, status: 'already open' },
|
|
@@ -2698,7 +3145,7 @@ export class ChannelRegistry {
|
|
|
2698
3145
|
...(input.beforeMessageId ? { beforeMessageId: input.beforeMessageId } : {}),
|
|
2699
3146
|
}
|
|
2700
3147
|
: undefined;
|
|
2701
|
-
const result = await this.openChannelNow(entry, 'agent-tool', history);
|
|
3148
|
+
const { result } = await this.openChannelNow(entry, 'agent-tool', history);
|
|
2702
3149
|
return {
|
|
2703
3150
|
success: true,
|
|
2704
3151
|
data: {
|
|
@@ -2709,9 +3156,18 @@ export class ChannelRegistry {
|
|
|
2709
3156
|
},
|
|
2710
3157
|
};
|
|
2711
3158
|
} catch (err) {
|
|
3159
|
+
// The decision was recorded before the round-trip and outlives its
|
|
3160
|
+
// failure. Say so: otherwise a later delivery that opens the channel
|
|
3161
|
+
// would be the resident's first sign that it was still wanted open.
|
|
3162
|
+
const current = this.channels.get(`${entry.serverId}:${input.channelId}`);
|
|
3163
|
+
const stillSelected = current !== undefined && !current.open &&
|
|
3164
|
+
this.getDesiredState(entry.serverId, input.channelId) === 'open';
|
|
2712
3165
|
return {
|
|
2713
3166
|
success: false,
|
|
2714
|
-
error: `Failed to open channel: ${(err as Error).message}
|
|
3167
|
+
error: `Failed to open channel: ${(err as Error).message}` +
|
|
3168
|
+
(stillSelected
|
|
3169
|
+
? ' (it stays selected for opening: a later delivery or reconnect tries again, and channel_close withdraws that)'
|
|
3170
|
+
: ''),
|
|
2715
3171
|
isError: true,
|
|
2716
3172
|
};
|
|
2717
3173
|
}
|
|
@@ -2747,7 +3203,8 @@ export class ChannelRegistry {
|
|
|
2747
3203
|
}
|
|
2748
3204
|
|
|
2749
3205
|
const alreadyDesiredClosed = this.getDesiredState(entry.serverId, input.channelId) === 'closed';
|
|
2750
|
-
if (!entry.open && alreadyDesiredClosed)
|
|
3206
|
+
if (!entry.open && alreadyDesiredClosed && !this.hasPendingLifecycle(entry) &&
|
|
3207
|
+
!this.unconfirmedChannelTargets.has(entry)) {
|
|
2751
3208
|
return {
|
|
2752
3209
|
success: true,
|
|
2753
3210
|
data: { channelId: input.channelId, status: 'already closed' },
|
|
@@ -2816,8 +3273,10 @@ export class ChannelRegistry {
|
|
|
2816
3273
|
isError: true,
|
|
2817
3274
|
};
|
|
2818
3275
|
}
|
|
2819
|
-
await
|
|
2820
|
-
|
|
3276
|
+
const applied = await this.applyDesiredChannelState(entry.serverId, input.channelId);
|
|
3277
|
+
if (applied.open || this.getDesiredState(entry.serverId, input.channelId) !== 'closed') {
|
|
3278
|
+
throw new Error('Channel close was superseded by a newer lifecycle decision');
|
|
3279
|
+
}
|
|
2821
3280
|
return {
|
|
2822
3281
|
success: true,
|
|
2823
3282
|
data: { channelId: input.channelId, status: 'closed' },
|
|
@@ -2845,7 +3304,7 @@ export class ChannelRegistry {
|
|
|
2845
3304
|
if (entry.open || this.getDesiredState(entry.serverId, input.channelId) === 'open') {
|
|
2846
3305
|
return {
|
|
2847
3306
|
success: false,
|
|
2848
|
-
error: `Channel is already open: ${input.channelId}. Use channel_close to
|
|
3307
|
+
error: `Channel is already open: ${input.channelId}. Use channel_close to close it.`,
|
|
2849
3308
|
isError: true,
|
|
2850
3309
|
};
|
|
2851
3310
|
}
|
|
@@ -2883,6 +3342,23 @@ export class ChannelRegistry {
|
|
|
2883
3342
|
}
|
|
2884
3343
|
}
|
|
2885
3344
|
|
|
3345
|
+
// Where the host directs the lifecycle, a decline goes through the
|
|
3346
|
+
// lifecycle queue like any closed decision, unless a receipt confirmed
|
|
3347
|
+
// the transport closed with nothing pending: an unconfirmed transport may
|
|
3348
|
+
// be open, and pending work must settle on this decision. A failed close
|
|
3349
|
+
// is reported; the decision stands.
|
|
3350
|
+
let closeError: string | undefined;
|
|
3351
|
+
const current = this.channels.get(`${entry.serverId}:${input.channelId}`);
|
|
3352
|
+
if (current && this.directsLifecycle(entry.serverId) &&
|
|
3353
|
+
(this.unconfirmedChannelTargets.has(current) || this.hasPendingLifecycle(current))) {
|
|
3354
|
+
try {
|
|
3355
|
+
const applied = await this.applyDesiredChannelState(entry.serverId, input.channelId);
|
|
3356
|
+
if (applied.open) closeError = 'the close was superseded by a newer lifecycle decision';
|
|
3357
|
+
} catch (error) {
|
|
3358
|
+
closeError = (error as Error).message;
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
|
|
2886
3362
|
this.appendLifecycleEvent({
|
|
2887
3363
|
kind: 'invitation-declined',
|
|
2888
3364
|
serverId: entry.serverId,
|
|
@@ -2899,6 +3375,7 @@ export class ChannelRegistry {
|
|
|
2899
3375
|
acknowledged,
|
|
2900
3376
|
...(representation ? { representation } : {}),
|
|
2901
3377
|
...(acknowledgmentError ? { acknowledgmentError } : {}),
|
|
3378
|
+
...(closeError ? { closeError: `Failed to close the channel's transport: ${closeError}` } : {}),
|
|
2902
3379
|
},
|
|
2903
3380
|
};
|
|
2904
3381
|
}
|
|
@@ -2907,7 +3384,8 @@ export class ChannelRegistry {
|
|
|
2907
3384
|
* Handle the synthesized `think` tool — a private reasoning scratchpad. It
|
|
2908
3385
|
* sends nothing, and (unlike before) does NOT silence the turn: trailing
|
|
2909
3386
|
* plain text is still routed as the reply. The thought stays in the agent's
|
|
2910
|
-
* own context/chronicle. To deliberately not reply,
|
|
3387
|
+
* own context/chronicle. To deliberately not reply, an agent that may call
|
|
3388
|
+
* skip_reply uses it.
|
|
2911
3389
|
*/
|
|
2912
3390
|
private handleToolThink(input: { content?: string }): ToolResult {
|
|
2913
3391
|
return {
|
|
@@ -2916,10 +3394,12 @@ export class ChannelRegistry {
|
|
|
2916
3394
|
// tool_use block; don't duplicate it in the result.
|
|
2917
3395
|
data: {
|
|
2918
3396
|
noted: true,
|
|
3397
|
+
// No tool to end the turn with: this handler can't see whether the
|
|
3398
|
+
// caller may call skip_reply, and think's own description says so
|
|
3399
|
+
// where it may.
|
|
2919
3400
|
note:
|
|
2920
3401
|
'Thought recorded (private — not sent anywhere). Same-round text routing depends on ' +
|
|
2921
|
-
'your current same_round_think_text_policy; use agent_settings get to inspect it,
|
|
2922
|
-
'call skip_reply to end the turn without replying.',
|
|
3402
|
+
'your current same_round_think_text_policy; use agent_settings get to inspect it.',
|
|
2923
3403
|
},
|
|
2924
3404
|
};
|
|
2925
3405
|
}
|
|
@@ -2953,14 +3433,15 @@ export class ChannelRegistry {
|
|
|
2953
3433
|
* tool, so any trailing prose this turn is NOT posted. Replaces the old
|
|
2954
3434
|
* overloaded use of `think` for staying silent.
|
|
2955
3435
|
*/
|
|
2956
|
-
private handleToolSkipReply(input: { reason?: string; wake_in_seconds?:
|
|
3436
|
+
private handleToolSkipReply(input: { reason?: string; wake_in_seconds?: unknown }): ToolResult {
|
|
2957
3437
|
// The self-wake itself is armed by the framework (which owns the
|
|
2958
|
-
// EventGate) BEFORE
|
|
2959
|
-
//
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
3438
|
+
// EventGate) BEFORE its dispatch reaches this handler, from the same
|
|
3439
|
+
// reading, and the framework refuses a wake the host can't set before
|
|
3440
|
+
// dispatching. A caller of this registry's handleChannelToolCall
|
|
3441
|
+
// (AgentFramework.channels) goes past both, so its note names a wake
|
|
3442
|
+
// nothing armed.
|
|
3443
|
+
const wake = readSkipReplyWake(input);
|
|
3444
|
+
const selfWake = wake.kind === 'wake' ? wake.seconds : undefined;
|
|
2964
3445
|
return {
|
|
2965
3446
|
success: true,
|
|
2966
3447
|
// The note says "ended the turn" — make it TRUE. Without endTurn the
|
|
@@ -3053,10 +3534,220 @@ export class ChannelRegistry {
|
|
|
3053
3534
|
|
|
3054
3535
|
resolveLocus(conversationId: string): string | null {
|
|
3055
3536
|
const home = this.homeChannelResolver?.(conversationId);
|
|
3056
|
-
return home ?? this.activeChannelResolver?.(conversationId) ?? this.
|
|
3537
|
+
return home ?? this.activeChannelResolver?.(conversationId) ?? this.globalFallbackFor(conversationId);
|
|
3057
3538
|
}
|
|
3058
3539
|
|
|
3059
|
-
|
|
3540
|
+
/** The process-global fallback channel for this conversation, or null when it takes none (takesGlobalFallback). */
|
|
3541
|
+
private globalFallbackFor(conversationId: string): string | null {
|
|
3542
|
+
return this.takesGlobalFallback?.(conversationId) === false ? null : this.defaultPublishChannel;
|
|
3543
|
+
}
|
|
3544
|
+
|
|
3545
|
+
/**
|
|
3546
|
+
* Resolve a publish target to exactly one registered channel: `serverId`
|
|
3547
|
+
* plus `channelId` when the caller has the server, otherwise a channel id
|
|
3548
|
+
* registered by exactly one server. A shared id is refused, never resolved
|
|
3549
|
+
* to whichever server registered it first.
|
|
3550
|
+
*/
|
|
3551
|
+
resolveDestination(
|
|
3552
|
+
target: { serverId?: string; channelId: string },
|
|
3553
|
+
): { destination: PublishDestination } | { error: string } {
|
|
3554
|
+
const entry = this.findExactEntry(target);
|
|
3555
|
+
if ('error' in entry) return entry;
|
|
3556
|
+
return {
|
|
3557
|
+
destination: {
|
|
3558
|
+
serverId: entry.entry.serverId,
|
|
3559
|
+
channelId: entry.entry.descriptor.id,
|
|
3560
|
+
...(entry.entry.descriptor.label ? { label: entry.entry.descriptor.label } : {}),
|
|
3561
|
+
},
|
|
3562
|
+
};
|
|
3563
|
+
}
|
|
3564
|
+
|
|
3565
|
+
private findExactEntry(
|
|
3566
|
+
target: { serverId?: string; channelId: string },
|
|
3567
|
+
): { entry: ChannelEntry } | { error: string } {
|
|
3568
|
+
// A supplied selector is checked, never read as omission: an empty or
|
|
3569
|
+
// non-string serverId would otherwise widen to "any server".
|
|
3570
|
+
if (typeof target.channelId !== 'string' || !target.channelId) {
|
|
3571
|
+
return { error: 'the destination names no channel id' };
|
|
3572
|
+
}
|
|
3573
|
+
if (target.serverId !== undefined && (typeof target.serverId !== 'string' || !target.serverId)) {
|
|
3574
|
+
return { error: 'serverId must name a server (omit it to resolve the channel id alone)' };
|
|
3575
|
+
}
|
|
3576
|
+
const matches = [...this.channels.values()].filter(
|
|
3577
|
+
(e) => e.descriptor.id === target.channelId && (!target.serverId || e.serverId === target.serverId),
|
|
3578
|
+
);
|
|
3579
|
+
if (matches.length === 0) {
|
|
3580
|
+
return {
|
|
3581
|
+
error: target.serverId
|
|
3582
|
+
? `no registered channel "${target.channelId}" on server "${target.serverId}"`
|
|
3583
|
+
: `no registered channel "${target.channelId}"`,
|
|
3584
|
+
};
|
|
3585
|
+
}
|
|
3586
|
+
if (matches.length > 1) {
|
|
3587
|
+
return {
|
|
3588
|
+
error: `channel id "${target.channelId}" is registered by more than one MCPL server; the destination must name its server`,
|
|
3589
|
+
};
|
|
3590
|
+
}
|
|
3591
|
+
return { entry: matches[0]! };
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3594
|
+
/**
|
|
3595
|
+
* Publish `text` to one registered channel and report what the attempt
|
|
3596
|
+
* established (PublishOutcome). The delivery executor behind plain speech
|
|
3597
|
+
* (deliverSpeech / routeSpeech) and resident resends of held drafts.
|
|
3598
|
+
* channel_publish (and publishForAgent, which shares its handler) resolves
|
|
3599
|
+
* and sends on its own path, and reads its answer through the same
|
|
3600
|
+
* publishReceiptOutcome and publishErrorOutcome.
|
|
3601
|
+
*
|
|
3602
|
+
* Resolution is exact (resolveDestination): `serverId` plus `channelId`
|
|
3603
|
+
* when the caller has the server, otherwise the channel id must be
|
|
3604
|
+
* registered by exactly one server (a shared id is refused, never routed
|
|
3605
|
+
* through whichever server came first). An omitted `serverId` means "not
|
|
3606
|
+
* in use"; a supplied one must be a non-empty server id, and an empty or
|
|
3607
|
+
* non-string selector is refused rather than read as omission. It is not
|
|
3608
|
+
* possible to send into a closed channel: a closed but registered
|
|
3609
|
+
* destination is opened first (`openSource` names why), once its server
|
|
3610
|
+
* and that server's channels.publish grant are known to take the send, so
|
|
3611
|
+
* a refused send opens nothing; and a failed open sends nothing.
|
|
3612
|
+
*
|
|
3613
|
+
* Only the connector's `delivered: true` confirms a post. `failed` means
|
|
3614
|
+
* nothing was posted: the attempt stopped before the request was written,
|
|
3615
|
+
* or the connector answered `delivered: false` and named no message (no
|
|
3616
|
+
* `messageId`, or null). Everything else is `unknown`: once the request
|
|
3617
|
+
* has been handed to the transport, an error response, a timeout, a lost
|
|
3618
|
+
* connection, or a missing, malformed or contradictory receipt (such as
|
|
3619
|
+
* `delivered: false` beside a message id, valid or not) means the post may
|
|
3620
|
+
* or may not exist, and the caller must not treat it as safe to retry
|
|
3621
|
+
* blindly.
|
|
3622
|
+
*/
|
|
3623
|
+
async publish(
|
|
3624
|
+
conversationId: string,
|
|
3625
|
+
text: string,
|
|
3626
|
+
target: {
|
|
3627
|
+
serverId?: string;
|
|
3628
|
+
channelId: string;
|
|
3629
|
+
/** Set by speech routing (deliverSpeech): opening a closed channel
|
|
3630
|
+
* for this publish is logged under `[routeSpeech]`, with speech
|
|
3631
|
+
* routing's other lines. */
|
|
3632
|
+
routed?: boolean;
|
|
3633
|
+
},
|
|
3634
|
+
openSource: 'opened-by-delivery' | 'opened-by-reply' = 'opened-by-delivery',
|
|
3635
|
+
): Promise<PublishOutcome> {
|
|
3636
|
+
const at = (): number => Date.now();
|
|
3637
|
+
const found = this.findExactEntry(target);
|
|
3638
|
+
if ('error' in found) return { status: 'failed', reason: found.error, at: at() };
|
|
3639
|
+
let entry = found.entry;
|
|
3640
|
+
const destination: PublishDestination = {
|
|
3641
|
+
serverId: entry.serverId,
|
|
3642
|
+
channelId: entry.descriptor.id,
|
|
3643
|
+
...(entry.descriptor.label ? { label: entry.descriptor.label } : {}),
|
|
3644
|
+
};
|
|
3645
|
+
|
|
3646
|
+
// A send that would be refused is refused before anything opens: a
|
|
3647
|
+
// closed channel opens only for words that can go out.
|
|
3648
|
+
const server = this.serverRegistry.getServer(entry.serverId);
|
|
3649
|
+
if (!server) {
|
|
3650
|
+
return { status: 'failed', destination, reason: `server "${entry.serverId}" not found`, at: at() };
|
|
3651
|
+
}
|
|
3652
|
+
// §14.1: channels/publish requires channels.publish in the grant. The
|
|
3653
|
+
// host not sending is the enforcement.
|
|
3654
|
+
if (!CapabilityGrant.of(server).has('channels.publish')) {
|
|
3655
|
+
return {
|
|
3656
|
+
status: 'failed',
|
|
3657
|
+
destination,
|
|
3658
|
+
reason: `channels.publish not in "${entry.serverId}"'s effective grant (§14.1)`,
|
|
3659
|
+
at: at(),
|
|
3660
|
+
};
|
|
3661
|
+
}
|
|
3662
|
+
|
|
3663
|
+
// Lifecycle work already queued for the channel settles first: words go
|
|
3664
|
+
// out only into the channel it leaves, and fail visibly if it leaves
|
|
3665
|
+
// none open.
|
|
3666
|
+
if (this.hasPendingLifecycle(entry)) {
|
|
3667
|
+
try {
|
|
3668
|
+
entry = await this.waitForOpenChannel(entry);
|
|
3669
|
+
} catch (err) {
|
|
3670
|
+
return {
|
|
3671
|
+
status: 'failed',
|
|
3672
|
+
destination,
|
|
3673
|
+
reason: `lifecycle work prevented delivery: ${connectorText(err instanceof Error ? err.message : String(err))}`,
|
|
3674
|
+
at: at(),
|
|
3675
|
+
};
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
|
|
3679
|
+
// INVARIANT: it is not possible to send into a closed channel. A send
|
|
3680
|
+
// into a closed-but-registered channel (the canonical case: a DM that
|
|
3681
|
+
// woke the agent, since DMs register closed) OPENS it first, so typing
|
|
3682
|
+
// indicators, reaction machinery and inbound forwarding come alive with
|
|
3683
|
+
// the words. If the open fails, nothing goes out: a half-alive delivery
|
|
3684
|
+
// is worse than a loud marker. A channel decided closed is engaged even
|
|
3685
|
+
// while its transport is still open (a close that failed), as a reply
|
|
3686
|
+
// would engage it, wherever an open can succeed. Without the lifecycle
|
|
3687
|
+
// grant it can't, and a send opens before it publishes, so there a
|
|
3688
|
+
// channel whose transport is open keeps publishing.
|
|
3689
|
+
const engagesDecidedClosed = this.getDesiredState(entry.serverId, entry.descriptor.id) === 'closed' &&
|
|
3690
|
+
CapabilityGrant.of(server).has('channels.lifecycle');
|
|
3691
|
+
if (!entry.open || engagesDecidedClosed) {
|
|
3692
|
+
try {
|
|
3693
|
+
const { opened } = await this.openChannelNow(entry, openSource);
|
|
3694
|
+
entry = await this.waitForOpenChannel(entry);
|
|
3695
|
+
if (opened) {
|
|
3696
|
+
if (target.routed) {
|
|
3697
|
+
console.error(
|
|
3698
|
+
`[routeSpeech] ${conversationId}: locus ${logValue(entry.descriptor.id)} was closed — opened by delivery (speech implies engagement)`,
|
|
3699
|
+
);
|
|
3700
|
+
}
|
|
3701
|
+
this.emitTraceFn({
|
|
3702
|
+
type: 'mcpl:channel-opened-by-send',
|
|
3703
|
+
serverId: entry.serverId,
|
|
3704
|
+
channelId: entry.descriptor.id,
|
|
3705
|
+
});
|
|
3706
|
+
if (openSource === 'opened-by-delivery') {
|
|
3707
|
+
try {
|
|
3708
|
+
this.onChannelAutoOpened?.({
|
|
3709
|
+
conversationId,
|
|
3710
|
+
serverId: entry.serverId,
|
|
3711
|
+
source: 'opened-by-delivery',
|
|
3712
|
+
channels: [{ channelId: entry.descriptor.id, label: entry.descriptor.label }],
|
|
3713
|
+
});
|
|
3714
|
+
} catch (err) {
|
|
3715
|
+
console.error('onChannelAutoOpened (delivery) failed:', err);
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
} catch (err) {
|
|
3720
|
+
return {
|
|
3721
|
+
status: 'failed',
|
|
3722
|
+
destination,
|
|
3723
|
+
reason: `channel is closed and open failed: ${connectorText(err instanceof Error ? err.message : String(err))}`,
|
|
3724
|
+
at: at(),
|
|
3725
|
+
};
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
3728
|
+
|
|
3729
|
+
let result: unknown;
|
|
3730
|
+
try {
|
|
3731
|
+
result = await server.sendChannelsPublish({
|
|
3732
|
+
conversationId,
|
|
3733
|
+
channelId: entry.descriptor.id,
|
|
3734
|
+
content: [{ type: 'text', text }],
|
|
3735
|
+
});
|
|
3736
|
+
} catch (err) {
|
|
3737
|
+
return publishErrorOutcome(err, destination, at());
|
|
3738
|
+
}
|
|
3739
|
+
return publishReceiptOutcome(result, destination, at());
|
|
3740
|
+
}
|
|
3741
|
+
|
|
3742
|
+
/**
|
|
3743
|
+
* Plain speech for one turn segment: publish `text` to the turn's frozen
|
|
3744
|
+
* locus and report the PublishOutcome. A failure (or an uncertain outcome)
|
|
3745
|
+
* is surfaced as a trace and through onRouteFailure, so the agent learns
|
|
3746
|
+
* its words did not (or may not have) reached anyone. An ephemeral run's
|
|
3747
|
+
* speech with no locus is the exception: it is its caller's result, so it
|
|
3748
|
+
* returns as not delivered and surfaces nothing.
|
|
3749
|
+
*/
|
|
3750
|
+
async deliverSpeech(
|
|
3060
3751
|
conversationId: string,
|
|
3061
3752
|
text: string,
|
|
3062
3753
|
/** The turn's FROZEN locus, snapshotted once by the caller (resolveLocus
|
|
@@ -3065,123 +3756,98 @@ export class ChannelRegistry {
|
|
|
3065
3756
|
* dispatch resolving live can read the NEXT turn's trigger state or a
|
|
3066
3757
|
* stale global default and land the reply in the wrong channel (item-3,
|
|
3067
3758
|
* PR #32, and the 2026-07-22 Sol DM misroute). Explicit `null` means
|
|
3068
|
-
* "this turn is pinned to no locus": fail loudly rather than guess
|
|
3759
|
+
* "this turn is pinned to no locus": fail loudly rather than guess,
|
|
3760
|
+
* except for an ephemeral run, whose speech is then its caller's
|
|
3761
|
+
* result. */
|
|
3069
3762
|
locusChannelId: string | null,
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3763
|
+
/** `heldOnFailure`: the caller holds a `failed` delivery's words as a
|
|
3764
|
+
* draft with its own notice, so no failure marker is written for it.
|
|
3765
|
+
* An `unknown` outcome still gets its marker. */
|
|
3766
|
+
opts?: { heldOnFailure?: boolean },
|
|
3767
|
+
): Promise<PublishOutcome> {
|
|
3768
|
+
const fail = (channelId: string | null, reason: string, outcome: PublishOutcome): PublishOutcome => {
|
|
3769
|
+
const status = outcome.status === 'unknown' ? 'unknown' : 'failed';
|
|
3770
|
+
const detail = outcome.detail !== undefined ? connectorText(outcome.detail) : undefined;
|
|
3771
|
+
const detailText = detail !== undefined ? `; connector detail: ${detail}` : '';
|
|
3772
|
+
console.error(`[routeSpeech] ${conversationId}: ${reason}${detailText} — speech ${status === 'unknown' ? 'delivery NOT confirmed' : 'NOT routed'} (${text.length} chars stay in chronicle)`);
|
|
3076
3773
|
this.emitTraceFn({
|
|
3077
3774
|
type: 'mcpl:speech-route-failed',
|
|
3078
3775
|
conversationId,
|
|
3079
3776
|
channelId: channelId ?? '',
|
|
3080
3777
|
reason,
|
|
3081
3778
|
textLen: text.length,
|
|
3779
|
+
outcome: status,
|
|
3780
|
+
...(detail !== undefined ? { detail } : {}),
|
|
3082
3781
|
});
|
|
3083
|
-
|
|
3084
|
-
|
|
3782
|
+
if (status === 'unknown' || !opts?.heldOnFailure) {
|
|
3783
|
+
this.onRouteFailure?.({ conversationId, channelId, reason, textLen: text.length, outcome: status, ...(detail !== undefined ? { detail } : {}) });
|
|
3784
|
+
}
|
|
3785
|
+
return outcome;
|
|
3085
3786
|
};
|
|
3086
3787
|
|
|
3087
3788
|
// Runtime backstop for JS callers the compiler can't see: an omitted
|
|
3088
3789
|
// locus is a routing bug at the call site, never something to paper over
|
|
3089
3790
|
// with a live re-resolution.
|
|
3090
3791
|
if (locusChannelId === undefined) {
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
if (!
|
|
3792
|
+
const reason = 'caller passed no locus (routing bug: every speech path must snapshot the turn locus)';
|
|
3793
|
+
return fail(null, reason, { status: 'failed', reason, at: Date.now() });
|
|
3794
|
+
}
|
|
3795
|
+
if (!locusChannelId) {
|
|
3796
|
+
// An ephemeral run with no channel of its own: its plain speech is its
|
|
3797
|
+
// caller's result (takesGlobalFallback). It returns as not delivered,
|
|
3798
|
+
// with its own reason, and writes no line, trace or notice.
|
|
3799
|
+
if (this.takesGlobalFallback?.(conversationId) === false) {
|
|
3800
|
+
return { status: 'failed', reason: "an ephemeral run's plain speech with no channel of its own is its caller's result", at: Date.now() };
|
|
3801
|
+
}
|
|
3095
3802
|
// The turn froze with no locus (no home, no triggering channel, no
|
|
3096
3803
|
// global inbound ever seen) — the agent was told its prose stays in
|
|
3097
3804
|
// the archive; honor that.
|
|
3098
|
-
|
|
3805
|
+
const reason = 'turn has no locus (no home/trigger channel; nothing to deliver into)';
|
|
3806
|
+
return fail(null, reason, { status: 'failed', reason, at: Date.now() });
|
|
3099
3807
|
}
|
|
3100
3808
|
|
|
3101
|
-
const
|
|
3102
|
-
|
|
3103
|
-
|
|
3809
|
+
const outcome = await this.publish(conversationId, text, { channelId: locusChannelId, routed: true });
|
|
3810
|
+
const channelId = outcome.destination?.channelId ?? locusChannelId;
|
|
3811
|
+
if (outcome.status !== 'delivered') {
|
|
3812
|
+
return fail(channelId, outcome.reason ?? 'delivery not confirmed', outcome);
|
|
3104
3813
|
}
|
|
3105
3814
|
|
|
3106
|
-
|
|
3107
|
-
// routed into a closed-but-registered locus (the canonical case: a DM
|
|
3108
|
-
// that woke the agent — DMs register closed) OPENS the channel first,
|
|
3109
|
-
// so typing indicators, reaction machinery, and inbound forwarding come
|
|
3110
|
-
// alive with the reply. If the open fails, the speech does NOT go out:
|
|
3111
|
-
// a half-alive delivery is worse than a loud marker.
|
|
3112
|
-
if (!entry.open) {
|
|
3113
|
-
try {
|
|
3114
|
-
await this.openChannelNow(entry, 'opened-by-delivery');
|
|
3115
|
-
console.error(
|
|
3116
|
-
`[routeSpeech] ${conversationId}: locus ${logValue(channelId)} was closed — opened by delivery (speech implies engagement)`,
|
|
3117
|
-
);
|
|
3118
|
-
this.emitTraceFn({
|
|
3119
|
-
type: 'mcpl:channel-opened-by-send',
|
|
3120
|
-
serverId: entry.serverId,
|
|
3121
|
-
channelId,
|
|
3122
|
-
});
|
|
3123
|
-
try {
|
|
3124
|
-
this.onChannelAutoOpened?.({
|
|
3125
|
-
conversationId,
|
|
3126
|
-
serverId: entry.serverId,
|
|
3127
|
-
source: 'opened-by-delivery',
|
|
3128
|
-
channels: [{ channelId, label: entry.descriptor.label }],
|
|
3129
|
-
});
|
|
3130
|
-
} catch (err) {
|
|
3131
|
-
console.error('onChannelAutoOpened (delivery) failed:', err);
|
|
3132
|
-
}
|
|
3133
|
-
} catch (err) {
|
|
3134
|
-
return fail(channelId, `locus channel is closed and open failed: ${(err as Error).message}`);
|
|
3135
|
-
}
|
|
3136
|
-
}
|
|
3137
|
-
|
|
3138
|
-
const server = this.serverRegistry.getServer(entry.serverId);
|
|
3139
|
-
if (!server) {
|
|
3140
|
-
return fail(channelId, `server "${entry.serverId}" not found`);
|
|
3141
|
-
}
|
|
3142
|
-
// §14.1: channels/publish requires channels.publish in the grant. The
|
|
3143
|
-
// host not sending is the enforcement — a send to an ungranted server
|
|
3144
|
-
// would have it act on authority it was never told it has.
|
|
3145
|
-
if (!CapabilityGrant.of(server).has('channels.publish')) {
|
|
3146
|
-
return fail(channelId, `channels.publish not in "${entry.serverId}"'s effective grant (§14.1)`);
|
|
3147
|
-
}
|
|
3148
|
-
|
|
3149
|
-
const publishParams: ChannelsPublishParams = {
|
|
3150
|
-
conversationId,
|
|
3151
|
-
channelId,
|
|
3152
|
-
content: [{ type: 'text', text }],
|
|
3153
|
-
};
|
|
3154
|
-
const result = await server.sendChannelsPublish(publishParams);
|
|
3155
|
-
const delivered = (result as { delivered?: boolean } | undefined)?.delivered ?? true;
|
|
3156
|
-
// Surface the posted message's id (ChannelsPublishResult.messageId) so
|
|
3157
|
-
// trace consumers can act on the just-posted message — e.g. a TTS-relay
|
|
3158
|
-
// tap editing it down to the words actually voiced on interruption.
|
|
3159
|
-
// Previously this was silently dropped here.
|
|
3160
|
-
const messageId = (result as { messageId?: string } | undefined)?.messageId;
|
|
3161
|
-
|
|
3162
|
-
// The server accepted the publish RPC but reported the message was not
|
|
3163
|
-
// actually delivered (e.g. missing Send Messages permission). Previously
|
|
3164
|
-
// this returned `{ delivered: true }`, masking the failure. Surface it.
|
|
3165
|
-
if (delivered === false) {
|
|
3166
|
-
return fail(channelId, `server "${entry.serverId}" reported delivered:false for "${channelId}"`);
|
|
3167
|
-
}
|
|
3168
|
-
|
|
3169
|
-
console.error(`[routeSpeech] ${conversationId}: routed ${text.length} chars -> ${logValue(channelId)} (server=${entry.serverId}, delivered=${delivered})`);
|
|
3815
|
+
console.error(`[routeSpeech] ${conversationId}: routed ${text.length} chars -> ${logValue(channelId)} (server=${outcome.destination!.serverId}, delivered=true)`);
|
|
3170
3816
|
this.emitTraceFn({
|
|
3171
3817
|
type: 'mcpl:speech-routed',
|
|
3172
3818
|
conversationId,
|
|
3173
|
-
serverId:
|
|
3819
|
+
serverId: outcome.destination!.serverId,
|
|
3174
3820
|
channelId,
|
|
3175
|
-
delivered,
|
|
3821
|
+
delivered: true,
|
|
3176
3822
|
textLen: text.length,
|
|
3177
3823
|
text,
|
|
3178
|
-
|
|
3824
|
+
// Surface the posted message's id (ChannelsPublishResult.messageId) so
|
|
3825
|
+
// trace consumers can act on the just-posted message — e.g. a TTS-relay
|
|
3826
|
+
// tap editing it down to the words actually voiced on interruption.
|
|
3827
|
+
...(outcome.messageId !== undefined ? { messageId: outcome.messageId } : {}),
|
|
3179
3828
|
});
|
|
3829
|
+
return outcome;
|
|
3830
|
+
}
|
|
3180
3831
|
|
|
3181
|
-
|
|
3832
|
+
/** deliverSpeech, reduced to the confirmed delivery (null when there was none). */
|
|
3833
|
+
async routeSpeech(
|
|
3834
|
+
conversationId: string,
|
|
3835
|
+
text: string,
|
|
3836
|
+
locusChannelId: string | null,
|
|
3837
|
+
): Promise<{ delivered: boolean; channelId: string; messageId?: string } | null> {
|
|
3838
|
+
const outcome = await this.deliverSpeech(conversationId, text, locusChannelId);
|
|
3839
|
+
if (outcome.status !== 'delivered') return null;
|
|
3840
|
+
return {
|
|
3841
|
+
delivered: true,
|
|
3842
|
+
channelId: outcome.destination!.channelId,
|
|
3843
|
+
...(outcome.messageId !== undefined ? { messageId: outcome.messageId } : {}),
|
|
3844
|
+
};
|
|
3182
3845
|
}
|
|
3183
3846
|
|
|
3184
|
-
private async handleToolPublish(
|
|
3847
|
+
private async handleToolPublish(
|
|
3848
|
+
input: { channelId?: string; content?: string; text?: string },
|
|
3849
|
+
origin?: ChannelToolOrigin,
|
|
3850
|
+
): Promise<ToolResult> {
|
|
3185
3851
|
// Resolve content: accept both `content` and `text` (backward compat)
|
|
3186
3852
|
const messageText = input.content ?? input.text;
|
|
3187
3853
|
if (!messageText) {
|
|
@@ -3192,12 +3858,17 @@ export class ChannelRegistry {
|
|
|
3192
3858
|
};
|
|
3193
3859
|
}
|
|
3194
3860
|
|
|
3195
|
-
// Resolve channelId: default to most recent incoming channel
|
|
3196
|
-
|
|
3861
|
+
// Resolve channelId: default to the most recent incoming channel, which an
|
|
3862
|
+
// ephemeral run never takes (takesGlobalFallback), so it names one.
|
|
3863
|
+
const caller = origin?.kind === 'agent' ? origin.agentName : undefined;
|
|
3864
|
+
const channelId = input.channelId ?? (caller !== undefined ? this.globalFallbackFor(caller) : this.defaultPublishChannel);
|
|
3197
3865
|
if (!channelId) {
|
|
3198
3866
|
return {
|
|
3199
3867
|
success: false,
|
|
3200
|
-
error:
|
|
3868
|
+
error: caller !== undefined && this.takesGlobalFallback?.(caller) === false
|
|
3869
|
+
? 'Nothing was published: name the channel with channelId. A subagent has no default channel, since its plain '
|
|
3870
|
+
+ 'speech is returned to whoever called it, so channel_publish sends only to a channel it names.'
|
|
3871
|
+
: 'No channelId specified and no default channel available',
|
|
3201
3872
|
isError: true,
|
|
3202
3873
|
};
|
|
3203
3874
|
}
|
|
@@ -3220,31 +3891,44 @@ export class ChannelRegistry {
|
|
|
3220
3891
|
};
|
|
3221
3892
|
}
|
|
3222
3893
|
|
|
3223
|
-
|
|
3224
|
-
if (!CapabilityGrant.of(server).has('channels.publish')) {
|
|
3225
|
-
return {
|
|
3226
|
-
success: false,
|
|
3227
|
-
error: `channels.publish not in "${server.id}"'s effective grant (§14.1)`,
|
|
3228
|
-
isError: true,
|
|
3229
|
-
};
|
|
3230
|
-
}
|
|
3231
|
-
const publishParams: ChannelsPublishParams = {
|
|
3232
|
-
conversationId: '', // Framework will fill this when wired
|
|
3233
|
-
channelId,
|
|
3234
|
-
content: [{ type: 'text', text: messageText }],
|
|
3235
|
-
};
|
|
3236
|
-
|
|
3237
|
-
const result = await server.sendChannelsPublish(publishParams);
|
|
3238
|
-
return {
|
|
3239
|
-
success: true,
|
|
3240
|
-
data: result ?? { delivered: true },
|
|
3241
|
-
};
|
|
3242
|
-
} catch (err) {
|
|
3894
|
+
if (!CapabilityGrant.of(server).has('channels.publish')) {
|
|
3243
3895
|
return {
|
|
3244
3896
|
success: false,
|
|
3245
|
-
error: `
|
|
3897
|
+
error: `channels.publish not in "${server.id}"'s effective grant (§14.1)`,
|
|
3246
3898
|
isError: true,
|
|
3247
3899
|
};
|
|
3248
3900
|
}
|
|
3901
|
+
const publishParams: ChannelsPublishParams = {
|
|
3902
|
+
conversationId: '', // Framework will fill this when wired
|
|
3903
|
+
channelId,
|
|
3904
|
+
content: [{ type: 'text', text: messageText }],
|
|
3905
|
+
};
|
|
3906
|
+
const destination: PublishDestination = {
|
|
3907
|
+
serverId: entry.serverId,
|
|
3908
|
+
channelId: entry.descriptor.id,
|
|
3909
|
+
...(entry.descriptor.label ? { label: entry.descriptor.label } : {}),
|
|
3910
|
+
};
|
|
3911
|
+
|
|
3912
|
+
// The answer counts by the same rule as plain speech's: only the
|
|
3913
|
+
// connector's delivered:true confirms a post.
|
|
3914
|
+
let result: unknown;
|
|
3915
|
+
let outcome: PublishOutcome;
|
|
3916
|
+
try {
|
|
3917
|
+
result = await server.sendChannelsPublish(publishParams);
|
|
3918
|
+
outcome = publishReceiptOutcome(result, destination, Date.now());
|
|
3919
|
+
} catch (err) {
|
|
3920
|
+
outcome = publishErrorOutcome(err, destination, Date.now());
|
|
3921
|
+
}
|
|
3922
|
+
if (outcome.status === 'delivered') return { success: true, data: result };
|
|
3923
|
+
// The connector's data may say what a multi-part send already posted.
|
|
3924
|
+
const detail = outcome.status === 'unknown' && outcome.detail !== undefined ? ` Connector detail: ${connectorText(outcome.detail)}` : '';
|
|
3925
|
+
return {
|
|
3926
|
+
success: false,
|
|
3927
|
+
error: (outcome.status === 'failed'
|
|
3928
|
+
? `Publish to ${channelId} failed: ${outcome.reason}. Nothing was posted.`
|
|
3929
|
+
: `Publish to ${channelId} wasn't confirmed: ${outcome.reason}. ` +
|
|
3930
|
+
'It may or may not have been posted, so sending it again could post it twice.') + detail,
|
|
3931
|
+
isError: true,
|
|
3932
|
+
};
|
|
3249
3933
|
}
|
|
3250
3934
|
}
|