@animalabs/agent-framework 0.22.0 → 0.22.2
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 +12 -5
- package/dist/src/agent.d.ts +192 -19
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +407 -66
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server-core.d.ts +41 -0
- package/dist/src/api/mcp-server-core.d.ts.map +1 -0
- package/dist/src/api/mcp-server-core.js +593 -0
- package/dist/src/api/mcp-server-core.js.map +1 -0
- package/dist/src/api/mcp-server.d.ts +2 -36
- package/dist/src/api/mcp-server.d.ts.map +1 -1
- package/dist/src/api/mcp-server.js +15 -595
- package/dist/src/api/mcp-server.js.map +1 -1
- package/dist/src/api/server.d.ts +9 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +84 -23
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/api/types.d.ts +9 -1
- package/dist/src/api/types.d.ts.map +1 -1
- package/dist/src/code-execution/py-runner.d.ts.map +1 -1
- package/dist/src/code-execution/py-runner.js.map +1 -1
- package/dist/src/framework.d.ts +682 -103
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +3202 -718
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/gate-script.d.ts.map +1 -1
- package/dist/src/gate/gate-script.js.map +1 -1
- package/dist/src/index.d.ts +4 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/capability-grant.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +26 -7
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +102 -22
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/errors.js.map +1 -1
- package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
- package/dist/src/mcpl/feature-set-manager.js +4 -3
- package/dist/src/mcpl/feature-set-manager.js.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
- package/dist/src/mcpl/index.d.ts +2 -0
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/log-value.d.ts +54 -0
- package/dist/src/mcpl/log-value.d.ts.map +1 -0
- package/dist/src/mcpl/log-value.js +109 -0
- package/dist/src/mcpl/log-value.js.map +1 -0
- package/dist/src/mcpl/prose-stream-router.d.ts +6 -6
- package/dist/src/mcpl/prose-stream-router.d.ts.map +1 -1
- package/dist/src/mcpl/prose-stream-router.js +4 -4
- package/dist/src/mcpl/prose-stream-router.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +38 -3
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +81 -16
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts +26 -3
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +87 -23
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/reference-fetcher.d.ts +2 -1
- package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
- package/dist/src/mcpl/reference-fetcher.js +20 -9
- package/dist/src/mcpl/reference-fetcher.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +29 -6
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +57 -16
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/refusal-lines.d.ts +37 -0
- package/dist/src/mcpl/refusal-lines.d.ts.map +1 -0
- package/dist/src/mcpl/refusal-lines.js +61 -0
- package/dist/src/mcpl/refusal-lines.js.map +1 -0
- package/dist/src/mcpl/scope-manager.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +46 -3
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +98 -15
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/server-registry.d.ts.map +1 -1
- package/dist/src/mcpl/tool-classes.d.ts +1 -1
- package/dist/src/mcpl/tool-classes.d.ts.map +1 -1
- package/dist/src/mcpl/tool-classes.js +3 -2
- package/dist/src/mcpl/tool-classes.js.map +1 -1
- package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
- package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +23 -5
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +103 -10
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +30 -26
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +9 -3
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +3 -2
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/api/index.d.ts.map +1 -1
- package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
- package/dist/src/modules/discord/discord-js-client.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +4 -2
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/health/index.d.ts.map +1 -1
- package/dist/src/modules/health/index.js.map +1 -1
- package/dist/src/modules/history/index.d.ts.map +1 -1
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/history/semantic.d.ts.map +1 -1
- package/dist/src/modules/history/semantic.js.map +1 -1
- package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
- package/dist/src/modules/workspace/branch-intent.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +22 -4
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +63 -25
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +8 -4
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +85 -16
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +56 -11
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +288 -43
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +31 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +63 -12
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +27 -2
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/operator-log.d.ts +41 -0
- package/dist/src/operator-log.d.ts.map +1 -1
- package/dist/src/operator-log.js.map +1 -1
- package/dist/src/provider-waits.d.ts +216 -0
- package/dist/src/provider-waits.d.ts.map +1 -0
- package/dist/src/provider-waits.js +482 -0
- package/dist/src/provider-waits.js.map +1 -0
- package/dist/src/queue.d.ts.map +1 -1
- package/dist/src/record-journal.d.ts.map +1 -1
- package/dist/src/record-journal.js.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
- package/dist/src/recovery/offline-branch.d.ts.map +1 -1
- package/dist/src/recovery/offline-branch.js +29 -24
- package/dist/src/recovery/offline-branch.js.map +1 -1
- package/dist/src/recovery/recover-cli.js.map +1 -1
- package/dist/src/refusal-reactions.d.ts +14 -12
- package/dist/src/refusal-reactions.d.ts.map +1 -1
- package/dist/src/refusal-reactions.js +14 -12
- package/dist/src/refusal-reactions.js.map +1 -1
- package/dist/src/runtime/liveness-watchdog.d.ts.map +1 -1
- package/dist/src/runtime/liveness-watchdog.js.map +1 -1
- package/dist/src/shutdown.d.ts.map +1 -1
- package/dist/src/shutdown.js.map +1 -1
- package/dist/src/stored-removal.d.ts +73 -0
- package/dist/src/stored-removal.d.ts.map +1 -0
- package/dist/src/stored-removal.js +100 -0
- package/dist/src/stored-removal.js.map +1 -0
- package/dist/src/thinking-token-stamp.js.map +1 -1
- package/dist/src/tool-image-ledger.d.ts.map +1 -1
- package/dist/src/tool-image-ledger.js.map +1 -1
- package/dist/src/tool-presentation.d.ts.map +1 -1
- package/dist/src/tool-presentation.js.map +1 -1
- package/dist/src/tool-result-guard.d.ts +30 -13
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +71 -25
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +1 -1
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/types/agent.d.ts +42 -6
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +12 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +10 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +20 -8
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +75 -13
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/src/usage/usage-tracker.d.ts.map +1 -1
- package/dist/test/agent-runtime-settings.test.js +1139 -3
- package/dist/test/agent-runtime-settings.test.js.map +1 -1
- package/dist/test/batched-wake-locus.test.js.map +1 -1
- package/dist/test/channel-label-history.test.js.map +1 -1
- package/dist/test/code-execution.test.js.map +1 -1
- package/dist/test/conversation-fork-injection-scoping.test.js.map +1 -1
- package/dist/test/conversation-router.test.js.map +1 -1
- package/dist/test/conversation-routing.test.js +240 -6
- package/dist/test/conversation-routing.test.js.map +1 -1
- package/dist/test/discord-awareness-outbox.test.js +107 -0
- package/dist/test/discord-awareness-outbox.test.js.map +1 -1
- package/dist/test/discord-typing-keepalive.test.d.ts +2 -0
- package/dist/test/discord-typing-keepalive.test.d.ts.map +1 -0
- package/dist/test/discord-typing-keepalive.test.js +77 -0
- package/dist/test/discord-typing-keepalive.test.js.map +1 -0
- package/dist/test/ephemeral-settle.test.js.map +1 -1
- package/dist/test/event-gate-passthrough.test.js.map +1 -1
- package/dist/test/event-gate-policy-mutation.test.js.map +1 -1
- package/dist/test/event-gate-rate-limit.test.js.map +1 -1
- package/dist/test/event-gate-script.test.js.map +1 -1
- package/dist/test/event-gate-selfwake.test.js.map +1 -1
- package/dist/test/event-gate-tags.test.js.map +1 -1
- package/dist/test/event-gate.test.js.map +1 -1
- package/dist/test/explicit-prose-routing.test.js +363 -44
- package/dist/test/explicit-prose-routing.test.js.map +1 -1
- package/dist/test/framework.test.js +160 -1
- package/dist/test/framework.test.js.map +1 -1
- package/dist/test/helpers/coalescing-fixture.d.ts +2 -0
- package/dist/test/helpers/coalescing-fixture.d.ts.map +1 -1
- package/dist/test/helpers/coalescing-fixture.js +11 -4
- package/dist/test/helpers/coalescing-fixture.js.map +1 -1
- package/dist/test/helpers/mock-membrane.d.ts.map +1 -1
- package/dist/test/helpers/mock-membrane.js.map +1 -1
- package/dist/test/hide-then-turn.test.d.ts +2 -0
- package/dist/test/hide-then-turn.test.d.ts.map +1 -0
- package/dist/test/hide-then-turn.test.js +101 -0
- package/dist/test/hide-then-turn.test.js.map +1 -0
- package/dist/test/history-module-overview.test.js.map +1 -1
- package/dist/test/history-module-semantic.test.js.map +1 -1
- package/dist/test/history-module.test.js.map +1 -1
- package/dist/test/history-semantic-review-findings.test.js.map +1 -1
- package/dist/test/host-quiesce.test.js +125 -0
- package/dist/test/host-quiesce.test.js.map +1 -1
- package/dist/test/inference-exhaustion-rewind.test.js +220 -14
- package/dist/test/inference-exhaustion-rewind.test.js.map +1 -1
- package/dist/test/inference-failure-bounds.test.d.ts +2 -0
- package/dist/test/inference-failure-bounds.test.d.ts.map +1 -0
- package/dist/test/inference-failure-bounds.test.js +1202 -0
- package/dist/test/inference-failure-bounds.test.js.map +1 -0
- package/dist/test/kv-unified-open-flight-retry.test.js.map +1 -1
- package/dist/test/late-events-after-stop.test.d.ts +2 -0
- package/dist/test/late-events-after-stop.test.d.ts.map +1 -0
- package/dist/test/late-events-after-stop.test.js +122 -0
- package/dist/test/late-events-after-stop.test.js.map +1 -0
- package/dist/test/liveness-watchdog.test.js.map +1 -1
- package/dist/test/maintenance-timer.test.js +1564 -2
- package/dist/test/maintenance-timer.test.js.map +1 -1
- package/dist/test/mcp-server-bin.test.d.ts +2 -0
- package/dist/test/mcp-server-bin.test.d.ts.map +1 -0
- package/dist/test/mcp-server-bin.test.js +142 -0
- package/dist/test/mcp-server-bin.test.js.map +1 -0
- package/dist/test/mcpl-awareness-barrier.test.js +141 -0
- package/dist/test/mcpl-awareness-barrier.test.js.map +1 -1
- package/dist/test/mcpl-child-env.test.js +1 -1
- package/dist/test/mcpl-child-env.test.js.map +1 -1
- package/dist/test/mcpl-coalesced-content-parity.test.d.ts +2 -0
- package/dist/test/mcpl-coalesced-content-parity.test.d.ts.map +1 -0
- package/dist/test/mcpl-coalesced-content-parity.test.js +532 -0
- package/dist/test/mcpl-coalesced-content-parity.test.js.map +1 -0
- package/dist/test/mcpl-coalescing.test.js +78 -16
- package/dist/test/mcpl-coalescing.test.js.map +1 -1
- package/dist/test/mcpl-config-diagnostics.test.js +6 -3
- package/dist/test/mcpl-config-diagnostics.test.js.map +1 -1
- package/dist/test/mcpl-gate-roundtrip.test.js.map +1 -1
- package/dist/test/mcpl-manifest-changed.test.js +2 -1
- package/dist/test/mcpl-manifest-changed.test.js.map +1 -1
- package/dist/test/mcpl-reconnect-events.test.js.map +1 -1
- package/dist/test/mcpl-reconnect-reregistration.test.js +2 -0
- package/dist/test/mcpl-reconnect-reregistration.test.js.map +1 -1
- package/dist/test/mcpl-reference-fetcher.test.js +232 -4
- package/dist/test/mcpl-reference-fetcher.test.js.map +1 -1
- package/dist/test/mcpl-reference-fields.test.js +69 -2
- package/dist/test/mcpl-reference-fields.test.js.map +1 -1
- package/dist/test/mcpl-rejection-log-lines.test.d.ts +2 -0
- package/dist/test/mcpl-rejection-log-lines.test.d.ts.map +1 -0
- package/dist/test/mcpl-rejection-log-lines.test.js +349 -0
- package/dist/test/mcpl-rejection-log-lines.test.js.map +1 -0
- package/dist/test/mcpl-stderr-tail.test.d.ts +2 -0
- package/dist/test/mcpl-stderr-tail.test.d.ts.map +1 -0
- package/dist/test/mcpl-stderr-tail.test.js +210 -0
- package/dist/test/mcpl-stderr-tail.test.js.map +1 -0
- package/dist/test/mcpl-tool-lifecycle-e2e.test.js.map +1 -1
- package/dist/test/mcpl-tool-lifecycle-emitter.test.js.map +1 -1
- package/dist/test/mcpl-tool-lifecycle-script.test.js.map +1 -1
- package/dist/test/mcpl-tool-lifecycle.test.js.map +1 -1
- package/dist/test/mcpl-transport-close.test.d.ts +2 -0
- package/dist/test/mcpl-transport-close.test.d.ts.map +1 -0
- package/dist/test/mcpl-transport-close.test.js +373 -0
- package/dist/test/mcpl-transport-close.test.js.map +1 -0
- package/dist/test/mcpl-transport-spawn.test.d.ts +2 -0
- package/dist/test/mcpl-transport-spawn.test.d.ts.map +1 -0
- package/dist/test/mcpl-transport-spawn.test.js +58 -0
- package/dist/test/mcpl-transport-spawn.test.js.map +1 -0
- package/dist/test/mcpl-ws-transport.test.js +104 -0
- package/dist/test/mcpl-ws-transport.test.js.map +1 -1
- package/dist/test/offline-recovery-branch.test.js +344 -0
- package/dist/test/offline-recovery-branch.test.js.map +1 -1
- package/dist/test/operator-surgery.test.js +197 -0
- package/dist/test/operator-surgery.test.js.map +1 -1
- package/dist/test/ops-alert.test.js +2 -1
- package/dist/test/ops-alert.test.js.map +1 -1
- package/dist/test/overbudget-drain-breaker.test.js +1 -0
- package/dist/test/overbudget-drain-breaker.test.js.map +1 -1
- package/dist/test/pack-installs-alone.test.d.ts +2 -0
- package/dist/test/pack-installs-alone.test.d.ts.map +1 -0
- package/dist/test/pack-installs-alone.test.js +284 -0
- package/dist/test/pack-installs-alone.test.js.map +1 -0
- package/dist/test/physical-window-restart.test.d.ts +6 -4
- package/dist/test/physical-window-restart.test.d.ts.map +1 -1
- package/dist/test/physical-window-restart.test.js +275 -10
- package/dist/test/physical-window-restart.test.js.map +1 -1
- package/dist/test/placed-reaction-baseline.test.d.ts +2 -0
- package/dist/test/placed-reaction-baseline.test.d.ts.map +1 -0
- package/dist/test/placed-reaction-baseline.test.js +312 -0
- package/dist/test/placed-reaction-baseline.test.js.map +1 -0
- package/dist/test/present-while-acting.test.js +949 -6
- package/dist/test/present-while-acting.test.js.map +1 -1
- package/dist/test/provider-acceleration-cooldown.test.js +5 -1
- package/dist/test/provider-acceleration-cooldown.test.js.map +1 -1
- package/dist/test/provider-waits.test.d.ts +2 -0
- package/dist/test/provider-waits.test.d.ts.map +1 -0
- package/dist/test/provider-waits.test.js +2259 -0
- package/dist/test/provider-waits.test.js.map +1 -0
- package/dist/test/puppet-tool-call.test.js +2 -0
- package/dist/test/puppet-tool-call.test.js.map +1 -1
- package/dist/test/reaction-baseline-platform.test.d.ts +2 -0
- package/dist/test/reaction-baseline-platform.test.d.ts.map +1 -0
- package/dist/test/reaction-baseline-platform.test.js +124 -0
- package/dist/test/reaction-baseline-platform.test.js.map +1 -0
- package/dist/test/record-journal.test.js.map +1 -1
- package/dist/test/refusal-reaction-baseline.test.js +10 -3
- package/dist/test/refusal-reaction-baseline.test.js.map +1 -1
- package/dist/test/refusal-rewind.test.js +9 -0
- package/dist/test/refusal-rewind.test.js.map +1 -1
- package/dist/test/refusal-single-call.test.js.map +1 -1
- package/dist/test/release-bump.test.d.ts +2 -0
- package/dist/test/release-bump.test.d.ts.map +1 -0
- package/dist/test/release-bump.test.js +248 -0
- package/dist/test/release-bump.test.js.map +1 -0
- package/dist/test/release-changelog.test.js.map +1 -1
- package/dist/test/release-holds.test.d.ts +2 -0
- package/dist/test/release-holds.test.d.ts.map +1 -0
- package/dist/test/release-holds.test.js +456 -0
- package/dist/test/release-holds.test.js.map +1 -0
- package/dist/test/same-round-think-policy.test.js.map +1 -1
- package/dist/test/save-recent-image.test.js +138 -2
- package/dist/test/save-recent-image.test.js.map +1 -1
- package/dist/test/shutdown-errors.test.js.map +1 -1
- package/dist/test/silent-heartbeat-rows.test.js.map +1 -1
- package/dist/test/stop-with-host-operations.test.d.ts +20 -0
- package/dist/test/stop-with-host-operations.test.d.ts.map +1 -0
- package/dist/test/stop-with-host-operations.test.js +1174 -0
- package/dist/test/stop-with-host-operations.test.js.map +1 -0
- package/dist/test/stop-with-work-in-flight.test.js +162 -50
- package/dist/test/stop-with-work-in-flight.test.js.map +1 -1
- package/dist/test/surgery-marks-receipt.test.d.ts +2 -0
- package/dist/test/surgery-marks-receipt.test.d.ts.map +1 -0
- package/dist/test/surgery-marks-receipt.test.js +540 -0
- package/dist/test/surgery-marks-receipt.test.js.map +1 -0
- package/dist/test/tool-batch-hook.test.js.map +1 -1
- package/dist/test/tool-result-guard.test.js +352 -16
- package/dist/test/tool-result-guard.test.js.map +1 -1
- package/dist/test/tool-result-spill.test.js.map +1 -1
- package/dist/test/tool-wrapper-prose-guard-integration.test.js.map +1 -1
- package/dist/test/trunk-channel-routing.test.js +330 -7
- package/dist/test/trunk-channel-routing.test.js.map +1 -1
- package/dist/test/tune-out-e2e.test.js.map +1 -1
- package/dist/test/unconvertible-tool-result.test.js.map +1 -1
- package/dist/test/unsent-compiles.test.d.ts +2 -0
- package/dist/test/unsent-compiles.test.d.ts.map +1 -0
- package/dist/test/unsent-compiles.test.js +139 -0
- package/dist/test/unsent-compiles.test.js.map +1 -0
- package/dist/test/workspace-branch-continuation.test.js.map +1 -1
- package/dist/test/workspace-materialize-freshness.test.js.map +1 -1
- package/dist/test/workspace-read-text.test.js.map +1 -1
- package/dist/test/workspace-three-way.test.js +1022 -7
- package/dist/test/workspace-three-way.test.js.map +1 -1
- package/dist/test/workspace-watcher.test.js.map +1 -1
- package/dist/test/xml-round-persist.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -6
- package/src/agent.ts +415 -67
- package/src/api/mcp-server-core.ts +681 -0
- package/src/api/mcp-server.ts +16 -685
- package/src/api/server.ts +87 -26
- package/src/api/types.ts +9 -1
- package/src/framework.ts +3389 -782
- package/src/index.ts +9 -4
- package/src/mcpl/channel-registry.ts +126 -31
- package/src/mcpl/feature-set-manager.ts +4 -3
- package/src/mcpl/index.ts +2 -0
- package/src/mcpl/log-value.ts +116 -0
- package/src/mcpl/prose-stream-router.ts +6 -6
- package/src/mcpl/push-coalescer.ts +76 -12
- package/src/mcpl/push-handler.ts +95 -23
- package/src/mcpl/reference-fetcher.ts +18 -8
- package/src/mcpl/references.ts +57 -13
- package/src/mcpl/refusal-lines.ts +59 -0
- package/src/mcpl/server-connection.ts +125 -15
- package/src/mcpl/tool-classes.ts +3 -2
- package/src/mcpl/transport.ts +105 -14
- package/src/mcpl/types.ts +5 -1
- package/src/module-registry.ts +17 -9
- package/src/modules/discord/index.ts +4 -2
- package/src/modules/workspace/disk-agreement.ts +74 -18
- package/src/modules/workspace/index.ts +85 -15
- package/src/modules/workspace/observe.ts +295 -43
- package/src/modules/workspace/reconcile.ts +90 -12
- package/src/modules/workspace/types.ts +26 -1
- package/src/operator-log.ts +30 -0
- package/src/provider-waits.ts +523 -0
- package/src/recovery/offline-branch.ts +32 -28
- package/src/refusal-reactions.ts +14 -12
- package/src/stored-removal.ts +126 -0
- package/src/tool-result-guard.ts +72 -24
- package/src/types/agent.ts +42 -6
- package/src/types/events.ts +12 -0
- package/src/types/framework.ts +10 -0
- package/src/types/module.ts +20 -8
- package/src/types/trace.ts +76 -14
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* bookkeeping and is unit-tested without a framework.
|
|
19
19
|
*/
|
|
20
20
|
import type { McplContentBlock, PushEventResult } from './types.js';
|
|
21
|
+
import { classifyBlock, isInlineContradiction } from './references.js';
|
|
21
22
|
import { isVisiblyEmptyContent } from './visible-content.js';
|
|
22
23
|
|
|
23
24
|
export const PUSH_COALESCING_SUPPORT = {
|
|
@@ -37,6 +38,18 @@ export class CoalesceError extends Error {
|
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
/**
|
|
42
|
+
* How the host names a coalesce rejection, from the code the wire answered
|
|
43
|
+
* with. `coalesce-failed` is the host's own failure: -32000 (coalescing
|
|
44
|
+
* unavailable, the host stopping, or an acceptance it couldn't make durable)
|
|
45
|
+
* and -32603 (an internal error). `coalesce-invalid` is every other answer,
|
|
46
|
+
* the sender's: -32602 for a malformed item, -32017 or -32023 for a channel
|
|
47
|
+
* that isn't permitted or isn't registered.
|
|
48
|
+
*/
|
|
49
|
+
export function coalesceRejectionReason(answered: number): 'coalesce-failed' | 'coalesce-invalid' {
|
|
50
|
+
return answered === -32000 || answered === -32603 ? 'coalesce-failed' : 'coalesce-invalid';
|
|
51
|
+
}
|
|
52
|
+
|
|
40
53
|
/** Content with nothing model-visible (see visible-content.ts): -32602. */
|
|
41
54
|
export class EmptyContentError extends CoalesceError {
|
|
42
55
|
readonly reason = 'empty-content';
|
|
@@ -212,7 +225,6 @@ export interface CoalescerOptions {
|
|
|
212
225
|
recoveryBackoffMs?: number;
|
|
213
226
|
maxSubjects?: number;
|
|
214
227
|
maxNotices?: number;
|
|
215
|
-
maxContentBytes?: number;
|
|
216
228
|
maxDataBytes?: number;
|
|
217
229
|
}
|
|
218
230
|
|
|
@@ -232,6 +244,32 @@ function mergeIdentity(prior: CoalescedOccurrence['identity'], next: CoalescedOc
|
|
|
232
244
|
/**
|
|
233
245
|
* Validate wire content before it is stored, rendered or converted.
|
|
234
246
|
*
|
|
247
|
+
* Its shape and its visible emptiness, nothing else. An incoming coalesced
|
|
248
|
+
* occurrence reaches context through the converters an uncoalesced message
|
|
249
|
+
* goes through: a plain occurrence the moment it is admitted (the coalescer
|
|
250
|
+
* only remembers where it landed), a deferred one when its render settles or
|
|
251
|
+
* falls back to it. Its content must be an array of objects. A block the
|
|
252
|
+
* converters can't render (a video, an unknown type, an image with neither
|
|
253
|
+
* uri nor data) becomes a placeholder, as it does uncoalesced, and so does a
|
|
254
|
+
* malformed one, since the converters take any object without throwing: a
|
|
255
|
+
* type, text, data, mimeType or uri that isn't a string counts as absent, a
|
|
256
|
+
* text block's as no text. The uncoalesced lane doesn't check the array:
|
|
257
|
+
* there a member that isn't an object becomes the untyped placeholder, and a
|
|
258
|
+
* null member or content that isn't an array throws in conversion
|
|
259
|
+
* (agent-framework#297 brings that lane to this rule).
|
|
260
|
+
* Nor does the uncoalesced path set a byte budget on content:
|
|
261
|
+
* an inline image is admitted here as it is there (agent-framework#266: a
|
|
262
|
+
* 1 MiB serialized-content budget here dropped every coalesced message
|
|
263
|
+
* carrying a screenshot). A deferred occurrence's content is held, until then,
|
|
264
|
+
* in the coalescer's state and its persisted snapshot and receipts.
|
|
265
|
+
*
|
|
266
|
+
* A render result, which the host delivers in the occurrence's place, gets
|
|
267
|
+
* these checks, as RFC-006 §5.2 gives it every check a pushed payload gets,
|
|
268
|
+
* and one more (`checkBlockShapes`): each block must be one the converters
|
|
269
|
+
* show as itself or as a reference stub, not as a placeholder. A malformed
|
|
270
|
+
* render falls back to the occurrence's own content; a pushed payload has
|
|
271
|
+
* nothing to fall back to, and gets its placeholders.
|
|
272
|
+
*
|
|
235
273
|
* Visibly-empty content is refused unless `allowEmpty`: an admitted
|
|
236
274
|
* occurrence with nothing to show would wake the model with no visible cause.
|
|
237
275
|
* Empty is legitimate only where the RFC gives it a meaning — a retraction
|
|
@@ -240,22 +278,38 @@ function mergeIdentity(prior: CoalescedOccurrence['identity'], next: CoalescedOc
|
|
|
240
278
|
*/
|
|
241
279
|
export function validateCoalescedContent(
|
|
242
280
|
content: unknown,
|
|
243
|
-
|
|
244
|
-
options: { allowEmpty?: boolean } = {},
|
|
281
|
+
options: { allowEmpty?: boolean; checkBlockShapes?: boolean } = {},
|
|
245
282
|
): asserts content is McplContentBlock[] {
|
|
246
283
|
if (!Array.isArray(content)) throw new CoalesceError('payload.content', 'content must be an array');
|
|
247
284
|
for (const b of content as Array<Record<string, unknown>>) {
|
|
248
285
|
if (!b || typeof b !== 'object' || Array.isArray(b)) throw new CoalesceError('payload.content', 'invalid content block');
|
|
249
|
-
if (
|
|
250
|
-
if (b.type === 'resource' && typeof b.uri === 'string') continue;
|
|
251
|
-
if ((b.type === 'image' || b.type === 'audio')
|
|
252
|
-
&& (typeof b.uri === 'string' || (typeof b.data === 'string' && typeof b.mimeType === 'string'))) continue;
|
|
253
|
-
throw new CoalesceError('payload.content', 'invalid content block');
|
|
286
|
+
if (options.checkBlockShapes && !showsAsItself(b)) throw new CoalesceError('payload.content', 'invalid content block');
|
|
254
287
|
}
|
|
255
|
-
if (Buffer.byteLength(JSON.stringify(content)) > maxBytes) throw new CoalesceError('payload.content', 'content exceeds host byte limit');
|
|
256
288
|
if (!options.allowEmpty && isVisiblyEmptyContent(content)) throw new EmptyContentError();
|
|
257
289
|
}
|
|
258
290
|
|
|
291
|
+
/**
|
|
292
|
+
* Whether the converters (`convertBlock` in push-handler.ts and
|
|
293
|
+
* channel-registry.ts) show a block as itself, its text or its image or
|
|
294
|
+
* audio source, or as a reference stub, and not as a placeholder: an
|
|
295
|
+
* unrecognized block, a no-data or no-media-type one, or withheld inline
|
|
296
|
+
* data. A text block whose text isn't a string, which they show as no text,
|
|
297
|
+
* is refused too. This follows their branches in their order.
|
|
298
|
+
*/
|
|
299
|
+
function showsAsItself(b: Record<string, unknown>): boolean {
|
|
300
|
+
if (b.type === 'text') return typeof b.text === 'string';
|
|
301
|
+
if (b.type === 'resource') return classifyBlock(b).kind === 'reference';
|
|
302
|
+
if (b.type !== 'image' && b.type !== 'audio') return false;
|
|
303
|
+
if (isInlineContradiction(b)) return false;
|
|
304
|
+
if (isFilledString(b.uri) && b.disposition) return classifyBlock(b).kind === 'reference';
|
|
305
|
+
if (isFilledString(b.data) && isFilledString(b.mimeType)) return true;
|
|
306
|
+
return b.type === 'image' && isFilledString(b.uri);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function isFilledString(value: unknown): value is string {
|
|
310
|
+
return typeof value === 'string' && value !== '';
|
|
311
|
+
}
|
|
312
|
+
|
|
259
313
|
/** Validate the `coalesce` member of either lane (§13). */
|
|
260
314
|
export function validateCoalesceMember(
|
|
261
315
|
c: unknown,
|
|
@@ -700,14 +754,24 @@ export class PushCoalescer<E = unknown> {
|
|
|
700
754
|
try {
|
|
701
755
|
const result = await this.host.render(occurrence, params);
|
|
702
756
|
if (rendering.cancelled) { this.host.audit({ kind: 'late-render', subject, eventId: occurrence.eventId, discarded: true }); }
|
|
703
|
-
// §5.2: an empty result is legitimate ("nothing happened")
|
|
704
|
-
|
|
757
|
+
// §5.2: an empty result is legitimate ("nothing happened"); each
|
|
758
|
+
// block must be well formed.
|
|
759
|
+
else validateCoalescedContent(result?.content, { allowEmpty: true, checkBlockShapes: true });
|
|
705
760
|
if (rendering.cancelled) throw new CancelledRender();
|
|
706
761
|
content = result.content;
|
|
707
762
|
timestamp = typeof result.timestamp === 'string' ? result.timestamp : new Date().toISOString();
|
|
708
763
|
source = 'render';
|
|
709
764
|
} catch (error) {
|
|
710
|
-
if (!(error instanceof CancelledRender))
|
|
765
|
+
if (!(error instanceof CancelledRender)) {
|
|
766
|
+
// The occurrence's own content is delivered instead; the host names
|
|
767
|
+
// the failure where operators look (a console line, not only this
|
|
768
|
+
// audit record).
|
|
769
|
+
this.host.audit({
|
|
770
|
+
kind: 'render-failed', subject, serverId: occurrence.serverId, eventId: occurrence.eventId,
|
|
771
|
+
...(error instanceof CoalesceError ? { field: error.field } : {}),
|
|
772
|
+
error: String(error),
|
|
773
|
+
});
|
|
774
|
+
}
|
|
711
775
|
} finally {
|
|
712
776
|
const left = (this.rendersInFlight.get(occurrence.serverId) ?? 1) - 1;
|
|
713
777
|
if (left > 0) this.rendersInFlight.set(occurrence.serverId, left); else this.rendersInFlight.delete(occurrence.serverId);
|
package/src/mcpl/push-handler.ts
CHANGED
|
@@ -18,8 +18,10 @@ import type {
|
|
|
18
18
|
import type { FeatureSetManager } from './feature-set-manager.js';
|
|
19
19
|
import { McplFeatureSetError } from './feature-set-manager.js';
|
|
20
20
|
import { expandCoreTags } from './tags.js';
|
|
21
|
-
import { EmptyContentError, validateCoalescedContent } from './push-coalescer.js';
|
|
21
|
+
import { coalesceRejectionReason, EmptyContentError, validateCoalescedContent } from './push-coalescer.js';
|
|
22
22
|
import { isSilentHeartbeatMarker, isVisiblyEmptyContent } from './visible-content.js';
|
|
23
|
+
import { logDetail, logValue } from './log-value.js';
|
|
24
|
+
import { RefusalLines } from './refusal-lines.js';
|
|
23
25
|
|
|
24
26
|
// ============================================================================
|
|
25
27
|
// McplPushEvent (the ProcessEvent shape pushed to the queue)
|
|
@@ -55,13 +57,17 @@ export interface McplPushEvent {
|
|
|
55
57
|
// ============================================================================
|
|
56
58
|
|
|
57
59
|
/**
|
|
58
|
-
* Convert a single MCPL wire-format content block to a membrane ContentBlock
|
|
59
|
-
*
|
|
60
|
+
* Convert a single MCPL wire-format content block to a membrane ContentBlock,
|
|
61
|
+
* as the channel lane's convertBlock (channel-registry.ts) does, apart from
|
|
62
|
+
* the provenance its reference stubs name. Hook context injections convert
|
|
63
|
+
* through hook-orchestrator.ts's own.
|
|
60
64
|
*/
|
|
61
65
|
export function convertBlock(block: McplContentBlock): ContentBlock {
|
|
62
66
|
switch (block.type) {
|
|
63
67
|
case 'text':
|
|
64
|
-
|
|
68
|
+
// Text that isn't a string is no text, as the request sanitizer and
|
|
69
|
+
// the visible-content check already read it.
|
|
70
|
+
return { type: 'text', text: typeof block.text === 'string' ? block.text : '' };
|
|
65
71
|
|
|
66
72
|
case 'image':
|
|
67
73
|
if (isInlineContradiction(block)) {
|
|
@@ -69,24 +75,24 @@ export function convertBlock(block: McplContentBlock): ContentBlock {
|
|
|
69
75
|
// closed, withhold the data (checked BEFORE the data branch).
|
|
70
76
|
return { type: 'text', text: INLINE_WITHHELD_TEXT };
|
|
71
77
|
}
|
|
72
|
-
if (block.uri && block.disposition) {
|
|
78
|
+
if (isFilledString(block.uri) && block.disposition) {
|
|
73
79
|
// RFC-005 uri-form media with a disposition claim: stub, do not
|
|
74
80
|
// hand the URI to the provider or inline it.
|
|
75
81
|
return { type: 'text', text: referenceStubOrNull(block, 'attachment on push event') ?? '[reference]' };
|
|
76
82
|
}
|
|
77
|
-
if (block.data && block.mimeType) {
|
|
83
|
+
if (isFilledString(block.data) && isFilledString(block.mimeType)) {
|
|
78
84
|
return {
|
|
79
85
|
type: 'image',
|
|
80
86
|
source: { type: 'base64', data: block.data, mediaType: block.mimeType },
|
|
81
87
|
} as ContentBlock;
|
|
82
88
|
}
|
|
83
|
-
if (block.uri) {
|
|
89
|
+
if (isFilledString(block.uri)) {
|
|
84
90
|
return {
|
|
85
91
|
type: 'image',
|
|
86
92
|
source: { type: 'url', url: block.uri },
|
|
87
93
|
} as ContentBlock;
|
|
88
94
|
}
|
|
89
|
-
return { type: 'text', text: '[Image: no data]' };
|
|
95
|
+
return { type: 'text', text: isFilledString(block.data) ? '[Image: no media type]' : '[Image: no data]' };
|
|
90
96
|
|
|
91
97
|
case 'audio':
|
|
92
98
|
if (isInlineContradiction(block)) {
|
|
@@ -94,31 +100,43 @@ export function convertBlock(block: McplContentBlock): ContentBlock {
|
|
|
94
100
|
// closed, withhold the data (checked BEFORE the data branch).
|
|
95
101
|
return { type: 'text', text: INLINE_WITHHELD_TEXT };
|
|
96
102
|
}
|
|
97
|
-
if (block.uri && block.disposition) {
|
|
103
|
+
if (isFilledString(block.uri) && block.disposition) {
|
|
98
104
|
// RFC-005 uri-form media with a disposition claim: stub, do not
|
|
99
105
|
// hand the URI to the provider or inline it.
|
|
100
106
|
return { type: 'text', text: referenceStubOrNull(block, 'attachment on push event') ?? '[reference]' };
|
|
101
107
|
}
|
|
102
|
-
if (block.data && block.mimeType) {
|
|
108
|
+
if (isFilledString(block.data) && isFilledString(block.mimeType)) {
|
|
103
109
|
return {
|
|
104
110
|
type: 'audio',
|
|
105
111
|
source: { type: 'base64', data: block.data, mediaType: block.mimeType },
|
|
106
112
|
} as ContentBlock;
|
|
107
113
|
}
|
|
108
|
-
return { type: 'text', text: '[Audio: no data]' };
|
|
114
|
+
return { type: 'text', text: isFilledString(block.data) ? '[Audio: no media type]' : '[Audio: no data]' };
|
|
109
115
|
|
|
110
116
|
case 'resource':
|
|
111
117
|
// RFC-005: reference blocks become bounded stubs — never raw URIs
|
|
112
118
|
// (a signed URL is a bearer credential that looks like a location).
|
|
113
119
|
return { type: 'text', text: referenceStubOrNull(block, 'attachment on push event') ?? '[reference]' };
|
|
114
120
|
|
|
115
|
-
default:
|
|
121
|
+
default: {
|
|
116
122
|
// Unknown wire block types previously fell off the exhaustive switch
|
|
117
|
-
// and propagated `undefined` into ContentBlock[]. Fail visibly.
|
|
118
|
-
|
|
123
|
+
// and propagated `undefined` into ContentBlock[]. Fail visibly. A type
|
|
124
|
+
// that isn't a string is no type, and is never converted to one: an
|
|
125
|
+
// object can throw when it is.
|
|
126
|
+
const type: unknown = (block as { type?: unknown }).type;
|
|
127
|
+
return { type: 'text', text: `[unrecognized content block: ${typeof type === 'string' ? type : 'untyped'}]` };
|
|
128
|
+
}
|
|
119
129
|
}
|
|
120
130
|
}
|
|
121
131
|
|
|
132
|
+
/** A string with something in it: the only data, mimeType or uri a block's
|
|
133
|
+
* source is built from, and the only uri that makes it a reference. Anything
|
|
134
|
+
* else a sender puts there leaves the block without it, so conversion never
|
|
135
|
+
* throws and never copies a non-string. */
|
|
136
|
+
function isFilledString(value: unknown): value is string {
|
|
137
|
+
return typeof value === 'string' && value !== '';
|
|
138
|
+
}
|
|
139
|
+
|
|
122
140
|
// ============================================================================
|
|
123
141
|
// LRU Dedup Set
|
|
124
142
|
// ============================================================================
|
|
@@ -171,6 +189,9 @@ export class PushHandler {
|
|
|
171
189
|
private emitTraceFn: (event: { type: string; [key: string]: unknown }) => void;
|
|
172
190
|
private shouldTriggerInference?: (content: string, metadata: Record<string, unknown>) => boolean;
|
|
173
191
|
private dedup = new DedupSet(1000);
|
|
192
|
+
/** Holds back repeats of the line for a channel-scoped push refused because
|
|
193
|
+
* its channel isn't declared (-32023) or may not be used (-32017). */
|
|
194
|
+
private channelRefusalLines = new RefusalLines();
|
|
174
195
|
|
|
175
196
|
constructor(
|
|
176
197
|
featureSetManager: FeatureSetManager,
|
|
@@ -221,7 +242,7 @@ export class PushHandler {
|
|
|
221
242
|
: 'Feature set validation failed';
|
|
222
243
|
// Loud rejection — a rejected push event is an agent that silently
|
|
223
244
|
// never hears the message. (2026-07-09 diagnosability pass.)
|
|
224
|
-
console.error(`[push-event-rejected] server=${serverId} eventId=${params.eventId} reason=${reason}`);
|
|
245
|
+
console.error(`[push-event-rejected] server=${serverId} eventId=${logValue(params.eventId ?? '')} reason=${logValue(reason)}`);
|
|
225
246
|
if (err instanceof McplFeatureSetError && responder?.respondError) {
|
|
226
247
|
responder.respondError(err.code, reason, { featureSet: err.featureSet });
|
|
227
248
|
} else {
|
|
@@ -240,11 +261,13 @@ export class PushHandler {
|
|
|
240
261
|
if (coalesced) {
|
|
241
262
|
// RFC-006 §13: malformed content is a -32602, checked before conversion.
|
|
242
263
|
try {
|
|
243
|
-
validateCoalescedContent(params.payload?.content,
|
|
264
|
+
validateCoalescedContent(params.payload?.content, { allowEmpty: emptyAllowed });
|
|
244
265
|
} catch (error) {
|
|
245
266
|
const err = error as Error & { code?: number; field?: string };
|
|
267
|
+
const code = err.code ?? -32602;
|
|
246
268
|
if (err instanceof EmptyContentError) this.traceEmptyRejection(serverId, params);
|
|
247
|
-
|
|
269
|
+
else this.traceCoalesceRejection(serverId, params, err, code);
|
|
270
|
+
if (responder?.respondError) responder.respondError(code, err.message, { field: err.field });
|
|
248
271
|
else responder?.respond({ accepted: false, reason: err instanceof EmptyContentError ? err.reason : err.message });
|
|
249
272
|
return;
|
|
250
273
|
}
|
|
@@ -265,7 +288,7 @@ export class PushHandler {
|
|
|
265
288
|
// coalescer's receipts instead (RFC-006 §3.1: a retry within the window
|
|
266
289
|
// gets its original result, which this set could not return).
|
|
267
290
|
if (!coalesced && this.dedup.checkAndAdd(params.eventId)) {
|
|
268
|
-
console.error(`[push-event-rejected] server=${serverId} eventId=${params.eventId} reason=duplicate`);
|
|
291
|
+
console.error(`[push-event-rejected] server=${serverId} eventId=${logValue(params.eventId ?? '')} reason=duplicate`);
|
|
269
292
|
responder?.respond({ accepted: false, reason: 'duplicate' });
|
|
270
293
|
return;
|
|
271
294
|
}
|
|
@@ -314,8 +337,9 @@ export class PushHandler {
|
|
|
314
337
|
responder?.respond(result);
|
|
315
338
|
} catch (error) {
|
|
316
339
|
const err = error as Error & { code?: number; field?: string };
|
|
317
|
-
|
|
318
|
-
|
|
340
|
+
const code = typeof err.code === 'number' ? err.code : -32603;
|
|
341
|
+
this.traceCoalesceRejection(serverId, params, err, code);
|
|
342
|
+
if (responder?.respondError) responder.respondError(code, err.message, { field: err.field });
|
|
319
343
|
else responder?.respond({ accepted: false, reason: err.message });
|
|
320
344
|
}
|
|
321
345
|
return;
|
|
@@ -334,13 +358,61 @@ export class PushHandler {
|
|
|
334
358
|
responder?.respond({ accepted: true, inferenceId });
|
|
335
359
|
}
|
|
336
360
|
|
|
337
|
-
/**
|
|
361
|
+
/**
|
|
362
|
+
* A coalesced occurrence the host refused, as loud as every other push
|
|
363
|
+
* rejection: its producer is told on the wire, and until now the host said
|
|
364
|
+
* nothing, so a dropped message left no trace where operators look
|
|
365
|
+
* (agent-framework#266). The producer gets the wire answer this lane gives
|
|
366
|
+
* (`answered`): the thrown error's code and message, or -32602 for a
|
|
367
|
+
* malformed occurrence without one. The reason follows that answer
|
|
368
|
+
* (coalesceRejectionReason): `coalesce-failed` when the host itself failed,
|
|
369
|
+
* `coalesce-invalid` when the producer's occurrence was refused, with the
|
|
370
|
+
* failing `field` and the error's message as `detail`. A missing eventId
|
|
371
|
+
* prints as `""` and traces as `''`, as the channel lane's missing id does.
|
|
372
|
+
* A channel-scoped push refused because its channel isn't declared or may
|
|
373
|
+
* not be used names the channel on its line and its trace, and is refused
|
|
374
|
+
* for as long as that lasts, so its line prints at most once a minute per
|
|
375
|
+
* channel (RefusalLines); every refusal is traced.
|
|
376
|
+
*/
|
|
377
|
+
private traceCoalesceRejection(
|
|
378
|
+
serverId: string,
|
|
379
|
+
params: PushEventParams,
|
|
380
|
+
error: { field?: unknown; message: string },
|
|
381
|
+
answered: number,
|
|
382
|
+
): void {
|
|
383
|
+
const reason = coalesceRejectionReason(answered);
|
|
384
|
+
const field = typeof error.field === 'string' && error.field ? error.field : undefined;
|
|
385
|
+
const scoped = params.coalesce?.channelId;
|
|
386
|
+
const channelId = (answered === -32023 || answered === -32017) && typeof scoped === 'string' ? scoped : undefined;
|
|
387
|
+
const held = channelId === undefined ? 0 : this.channelRefusalLines.admit(serverId, channelId);
|
|
388
|
+
if (held !== null) {
|
|
389
|
+
console.error(
|
|
390
|
+
`[push-event-rejected] server=${serverId} featureSet=${logValue(params.featureSet ?? '')} eventId=${logValue(params.eventId ?? '')} ` +
|
|
391
|
+
(channelId === undefined ? '' : `channel=${logValue(channelId)} `) +
|
|
392
|
+
`reason=${reason}${field ? ` field=${logValue(field)}` : ''}: ${logDetail(error.message)}` +
|
|
393
|
+
(held ? ` (${held} more since the last line)` : ''),
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
this.emitTraceFn({
|
|
397
|
+
type: 'mcpl:push-event-rejected',
|
|
398
|
+
serverId,
|
|
399
|
+
eventId: typeof params.eventId === 'string' ? params.eventId : '',
|
|
400
|
+
featureSet: params.featureSet,
|
|
401
|
+
reason,
|
|
402
|
+
...(field ? { field } : {}),
|
|
403
|
+
...(channelId === undefined ? {} : { channelId }),
|
|
404
|
+
detail: error.message,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Loud, like every other push rejection: the producer's wake went nowhere.
|
|
409
|
+
* A missing eventId prints as `""` and traces as `''`, as on every push line. */
|
|
338
410
|
private traceEmptyRejection(serverId: string, params: PushEventParams): void {
|
|
339
|
-
console.error(`[push-event-rejected] server=${serverId} eventId=${params.eventId} reason=empty-content`);
|
|
411
|
+
console.error(`[push-event-rejected] server=${serverId} eventId=${logValue(params.eventId ?? '')} reason=empty-content`);
|
|
340
412
|
this.emitTraceFn({
|
|
341
413
|
type: 'mcpl:push-event-rejected',
|
|
342
414
|
serverId,
|
|
343
|
-
eventId: params.eventId,
|
|
415
|
+
eventId: typeof params.eventId === 'string' ? params.eventId : '',
|
|
344
416
|
featureSet: params.featureSet,
|
|
345
417
|
reason: 'empty-content',
|
|
346
418
|
});
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
import { createHash } from 'node:crypto';
|
|
31
31
|
import type { ReferenceRecord } from './references.js';
|
|
32
|
-
import { isReferenceExpired } from './references.js';
|
|
32
|
+
import { EXPIRY_REASON, isReferenceExpired, referenceExpiry, REFERENCE_LIMITS, sanitizeLabel } from './references.js';
|
|
33
33
|
|
|
34
34
|
/** Per-fetch hard ceiling on actual bytes (overridable per server / call). */
|
|
35
35
|
export const DEFAULT_FETCH_MAX_BYTES = 256 * 1024 * 1024;
|
|
@@ -79,13 +79,14 @@ export function sniffMime(data: Buffer): string | null {
|
|
|
79
79
|
return null;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
/** A Map, so a server's type such as `constructor` finds no inherited key. */
|
|
83
|
+
const EXT_BY_MIME = new Map<string, string>(Object.entries({
|
|
83
84
|
'audio/wav': 'wav', 'audio/x-wav': 'wav', 'audio/mpeg': 'mp3', 'audio/mp4': 'm4a',
|
|
84
85
|
'audio/flac': 'flac', 'audio/midi': 'mid', 'audio/ogg': 'ogg',
|
|
85
86
|
'image/png': 'png', 'image/jpeg': 'jpg', 'image/gif': 'gif', 'image/webp': 'webp',
|
|
86
87
|
'video/mp4': 'mp4', 'application/json': 'json', 'text/plain': 'txt',
|
|
87
88
|
'application/pdf': 'pdf', 'application/zip': 'zip',
|
|
88
|
-
};
|
|
89
|
+
}));
|
|
89
90
|
|
|
90
91
|
/** ws→http / wss→https with default ports elided, for origin comparison. */
|
|
91
92
|
export function normalizedOrigin(url: string): string | null {
|
|
@@ -111,9 +112,11 @@ export class ReferenceFetcher {
|
|
|
111
112
|
private readonly saveFile: (fileName: string, data: Buffer, mimeType: string) => Promise<string | null>,
|
|
112
113
|
) {}
|
|
113
114
|
|
|
114
|
-
/** Would the eager (autofetch) default fetch this record?
|
|
115
|
+
/** Would the eager (autofetch) default fetch this record? Not one the
|
|
116
|
+
* fetcher refuses as expired, which would only take an eager slot. */
|
|
115
117
|
isEagerEligible(record: ReferenceRecord): boolean {
|
|
116
118
|
if (record.fetchedPath) return false;
|
|
119
|
+
if (isReferenceExpired(record.testimony)) return false;
|
|
117
120
|
if (record.serverId === undefined) return false;
|
|
118
121
|
const binding = this.resolveServer(record.serverId);
|
|
119
122
|
if (!binding) return false;
|
|
@@ -161,7 +164,8 @@ export class ReferenceFetcher {
|
|
|
161
164
|
|
|
162
165
|
const refusal = this.originCheck(record, binding);
|
|
163
166
|
if (refusal) return { ok: false, error: refusal };
|
|
164
|
-
|
|
167
|
+
const expiry = referenceExpiry(t);
|
|
168
|
+
if (expiry) return { ok: false, error: `reference ${EXPIRY_REASON[expiry]}` };
|
|
165
169
|
|
|
166
170
|
const ceiling = opts?.maxBytes ?? DEFAULT_FETCH_MAX_BYTES;
|
|
167
171
|
// Vector 4: a claim already over the ceiling refuses without dialing.
|
|
@@ -215,9 +219,15 @@ export class ReferenceFetcher {
|
|
|
215
219
|
digestVerified = true;
|
|
216
220
|
}
|
|
217
221
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
222
|
+
// The response's header and the server's claimed type are both the
|
|
223
|
+
// server's words, and either can become fetch_reference's mime_type,
|
|
224
|
+
// the saved type and the record's verified type, so each is shown as a
|
|
225
|
+
// label is. One left empty once sanitized counts as absent.
|
|
226
|
+
const header = resp.headers.get('content-type');
|
|
227
|
+
const headerMime = header !== null ? sanitizeLabel(header.split(';')[0], REFERENCE_LIMITS.mimeType) || null : null;
|
|
228
|
+
const claimedMime = t.mimeType !== undefined ? sanitizeLabel(t.mimeType, REFERENCE_LIMITS.mimeType) || undefined : undefined;
|
|
229
|
+
const observedMime = sniffMime(data) ?? headerMime ?? claimedMime ?? 'application/octet-stream';
|
|
230
|
+
const ext = EXT_BY_MIME.get(observedMime) ?? EXT_BY_MIME.get(headerMime ?? '') ?? EXT_BY_MIME.get(claimedMime ?? '') ?? 'bin';
|
|
221
231
|
// §7.3: host-generated storage name; the server's `name` is display-only.
|
|
222
232
|
const path = await this.saveFile(`${record.refId}.${ext}`, data, observedMime);
|
|
223
233
|
if (path === null) return { ok: false, error: 'no writable workspace mount to store the payload' };
|
package/src/mcpl/references.ts
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* - `disposition:"never"` withholds payload AND uri from model context
|
|
14
14
|
* unconditionally — enforced in code here, not by caller discipline (§5);
|
|
15
15
|
* - stub size is independent of every server-supplied field length (§5);
|
|
16
|
-
* - unparseable `expiresAt` fails closed (§7.4)
|
|
16
|
+
* - unparseable `expiresAt` fails closed (§7.4), and so does one rejected
|
|
17
|
+
* as a field: it's absent as testimony, never read as immortal;
|
|
17
18
|
* - unknown block types never propagate `undefined` (the pre-existing
|
|
18
19
|
* convertBlock switches had no default — a latent corruption this module
|
|
19
20
|
* retires).
|
|
@@ -141,16 +142,42 @@ export function isInlineContradiction(block: unknown): boolean {
|
|
|
141
142
|
export const INLINE_WITHHELD_TEXT =
|
|
142
143
|
'[inline content withheld: nonconformant bulk disposition on inline data]';
|
|
143
144
|
|
|
144
|
-
|
|
145
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Why a reference counts as expired, or null when it doesn't: its expiresAt
|
|
147
|
+
* has passed, doesn't parse, or was rejected as a field (§7.4, §8). A
|
|
148
|
+
* rejected expiry (past its schema maximum, or not a string, null included)
|
|
149
|
+
* is absent as testimony under the one invalid-field rule, but reading that
|
|
150
|
+
* absence as "never expires" would let field invalidity widen exposure:
|
|
151
|
+
* like an unparseable one, it fails closed. A reference that claims no
|
|
152
|
+
* expiry has none.
|
|
153
|
+
*/
|
|
154
|
+
export function referenceExpiry(
|
|
155
|
+
t: Pick<ReferenceTestimony, 'expiresAt' | 'rejectedFields'>, nowMs = Date.now(),
|
|
156
|
+
): 'rejected' | 'unparseable' | 'passed' | null {
|
|
157
|
+
if (t.rejectedFields.includes('expiresAt')) return 'rejected';
|
|
158
|
+
if (t.expiresAt === undefined) return null;
|
|
146
159
|
const exp = Date.parse(t.expiresAt);
|
|
147
|
-
|
|
160
|
+
if (Number.isNaN(exp)) return 'unparseable';
|
|
161
|
+
return exp <= nowMs ? 'passed' : null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function isReferenceExpired(t: Pick<ReferenceTestimony, 'expiresAt' | 'rejectedFields'>, nowMs = Date.now()): boolean {
|
|
165
|
+
return referenceExpiry(t, nowMs) !== null;
|
|
148
166
|
}
|
|
149
167
|
|
|
150
|
-
/**
|
|
168
|
+
/** Each expiry case as the stub and the fetcher's refusal both say it. */
|
|
169
|
+
export const EXPIRY_REASON = {
|
|
170
|
+
passed: 'expired: its expiresAt has passed',
|
|
171
|
+
unparseable: 'treated as expired: its expiresAt does not parse (fail closed)',
|
|
172
|
+
rejected: `treated as expired: its expiresAt was rejected as a field, over ${REFERENCE_LIMITS.expiresAt} characters or not a string (fail closed)`,
|
|
173
|
+
} as const;
|
|
174
|
+
|
|
175
|
+
/** Strip control characters (C0, DEL, C1) and every bidi control (marks,
|
|
176
|
+
* embeddings, overrides, isolates, and the Arabic letter mark), trim the
|
|
177
|
+
* whitespace (a BOM included) left at either end, and bound the length.
|
|
151
178
|
* Labels are labels — storage naming is host-generated elsewhere (§7.3). */
|
|
152
179
|
export function sanitizeLabel(s: string, maxChars: number): string {
|
|
153
|
-
const cleaned = s.replace(/[\
|
|
180
|
+
const cleaned = s.replace(/[\p{Cc}\p{Bidi_Control}]/gu, '').trim();
|
|
154
181
|
if (cleaned.length <= maxChars) return cleaned;
|
|
155
182
|
let cut = cleaned.slice(0, Math.max(0, maxChars - 1));
|
|
156
183
|
// never split a surrogate pair at the truncation boundary
|
|
@@ -187,9 +214,10 @@ const MAX_RECORDS = 2000;
|
|
|
187
214
|
/**
|
|
188
215
|
* Host-private store of received references. Raw URIs live here and in stubs
|
|
189
216
|
* never (under `never`) or by policy only (`ref`/absent). Ids are stable and
|
|
190
|
-
* never reused within the process (vector 20)
|
|
191
|
-
*
|
|
192
|
-
*
|
|
217
|
+
* never reused within the process (vector 20). Past MAX_RECORDS the record
|
|
218
|
+
* registered least recently is evicted (registering a uri the registry holds
|
|
219
|
+
* makes its record the newest), and a stale lookup returns undefined — a
|
|
220
|
+
* defined miss, never a different record.
|
|
193
221
|
*
|
|
194
222
|
* Process-wide singleton by design: ids are host-local *names*, not
|
|
195
223
|
* capabilities, and the record itself carries serverId for any future
|
|
@@ -222,6 +250,9 @@ export class ReferenceRegistry {
|
|
|
222
250
|
// describes verified bytes on disk and keeps the testimony they were
|
|
223
251
|
// verified against.
|
|
224
252
|
if (!existing.fetchedPath) existing.testimony = testimony;
|
|
253
|
+
// Newest again, so a uri just mentioned is the last to be evicted.
|
|
254
|
+
this.records.delete(existingId);
|
|
255
|
+
this.records.set(existingId, existing);
|
|
225
256
|
return existing;
|
|
226
257
|
}
|
|
227
258
|
}
|
|
@@ -257,6 +288,12 @@ export const referenceRegistry = new ReferenceRegistry();
|
|
|
257
288
|
// Stub building (§5)
|
|
258
289
|
// ============================================================================
|
|
259
290
|
|
|
291
|
+
/** A reference's type as its stub shows one, or '' when nothing is left of
|
|
292
|
+
* it once sanitized (the stub then leaves it out). */
|
|
293
|
+
export function referenceTypeLabel(mimeType: string | undefined): string {
|
|
294
|
+
return mimeType ? sanitizeLabel(mimeType, STUB_FIELD_CHARS) : '';
|
|
295
|
+
}
|
|
296
|
+
|
|
260
297
|
/**
|
|
261
298
|
* Build the model-visible stub for a reference. Output length is bounded by
|
|
262
299
|
* host constants and independent of every server-supplied field length.
|
|
@@ -267,18 +304,25 @@ export const referenceRegistry = new ReferenceRegistry();
|
|
|
267
304
|
export function buildReferenceStub(t: ReferenceTestimony, provenance?: string, serverId?: string): string {
|
|
268
305
|
const record = referenceRegistry.register(t, serverId);
|
|
269
306
|
const parts: string[] = [];
|
|
270
|
-
|
|
307
|
+
// A label is shown only when something is left of it once sanitized.
|
|
308
|
+
const name = t.name ? sanitizeLabel(t.name, STUB_FIELD_CHARS) : '';
|
|
309
|
+
if (name) parts.push(name);
|
|
271
310
|
const meta: string[] = [];
|
|
272
|
-
|
|
311
|
+
const mimeType = referenceTypeLabel(t.mimeType);
|
|
312
|
+
if (mimeType) meta.push(mimeType);
|
|
273
313
|
if (t.sizeBytes !== undefined) meta.push(`${formatSizeBytes(t.sizeBytes)} claimed`);
|
|
274
314
|
if (meta.length) parts.push(meta.join(', '));
|
|
275
|
-
|
|
315
|
+
const origin = provenance ? sanitizeLabel(provenance, STUB_FIELD_CHARS) : '';
|
|
316
|
+
if (origin) parts.push(origin);
|
|
276
317
|
if (record.fetchedPath) {
|
|
277
318
|
// Already materialized by the host-mediated fetcher: hand the model the
|
|
278
319
|
// workspace path (host-generated, §7.3) so existing tools just work.
|
|
279
320
|
parts.push(`saved: ${sanitizeLabel(record.fetchedPath, 256)}`);
|
|
280
321
|
} else {
|
|
281
|
-
|
|
322
|
+
// An expired reference says so rather than inviting a fetch_reference
|
|
323
|
+
// call the fetcher can only refuse.
|
|
324
|
+
const expiry = referenceExpiry(record.testimony);
|
|
325
|
+
parts.push(expiry ? EXPIRY_REASON[expiry] : 'fetch with fetch_reference');
|
|
282
326
|
}
|
|
283
327
|
return `[${record.refId}] ${parts.join(' — ') || 'referenced content'}`;
|
|
284
328
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { logValue } from './log-value.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* At most one host line a minute for a refusal a server keeps repeating for
|
|
5
|
+
* as long as a standing condition lasts: messages for a channel it never
|
|
6
|
+
* registered, or channel-scoped pushes to a channel it hasn't declared or may
|
|
7
|
+
* not use. A server that never registered a busy channel would otherwise
|
|
8
|
+
* print a line for every message in it. Only the line is held back: every
|
|
9
|
+
* refusal is still answered and traced. Each lane keeps its own records, as
|
|
10
|
+
* its lines are its own.
|
|
11
|
+
*
|
|
12
|
+
* Per server and channel, the first refusal prints. After that a refusal
|
|
13
|
+
* prints only once a minute has passed since that channel's last line, and
|
|
14
|
+
* its line names how many were held back since. The minute is timed with a
|
|
15
|
+
* monotonic clock, so a step of the wall clock can't silence a channel. A
|
|
16
|
+
* refusal within a minute of the channel's last line is counted into its
|
|
17
|
+
* next line, whatever happens to the channel in between (it may be
|
|
18
|
+
* registered, unregistered or reconnected).
|
|
19
|
+
*
|
|
20
|
+
* A record is keyed by the channel as its line prints it (logValue), so two
|
|
21
|
+
* ids share one exactly when their lines name them alike, which only ids
|
|
22
|
+
* over 256 units can. The records cover the 1,000 most recently refused
|
|
23
|
+
* channels: a new channel evicts the least recently refused one, with its
|
|
24
|
+
* minute and any count it held, and that channel's next refusal prints as a
|
|
25
|
+
* first.
|
|
26
|
+
*/
|
|
27
|
+
export class RefusalLines {
|
|
28
|
+
/** By key, from the least recently refused channel to the most. */
|
|
29
|
+
private readonly records = new Map<string, { at: number; since: number }>();
|
|
30
|
+
|
|
31
|
+
constructor(private readonly now: () => number = () => performance.now()) {}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Record one refusal for a server's channel. Returns null when its line is
|
|
35
|
+
* held back, or else how many of the channel's refusals were held back
|
|
36
|
+
* since its last line (0 for its first).
|
|
37
|
+
*/
|
|
38
|
+
admit(serverId: string, channelId: string): number | null {
|
|
39
|
+
// logValue never prints a NUL, so the key's last NUL keeps the two apart
|
|
40
|
+
// whatever a server id holds.
|
|
41
|
+
const key = `${serverId}\u0000${logValue(channelId)}`;
|
|
42
|
+
const now = this.now();
|
|
43
|
+
const record = this.records.get(key);
|
|
44
|
+
if (record) this.records.delete(key);
|
|
45
|
+
else if (this.records.size >= MAX_CHANNELS) this.records.delete(this.records.keys().next().value!);
|
|
46
|
+
if (!record || now - record.at >= EVERY_MS) {
|
|
47
|
+
this.records.set(key, { at: now, since: 0 });
|
|
48
|
+
return record?.since ?? 0;
|
|
49
|
+
}
|
|
50
|
+
record.since++;
|
|
51
|
+
this.records.set(key, record);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A channel's line prints at most this often. */
|
|
57
|
+
const EVERY_MS = 60_000;
|
|
58
|
+
/** The channels the records cover at once. */
|
|
59
|
+
const MAX_CHANNELS = 1_000;
|