@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
|
@@ -12,18 +12,57 @@
|
|
|
12
12
|
* - Expose synthesized tools: channel_list, channel_open, channel_close, channel_publish
|
|
13
13
|
* - Build channel context for beforeInference params
|
|
14
14
|
*/
|
|
15
|
-
import {
|
|
15
|
+
import { convertBlock } from './convert-block.js';
|
|
16
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
17
|
+
import { ensureStateRegistered } from '../module-registry.js';
|
|
16
18
|
import { expandCoreTags } from './tags.js';
|
|
17
19
|
import { coalesceRejectionReason, EmptyContentError, validateCoalescedContent } from './push-coalescer.js';
|
|
18
|
-
import {
|
|
20
|
+
import { isVisiblyEmptyConversion } from './visible-content.js';
|
|
19
21
|
import { logDetail, logValue } from './log-value.js';
|
|
20
22
|
import { RefusalLines } from './refusal-lines.js';
|
|
21
23
|
import { CapabilityGrant } from './capability-grant.js';
|
|
24
|
+
import { McplRequestError } from './server-connection.js';
|
|
25
|
+
import { readSkipReplyWake } from './skip-reply-wake.js';
|
|
22
26
|
// ============================================================================
|
|
23
27
|
// Typing indicator interval (Discord typing lasts ~10s, so 7s keeps it alive)
|
|
24
28
|
// ============================================================================
|
|
25
29
|
const TYPING_INTERVAL_MS = 7_000;
|
|
30
|
+
const MAX_CHANNEL_LIFECYCLE_ATTEMPTS = 5;
|
|
31
|
+
/** A successful RPC can still fail to establish the latest channel target. */
|
|
32
|
+
class ChannelLifecycleConvergenceError extends Error {
|
|
33
|
+
}
|
|
34
|
+
/** A channels/open answered for a channel other than the one it named. */
|
|
35
|
+
class ChannelOpenSubstitutionError extends Error {
|
|
36
|
+
}
|
|
37
|
+
/** An operation queued for a server's registrations that were removed. */
|
|
38
|
+
class ChannelLifecycleCancelledError extends Error {
|
|
39
|
+
}
|
|
26
40
|
const CHANNEL_LIFECYCLE_LOG_ID = 'mcpl/channel-lifecycle';
|
|
41
|
+
/**
|
|
42
|
+
* What a channels/open answer says about which channel the server opened:
|
|
43
|
+
* its `channel.id`, or else the `channelId` of a history item. Each is
|
|
44
|
+
* evidence. A string other than the requested id names another channel (ids
|
|
45
|
+
* compare exactly, as the server's traffic is matched). Null or absence names
|
|
46
|
+
* no channel, and gives undefined, as does an answer that names none at all.
|
|
47
|
+
* Any other value is malformed: it names nothing the host could register.
|
|
48
|
+
*/
|
|
49
|
+
function substitutedChannel(result, requested) {
|
|
50
|
+
const named = (value) => {
|
|
51
|
+
if (value === undefined || value === null || value === requested)
|
|
52
|
+
return undefined;
|
|
53
|
+
return typeof value === 'string' ? { kind: 'other', id: value } : { kind: 'malformed', value };
|
|
54
|
+
};
|
|
55
|
+
const answered = named(result?.channel?.id);
|
|
56
|
+
if (answered)
|
|
57
|
+
return answered;
|
|
58
|
+
const history = Array.isArray(result?.history) ? result.history : [];
|
|
59
|
+
for (const item of history) {
|
|
60
|
+
const from = named(item?.channelId);
|
|
61
|
+
if (from)
|
|
62
|
+
return from;
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
27
66
|
/**
|
|
28
67
|
* Durable "which label did this channelId have, each time we saw it" log.
|
|
29
68
|
* `resolveProseTarget()` resolves a label to an id only for channels
|
|
@@ -82,84 +121,25 @@ function shallowEqualRecord(a, b) {
|
|
|
82
121
|
}
|
|
83
122
|
return true;
|
|
84
123
|
}
|
|
124
|
+
function sameChannelTarget(a, b) {
|
|
125
|
+
return a.type === b.type && isDeepStrictEqual(a.address, b.address);
|
|
126
|
+
}
|
|
85
127
|
// ============================================================================
|
|
86
|
-
//
|
|
128
|
+
// Channel Tool Definitions
|
|
87
129
|
// ============================================================================
|
|
88
130
|
/**
|
|
89
|
-
*
|
|
131
|
+
* `journal`'s description. It tells the model to call `skip_reply` to end the
|
|
132
|
+
* turn only where the agent may call it: the framework builds each agent's
|
|
133
|
+
* surface with that agent's permission, and a refused `skip_reply` leaves the
|
|
134
|
+
* turn going on.
|
|
90
135
|
*/
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
case 'image':
|
|
98
|
-
if (isInlineContradiction(block)) {
|
|
99
|
-
// RFC-005 vector 2: inline data claiming bulk disposition — fail
|
|
100
|
-
// closed, withhold the data (checked BEFORE the data branch).
|
|
101
|
-
return { type: 'text', text: INLINE_WITHHELD_TEXT };
|
|
102
|
-
}
|
|
103
|
-
if (isFilledString(block.uri) && block.disposition) {
|
|
104
|
-
// RFC-005 uri-form media with a disposition claim: stub, do not
|
|
105
|
-
// hand the URI to the provider or inline it.
|
|
106
|
-
return { type: 'text', text: referenceStubOrNull(block, 'attachment on channel message') ?? '[reference]' };
|
|
107
|
-
}
|
|
108
|
-
if (isFilledString(block.data) && isFilledString(block.mimeType)) {
|
|
109
|
-
return {
|
|
110
|
-
type: 'image',
|
|
111
|
-
source: { type: 'base64', data: block.data, mediaType: block.mimeType },
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
if (isFilledString(block.uri)) {
|
|
115
|
-
return {
|
|
116
|
-
type: 'image',
|
|
117
|
-
source: { type: 'url', url: block.uri },
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
return { type: 'text', text: isFilledString(block.data) ? '[Image: no media type]' : '[Image: no data]' };
|
|
121
|
-
case 'audio':
|
|
122
|
-
if (isInlineContradiction(block)) {
|
|
123
|
-
// RFC-005 vector 2: inline data claiming bulk disposition — fail
|
|
124
|
-
// closed, withhold the data (checked BEFORE the data branch).
|
|
125
|
-
return { type: 'text', text: INLINE_WITHHELD_TEXT };
|
|
126
|
-
}
|
|
127
|
-
if (isFilledString(block.uri) && block.disposition) {
|
|
128
|
-
// RFC-005 uri-form media with a disposition claim: stub, do not
|
|
129
|
-
// hand the URI to the provider or inline it.
|
|
130
|
-
return { type: 'text', text: referenceStubOrNull(block, 'attachment on channel message') ?? '[reference]' };
|
|
131
|
-
}
|
|
132
|
-
if (isFilledString(block.data) && isFilledString(block.mimeType)) {
|
|
133
|
-
return {
|
|
134
|
-
type: 'audio',
|
|
135
|
-
source: { type: 'base64', data: block.data, mediaType: block.mimeType },
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
return { type: 'text', text: isFilledString(block.data) ? '[Audio: no media type]' : '[Audio: no data]' };
|
|
139
|
-
case 'resource':
|
|
140
|
-
// RFC-005: reference blocks become bounded stubs — never raw URIs
|
|
141
|
-
// (a signed URL is a bearer credential that looks like a location).
|
|
142
|
-
return { type: 'text', text: referenceStubOrNull(block, 'attachment on channel message') ?? '[reference]' };
|
|
143
|
-
default: {
|
|
144
|
-
// Unknown wire block types previously fell off the exhaustive switch
|
|
145
|
-
// and propagated `undefined` into ContentBlock[]. Fail visibly. A type
|
|
146
|
-
// that isn't a string is no type, and is never converted to one: an
|
|
147
|
-
// object can throw when it is.
|
|
148
|
-
const type = block.type;
|
|
149
|
-
return { type: 'text', text: `[unrecognized content block: ${typeof type === 'string' ? type : 'untyped'}]` };
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
/** A string with something in it: the only data, mimeType or uri a block's
|
|
154
|
-
* source is built from, and the only uri that makes it a reference. Anything
|
|
155
|
-
* else a sender puts there leaves the block without it, so conversion never
|
|
156
|
-
* throws and never copies a non-string. */
|
|
157
|
-
function isFilledString(value) {
|
|
158
|
-
return typeof value === 'string' && value !== '';
|
|
136
|
+
export function journalToolDescription(canSkipReply) {
|
|
137
|
+
return 'Write an entry in your private journal. The entry stays in your own context and ' +
|
|
138
|
+
'memory and is NOT sent to any channel, surface or person. Use it for anything longer ' +
|
|
139
|
+
'than a line that you want to keep for yourself — reflections, what you decided and ' +
|
|
140
|
+
'why, notes for later. It does not end your turn and does not affect where ordinary ' +
|
|
141
|
+
'text is routed' + (canSkipReply ? '; to end the turn without replying, call skip_reply afterwards.' : '.');
|
|
159
142
|
}
|
|
160
|
-
// ============================================================================
|
|
161
|
-
// Channel Tool Definitions
|
|
162
|
-
// ============================================================================
|
|
163
143
|
const CHANNEL_TOOL_DEFINITIONS = [
|
|
164
144
|
{
|
|
165
145
|
name: 'channel_list',
|
|
@@ -168,9 +148,8 @@ const CHANNEL_TOOL_DEFINITIONS = [
|
|
|
168
148
|
},
|
|
169
149
|
{
|
|
170
150
|
name: 'channel_open',
|
|
171
|
-
description: 'Open a channel
|
|
172
|
-
'
|
|
173
|
-
'history preceding the message that invited you into the channel.',
|
|
151
|
+
description: 'Open a channel. What an open channel delivers to you depends on its MCPL server. ' +
|
|
152
|
+
'Optionally request history preceding the message that invited you into the channel.',
|
|
174
153
|
inputSchema: {
|
|
175
154
|
type: 'object',
|
|
176
155
|
properties: {
|
|
@@ -209,7 +188,8 @@ const CHANNEL_TOOL_DEFINITIONS = [
|
|
|
209
188
|
},
|
|
210
189
|
{
|
|
211
190
|
name: 'channel_close',
|
|
212
|
-
description: 'Close a channel
|
|
191
|
+
description: 'Close a channel. Messages that address you still reach you; what else stops reaching you ' +
|
|
192
|
+
'depends on its server.',
|
|
213
193
|
inputSchema: {
|
|
214
194
|
type: 'object',
|
|
215
195
|
properties: {
|
|
@@ -221,11 +201,11 @@ const CHANNEL_TOOL_DEFINITIONS = [
|
|
|
221
201
|
},
|
|
222
202
|
{
|
|
223
203
|
name: 'channel_publish',
|
|
224
|
-
description: 'Publish a message to a channel. If channelId is omitted, publishes to the most recent incoming channel.',
|
|
204
|
+
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.',
|
|
225
205
|
inputSchema: {
|
|
226
206
|
type: 'object',
|
|
227
207
|
properties: {
|
|
228
|
-
channelId: { type: 'string', description: 'ID of the channel to publish to (defaults to the most recent incoming channel)' },
|
|
208
|
+
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)' },
|
|
229
209
|
content: { type: 'string', description: 'Text content to publish' },
|
|
230
210
|
text: { type: 'string', description: 'Alias for content' },
|
|
231
211
|
},
|
|
@@ -234,6 +214,9 @@ const CHANNEL_TOOL_DEFINITIONS = [
|
|
|
234
214
|
},
|
|
235
215
|
{
|
|
236
216
|
name: 'think',
|
|
217
|
+
// No agent's surface shows this description: getToolsForAgent gives every
|
|
218
|
+
// agent its own think (buildThinkTool), which names skip_reply only where
|
|
219
|
+
// that agent may call it.
|
|
237
220
|
description: 'Reason privately. The content stays in your own context and is NOT sent to any ' +
|
|
238
221
|
'channel or surface. Same-round routing of ordinary text beside think() depends on ' +
|
|
239
222
|
'your current same_round_think_text_policy; inspect or change it with agent_settings. ' +
|
|
@@ -252,11 +235,7 @@ const CHANNEL_TOOL_DEFINITIONS = [
|
|
|
252
235
|
},
|
|
253
236
|
{
|
|
254
237
|
name: 'journal',
|
|
255
|
-
description:
|
|
256
|
-
'memory and is NOT sent to any channel, surface or person. Use it for anything longer ' +
|
|
257
|
-
'than a line that you want to keep for yourself — reflections, what you decided and ' +
|
|
258
|
-
'why, notes for later. It does not end your turn and does not affect where ordinary ' +
|
|
259
|
-
'text is routed; to end the turn without replying, call skip_reply afterwards.',
|
|
238
|
+
description: journalToolDescription(true),
|
|
260
239
|
inputSchema: {
|
|
261
240
|
type: 'object',
|
|
262
241
|
properties: {
|
|
@@ -283,14 +262,95 @@ const CHANNEL_TOOL_DEFINITIONS = [
|
|
|
283
262
|
wake_in_seconds: {
|
|
284
263
|
type: 'number',
|
|
285
264
|
description: 'Optional self-wake: if nothing else wakes you first, you wake again after this ' +
|
|
286
|
-
'many seconds
|
|
287
|
-
'wake before then cancels it. Omit to stay idle until the
|
|
265
|
+
'many seconds, from 1 to 3600; a larger number waits 3600 (1 = go again almost ' +
|
|
266
|
+
'immediately). Any other wake before then cancels it. Omit to stay idle until the ' +
|
|
267
|
+
'next external wake.',
|
|
288
268
|
},
|
|
289
269
|
},
|
|
290
270
|
required: [],
|
|
291
271
|
},
|
|
292
272
|
},
|
|
293
273
|
];
|
|
274
|
+
// ============================================================================
|
|
275
|
+
// Constructor Options
|
|
276
|
+
// ============================================================================
|
|
277
|
+
/** Bound connector values for notices, logs and an outcome's reason. A
|
|
278
|
+
* PublishOutcome keeps the connector's structured data (`detail`) whole. */
|
|
279
|
+
export function connectorText(value) {
|
|
280
|
+
let text;
|
|
281
|
+
try {
|
|
282
|
+
text = typeof value === 'string' ? value : JSON.stringify(value) ?? String(value);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return '[unserializable connector value]';
|
|
286
|
+
}
|
|
287
|
+
const limit = 2000;
|
|
288
|
+
const suffix = '… [truncated]';
|
|
289
|
+
return text.length > limit ? text.slice(0, limit - suffix.length) + suffix : text;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* What a channels/publish request that threw establishes. Only a request
|
|
293
|
+
* that never left the host proves nothing was posted. An error response may
|
|
294
|
+
* follow a partial post (a multi-part send whose later part failed), so it
|
|
295
|
+
* is unknown, with the connector's own words, bounded, and its data kept for
|
|
296
|
+
* the resident to judge. The publish executor and channel_publish both read
|
|
297
|
+
* a failed request this way.
|
|
298
|
+
*/
|
|
299
|
+
function publishErrorOutcome(err, destination, at) {
|
|
300
|
+
// This host's own words for a request that wasn't sent or wasn't answered
|
|
301
|
+
// are sentences, and every composition of the reason adds its own ending,
|
|
302
|
+
// so one final period comes off them. A server's error answer keeps its
|
|
303
|
+
// words as they came.
|
|
304
|
+
const text = connectorText(err instanceof Error ? err.message : String(err));
|
|
305
|
+
const message = err instanceof McplRequestError && err.outcome !== 'error-response' ? text.replace(/(?<!\.)\.$/, '') : text;
|
|
306
|
+
const notSent = err instanceof McplRequestError && err.outcome === 'not-sent';
|
|
307
|
+
const detail = err instanceof McplRequestError ? err.data : undefined;
|
|
308
|
+
return {
|
|
309
|
+
status: notSent ? 'failed' : 'unknown',
|
|
310
|
+
destination,
|
|
311
|
+
reason: notSent ? message : `${message} (delivery uncertain)`,
|
|
312
|
+
...(detail !== undefined ? { detail } : {}),
|
|
313
|
+
at,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* What a channels/publish answer establishes. A final publish is a request,
|
|
318
|
+
* so success requires an explicit receipt; a missing or malformed one leaves
|
|
319
|
+
* delivery uncertain (anima-research/agent-framework#163's rule).
|
|
320
|
+
* `delivered: false` proves nothing went out only when the receipt names no
|
|
321
|
+
* message at all: a message id beside it, valid or malformed, contradicts it,
|
|
322
|
+
* since something may have been posted. The publish executor and
|
|
323
|
+
* channel_publish both read an answer this way.
|
|
324
|
+
*/
|
|
325
|
+
function publishReceiptOutcome(result, destination, at) {
|
|
326
|
+
const receipt = (result ?? {});
|
|
327
|
+
const messageId = typeof receipt.messageId === 'string' && receipt.messageId ? receipt.messageId : undefined;
|
|
328
|
+
if (receipt.delivered === true) {
|
|
329
|
+
return { status: 'delivered', destination, ...(messageId ? { messageId } : {}), at };
|
|
330
|
+
}
|
|
331
|
+
if (receipt.delivered === false) {
|
|
332
|
+
if (receipt.messageId === undefined || receipt.messageId === null) {
|
|
333
|
+
return { status: 'failed', destination, reason: `server "${destination.serverId}" reported delivered:false`, at };
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
status: 'unknown',
|
|
337
|
+
destination,
|
|
338
|
+
...(messageId ? { messageId } : {}),
|
|
339
|
+
reason: messageId
|
|
340
|
+
? `server "${destination.serverId}" reported delivered:false but named posted message ${connectorText(messageId)} (delivery uncertain)`
|
|
341
|
+
: `server "${destination.serverId}" reported delivered:false beside a malformed message id ` +
|
|
342
|
+
`${connectorText(receipt.messageId)} (contradictory receipt; delivery uncertain)`,
|
|
343
|
+
at,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
status: 'unknown',
|
|
348
|
+
destination,
|
|
349
|
+
...(messageId ? { messageId } : {}),
|
|
350
|
+
reason: `server "${destination.serverId}" returned no valid delivery receipt (delivery uncertain)`,
|
|
351
|
+
at,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
294
354
|
/** The machine decision sources a module-origin channel_close may record.
|
|
295
355
|
* Closed on purpose: honest provenance means a small, auditable vocabulary,
|
|
296
356
|
* not arbitrary self-description. */
|
|
@@ -306,6 +366,7 @@ export class ChannelRegistry {
|
|
|
306
366
|
onChannelAutoOpened;
|
|
307
367
|
homeChannelResolver;
|
|
308
368
|
activeChannelResolver;
|
|
369
|
+
takesGlobalFallback;
|
|
309
370
|
store;
|
|
310
371
|
/** Registered channels, keyed by `{serverId}:{channelId}`. */
|
|
311
372
|
channels = new Map();
|
|
@@ -374,9 +435,58 @@ export class ChannelRegistry {
|
|
|
374
435
|
/** One-time migration inputs from the retired recipe auto-open policy. */
|
|
375
436
|
legacyPolicies = new Map();
|
|
376
437
|
migratedLegacyPolicies = new Set();
|
|
438
|
+
// Keep one transport operation in flight per logical channel, including
|
|
439
|
+
// across descriptor replacement. Settled tails release themselves; a
|
|
440
|
+
// failed operation must not poison later decisions.
|
|
441
|
+
channelLifecycleTails = new Map();
|
|
442
|
+
// Bumped when a server's registrations are removed: an operation queued
|
|
443
|
+
// before that is cancelled when it would run, rather than acting on a
|
|
444
|
+
// closing connection or on a later re-registration's entries.
|
|
445
|
+
serverRemovals = new Map();
|
|
446
|
+
// Operations queued or running per channel that will still read its
|
|
447
|
+
// target before confirming anything. Unlike the tail, which settles a few
|
|
448
|
+
// microtasks after its operation's last check, this drops at that check.
|
|
449
|
+
lifecycleOutstanding = new Map();
|
|
450
|
+
// Losing an open receipt does not confirm closure. Keep that distinction
|
|
451
|
+
// private while preserving the public open:boolean availability projection.
|
|
452
|
+
unconfirmedChannelTargets = new WeakSet();
|
|
453
|
+
invalidateTransportConfirmation(entry) {
|
|
454
|
+
entry.open = false;
|
|
455
|
+
this.unconfirmedChannelTargets.add(entry);
|
|
456
|
+
}
|
|
457
|
+
/** Whether the host directs this server's channel lifecycle (§14.1: its
|
|
458
|
+
* `channels.lifecycle` grant). Where it doesn't, the server opens and
|
|
459
|
+
* closes its own channels, and sends no receipt the host could wait for. */
|
|
460
|
+
directsLifecycle(serverId) {
|
|
461
|
+
return CapabilityGrant.of(this.serverRegistry.getServer(serverId)).has('channels.lifecycle');
|
|
462
|
+
}
|
|
463
|
+
/** A registration's entry: nothing about its transport is known until a
|
|
464
|
+
* receipt for its target confirms it open or closed. */
|
|
465
|
+
registerEntry(key, serverId, descriptor) {
|
|
466
|
+
const entry = { serverId, descriptor, open: false };
|
|
467
|
+
this.channels.set(key, entry);
|
|
468
|
+
this.unconfirmedChannelTargets.add(entry);
|
|
469
|
+
}
|
|
470
|
+
hasPendingLifecycle(entry) {
|
|
471
|
+
return this.channelLifecycleTails.has(this.lifecycleKey(entry.serverId, entry.descriptor.id));
|
|
472
|
+
}
|
|
377
473
|
handleCoalescedIncoming;
|
|
474
|
+
acceptInbound;
|
|
475
|
+
channelTools;
|
|
378
476
|
constructor(serverRegistry, featureSetManager, pushEventFn, emitTraceFn, options) {
|
|
379
477
|
this.handleCoalescedIncoming = options?.handleCoalescedIncoming;
|
|
478
|
+
this.acceptInbound = options?.acceptInbound;
|
|
479
|
+
this.channelTools = CHANNEL_TOOL_DEFINITIONS.map((tool) => {
|
|
480
|
+
if (tool.name !== 'skip_reply' || options?.eventGatePresent === true)
|
|
481
|
+
return tool;
|
|
482
|
+
const schema = tool.inputSchema;
|
|
483
|
+
const { wake_in_seconds: _wake, ...properties } = schema.properties;
|
|
484
|
+
return {
|
|
485
|
+
...tool,
|
|
486
|
+
description: tool.description.replace(' To end this turn but come back on your own shortly, set wake_in_seconds.', ''),
|
|
487
|
+
inputSchema: { ...schema, properties },
|
|
488
|
+
};
|
|
489
|
+
});
|
|
380
490
|
this.serverRegistry = serverRegistry;
|
|
381
491
|
this.featureSetManager = featureSetManager;
|
|
382
492
|
this.pushEventFn = pushEventFn;
|
|
@@ -387,6 +497,7 @@ export class ChannelRegistry {
|
|
|
387
497
|
this.onChannelAutoOpened = options?.onChannelAutoOpened;
|
|
388
498
|
this.homeChannelResolver = options?.homeChannelResolver;
|
|
389
499
|
this.activeChannelResolver = options?.activeChannelResolver;
|
|
500
|
+
this.takesGlobalFallback = options?.takesGlobalFallback;
|
|
390
501
|
this.store = options?.store;
|
|
391
502
|
this.initializeLifecycleStore();
|
|
392
503
|
this.initializeLabelHistoryStore();
|
|
@@ -422,11 +533,7 @@ export class ChannelRegistry {
|
|
|
422
533
|
continue;
|
|
423
534
|
}
|
|
424
535
|
const key = `${serverId}:${channel.id}`;
|
|
425
|
-
this.
|
|
426
|
-
serverId,
|
|
427
|
-
descriptor: channel,
|
|
428
|
-
open: false,
|
|
429
|
-
});
|
|
536
|
+
this.registerEntry(key, serverId, channel);
|
|
430
537
|
this.appendLabelSighting(channel.id, channel.label, extractDmMeta(channel.metadata));
|
|
431
538
|
registeredIds.push(channel.id);
|
|
432
539
|
results.push({ id: channel.id, accepted: true });
|
|
@@ -459,6 +566,7 @@ export class ChannelRegistry {
|
|
|
459
566
|
// channels/register — a changed-set cannot smuggle in what register
|
|
460
567
|
// would have rejected (PR #79 review blocker 5).
|
|
461
568
|
const addedResults = [];
|
|
569
|
+
const retargeted = [];
|
|
462
570
|
// Process removed channels — itemized truthfully: removal of a channel
|
|
463
571
|
// we never had is reported, not silently accepted.
|
|
464
572
|
if (params.removed) {
|
|
@@ -487,6 +595,21 @@ export class ChannelRegistry {
|
|
|
487
595
|
this.emitTraceFn({ type: 'mcpl:channel-descriptor-rejected', serverId, channelId: channel.id, reason: 'update for unregistered channel' });
|
|
488
596
|
continue;
|
|
489
597
|
}
|
|
598
|
+
// A live subscription to the old target does not confirm the new
|
|
599
|
+
// type/address. Label-only updates preserve that confirmation. A
|
|
600
|
+
// channel decided open (or tuned out) is opened at its new target.
|
|
601
|
+
if (!sameChannelTarget(existing.descriptor, channel)) {
|
|
602
|
+
this.invalidateTransportConfirmation(existing);
|
|
603
|
+
// An operation queued or running for this channel reads its target
|
|
604
|
+
// again before it confirms anything, and after a failed RPC, so it
|
|
605
|
+
// opens the new one. With none, a channel decided open (or tuned
|
|
606
|
+
// out) is reconciled here.
|
|
607
|
+
const desired = this.getDesiredState(serverId, channel.id);
|
|
608
|
+
if ((desired === 'open' || desired === 'tuned-out') &&
|
|
609
|
+
!this.lifecycleOutstanding.has(this.lifecycleKey(serverId, channel.id))) {
|
|
610
|
+
retargeted.push(channel);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
490
613
|
existing.descriptor = channel;
|
|
491
614
|
this.appendLabelSighting(channel.id, channel.label, extractDmMeta(channel.metadata));
|
|
492
615
|
addedResults.push({ id: channel.id, accepted: true });
|
|
@@ -503,11 +626,7 @@ export class ChannelRegistry {
|
|
|
503
626
|
continue;
|
|
504
627
|
}
|
|
505
628
|
const key = `${serverId}:${channel.id}`;
|
|
506
|
-
this.
|
|
507
|
-
serverId,
|
|
508
|
-
descriptor: channel,
|
|
509
|
-
open: false,
|
|
510
|
-
});
|
|
629
|
+
this.registerEntry(key, serverId, channel);
|
|
511
630
|
this.appendLabelSighting(channel.id, channel.label, extractDmMeta(channel.metadata));
|
|
512
631
|
addedResults.push({ id: channel.id, accepted: true });
|
|
513
632
|
accepted.push(channel);
|
|
@@ -520,8 +639,9 @@ export class ChannelRegistry {
|
|
|
520
639
|
// deadlocks both sides until one times out (#160). The verdicts are
|
|
521
640
|
// settled above, so nothing in the response depends on reconciling.
|
|
522
641
|
responder?.respond({ results: addedResults });
|
|
523
|
-
|
|
524
|
-
|
|
642
|
+
const reconcile = [...accepted, ...retargeted];
|
|
643
|
+
if (reconcile.length > 0)
|
|
644
|
+
await this.reconcileChannels(serverId, reconcile);
|
|
525
645
|
this.emitTraceFn({
|
|
526
646
|
type: 'mcpl:channels-changed',
|
|
527
647
|
serverId,
|
|
@@ -677,8 +797,8 @@ export class ChannelRegistry {
|
|
|
677
797
|
}
|
|
678
798
|
if (message.tags)
|
|
679
799
|
message.tags = expandCoreTags(message.tags);
|
|
680
|
-
const convertedContent = message.content.map(convertBlock);
|
|
681
|
-
if (!emptyAllowed &&
|
|
800
|
+
const convertedContent = message.content.map((block) => convertBlock(block, 'attachment on channel message', serverId));
|
|
801
|
+
if (!emptyAllowed && isVisiblyEmptyConversion(message.content, convertedContent)) {
|
|
682
802
|
rejectEmpty();
|
|
683
803
|
continue;
|
|
684
804
|
}
|
|
@@ -688,15 +808,34 @@ export class ChannelRegistry {
|
|
|
688
808
|
// exactly the authority a self-attested channel would be stealing).
|
|
689
809
|
// A coalesced item is "accepted" only once the coalescer admits it, so
|
|
690
810
|
// for those this runs after the hook (below).
|
|
811
|
+
const incomingEntry = this.channels.get(incomingKey);
|
|
691
812
|
const markAccepted = () => {
|
|
692
813
|
this.defaultPublishChannel = message.channelId;
|
|
693
814
|
this.defaultPublishMessageId = message.messageId;
|
|
694
815
|
this.defaultPublishThreadId = message.threadId;
|
|
695
|
-
//
|
|
696
|
-
// the
|
|
697
|
-
//
|
|
698
|
-
//
|
|
699
|
-
|
|
816
|
+
// Where the host directs this server's lifecycle, only a receipt for
|
|
817
|
+
// the current target confirms it: traffic shows the transport open
|
|
818
|
+
// only for a confirmed target, and only for a channel still decided
|
|
819
|
+
// open or tuned out. After a retarget, traffic for this id may still
|
|
820
|
+
// come from the old subscription, and a close receipt that landed
|
|
821
|
+
// first (or during the coalescer's await) stands. Where the host
|
|
822
|
+
// doesn't direct it, the server opens its own channels and its
|
|
823
|
+
// traffic is the only evidence the host gets, so it marks the
|
|
824
|
+
// channel open, as it always has. The entry is the one the message
|
|
825
|
+
// was accepted for: one a re-registration put in its place meanwhile
|
|
826
|
+
// waits for its own evidence. Durable desired state still changes
|
|
827
|
+
// exclusively through lifecycle operations.
|
|
828
|
+
if (this.directsLifecycle(serverId)) {
|
|
829
|
+
if (this.unconfirmedChannelTargets.has(incomingEntry))
|
|
830
|
+
return;
|
|
831
|
+
const desired = this.getDesiredState(serverId, message.channelId);
|
|
832
|
+
if (desired !== 'open' && desired !== 'tuned-out')
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
else {
|
|
836
|
+
this.unconfirmedChannelTargets.delete(incomingEntry);
|
|
837
|
+
}
|
|
838
|
+
incomingEntry.open = true;
|
|
700
839
|
};
|
|
701
840
|
if (!coalesced)
|
|
702
841
|
markAccepted();
|
|
@@ -705,7 +844,7 @@ export class ChannelRegistry {
|
|
|
705
844
|
if (this.shouldTriggerInference) {
|
|
706
845
|
// The text the agent's context gets, as the push lane's filter reads
|
|
707
846
|
// it: the converted text blocks, a placeholder or reference stub
|
|
708
|
-
// among them,
|
|
847
|
+
// among them, NON_STRING_TEXT_PLACEHOLDER included.
|
|
709
848
|
const textContent = convertedContent
|
|
710
849
|
.filter((b) => b.type === 'text')
|
|
711
850
|
.map((b) => b.text)
|
|
@@ -734,6 +873,7 @@ export class ChannelRegistry {
|
|
|
734
873
|
metadata: message.metadata,
|
|
735
874
|
...(message.tags ? { tags: message.tags } : {}),
|
|
736
875
|
triggerInference,
|
|
876
|
+
acceptedAt: Date.now(),
|
|
737
877
|
};
|
|
738
878
|
if (coalesced) {
|
|
739
879
|
// RFC-006 §14.3: a coalesced message is admitted like any other and
|
|
@@ -759,6 +899,12 @@ export class ChannelRegistry {
|
|
|
759
899
|
}
|
|
760
900
|
continue;
|
|
761
901
|
}
|
|
902
|
+
// The source envelope is frozen here, at admission, before the message
|
|
903
|
+
// is queued or acknowledged: a rename or rebind while it waits in the
|
|
904
|
+
// queue cannot change where it says it came from.
|
|
905
|
+
const inboundSource = this.acceptInbound?.(event);
|
|
906
|
+
if (inboundSource)
|
|
907
|
+
event.inboundSource = inboundSource;
|
|
762
908
|
// Push to the processing queue
|
|
763
909
|
// Cast through unknown because McplChannelIncomingEvent matches the
|
|
764
910
|
// CustomEvent `${string}:${string}` type pattern but lacks an index signature.
|
|
@@ -801,27 +947,19 @@ export class ChannelRegistry {
|
|
|
801
947
|
// time a message reveals it (labels must show people, not ids).
|
|
802
948
|
if (existing.descriptor.metadata?.lazyRegistered &&
|
|
803
949
|
label && existing.descriptor.label === channelId) {
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
}
|
|
950
|
+
const metadata = extraMetadata
|
|
951
|
+
? { ...existing.descriptor.metadata, ...extraMetadata }
|
|
952
|
+
: existing.descriptor.metadata;
|
|
808
953
|
// The channel now has a real label, not just the bare id — durable.
|
|
809
|
-
|
|
954
|
+
// Recorded before the live descriptor takes it, since this branch's
|
|
955
|
+
// guard reads the descriptor: an append that throws leaves the
|
|
956
|
+
// placeholder, so the next push tries again.
|
|
957
|
+
this.appendLabelSighting(existing.descriptor.id, label, extractDmMeta(metadata));
|
|
958
|
+
existing.descriptor.label = label;
|
|
959
|
+
existing.descriptor.metadata = metadata;
|
|
810
960
|
}
|
|
811
961
|
return;
|
|
812
962
|
}
|
|
813
|
-
const key = `${serverId}:${channelId}`;
|
|
814
|
-
this.channels.set(key, {
|
|
815
|
-
serverId,
|
|
816
|
-
descriptor: {
|
|
817
|
-
id: channelId,
|
|
818
|
-
type: serverId,
|
|
819
|
-
label: label ?? channelId,
|
|
820
|
-
direction: 'bidirectional',
|
|
821
|
-
metadata: { lazyRegistered: true, ...(extraMetadata ?? {}) },
|
|
822
|
-
},
|
|
823
|
-
open: false,
|
|
824
|
-
});
|
|
825
963
|
// Only persist a REAL label durably. Falling back to the bare
|
|
826
964
|
// channelId as a placeholder (no server-supplied label at all — e.g. a
|
|
827
965
|
// Discord DM push missing both channelName and an author name,
|
|
@@ -831,11 +969,24 @@ export class ChannelRegistry {
|
|
|
831
969
|
// label-less post-restart event takes — recording the placeholder here
|
|
832
970
|
// would clobber the good label initializeLabelHistoryStore() just
|
|
833
971
|
// replayed, defeating the entire point of the durable log. The live
|
|
834
|
-
// descriptor
|
|
972
|
+
// descriptor below still carries the placeholder for in-process
|
|
835
973
|
// routing; only the durable write is skipped when there's no real
|
|
836
|
-
// label to record.
|
|
974
|
+
// label to record. A real label is recorded before the entry exists:
|
|
975
|
+
// an append that throws registers nothing, so the next push tries again.
|
|
837
976
|
if (label)
|
|
838
977
|
this.appendLabelSighting(channelId, label, extractDmMeta(extraMetadata));
|
|
978
|
+
const key = `${serverId}:${channelId}`;
|
|
979
|
+
this.channels.set(key, {
|
|
980
|
+
serverId,
|
|
981
|
+
descriptor: {
|
|
982
|
+
id: channelId,
|
|
983
|
+
type: serverId,
|
|
984
|
+
label: label ?? channelId,
|
|
985
|
+
direction: 'bidirectional',
|
|
986
|
+
metadata: { lazyRegistered: true, ...(extraMetadata ?? {}) },
|
|
987
|
+
},
|
|
988
|
+
open: false,
|
|
989
|
+
});
|
|
839
990
|
this.emitTraceFn({
|
|
840
991
|
type: 'mcpl:channel-lazy-registered',
|
|
841
992
|
serverId,
|
|
@@ -958,6 +1109,12 @@ export class ChannelRegistry {
|
|
|
958
1109
|
getDescriptor(channelId) {
|
|
959
1110
|
return this.findChannelEntry(channelId)?.descriptor;
|
|
960
1111
|
}
|
|
1112
|
+
/** The registered label of `channelId` on exactly `serverId` (no
|
|
1113
|
+
* cross-server first match), or undefined when it has none. */
|
|
1114
|
+
getChannelLabel(serverId, channelId) {
|
|
1115
|
+
const label = this.channels.get(`${serverId}:${channelId}`)?.descriptor.label;
|
|
1116
|
+
return typeof label === 'string' && label.length > 0 ? label : undefined;
|
|
1117
|
+
}
|
|
961
1118
|
isChannelOpen(channelId) {
|
|
962
1119
|
return this.findChannelEntry(channelId)?.open === true;
|
|
963
1120
|
}
|
|
@@ -983,7 +1140,7 @@ export class ChannelRegistry {
|
|
|
983
1140
|
* Get synthesized tool definitions for channel operations.
|
|
984
1141
|
*/
|
|
985
1142
|
getChannelTools() {
|
|
986
|
-
return
|
|
1143
|
+
return this.channelTools;
|
|
987
1144
|
}
|
|
988
1145
|
/**
|
|
989
1146
|
* Handle a call to one of the synthesized channel tools.
|
|
@@ -1008,7 +1165,7 @@ export class ChannelRegistry {
|
|
|
1008
1165
|
case 'channel_decline':
|
|
1009
1166
|
return this.handleToolDecline(input);
|
|
1010
1167
|
case 'channel_publish':
|
|
1011
|
-
return this.handleToolPublish(input);
|
|
1168
|
+
return this.handleToolPublish(input, origin);
|
|
1012
1169
|
case 'think':
|
|
1013
1170
|
return this.handleToolThink(input);
|
|
1014
1171
|
case 'journal':
|
|
@@ -1036,9 +1193,15 @@ export class ChannelRegistry {
|
|
|
1036
1193
|
// (heartbeats). Without this, a fork
|
|
1037
1194
|
// was advertised the global locus but published somewhere else; and a trunk
|
|
1038
1195
|
// agent was told the wrong channel under concurrency.
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
const
|
|
1196
|
+
// Each lookup is keyed on whether a name was given, not its truthiness:
|
|
1197
|
+
// '' is a name an ephemeral run can have, and resolveLocus reads it too.
|
|
1198
|
+
const home = agentName !== undefined ? this.homeChannelResolver?.(agentName) : undefined;
|
|
1199
|
+
const active = agentName !== undefined ? this.activeChannelResolver?.(agentName) : undefined;
|
|
1200
|
+
// An ephemeral run takes no global fallback (takesGlobalFallback): neither
|
|
1201
|
+
// the latest inbound message's channel as its outgoing one, nor that
|
|
1202
|
+
// message as the one it is replying to.
|
|
1203
|
+
const fallback = agentName !== undefined ? this.globalFallbackFor(agentName) : this.defaultPublishChannel;
|
|
1204
|
+
const outgoing = home ?? active ?? fallback;
|
|
1042
1205
|
if (openChannels.length === 0 && !outgoing) {
|
|
1043
1206
|
return undefined;
|
|
1044
1207
|
}
|
|
@@ -1046,9 +1209,9 @@ export class ChannelRegistry {
|
|
|
1046
1209
|
// Incoming: the most-recent inbound message (what the agent is replying to).
|
|
1047
1210
|
// Left process-global — per-channel inbound tracking (the right messageId for
|
|
1048
1211
|
// a fork's own channel) is a separate concern from the outbound routing fix.
|
|
1049
|
-
if (
|
|
1212
|
+
if (fallback && this.defaultPublishMessageId) {
|
|
1050
1213
|
context.incoming = {
|
|
1051
|
-
channelId:
|
|
1214
|
+
channelId: fallback,
|
|
1052
1215
|
messageId: this.defaultPublishMessageId,
|
|
1053
1216
|
threadId: this.defaultPublishThreadId,
|
|
1054
1217
|
};
|
|
@@ -1074,6 +1237,10 @@ export class ChannelRegistry {
|
|
|
1074
1237
|
* server's channels don't linger and route speech into the void.
|
|
1075
1238
|
*/
|
|
1076
1239
|
removeServer(serverId) {
|
|
1240
|
+
// Every lifecycle operation queued for this server is cancelled when it
|
|
1241
|
+
// would run (ChannelLifecycleCancelledError), so none acts on a closing
|
|
1242
|
+
// connection or on a later re-registration's entries.
|
|
1243
|
+
this.serverRemovals.set(serverId, (this.serverRemovals.get(serverId) ?? 0) + 1);
|
|
1077
1244
|
for (const [key, entry] of this.channels) {
|
|
1078
1245
|
if (entry.serverId !== serverId)
|
|
1079
1246
|
continue;
|
|
@@ -1112,14 +1279,7 @@ export class ChannelRegistry {
|
|
|
1112
1279
|
initializeLifecycleStore() {
|
|
1113
1280
|
if (!this.store)
|
|
1114
1281
|
return;
|
|
1115
|
-
|
|
1116
|
-
this.store.registerState({ id: CHANNEL_LIFECYCLE_LOG_ID, strategy: 'append_log' });
|
|
1117
|
-
}
|
|
1118
|
-
catch (error) {
|
|
1119
|
-
if (!(error instanceof Error) || !error.message.includes('State already exists')) {
|
|
1120
|
-
throw error;
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1282
|
+
ensureStateRegistered(this.store, { id: CHANNEL_LIFECYCLE_LOG_ID, strategy: 'append_log' });
|
|
1123
1283
|
const raw = this.store.getStateJson(CHANNEL_LIFECYCLE_LOG_ID);
|
|
1124
1284
|
if (!Array.isArray(raw))
|
|
1125
1285
|
return;
|
|
@@ -1157,6 +1317,15 @@ export class ChannelRegistry {
|
|
|
1157
1317
|
}
|
|
1158
1318
|
}
|
|
1159
1319
|
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Each writer appends its record here before it changes memory for it, so
|
|
1322
|
+
* an append that throws, as on a full disk, fails the operation with that
|
|
1323
|
+
* change unmade, and memory and the log still agree. A retry then records
|
|
1324
|
+
* the change rather than finding it in memory already, where
|
|
1325
|
+
* setDesiredState's same-state return would record nothing, and a restart
|
|
1326
|
+
* replays what memory held. appendLabelSighting keeps the same order for
|
|
1327
|
+
* the label history.
|
|
1328
|
+
*/
|
|
1160
1329
|
appendLifecycleEvent(event) {
|
|
1161
1330
|
this.store?.appendToStateJson(CHANNEL_LIFECYCLE_LOG_ID, event);
|
|
1162
1331
|
}
|
|
@@ -1173,14 +1342,7 @@ export class ChannelRegistry {
|
|
|
1173
1342
|
initializeLabelHistoryStore() {
|
|
1174
1343
|
if (!this.store)
|
|
1175
1344
|
return;
|
|
1176
|
-
|
|
1177
|
-
this.store.registerState({ id: CHANNEL_LABEL_HISTORY_LOG_ID, strategy: 'append_log' });
|
|
1178
|
-
}
|
|
1179
|
-
catch (error) {
|
|
1180
|
-
if (!(error instanceof Error) || !error.message.includes('State already exists')) {
|
|
1181
|
-
throw error;
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1345
|
+
ensureStateRegistered(this.store, { id: CHANNEL_LABEL_HISTORY_LOG_ID, strategy: 'append_log' });
|
|
1184
1346
|
const raw = this.store.getStateJson(CHANNEL_LABEL_HISTORY_LOG_ID);
|
|
1185
1347
|
if (!Array.isArray(raw))
|
|
1186
1348
|
return;
|
|
@@ -1260,6 +1422,16 @@ export class ChannelRegistry {
|
|
|
1260
1422
|
const isDmUnchanged = isDm === undefined || this.dmClassified.get(channelId) === true;
|
|
1261
1423
|
if (labelUnchanged && recipientIdUnchanged && recipientNameUnchanged && isDmUnchanged)
|
|
1262
1424
|
return;
|
|
1425
|
+
// The log first, as for lifecycle events: an append that throws leaves
|
|
1426
|
+
// the history in memory as it was, so the next sighting records it.
|
|
1427
|
+
this.store?.appendToStateJson(CHANNEL_LABEL_HISTORY_LOG_ID, {
|
|
1428
|
+
channelId,
|
|
1429
|
+
label,
|
|
1430
|
+
ts: Date.now(),
|
|
1431
|
+
...(recipientId ? { recipientId } : {}),
|
|
1432
|
+
...(recipientName ? { recipientName } : {}),
|
|
1433
|
+
...(isDm ? { isDm } : {}),
|
|
1434
|
+
});
|
|
1263
1435
|
this.labelHistory.set(channelId, label);
|
|
1264
1436
|
let seen = this.allLabelsSeen.get(channelId);
|
|
1265
1437
|
if (!seen) {
|
|
@@ -1273,14 +1445,6 @@ export class ChannelRegistry {
|
|
|
1273
1445
|
this.dmRecipientNames.set(channelId, recipientName);
|
|
1274
1446
|
if (isDm)
|
|
1275
1447
|
this.dmClassified.set(channelId, true);
|
|
1276
|
-
this.store?.appendToStateJson(CHANNEL_LABEL_HISTORY_LOG_ID, {
|
|
1277
|
-
channelId,
|
|
1278
|
-
label,
|
|
1279
|
-
ts: Date.now(),
|
|
1280
|
-
...(recipientId ? { recipientId } : {}),
|
|
1281
|
-
...(recipientName ? { recipientName } : {}),
|
|
1282
|
-
...(isDm ? { isDm } : {}),
|
|
1283
|
-
});
|
|
1284
1448
|
}
|
|
1285
1449
|
/**
|
|
1286
1450
|
* Look up a label spec against label *history* rather than the live
|
|
@@ -1498,7 +1662,6 @@ export class ChannelRegistry {
|
|
|
1498
1662
|
const key = this.lifecycleKey(serverId, channelId);
|
|
1499
1663
|
if (this.desiredStates.get(key)?.state === desired)
|
|
1500
1664
|
return;
|
|
1501
|
-
this.desiredStates.set(key, { state: desired, source });
|
|
1502
1665
|
this.appendLifecycleEvent({
|
|
1503
1666
|
kind: 'desired-state',
|
|
1504
1667
|
serverId,
|
|
@@ -1507,6 +1670,7 @@ export class ChannelRegistry {
|
|
|
1507
1670
|
source,
|
|
1508
1671
|
timestamp: new Date().toISOString(),
|
|
1509
1672
|
});
|
|
1673
|
+
this.desiredStates.set(key, { state: desired, source });
|
|
1510
1674
|
}
|
|
1511
1675
|
// ==========================================================================
|
|
1512
1676
|
// Tune-out state (issue #77) — durable in the lifecycle log
|
|
@@ -1519,12 +1683,6 @@ export class ChannelRegistry {
|
|
|
1519
1683
|
*/
|
|
1520
1684
|
enterTuneOut(serverId, channelId, params, source) {
|
|
1521
1685
|
const key = this.lifecycleKey(serverId, channelId);
|
|
1522
|
-
this.desiredStates.set(key, {
|
|
1523
|
-
state: 'tuned-out',
|
|
1524
|
-
source,
|
|
1525
|
-
tuneOut: params,
|
|
1526
|
-
wakeCount: 0,
|
|
1527
|
-
});
|
|
1528
1686
|
this.appendLifecycleEvent({
|
|
1529
1687
|
kind: 'desired-state',
|
|
1530
1688
|
serverId,
|
|
@@ -1534,6 +1692,12 @@ export class ChannelRegistry {
|
|
|
1534
1692
|
source,
|
|
1535
1693
|
timestamp: new Date().toISOString(),
|
|
1536
1694
|
});
|
|
1695
|
+
this.desiredStates.set(key, {
|
|
1696
|
+
state: 'tuned-out',
|
|
1697
|
+
source,
|
|
1698
|
+
tuneOut: params,
|
|
1699
|
+
wakeCount: 0,
|
|
1700
|
+
});
|
|
1537
1701
|
}
|
|
1538
1702
|
/**
|
|
1539
1703
|
* End the active tune-out, returning the channel to `nextState`.
|
|
@@ -1547,7 +1711,6 @@ export class ChannelRegistry {
|
|
|
1547
1711
|
if (current?.state !== 'tuned-out' || !current.tuneOut)
|
|
1548
1712
|
return null;
|
|
1549
1713
|
const ended = { params: current.tuneOut, wakeCount: current.wakeCount ?? 0 };
|
|
1550
|
-
this.desiredStates.set(key, { state: nextState, source });
|
|
1551
1714
|
this.appendLifecycleEvent({
|
|
1552
1715
|
kind: 'desired-state',
|
|
1553
1716
|
serverId,
|
|
@@ -1556,6 +1719,7 @@ export class ChannelRegistry {
|
|
|
1556
1719
|
source,
|
|
1557
1720
|
timestamp: new Date().toISOString(),
|
|
1558
1721
|
});
|
|
1722
|
+
this.desiredStates.set(key, { state: nextState, source });
|
|
1559
1723
|
return ended;
|
|
1560
1724
|
}
|
|
1561
1725
|
/**
|
|
@@ -1570,16 +1734,17 @@ export class ChannelRegistry {
|
|
|
1570
1734
|
const current = this.desiredStates.get(key);
|
|
1571
1735
|
if (current?.state !== 'tuned-out' || !current.tuneOut)
|
|
1572
1736
|
return null;
|
|
1573
|
-
|
|
1737
|
+
const wakeCount = (current.wakeCount ?? 0) + 1;
|
|
1574
1738
|
this.appendLifecycleEvent({
|
|
1575
1739
|
kind: 'tune-out-wake',
|
|
1576
1740
|
serverId,
|
|
1577
1741
|
channelId,
|
|
1578
1742
|
epochId: current.tuneOut.epochId,
|
|
1579
|
-
wakeCount
|
|
1743
|
+
wakeCount,
|
|
1580
1744
|
timestamp: new Date().toISOString(),
|
|
1581
1745
|
});
|
|
1582
|
-
|
|
1746
|
+
current.wakeCount = wakeCount;
|
|
1747
|
+
return { params: current.tuneOut, wakeCount };
|
|
1583
1748
|
}
|
|
1584
1749
|
/** Active tune-out params + wake count for a channel, or null. */
|
|
1585
1750
|
getTuneOutState(serverId, channelId) {
|
|
@@ -1603,19 +1768,13 @@ export class ChannelRegistry {
|
|
|
1603
1768
|
readCoordinatorState(stateId) {
|
|
1604
1769
|
if (!this.store)
|
|
1605
1770
|
return null;
|
|
1606
|
-
|
|
1607
|
-
this.store.registerState({ id: stateId, strategy: 'snapshot' });
|
|
1608
|
-
}
|
|
1609
|
-
catch { /* already registered */ }
|
|
1771
|
+
ensureStateRegistered(this.store, { id: stateId, strategy: 'snapshot' });
|
|
1610
1772
|
return this.store.getStateJson(stateId);
|
|
1611
1773
|
}
|
|
1612
1774
|
writeCoordinatorState(stateId, value) {
|
|
1613
1775
|
if (!this.store)
|
|
1614
1776
|
return;
|
|
1615
|
-
|
|
1616
|
-
this.store.registerState({ id: stateId, strategy: 'snapshot' });
|
|
1617
|
-
}
|
|
1618
|
-
catch { /* already registered */ }
|
|
1777
|
+
ensureStateRegistered(this.store, { id: stateId, strategy: 'snapshot' });
|
|
1619
1778
|
this.store.setStateJson(stateId, value);
|
|
1620
1779
|
}
|
|
1621
1780
|
/**
|
|
@@ -1646,13 +1805,13 @@ export class ChannelRegistry {
|
|
|
1646
1805
|
: 'closed';
|
|
1647
1806
|
this.setDesiredState(serverId, channel.id, desired, 'legacy-recipe-migration');
|
|
1648
1807
|
}
|
|
1649
|
-
this.migratedLegacyPolicies.add(serverId);
|
|
1650
1808
|
this.appendLifecycleEvent({
|
|
1651
1809
|
kind: 'legacy-policy-migrated',
|
|
1652
1810
|
serverId,
|
|
1653
1811
|
source: Array.isArray(policy) ? 'allow-list' : policy,
|
|
1654
1812
|
timestamp: new Date().toISOString(),
|
|
1655
1813
|
});
|
|
1814
|
+
this.migratedLegacyPolicies.add(serverId);
|
|
1656
1815
|
}
|
|
1657
1816
|
/**
|
|
1658
1817
|
* True when the server's recipe subscription policy wants this channel
|
|
@@ -1700,6 +1859,223 @@ export class ChannelRegistry {
|
|
|
1700
1859
|
: 'default-closed');
|
|
1701
1860
|
return channel.initiallyOpen !== true && byPolicy;
|
|
1702
1861
|
}
|
|
1862
|
+
/**
|
|
1863
|
+
* Apply current durable intent, serializing the transport rather than the
|
|
1864
|
+
* server's registration ACK. Re-read after every await: registration,
|
|
1865
|
+
* descriptor updates, or a newer decision may have replaced our snapshot.
|
|
1866
|
+
*/
|
|
1867
|
+
applyDesiredChannelState(serverId, channelId, options) {
|
|
1868
|
+
const lifecycleKey = this.lifecycleKey(serverId, channelId);
|
|
1869
|
+
const removals = this.serverRemovals.get(serverId) ?? 0;
|
|
1870
|
+
const previous = this.channelLifecycleTails.get(lifecycleKey) ?? Promise.resolve();
|
|
1871
|
+
this.lifecycleOutstanding.set(lifecycleKey, (this.lifecycleOutstanding.get(lifecycleKey) ?? 0) + 1);
|
|
1872
|
+
const operation = previous.catch(() => { }).then(() => this.runLifecycleOperation(serverId, channelId, removals, options));
|
|
1873
|
+
// Keep the rejection visible to existing joiners, while observing it
|
|
1874
|
+
// immediately and letting each queued successor start after either result.
|
|
1875
|
+
const tail = operation.then(() => { });
|
|
1876
|
+
this.channelLifecycleTails.set(lifecycleKey, tail);
|
|
1877
|
+
const cleanup = () => {
|
|
1878
|
+
if (this.channelLifecycleTails.get(lifecycleKey) === tail) {
|
|
1879
|
+
this.channelLifecycleTails.delete(lifecycleKey);
|
|
1880
|
+
}
|
|
1881
|
+
};
|
|
1882
|
+
void tail.then(cleanup, cleanup);
|
|
1883
|
+
return operation;
|
|
1884
|
+
}
|
|
1885
|
+
/** One queued lifecycle operation, run when its turn in the queue comes. */
|
|
1886
|
+
async runLifecycleOperation(serverId, channelId, removals, options) {
|
|
1887
|
+
const channelKey = `${serverId}:${channelId}`;
|
|
1888
|
+
const lifecycleKey = this.lifecycleKey(serverId, channelId);
|
|
1889
|
+
try {
|
|
1890
|
+
let attempts = 0;
|
|
1891
|
+
for (;;) {
|
|
1892
|
+
if ((this.serverRemovals.get(serverId) ?? 0) !== removals) {
|
|
1893
|
+
throw new ChannelLifecycleCancelledError(`Channel ${channelId}: its server's registrations were removed while this operation was queued`);
|
|
1894
|
+
}
|
|
1895
|
+
const entry = this.channels.get(channelKey);
|
|
1896
|
+
if (!entry)
|
|
1897
|
+
throw new Error(`Channel is no longer registered: ${channelId}`);
|
|
1898
|
+
const server = this.serverRegistry.getServer(serverId);
|
|
1899
|
+
if (!server)
|
|
1900
|
+
throw new Error(`Server not found: ${serverId}`);
|
|
1901
|
+
if (!CapabilityGrant.of(server).has('channels.lifecycle')) {
|
|
1902
|
+
throw new Error(`channels.lifecycle not in "${serverId}"'s effective grant (§14.1)`);
|
|
1903
|
+
}
|
|
1904
|
+
const descriptor = entry.descriptor;
|
|
1905
|
+
const desired = this.getDesiredState(serverId, channelId);
|
|
1906
|
+
const open = desired === 'open' || desired === 'tuned-out';
|
|
1907
|
+
// Per-RPC timeouts do not bound a stream of promptly successful but
|
|
1908
|
+
// continually superseded receipts. Bound this whole operation, then
|
|
1909
|
+
// release its queue slot and surface the failure to delivery joiners.
|
|
1910
|
+
// Its open flag needs no change here: every retry follows a replaced
|
|
1911
|
+
// registration, a retarget (already invalidated, whether the old
|
|
1912
|
+
// target's RPC was answered or failed), or a receipt for this very
|
|
1913
|
+
// target.
|
|
1914
|
+
if (attempts >= MAX_CHANNEL_LIFECYCLE_ATTEMPTS) {
|
|
1915
|
+
const error = new ChannelLifecycleConvergenceError(`Channel ${channelId} did not converge after ${attempts} lifecycle attempts; its target or desired state kept changing`);
|
|
1916
|
+
this.emitTraceFn({
|
|
1917
|
+
type: 'mcpl:channel-reconcile-failed',
|
|
1918
|
+
serverId, channelId, desired, attempts, error: error.message,
|
|
1919
|
+
});
|
|
1920
|
+
throw error;
|
|
1921
|
+
}
|
|
1922
|
+
attempts++;
|
|
1923
|
+
const sameTarget = () => this.serverRegistry.getServer(serverId) === server &&
|
|
1924
|
+
sameChannelTarget(entry.descriptor, descriptor);
|
|
1925
|
+
let result;
|
|
1926
|
+
try {
|
|
1927
|
+
if (open) {
|
|
1928
|
+
result = await server.sendChannelsOpen({
|
|
1929
|
+
channelId, type: descriptor.type, address: descriptor.address,
|
|
1930
|
+
...(options?.history ? { history: options.history } : {}),
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
else {
|
|
1934
|
+
await server.sendChannelsClose({ channelId });
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
catch (error) {
|
|
1938
|
+
// An RPC can fail after the server acted, so a failure for the
|
|
1939
|
+
// current target leaves its outcome unknown: it is marked
|
|
1940
|
+
// unconfirmed with open unchanged, which keeps a confirmed-open
|
|
1941
|
+
// channel usable after a failed backscroll (the mark is read only
|
|
1942
|
+
// while not open). A failure from an older connection or target
|
|
1943
|
+
// also withdraws the open flag.
|
|
1944
|
+
if (this.channels.get(channelKey) === entry) {
|
|
1945
|
+
if (sameTarget())
|
|
1946
|
+
this.unconfirmedChannelTargets.add(entry);
|
|
1947
|
+
else
|
|
1948
|
+
this.invalidateTransportConfirmation(entry);
|
|
1949
|
+
}
|
|
1950
|
+
// A retarget while this RPC was out queued nothing for the new
|
|
1951
|
+
// target, since handleChanged leaves that to this operation. So a
|
|
1952
|
+
// failure for the old target says nothing about the new one, and a
|
|
1953
|
+
// channel still decided open or tuned out tries its new target,
|
|
1954
|
+
// within the attempt limit.
|
|
1955
|
+
if (this.channels.get(channelKey) === entry && !sameChannelTarget(entry.descriptor, descriptor)) {
|
|
1956
|
+
const now = this.getDesiredState(serverId, channelId);
|
|
1957
|
+
if (now === 'open' || now === 'tuned-out')
|
|
1958
|
+
continue;
|
|
1959
|
+
}
|
|
1960
|
+
throw error;
|
|
1961
|
+
}
|
|
1962
|
+
// A server can answer for a channel other than the one named: one
|
|
1963
|
+
// that falls back to its first channel of the type for an id it no
|
|
1964
|
+
// longer knows opens that channel and returns its history. Whatever
|
|
1965
|
+
// becomes of this receipt, the server did that: say so, and undo the
|
|
1966
|
+
// open it made if the host wants that channel closed.
|
|
1967
|
+
const substitute = open ? substitutedChannel(result, channelId) : undefined;
|
|
1968
|
+
if (substitute !== undefined) {
|
|
1969
|
+
// Each id prints through logValue, as the rejection lines' ids do,
|
|
1970
|
+
// so an odd one can't add a line or a field.
|
|
1971
|
+
const answered = logValue(substitute.kind === 'other' ? substitute.id : substitute.value);
|
|
1972
|
+
console.error(`[channel-open-substituted] server=${serverId} requested=${logValue(channelId)} answered=${answered}`);
|
|
1973
|
+
this.emitTraceFn({ type: 'mcpl:channel-open-substituted', serverId, channelId, answeredFor: answered });
|
|
1974
|
+
// A malformed id names nothing registered here, so there is
|
|
1975
|
+
// nothing to close.
|
|
1976
|
+
if (substitute.kind === 'other')
|
|
1977
|
+
this.closeSubstitutedChannel(serverId, substitute.id);
|
|
1978
|
+
}
|
|
1979
|
+
// Never apply an old receipt to the current target, even transiently:
|
|
1980
|
+
// a corrective open might fail before that stale bit is replaced.
|
|
1981
|
+
if (this.channels.get(channelKey) !== entry)
|
|
1982
|
+
continue;
|
|
1983
|
+
if (!sameTarget()) {
|
|
1984
|
+
this.invalidateTransportConfirmation(entry);
|
|
1985
|
+
continue;
|
|
1986
|
+
}
|
|
1987
|
+
// A current receipt for another channel confirms nothing about this
|
|
1988
|
+
// one, and its history is another channel's. So the open fails,
|
|
1989
|
+
// nothing in the answer is used, and this channel's open state is
|
|
1990
|
+
// unconfirmed: a server that doesn't resolve this id may not resolve
|
|
1991
|
+
// its sends either.
|
|
1992
|
+
if (substitute !== undefined) {
|
|
1993
|
+
this.invalidateTransportConfirmation(entry);
|
|
1994
|
+
throw new ChannelOpenSubstitutionError(`Server "${serverId}" answered channels/open for ${channelId} with ` +
|
|
1995
|
+
(substitute.kind === 'other' ? 'a different channel' : 'a channel id that is not a string') +
|
|
1996
|
+
`; ${channelId} is not marked open, and nothing in the answer is used`);
|
|
1997
|
+
}
|
|
1998
|
+
entry.open = open;
|
|
1999
|
+
this.unconfirmedChannelTargets.delete(entry);
|
|
2000
|
+
const now = this.getDesiredState(serverId, channelId);
|
|
2001
|
+
if (open !== (now === 'open' || now === 'tuned-out'))
|
|
2002
|
+
continue;
|
|
2003
|
+
return { open, result };
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
finally {
|
|
2007
|
+
// Synchronous with the operation's last look at its target: from here
|
|
2008
|
+
// on, nothing it does will open a new target.
|
|
2009
|
+
const left = (this.lifecycleOutstanding.get(lifecycleKey) ?? 1) - 1;
|
|
2010
|
+
if (left > 0)
|
|
2011
|
+
this.lifecycleOutstanding.set(lifecycleKey, left);
|
|
2012
|
+
else
|
|
2013
|
+
this.lifecycleOutstanding.delete(lifecycleKey);
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
/**
|
|
2017
|
+
* A server that answered an open with another channel really opened that
|
|
2018
|
+
* one. When it's registered here and the host wants it closed, close it
|
|
2019
|
+
* through its own lifecycle queue: otherwise its traffic would reach the
|
|
2020
|
+
* resident, marked open by its first incoming message. A channel the host
|
|
2021
|
+
* wants open is left alone, and a close can't be answered for another
|
|
2022
|
+
* channel, so this never starts another substitution. An id the host
|
|
2023
|
+
* doesn't know needs nothing: §14.5 refuses its traffic.
|
|
2024
|
+
*/
|
|
2025
|
+
closeSubstitutedChannel(serverId, channelId) {
|
|
2026
|
+
if (!this.channels.has(`${serverId}:${channelId}`))
|
|
2027
|
+
return;
|
|
2028
|
+
if (this.getDesiredState(serverId, channelId) !== 'closed')
|
|
2029
|
+
return;
|
|
2030
|
+
this.applyDesiredChannelState(serverId, channelId).catch((err) => {
|
|
2031
|
+
// Removal while queued is cancellation, as in reconcileChannels, and
|
|
2032
|
+
// exhausted convergence emitted its own diagnostic.
|
|
2033
|
+
if (!this.channels.has(`${serverId}:${channelId}`) ||
|
|
2034
|
+
err instanceof ChannelLifecycleCancelledError ||
|
|
2035
|
+
err instanceof ChannelLifecycleConvergenceError)
|
|
2036
|
+
return;
|
|
2037
|
+
this.emitTraceFn({
|
|
2038
|
+
type: 'mcpl:channel-reconcile-failed',
|
|
2039
|
+
serverId, channelId, desired: 'closed', error: err.message,
|
|
2040
|
+
});
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
/**
|
|
2044
|
+
* Join outstanding lifecycle work without recording new intent. Retargeting
|
|
2045
|
+
* may have invalidated open confirmation while the decision remains pending.
|
|
2046
|
+
* A close that settles during the wait remains authoritative; waiting for
|
|
2047
|
+
* it is not an instruction to open the channel again.
|
|
2048
|
+
*/
|
|
2049
|
+
async waitForOpenChannel(entry) {
|
|
2050
|
+
const lifecycleKey = this.lifecycleKey(entry.serverId, entry.descriptor.id);
|
|
2051
|
+
for (;;) {
|
|
2052
|
+
const pending = this.channelLifecycleTails.get(lifecycleKey);
|
|
2053
|
+
if (!pending)
|
|
2054
|
+
break;
|
|
2055
|
+
try {
|
|
2056
|
+
await pending;
|
|
2057
|
+
}
|
|
2058
|
+
catch (error) {
|
|
2059
|
+
// A redundant backscroll failure can leave a known-open channel
|
|
2060
|
+
// usable. Exhausted convergence is different: it never established
|
|
2061
|
+
// the current target, so joined delivery must fail visibly.
|
|
2062
|
+
if (error instanceof ChannelLifecycleConvergenceError)
|
|
2063
|
+
throw error;
|
|
2064
|
+
}
|
|
2065
|
+
// A settled tail releases its own key. One still current after it
|
|
2066
|
+
// settled would be awaited again without ever yielding to the event
|
|
2067
|
+
// loop, so stop waiting rather than spin.
|
|
2068
|
+
if (this.channelLifecycleTails.get(lifecycleKey) === pending)
|
|
2069
|
+
break;
|
|
2070
|
+
}
|
|
2071
|
+
const current = this.channels.get(`${entry.serverId}:${entry.descriptor.id}`);
|
|
2072
|
+
if (!current)
|
|
2073
|
+
throw new Error('Channel registration was removed while delivery waited');
|
|
2074
|
+
if (!current.open || this.getDesiredState(current.serverId, current.descriptor.id) === 'closed') {
|
|
2075
|
+
throw new Error('Channel is not confirmed open for delivery, or was selected for closure while delivery waited');
|
|
2076
|
+
}
|
|
2077
|
+
return current;
|
|
2078
|
+
}
|
|
1703
2079
|
async reconcileChannels(serverId, channels) {
|
|
1704
2080
|
const server = this.serverRegistry.getServer(serverId);
|
|
1705
2081
|
if (!server)
|
|
@@ -1720,57 +2096,17 @@ export class ChannelRegistry {
|
|
|
1720
2096
|
// announced to the agent as one batched notice after the loop, so a
|
|
1721
2097
|
// first boot on a new policy doesn't produce one notice per channel.
|
|
1722
2098
|
const policyOpened = [];
|
|
2099
|
+
// Record all admissions before waiting on any channel. A slow first RPC
|
|
2100
|
+
// must not delay intent for the rest of this registration batch.
|
|
1723
2101
|
for (const channel of channels) {
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
const key = `${serverId}:${channel.id}`;
|
|
1728
|
-
const desired = this.getDesiredState(serverId, channel.id);
|
|
1729
|
-
const entry = this.channels.get(key);
|
|
1730
|
-
// Tuned-out sits on the OPEN side of reconcile: traffic must keep
|
|
1731
|
-
// arriving (the subconscious reads it); only main's wake/visibility
|
|
1732
|
-
// is diverted, downstream at ingestion.
|
|
1733
|
-
if (desired !== 'open' && desired !== 'tuned-out') {
|
|
1734
|
-
try {
|
|
1735
|
-
await server.sendChannelsClose({ channelId: channel.id });
|
|
1736
|
-
if (entry)
|
|
1737
|
-
entry.open = false;
|
|
1738
|
-
}
|
|
1739
|
-
catch (err) {
|
|
1740
|
-
this.emitTraceFn({
|
|
1741
|
-
type: 'mcpl:channel-reconcile-failed',
|
|
1742
|
-
serverId,
|
|
1743
|
-
channelId: channel.id,
|
|
1744
|
-
desired,
|
|
1745
|
-
error: err.message,
|
|
1746
|
-
});
|
|
1747
|
-
}
|
|
1748
|
-
continue;
|
|
1749
|
-
}
|
|
1750
|
-
try {
|
|
1751
|
-
await server.sendChannelsOpen({
|
|
1752
|
-
channelId: channel.id,
|
|
1753
|
-
type: channel.type,
|
|
1754
|
-
address: channel.address,
|
|
1755
|
-
});
|
|
1756
|
-
if (entry)
|
|
1757
|
-
entry.open = true;
|
|
1758
|
-
}
|
|
1759
|
-
catch (err) {
|
|
1760
|
-
if (entry)
|
|
1761
|
-
entry.open = false;
|
|
1762
|
-
this.emitTraceFn({
|
|
1763
|
-
type: 'mcpl:channel-reconcile-failed',
|
|
1764
|
-
serverId,
|
|
1765
|
-
channelId: channel.id,
|
|
1766
|
-
desired,
|
|
1767
|
-
error: err.message,
|
|
1768
|
-
});
|
|
2102
|
+
const entry = this.channels.get(`${serverId}:${channel.id}`);
|
|
2103
|
+
if (entry && this.ensureInitialDesiredState(serverId, entry.descriptor)) {
|
|
2104
|
+
policyOpened.push({ channelId: channel.id, label: entry.descriptor.label });
|
|
1769
2105
|
}
|
|
1770
2106
|
}
|
|
1771
|
-
// Announce
|
|
1772
|
-
//
|
|
1773
|
-
//
|
|
2107
|
+
// Announce the durable admission decision before awaiting transport.
|
|
2108
|
+
// This is intent, not confirmation that an open RPC succeeded. It stays
|
|
2109
|
+
// truthful if the first attempt fails, and a retry needs no new admission.
|
|
1774
2110
|
if (policyOpened.length > 0) {
|
|
1775
2111
|
try {
|
|
1776
2112
|
this.onChannelAutoOpened?.({
|
|
@@ -1783,6 +2119,32 @@ export class ChannelRegistry {
|
|
|
1783
2119
|
console.error('onChannelAutoOpened (policy) failed:', err);
|
|
1784
2120
|
}
|
|
1785
2121
|
}
|
|
2122
|
+
// Each channel has its own lifecycle queue, so a held RPC for one (up to
|
|
2123
|
+
// its timeout, times its attempts) doesn't delay its siblings.
|
|
2124
|
+
await Promise.allSettled(channels.map(async (channel) => {
|
|
2125
|
+
if (!this.channels.has(`${serverId}:${channel.id}`))
|
|
2126
|
+
return;
|
|
2127
|
+
try {
|
|
2128
|
+
await this.applyDesiredChannelState(serverId, channel.id);
|
|
2129
|
+
}
|
|
2130
|
+
catch (err) {
|
|
2131
|
+
// Removal while queued is cancellation, not a failed subscription.
|
|
2132
|
+
// Exhausted convergence and a substituted answer already emitted
|
|
2133
|
+
// their own diagnostics.
|
|
2134
|
+
if (!this.channels.has(`${serverId}:${channel.id}`) ||
|
|
2135
|
+
err instanceof ChannelLifecycleCancelledError ||
|
|
2136
|
+
err instanceof ChannelLifecycleConvergenceError ||
|
|
2137
|
+
err instanceof ChannelOpenSubstitutionError)
|
|
2138
|
+
return;
|
|
2139
|
+
this.emitTraceFn({
|
|
2140
|
+
type: 'mcpl:channel-reconcile-failed',
|
|
2141
|
+
serverId,
|
|
2142
|
+
channelId: channel.id,
|
|
2143
|
+
desired: this.getDesiredState(serverId, channel.id),
|
|
2144
|
+
error: err.message,
|
|
2145
|
+
});
|
|
2146
|
+
}
|
|
2147
|
+
}));
|
|
1786
2148
|
}
|
|
1787
2149
|
// ==========================================================================
|
|
1788
2150
|
// Private: Typing notification
|
|
@@ -1879,33 +2241,35 @@ export class ChannelRegistry {
|
|
|
1879
2241
|
};
|
|
1880
2242
|
}
|
|
1881
2243
|
/**
|
|
1882
|
-
*
|
|
1883
|
-
*
|
|
1884
|
-
*
|
|
1885
|
-
*
|
|
1886
|
-
*
|
|
2244
|
+
* Record durable desired-open intent, then queue the channel's lifecycle
|
|
2245
|
+
* work (applyDesiredChannelState), which tells the server to subscribe and
|
|
2246
|
+
* marks the live entry open on a receipt for its current target. Every
|
|
2247
|
+
* open the agent or its speech asks for — the channel_open tool, delivery
|
|
2248
|
+
* into a closed locus, an explicit send into a closed channel — comes
|
|
2249
|
+
* here. Reconciliation queues that work directly, as the closes do
|
|
2250
|
+
* (channel_close, a decline, and the close of a substituted channel).
|
|
1887
2251
|
* Desired state is recorded BEFORE the server round-trip: intent sticks
|
|
1888
2252
|
* even if the subscribe fails, and reconciliation retries later.
|
|
2253
|
+
* Automatic delivery needs open transport, not a change of attention:
|
|
2254
|
+
* preserve tune-out epochs both before and during the round-trip.
|
|
2255
|
+
*
|
|
2256
|
+
* `opened` says whether this call turned a closed channel open, which is
|
|
2257
|
+
* what the delivery and reply notices tell the resident. Reconfirming the
|
|
2258
|
+
* transport of a channel already decided open or tuned out isn't one.
|
|
1889
2259
|
*/
|
|
1890
2260
|
async openChannelNow(entry, source, history) {
|
|
1891
|
-
|
|
1892
|
-
const
|
|
1893
|
-
|
|
1894
|
-
|
|
2261
|
+
const transportOnly = source !== 'agent-tool';
|
|
2262
|
+
const before = this.getDesiredState(entry.serverId, entry.descriptor.id);
|
|
2263
|
+
const opened = before !== 'open' && before !== 'tuned-out';
|
|
2264
|
+
if (!transportOnly || before !== 'tuned-out') {
|
|
2265
|
+
this.setDesiredState(entry.serverId, entry.descriptor.id, 'open', source);
|
|
1895
2266
|
}
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
throw new Error(
|
|
2267
|
+
const applied = await this.applyDesiredChannelState(entry.serverId, entry.descriptor.id, { history });
|
|
2268
|
+
const desired = this.getDesiredState(entry.serverId, entry.descriptor.id);
|
|
2269
|
+
if (!applied.open || (desired !== 'open' && !(transportOnly && desired === 'tuned-out'))) {
|
|
2270
|
+
throw new Error('Channel open was superseded by a newer lifecycle decision');
|
|
1900
2271
|
}
|
|
1901
|
-
|
|
1902
|
-
channelId: entry.descriptor.id,
|
|
1903
|
-
type: entry.descriptor.type,
|
|
1904
|
-
address: entry.descriptor.address,
|
|
1905
|
-
...(history ? { history } : {}),
|
|
1906
|
-
});
|
|
1907
|
-
entry.open = true;
|
|
1908
|
-
return result;
|
|
2272
|
+
return { result: applied.result, opened };
|
|
1909
2273
|
}
|
|
1910
2274
|
/**
|
|
1911
2275
|
* Resolve a `>>` prose-routing target (explicit prose routing —
|
|
@@ -2091,19 +2455,25 @@ export class ChannelRegistry {
|
|
|
2091
2455
|
return { status: 'unknown-channel' };
|
|
2092
2456
|
if (matches.length > 1)
|
|
2093
2457
|
return { status: 'ambiguous' };
|
|
2094
|
-
|
|
2095
|
-
const resolved = { channelId: entry.descriptor.id, label: entry.descriptor.label };
|
|
2096
|
-
if (entry.open && this.getDesiredState(entry.serverId, entry.descriptor.id) === 'open') {
|
|
2097
|
-
return { status: 'already-open', ...resolved };
|
|
2098
|
-
}
|
|
2458
|
+
let entry = matches[0];
|
|
2099
2459
|
try {
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2460
|
+
if (this.hasPendingLifecycle(entry)) {
|
|
2461
|
+
entry = await this.waitForOpenChannel(entry);
|
|
2462
|
+
}
|
|
2463
|
+
const desired = this.getDesiredState(entry.serverId, entry.descriptor.id);
|
|
2464
|
+
if (entry.open && (desired === 'open' || desired === 'tuned-out')) {
|
|
2465
|
+
return { status: 'already-open', channelId: entry.descriptor.id, label: entry.descriptor.label };
|
|
2466
|
+
}
|
|
2467
|
+
const { opened } = await this.openChannelNow(entry, 'opened-by-reply');
|
|
2468
|
+
entry = await this.waitForOpenChannel(entry);
|
|
2469
|
+
if (opened) {
|
|
2470
|
+
this.emitTraceFn({
|
|
2471
|
+
type: 'mcpl:channel-opened-by-send',
|
|
2472
|
+
serverId: entry.serverId,
|
|
2473
|
+
channelId: entry.descriptor.id,
|
|
2474
|
+
});
|
|
2475
|
+
}
|
|
2476
|
+
return { status: opened ? 'opened' : 'already-open', channelId: entry.descriptor.id, label: entry.descriptor.label };
|
|
2107
2477
|
}
|
|
2108
2478
|
catch (err) {
|
|
2109
2479
|
this.emitTraceFn({
|
|
@@ -2112,7 +2482,9 @@ export class ChannelRegistry {
|
|
|
2112
2482
|
channelId: entry.descriptor.id,
|
|
2113
2483
|
error: err.message,
|
|
2114
2484
|
});
|
|
2115
|
-
|
|
2485
|
+
const current = this.channels.get(`${entry.serverId}:${entry.descriptor.id}`);
|
|
2486
|
+
return { status: 'open-failed', channelId: entry.descriptor.id,
|
|
2487
|
+
...(current ? { label: current.descriptor.label } : {}) };
|
|
2116
2488
|
}
|
|
2117
2489
|
}
|
|
2118
2490
|
async handleToolOpen(input) {
|
|
@@ -2128,11 +2500,13 @@ export class ChannelRegistry {
|
|
|
2128
2500
|
// A tuned-out channel is transport-open but attention-diverted; a plain
|
|
2129
2501
|
// open would silently discard the epoch (and its pending backlog dump).
|
|
2130
2502
|
// Cancelling is the tune-out coordinator's flow, reached via its own
|
|
2131
|
-
// tool — refuse with the pointer rather than eat the state.
|
|
2503
|
+
// tool — refuse with the pointer rather than eat the state. As an error
|
|
2504
|
+
// result, the agent reads the pointer itself; `data` names the refusal
|
|
2505
|
+
// for an in-process caller.
|
|
2132
2506
|
if (this.getDesiredState(entry.serverId, input.channelId) === 'tuned-out') {
|
|
2133
2507
|
return {
|
|
2134
2508
|
success: false,
|
|
2135
|
-
isError:
|
|
2509
|
+
isError: true,
|
|
2136
2510
|
error: `Channel ${input.channelId} is tuned out. Cancel the tune-out ` +
|
|
2137
2511
|
`(tune_out with mode: 'cancel') to resume normal attention — ` +
|
|
2138
2512
|
`cancelling delivers the diverted backlog.`,
|
|
@@ -2140,7 +2514,7 @@ export class ChannelRegistry {
|
|
|
2140
2514
|
};
|
|
2141
2515
|
}
|
|
2142
2516
|
const alreadyDesiredOpen = this.getDesiredState(entry.serverId, input.channelId) === 'open';
|
|
2143
|
-
if (entry.open && alreadyDesiredOpen && !input.backscroll) {
|
|
2517
|
+
if (entry.open && alreadyDesiredOpen && !input.backscroll && !this.hasPendingLifecycle(entry)) {
|
|
2144
2518
|
return {
|
|
2145
2519
|
success: true,
|
|
2146
2520
|
data: { channelId: input.channelId, status: 'already open' },
|
|
@@ -2153,7 +2527,7 @@ export class ChannelRegistry {
|
|
|
2153
2527
|
...(input.beforeMessageId ? { beforeMessageId: input.beforeMessageId } : {}),
|
|
2154
2528
|
}
|
|
2155
2529
|
: undefined;
|
|
2156
|
-
const result = await this.openChannelNow(entry, 'agent-tool', history);
|
|
2530
|
+
const { result } = await this.openChannelNow(entry, 'agent-tool', history);
|
|
2157
2531
|
return {
|
|
2158
2532
|
success: true,
|
|
2159
2533
|
data: {
|
|
@@ -2165,9 +2539,18 @@ export class ChannelRegistry {
|
|
|
2165
2539
|
};
|
|
2166
2540
|
}
|
|
2167
2541
|
catch (err) {
|
|
2542
|
+
// The decision was recorded before the round-trip and outlives its
|
|
2543
|
+
// failure. Say so: otherwise a later delivery that opens the channel
|
|
2544
|
+
// would be the resident's first sign that it was still wanted open.
|
|
2545
|
+
const current = this.channels.get(`${entry.serverId}:${input.channelId}`);
|
|
2546
|
+
const stillSelected = current !== undefined && !current.open &&
|
|
2547
|
+
this.getDesiredState(entry.serverId, input.channelId) === 'open';
|
|
2168
2548
|
return {
|
|
2169
2549
|
success: false,
|
|
2170
|
-
error: `Failed to open channel: ${err.message}
|
|
2550
|
+
error: `Failed to open channel: ${err.message}` +
|
|
2551
|
+
(stillSelected
|
|
2552
|
+
? ' (it stays selected for opening: a later delivery or reconnect tries again, and channel_close withdraws that)'
|
|
2553
|
+
: ''),
|
|
2171
2554
|
isError: true,
|
|
2172
2555
|
};
|
|
2173
2556
|
}
|
|
@@ -2183,7 +2566,8 @@ export class ChannelRegistry {
|
|
|
2183
2566
|
};
|
|
2184
2567
|
}
|
|
2185
2568
|
const alreadyDesiredClosed = this.getDesiredState(entry.serverId, input.channelId) === 'closed';
|
|
2186
|
-
if (!entry.open && alreadyDesiredClosed)
|
|
2569
|
+
if (!entry.open && alreadyDesiredClosed && !this.hasPendingLifecycle(entry) &&
|
|
2570
|
+
!this.unconfirmedChannelTargets.has(entry)) {
|
|
2187
2571
|
return {
|
|
2188
2572
|
success: true,
|
|
2189
2573
|
data: { channelId: input.channelId, status: 'already closed' },
|
|
@@ -2245,8 +2629,10 @@ export class ChannelRegistry {
|
|
|
2245
2629
|
isError: true,
|
|
2246
2630
|
};
|
|
2247
2631
|
}
|
|
2248
|
-
await
|
|
2249
|
-
|
|
2632
|
+
const applied = await this.applyDesiredChannelState(entry.serverId, input.channelId);
|
|
2633
|
+
if (applied.open || this.getDesiredState(entry.serverId, input.channelId) !== 'closed') {
|
|
2634
|
+
throw new Error('Channel close was superseded by a newer lifecycle decision');
|
|
2635
|
+
}
|
|
2250
2636
|
return {
|
|
2251
2637
|
success: true,
|
|
2252
2638
|
data: { channelId: input.channelId, status: 'closed' },
|
|
@@ -2269,7 +2655,7 @@ export class ChannelRegistry {
|
|
|
2269
2655
|
if (entry.open || this.getDesiredState(entry.serverId, input.channelId) === 'open') {
|
|
2270
2656
|
return {
|
|
2271
2657
|
success: false,
|
|
2272
|
-
error: `Channel is already open: ${input.channelId}. Use channel_close to
|
|
2658
|
+
error: `Channel is already open: ${input.channelId}. Use channel_close to close it.`,
|
|
2273
2659
|
isError: true,
|
|
2274
2660
|
};
|
|
2275
2661
|
}
|
|
@@ -2307,6 +2693,24 @@ export class ChannelRegistry {
|
|
|
2307
2693
|
}
|
|
2308
2694
|
}
|
|
2309
2695
|
}
|
|
2696
|
+
// Where the host directs the lifecycle, a decline goes through the
|
|
2697
|
+
// lifecycle queue like any closed decision, unless a receipt confirmed
|
|
2698
|
+
// the transport closed with nothing pending: an unconfirmed transport may
|
|
2699
|
+
// be open, and pending work must settle on this decision. A failed close
|
|
2700
|
+
// is reported; the decision stands.
|
|
2701
|
+
let closeError;
|
|
2702
|
+
const current = this.channels.get(`${entry.serverId}:${input.channelId}`);
|
|
2703
|
+
if (current && this.directsLifecycle(entry.serverId) &&
|
|
2704
|
+
(this.unconfirmedChannelTargets.has(current) || this.hasPendingLifecycle(current))) {
|
|
2705
|
+
try {
|
|
2706
|
+
const applied = await this.applyDesiredChannelState(entry.serverId, input.channelId);
|
|
2707
|
+
if (applied.open)
|
|
2708
|
+
closeError = 'the close was superseded by a newer lifecycle decision';
|
|
2709
|
+
}
|
|
2710
|
+
catch (error) {
|
|
2711
|
+
closeError = error.message;
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2310
2714
|
this.appendLifecycleEvent({
|
|
2311
2715
|
kind: 'invitation-declined',
|
|
2312
2716
|
serverId: entry.serverId,
|
|
@@ -2323,6 +2727,7 @@ export class ChannelRegistry {
|
|
|
2323
2727
|
acknowledged,
|
|
2324
2728
|
...(representation ? { representation } : {}),
|
|
2325
2729
|
...(acknowledgmentError ? { acknowledgmentError } : {}),
|
|
2730
|
+
...(closeError ? { closeError: `Failed to close the channel's transport: ${closeError}` } : {}),
|
|
2326
2731
|
},
|
|
2327
2732
|
};
|
|
2328
2733
|
}
|
|
@@ -2330,7 +2735,8 @@ export class ChannelRegistry {
|
|
|
2330
2735
|
* Handle the synthesized `think` tool — a private reasoning scratchpad. It
|
|
2331
2736
|
* sends nothing, and (unlike before) does NOT silence the turn: trailing
|
|
2332
2737
|
* plain text is still routed as the reply. The thought stays in the agent's
|
|
2333
|
-
* own context/chronicle. To deliberately not reply,
|
|
2738
|
+
* own context/chronicle. To deliberately not reply, an agent that may call
|
|
2739
|
+
* skip_reply uses it.
|
|
2334
2740
|
*/
|
|
2335
2741
|
handleToolThink(input) {
|
|
2336
2742
|
return {
|
|
@@ -2339,9 +2745,11 @@ export class ChannelRegistry {
|
|
|
2339
2745
|
// tool_use block; don't duplicate it in the result.
|
|
2340
2746
|
data: {
|
|
2341
2747
|
noted: true,
|
|
2748
|
+
// No tool to end the turn with: this handler can't see whether the
|
|
2749
|
+
// caller may call skip_reply, and think's own description says so
|
|
2750
|
+
// where it may.
|
|
2342
2751
|
note: 'Thought recorded (private — not sent anywhere). Same-round text routing depends on ' +
|
|
2343
|
-
'your current same_round_think_text_policy; use agent_settings get to inspect it,
|
|
2344
|
-
'call skip_reply to end the turn without replying.',
|
|
2752
|
+
'your current same_round_think_text_policy; use agent_settings get to inspect it.',
|
|
2345
2753
|
},
|
|
2346
2754
|
};
|
|
2347
2755
|
}
|
|
@@ -2375,12 +2783,13 @@ export class ChannelRegistry {
|
|
|
2375
2783
|
*/
|
|
2376
2784
|
handleToolSkipReply(input) {
|
|
2377
2785
|
// The self-wake itself is armed by the framework (which owns the
|
|
2378
|
-
// EventGate) BEFORE
|
|
2379
|
-
//
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2786
|
+
// EventGate) BEFORE its dispatch reaches this handler, from the same
|
|
2787
|
+
// reading, and the framework refuses a wake the host can't set before
|
|
2788
|
+
// dispatching. A caller of this registry's handleChannelToolCall
|
|
2789
|
+
// (AgentFramework.channels) goes past both, so its note names a wake
|
|
2790
|
+
// nothing armed.
|
|
2791
|
+
const wake = readSkipReplyWake(input);
|
|
2792
|
+
const selfWake = wake.kind === 'wake' ? wake.seconds : undefined;
|
|
2384
2793
|
return {
|
|
2385
2794
|
success: true,
|
|
2386
2795
|
// The note says "ended the turn" — make it TRUE. Without endTurn the
|
|
@@ -2464,122 +2873,283 @@ export class ChannelRegistry {
|
|
|
2464
2873
|
}
|
|
2465
2874
|
resolveLocus(conversationId) {
|
|
2466
2875
|
const home = this.homeChannelResolver?.(conversationId);
|
|
2467
|
-
return home ?? this.activeChannelResolver?.(conversationId) ?? this.
|
|
2876
|
+
return home ?? this.activeChannelResolver?.(conversationId) ?? this.globalFallbackFor(conversationId);
|
|
2468
2877
|
}
|
|
2469
|
-
|
|
2878
|
+
/** The process-global fallback channel for this conversation, or null when it takes none (takesGlobalFallback). */
|
|
2879
|
+
globalFallbackFor(conversationId) {
|
|
2880
|
+
return this.takesGlobalFallback?.(conversationId) === false ? null : this.defaultPublishChannel;
|
|
2881
|
+
}
|
|
2882
|
+
/**
|
|
2883
|
+
* Resolve a publish target to exactly one registered channel: `serverId`
|
|
2884
|
+
* plus `channelId` when the caller has the server, otherwise a channel id
|
|
2885
|
+
* registered by exactly one server. A shared id is refused, never resolved
|
|
2886
|
+
* to whichever server registered it first.
|
|
2887
|
+
*/
|
|
2888
|
+
resolveDestination(target) {
|
|
2889
|
+
const entry = this.findExactEntry(target);
|
|
2890
|
+
if ('error' in entry)
|
|
2891
|
+
return entry;
|
|
2892
|
+
return {
|
|
2893
|
+
destination: {
|
|
2894
|
+
serverId: entry.entry.serverId,
|
|
2895
|
+
channelId: entry.entry.descriptor.id,
|
|
2896
|
+
...(entry.entry.descriptor.label ? { label: entry.entry.descriptor.label } : {}),
|
|
2897
|
+
},
|
|
2898
|
+
};
|
|
2899
|
+
}
|
|
2900
|
+
findExactEntry(target) {
|
|
2901
|
+
// A supplied selector is checked, never read as omission: an empty or
|
|
2902
|
+
// non-string serverId would otherwise widen to "any server".
|
|
2903
|
+
if (typeof target.channelId !== 'string' || !target.channelId) {
|
|
2904
|
+
return { error: 'the destination names no channel id' };
|
|
2905
|
+
}
|
|
2906
|
+
if (target.serverId !== undefined && (typeof target.serverId !== 'string' || !target.serverId)) {
|
|
2907
|
+
return { error: 'serverId must name a server (omit it to resolve the channel id alone)' };
|
|
2908
|
+
}
|
|
2909
|
+
const matches = [...this.channels.values()].filter((e) => e.descriptor.id === target.channelId && (!target.serverId || e.serverId === target.serverId));
|
|
2910
|
+
if (matches.length === 0) {
|
|
2911
|
+
return {
|
|
2912
|
+
error: target.serverId
|
|
2913
|
+
? `no registered channel "${target.channelId}" on server "${target.serverId}"`
|
|
2914
|
+
: `no registered channel "${target.channelId}"`,
|
|
2915
|
+
};
|
|
2916
|
+
}
|
|
2917
|
+
if (matches.length > 1) {
|
|
2918
|
+
return {
|
|
2919
|
+
error: `channel id "${target.channelId}" is registered by more than one MCPL server; the destination must name its server`,
|
|
2920
|
+
};
|
|
2921
|
+
}
|
|
2922
|
+
return { entry: matches[0] };
|
|
2923
|
+
}
|
|
2924
|
+
/**
|
|
2925
|
+
* Publish `text` to one registered channel and report what the attempt
|
|
2926
|
+
* established (PublishOutcome). The delivery executor behind plain speech
|
|
2927
|
+
* (deliverSpeech / routeSpeech) and resident resends of held drafts.
|
|
2928
|
+
* channel_publish (and publishForAgent, which shares its handler) resolves
|
|
2929
|
+
* and sends on its own path, and reads its answer through the same
|
|
2930
|
+
* publishReceiptOutcome and publishErrorOutcome.
|
|
2931
|
+
*
|
|
2932
|
+
* Resolution is exact (resolveDestination): `serverId` plus `channelId`
|
|
2933
|
+
* when the caller has the server, otherwise the channel id must be
|
|
2934
|
+
* registered by exactly one server (a shared id is refused, never routed
|
|
2935
|
+
* through whichever server came first). An omitted `serverId` means "not
|
|
2936
|
+
* in use"; a supplied one must be a non-empty server id, and an empty or
|
|
2937
|
+
* non-string selector is refused rather than read as omission. It is not
|
|
2938
|
+
* possible to send into a closed channel: a closed but registered
|
|
2939
|
+
* destination is opened first (`openSource` names why), once its server
|
|
2940
|
+
* and that server's channels.publish grant are known to take the send, so
|
|
2941
|
+
* a refused send opens nothing; and a failed open sends nothing.
|
|
2942
|
+
*
|
|
2943
|
+
* Only the connector's `delivered: true` confirms a post. `failed` means
|
|
2944
|
+
* nothing was posted: the attempt stopped before the request was written,
|
|
2945
|
+
* or the connector answered `delivered: false` and named no message (no
|
|
2946
|
+
* `messageId`, or null). Everything else is `unknown`: once the request
|
|
2947
|
+
* has been handed to the transport, an error response, a timeout, a lost
|
|
2948
|
+
* connection, or a missing, malformed or contradictory receipt (such as
|
|
2949
|
+
* `delivered: false` beside a message id, valid or not) means the post may
|
|
2950
|
+
* or may not exist, and the caller must not treat it as safe to retry
|
|
2951
|
+
* blindly.
|
|
2952
|
+
*/
|
|
2953
|
+
async publish(conversationId, text, target, openSource = 'opened-by-delivery') {
|
|
2954
|
+
const at = () => Date.now();
|
|
2955
|
+
const found = this.findExactEntry(target);
|
|
2956
|
+
if ('error' in found)
|
|
2957
|
+
return { status: 'failed', reason: found.error, at: at() };
|
|
2958
|
+
let entry = found.entry;
|
|
2959
|
+
const destination = {
|
|
2960
|
+
serverId: entry.serverId,
|
|
2961
|
+
channelId: entry.descriptor.id,
|
|
2962
|
+
...(entry.descriptor.label ? { label: entry.descriptor.label } : {}),
|
|
2963
|
+
};
|
|
2964
|
+
// A send that would be refused is refused before anything opens: a
|
|
2965
|
+
// closed channel opens only for words that can go out.
|
|
2966
|
+
const server = this.serverRegistry.getServer(entry.serverId);
|
|
2967
|
+
if (!server) {
|
|
2968
|
+
return { status: 'failed', destination, reason: `server "${entry.serverId}" not found`, at: at() };
|
|
2969
|
+
}
|
|
2970
|
+
// §14.1: channels/publish requires channels.publish in the grant. The
|
|
2971
|
+
// host not sending is the enforcement.
|
|
2972
|
+
if (!CapabilityGrant.of(server).has('channels.publish')) {
|
|
2973
|
+
return {
|
|
2974
|
+
status: 'failed',
|
|
2975
|
+
destination,
|
|
2976
|
+
reason: `channels.publish not in "${entry.serverId}"'s effective grant (§14.1)`,
|
|
2977
|
+
at: at(),
|
|
2978
|
+
};
|
|
2979
|
+
}
|
|
2980
|
+
// Lifecycle work already queued for the channel settles first: words go
|
|
2981
|
+
// out only into the channel it leaves, and fail visibly if it leaves
|
|
2982
|
+
// none open.
|
|
2983
|
+
if (this.hasPendingLifecycle(entry)) {
|
|
2984
|
+
try {
|
|
2985
|
+
entry = await this.waitForOpenChannel(entry);
|
|
2986
|
+
}
|
|
2987
|
+
catch (err) {
|
|
2988
|
+
return {
|
|
2989
|
+
status: 'failed',
|
|
2990
|
+
destination,
|
|
2991
|
+
reason: `lifecycle work prevented delivery: ${connectorText(err instanceof Error ? err.message : String(err))}`,
|
|
2992
|
+
at: at(),
|
|
2993
|
+
};
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
// INVARIANT: it is not possible to send into a closed channel. A send
|
|
2997
|
+
// into a closed-but-registered channel (the canonical case: a DM that
|
|
2998
|
+
// woke the agent, since DMs register closed) OPENS it first, so typing
|
|
2999
|
+
// indicators, reaction machinery and inbound forwarding come alive with
|
|
3000
|
+
// the words. If the open fails, nothing goes out: a half-alive delivery
|
|
3001
|
+
// is worse than a loud marker. A channel decided closed is engaged even
|
|
3002
|
+
// while its transport is still open (a close that failed), as a reply
|
|
3003
|
+
// would engage it, wherever an open can succeed. Without the lifecycle
|
|
3004
|
+
// grant it can't, and a send opens before it publishes, so there a
|
|
3005
|
+
// channel whose transport is open keeps publishing.
|
|
3006
|
+
const engagesDecidedClosed = this.getDesiredState(entry.serverId, entry.descriptor.id) === 'closed' &&
|
|
3007
|
+
CapabilityGrant.of(server).has('channels.lifecycle');
|
|
3008
|
+
if (!entry.open || engagesDecidedClosed) {
|
|
3009
|
+
try {
|
|
3010
|
+
const { opened } = await this.openChannelNow(entry, openSource);
|
|
3011
|
+
entry = await this.waitForOpenChannel(entry);
|
|
3012
|
+
if (opened) {
|
|
3013
|
+
if (target.routed) {
|
|
3014
|
+
console.error(`[routeSpeech] ${conversationId}: locus ${logValue(entry.descriptor.id)} was closed — opened by delivery (speech implies engagement)`);
|
|
3015
|
+
}
|
|
3016
|
+
this.emitTraceFn({
|
|
3017
|
+
type: 'mcpl:channel-opened-by-send',
|
|
3018
|
+
serverId: entry.serverId,
|
|
3019
|
+
channelId: entry.descriptor.id,
|
|
3020
|
+
});
|
|
3021
|
+
if (openSource === 'opened-by-delivery') {
|
|
3022
|
+
try {
|
|
3023
|
+
this.onChannelAutoOpened?.({
|
|
3024
|
+
conversationId,
|
|
3025
|
+
serverId: entry.serverId,
|
|
3026
|
+
source: 'opened-by-delivery',
|
|
3027
|
+
channels: [{ channelId: entry.descriptor.id, label: entry.descriptor.label }],
|
|
3028
|
+
});
|
|
3029
|
+
}
|
|
3030
|
+
catch (err) {
|
|
3031
|
+
console.error('onChannelAutoOpened (delivery) failed:', err);
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
catch (err) {
|
|
3037
|
+
return {
|
|
3038
|
+
status: 'failed',
|
|
3039
|
+
destination,
|
|
3040
|
+
reason: `channel is closed and open failed: ${connectorText(err instanceof Error ? err.message : String(err))}`,
|
|
3041
|
+
at: at(),
|
|
3042
|
+
};
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
let result;
|
|
3046
|
+
try {
|
|
3047
|
+
result = await server.sendChannelsPublish({
|
|
3048
|
+
conversationId,
|
|
3049
|
+
channelId: entry.descriptor.id,
|
|
3050
|
+
content: [{ type: 'text', text }],
|
|
3051
|
+
});
|
|
3052
|
+
}
|
|
3053
|
+
catch (err) {
|
|
3054
|
+
return publishErrorOutcome(err, destination, at());
|
|
3055
|
+
}
|
|
3056
|
+
return publishReceiptOutcome(result, destination, at());
|
|
3057
|
+
}
|
|
3058
|
+
/**
|
|
3059
|
+
* Plain speech for one turn segment: publish `text` to the turn's frozen
|
|
3060
|
+
* locus and report the PublishOutcome. A failure (or an uncertain outcome)
|
|
3061
|
+
* is surfaced as a trace and through onRouteFailure, so the agent learns
|
|
3062
|
+
* its words did not (or may not have) reached anyone. An ephemeral run's
|
|
3063
|
+
* speech with no locus is the exception: it is its caller's result, so it
|
|
3064
|
+
* returns as not delivered and surfaces nothing.
|
|
3065
|
+
*/
|
|
3066
|
+
async deliverSpeech(conversationId, text,
|
|
2470
3067
|
/** The turn's FROZEN locus, snapshotted once by the caller (resolveLocus
|
|
2471
3068
|
* at turn start) and passed to every segment of the turn. Mandatory:
|
|
2472
3069
|
* there is deliberately no live-resolution fallback here — a late
|
|
2473
3070
|
* dispatch resolving live can read the NEXT turn's trigger state or a
|
|
2474
3071
|
* stale global default and land the reply in the wrong channel (item-3,
|
|
2475
3072
|
* PR #32, and the 2026-07-22 Sol DM misroute). Explicit `null` means
|
|
2476
|
-
* "this turn is pinned to no locus": fail loudly rather than guess
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
3073
|
+
* "this turn is pinned to no locus": fail loudly rather than guess,
|
|
3074
|
+
* except for an ephemeral run, whose speech is then its caller's
|
|
3075
|
+
* result. */
|
|
3076
|
+
locusChannelId,
|
|
3077
|
+
/** `heldOnFailure`: the caller holds a `failed` delivery's words as a
|
|
3078
|
+
* draft with its own notice, so no failure marker is written for it.
|
|
3079
|
+
* An `unknown` outcome still gets its marker. */
|
|
3080
|
+
opts) {
|
|
3081
|
+
const fail = (channelId, reason, outcome) => {
|
|
3082
|
+
const status = outcome.status === 'unknown' ? 'unknown' : 'failed';
|
|
3083
|
+
const detail = outcome.detail !== undefined ? connectorText(outcome.detail) : undefined;
|
|
3084
|
+
const detailText = detail !== undefined ? `; connector detail: ${detail}` : '';
|
|
3085
|
+
console.error(`[routeSpeech] ${conversationId}: ${reason}${detailText} — speech ${status === 'unknown' ? 'delivery NOT confirmed' : 'NOT routed'} (${text.length} chars stay in chronicle)`);
|
|
2483
3086
|
this.emitTraceFn({
|
|
2484
3087
|
type: 'mcpl:speech-route-failed',
|
|
2485
3088
|
conversationId,
|
|
2486
3089
|
channelId: channelId ?? '',
|
|
2487
3090
|
reason,
|
|
2488
3091
|
textLen: text.length,
|
|
3092
|
+
outcome: status,
|
|
3093
|
+
...(detail !== undefined ? { detail } : {}),
|
|
2489
3094
|
});
|
|
2490
|
-
|
|
2491
|
-
|
|
3095
|
+
if (status === 'unknown' || !opts?.heldOnFailure) {
|
|
3096
|
+
this.onRouteFailure?.({ conversationId, channelId, reason, textLen: text.length, outcome: status, ...(detail !== undefined ? { detail } : {}) });
|
|
3097
|
+
}
|
|
3098
|
+
return outcome;
|
|
2492
3099
|
};
|
|
2493
3100
|
// Runtime backstop for JS callers the compiler can't see: an omitted
|
|
2494
3101
|
// locus is a routing bug at the call site, never something to paper over
|
|
2495
3102
|
// with a live re-resolution.
|
|
2496
3103
|
if (locusChannelId === undefined) {
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
if (!
|
|
3104
|
+
const reason = 'caller passed no locus (routing bug: every speech path must snapshot the turn locus)';
|
|
3105
|
+
return fail(null, reason, { status: 'failed', reason, at: Date.now() });
|
|
3106
|
+
}
|
|
3107
|
+
if (!locusChannelId) {
|
|
3108
|
+
// An ephemeral run with no channel of its own: its plain speech is its
|
|
3109
|
+
// caller's result (takesGlobalFallback). It returns as not delivered,
|
|
3110
|
+
// with its own reason, and writes no line, trace or notice.
|
|
3111
|
+
if (this.takesGlobalFallback?.(conversationId) === false) {
|
|
3112
|
+
return { status: 'failed', reason: "an ephemeral run's plain speech with no channel of its own is its caller's result", at: Date.now() };
|
|
3113
|
+
}
|
|
2501
3114
|
// The turn froze with no locus (no home, no triggering channel, no
|
|
2502
3115
|
// global inbound ever seen) — the agent was told its prose stays in
|
|
2503
3116
|
// the archive; honor that.
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
const entry = this.findChannelEntry(channelId);
|
|
2507
|
-
if (!entry) {
|
|
2508
|
-
return fail(channelId, `no registered channel for locus "${channelId}"`);
|
|
2509
|
-
}
|
|
2510
|
-
// INVARIANT: it is not possible to send into a closed channel. Speech
|
|
2511
|
-
// routed into a closed-but-registered locus (the canonical case: a DM
|
|
2512
|
-
// that woke the agent — DMs register closed) OPENS the channel first,
|
|
2513
|
-
// so typing indicators, reaction machinery, and inbound forwarding come
|
|
2514
|
-
// alive with the reply. If the open fails, the speech does NOT go out:
|
|
2515
|
-
// a half-alive delivery is worse than a loud marker.
|
|
2516
|
-
if (!entry.open) {
|
|
2517
|
-
try {
|
|
2518
|
-
await this.openChannelNow(entry, 'opened-by-delivery');
|
|
2519
|
-
console.error(`[routeSpeech] ${conversationId}: locus ${logValue(channelId)} was closed — opened by delivery (speech implies engagement)`);
|
|
2520
|
-
this.emitTraceFn({
|
|
2521
|
-
type: 'mcpl:channel-opened-by-send',
|
|
2522
|
-
serverId: entry.serverId,
|
|
2523
|
-
channelId,
|
|
2524
|
-
});
|
|
2525
|
-
try {
|
|
2526
|
-
this.onChannelAutoOpened?.({
|
|
2527
|
-
conversationId,
|
|
2528
|
-
serverId: entry.serverId,
|
|
2529
|
-
source: 'opened-by-delivery',
|
|
2530
|
-
channels: [{ channelId, label: entry.descriptor.label }],
|
|
2531
|
-
});
|
|
2532
|
-
}
|
|
2533
|
-
catch (err) {
|
|
2534
|
-
console.error('onChannelAutoOpened (delivery) failed:', err);
|
|
2535
|
-
}
|
|
2536
|
-
}
|
|
2537
|
-
catch (err) {
|
|
2538
|
-
return fail(channelId, `locus channel is closed and open failed: ${err.message}`);
|
|
2539
|
-
}
|
|
2540
|
-
}
|
|
2541
|
-
const server = this.serverRegistry.getServer(entry.serverId);
|
|
2542
|
-
if (!server) {
|
|
2543
|
-
return fail(channelId, `server "${entry.serverId}" not found`);
|
|
3117
|
+
const reason = 'turn has no locus (no home/trigger channel; nothing to deliver into)';
|
|
3118
|
+
return fail(null, reason, { status: 'failed', reason, at: Date.now() });
|
|
2544
3119
|
}
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
return fail(channelId, `channels.publish not in "${entry.serverId}"'s effective grant (§14.1)`);
|
|
3120
|
+
const outcome = await this.publish(conversationId, text, { channelId: locusChannelId, routed: true });
|
|
3121
|
+
const channelId = outcome.destination?.channelId ?? locusChannelId;
|
|
3122
|
+
if (outcome.status !== 'delivered') {
|
|
3123
|
+
return fail(channelId, outcome.reason ?? 'delivery not confirmed', outcome);
|
|
2550
3124
|
}
|
|
2551
|
-
|
|
2552
|
-
conversationId,
|
|
2553
|
-
channelId,
|
|
2554
|
-
content: [{ type: 'text', text }],
|
|
2555
|
-
};
|
|
2556
|
-
const result = await server.sendChannelsPublish(publishParams);
|
|
2557
|
-
const delivered = result?.delivered ?? true;
|
|
2558
|
-
// Surface the posted message's id (ChannelsPublishResult.messageId) so
|
|
2559
|
-
// trace consumers can act on the just-posted message — e.g. a TTS-relay
|
|
2560
|
-
// tap editing it down to the words actually voiced on interruption.
|
|
2561
|
-
// Previously this was silently dropped here.
|
|
2562
|
-
const messageId = result?.messageId;
|
|
2563
|
-
// The server accepted the publish RPC but reported the message was not
|
|
2564
|
-
// actually delivered (e.g. missing Send Messages permission). Previously
|
|
2565
|
-
// this returned `{ delivered: true }`, masking the failure. Surface it.
|
|
2566
|
-
if (delivered === false) {
|
|
2567
|
-
return fail(channelId, `server "${entry.serverId}" reported delivered:false for "${channelId}"`);
|
|
2568
|
-
}
|
|
2569
|
-
console.error(`[routeSpeech] ${conversationId}: routed ${text.length} chars -> ${logValue(channelId)} (server=${entry.serverId}, delivered=${delivered})`);
|
|
3125
|
+
console.error(`[routeSpeech] ${conversationId}: routed ${text.length} chars -> ${logValue(channelId)} (server=${outcome.destination.serverId}, delivered=true)`);
|
|
2570
3126
|
this.emitTraceFn({
|
|
2571
3127
|
type: 'mcpl:speech-routed',
|
|
2572
3128
|
conversationId,
|
|
2573
|
-
serverId:
|
|
3129
|
+
serverId: outcome.destination.serverId,
|
|
2574
3130
|
channelId,
|
|
2575
|
-
delivered,
|
|
3131
|
+
delivered: true,
|
|
2576
3132
|
textLen: text.length,
|
|
2577
3133
|
text,
|
|
2578
|
-
|
|
3134
|
+
// Surface the posted message's id (ChannelsPublishResult.messageId) so
|
|
3135
|
+
// trace consumers can act on the just-posted message — e.g. a TTS-relay
|
|
3136
|
+
// tap editing it down to the words actually voiced on interruption.
|
|
3137
|
+
...(outcome.messageId !== undefined ? { messageId: outcome.messageId } : {}),
|
|
2579
3138
|
});
|
|
2580
|
-
return
|
|
3139
|
+
return outcome;
|
|
2581
3140
|
}
|
|
2582
|
-
|
|
3141
|
+
/** deliverSpeech, reduced to the confirmed delivery (null when there was none). */
|
|
3142
|
+
async routeSpeech(conversationId, text, locusChannelId) {
|
|
3143
|
+
const outcome = await this.deliverSpeech(conversationId, text, locusChannelId);
|
|
3144
|
+
if (outcome.status !== 'delivered')
|
|
3145
|
+
return null;
|
|
3146
|
+
return {
|
|
3147
|
+
delivered: true,
|
|
3148
|
+
channelId: outcome.destination.channelId,
|
|
3149
|
+
...(outcome.messageId !== undefined ? { messageId: outcome.messageId } : {}),
|
|
3150
|
+
};
|
|
3151
|
+
}
|
|
3152
|
+
async handleToolPublish(input, origin) {
|
|
2583
3153
|
// Resolve content: accept both `content` and `text` (backward compat)
|
|
2584
3154
|
const messageText = input.content ?? input.text;
|
|
2585
3155
|
if (!messageText) {
|
|
@@ -2589,12 +3159,17 @@ export class ChannelRegistry {
|
|
|
2589
3159
|
isError: true,
|
|
2590
3160
|
};
|
|
2591
3161
|
}
|
|
2592
|
-
// Resolve channelId: default to most recent incoming channel
|
|
2593
|
-
|
|
3162
|
+
// Resolve channelId: default to the most recent incoming channel, which an
|
|
3163
|
+
// ephemeral run never takes (takesGlobalFallback), so it names one.
|
|
3164
|
+
const caller = origin?.kind === 'agent' ? origin.agentName : undefined;
|
|
3165
|
+
const channelId = input.channelId ?? (caller !== undefined ? this.globalFallbackFor(caller) : this.defaultPublishChannel);
|
|
2594
3166
|
if (!channelId) {
|
|
2595
3167
|
return {
|
|
2596
3168
|
success: false,
|
|
2597
|
-
error:
|
|
3169
|
+
error: caller !== undefined && this.takesGlobalFallback?.(caller) === false
|
|
3170
|
+
? 'Nothing was published: name the channel with channelId. A subagent has no default channel, since its plain '
|
|
3171
|
+
+ 'speech is returned to whoever called it, so channel_publish sends only to a channel it names.'
|
|
3172
|
+
: 'No channelId specified and no default channel available',
|
|
2598
3173
|
isError: true,
|
|
2599
3174
|
};
|
|
2600
3175
|
}
|
|
@@ -2614,32 +3189,46 @@ export class ChannelRegistry {
|
|
|
2614
3189
|
isError: true,
|
|
2615
3190
|
};
|
|
2616
3191
|
}
|
|
2617
|
-
|
|
2618
|
-
if (!CapabilityGrant.of(server).has('channels.publish')) {
|
|
2619
|
-
return {
|
|
2620
|
-
success: false,
|
|
2621
|
-
error: `channels.publish not in "${server.id}"'s effective grant (§14.1)`,
|
|
2622
|
-
isError: true,
|
|
2623
|
-
};
|
|
2624
|
-
}
|
|
2625
|
-
const publishParams = {
|
|
2626
|
-
conversationId: '', // Framework will fill this when wired
|
|
2627
|
-
channelId,
|
|
2628
|
-
content: [{ type: 'text', text: messageText }],
|
|
2629
|
-
};
|
|
2630
|
-
const result = await server.sendChannelsPublish(publishParams);
|
|
2631
|
-
return {
|
|
2632
|
-
success: true,
|
|
2633
|
-
data: result ?? { delivered: true },
|
|
2634
|
-
};
|
|
2635
|
-
}
|
|
2636
|
-
catch (err) {
|
|
3192
|
+
if (!CapabilityGrant.of(server).has('channels.publish')) {
|
|
2637
3193
|
return {
|
|
2638
3194
|
success: false,
|
|
2639
|
-
error: `
|
|
3195
|
+
error: `channels.publish not in "${server.id}"'s effective grant (§14.1)`,
|
|
2640
3196
|
isError: true,
|
|
2641
3197
|
};
|
|
2642
3198
|
}
|
|
3199
|
+
const publishParams = {
|
|
3200
|
+
conversationId: '', // Framework will fill this when wired
|
|
3201
|
+
channelId,
|
|
3202
|
+
content: [{ type: 'text', text: messageText }],
|
|
3203
|
+
};
|
|
3204
|
+
const destination = {
|
|
3205
|
+
serverId: entry.serverId,
|
|
3206
|
+
channelId: entry.descriptor.id,
|
|
3207
|
+
...(entry.descriptor.label ? { label: entry.descriptor.label } : {}),
|
|
3208
|
+
};
|
|
3209
|
+
// The answer counts by the same rule as plain speech's: only the
|
|
3210
|
+
// connector's delivered:true confirms a post.
|
|
3211
|
+
let result;
|
|
3212
|
+
let outcome;
|
|
3213
|
+
try {
|
|
3214
|
+
result = await server.sendChannelsPublish(publishParams);
|
|
3215
|
+
outcome = publishReceiptOutcome(result, destination, Date.now());
|
|
3216
|
+
}
|
|
3217
|
+
catch (err) {
|
|
3218
|
+
outcome = publishErrorOutcome(err, destination, Date.now());
|
|
3219
|
+
}
|
|
3220
|
+
if (outcome.status === 'delivered')
|
|
3221
|
+
return { success: true, data: result };
|
|
3222
|
+
// The connector's data may say what a multi-part send already posted.
|
|
3223
|
+
const detail = outcome.status === 'unknown' && outcome.detail !== undefined ? ` Connector detail: ${connectorText(outcome.detail)}` : '';
|
|
3224
|
+
return {
|
|
3225
|
+
success: false,
|
|
3226
|
+
error: (outcome.status === 'failed'
|
|
3227
|
+
? `Publish to ${channelId} failed: ${outcome.reason}. Nothing was posted.`
|
|
3228
|
+
: `Publish to ${channelId} wasn't confirmed: ${outcome.reason}. ` +
|
|
3229
|
+
'It may or may not have been posted, so sending it again could post it twice.') + detail,
|
|
3230
|
+
isError: true,
|
|
3231
|
+
};
|
|
2643
3232
|
}
|
|
2644
3233
|
}
|
|
2645
3234
|
//# sourceMappingURL=channel-registry.js.map
|