@animalabs/agent-framework 0.22.1 → 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 +1 -1
- package/dist/src/agent.d.ts +192 -19
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +404 -65
- 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 +50 -26
- 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 +393 -113
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1748 -582
- 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 +2 -2
- 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/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.map +1 -1
- package/dist/src/mcpl/reference-fetcher.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +2 -2
- package/dist/src/mcpl/references.d.ts.map +1 -1
- 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 +18 -2
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +66 -11
- 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.map +1 -1
- 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 +39 -12
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +43 -8
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +264 -39
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +17 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +57 -14
- 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.map +1 -1
- package/dist/src/operator-log.js.map +1 -1
- package/dist/src/provider-waits.d.ts.map +1 -1
- package/dist/src/provider-waits.js.map +1 -1
- 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/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.map +1 -1
- 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/framework.d.ts +10 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +8 -4
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +61 -10
- 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 +104 -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 -42
- package/dist/test/explicit-prose-routing.test.js.map +1 -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 +8 -2
- 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 +81 -3
- package/dist/test/inference-exhaustion-rewind.test.js.map +1 -1
- package/dist/test/inference-failure-bounds.test.js +1 -0
- package/dist/test/inference-failure-bounds.test.js.map +1 -1
- package/dist/test/kv-unified-open-flight-retry.test.js.map +1 -1
- 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.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.map +1 -1
- 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.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.map +1 -1
- 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 +1 -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/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.js.map +1 -1
- 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.map +1 -1
- package/dist/test/provider-waits.test.js +67 -4
- package/dist/test/provider-waits.test.js.map +1 -1
- 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.js.map +1 -1
- package/dist/test/record-journal.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/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.js.map +1 -1
- package/dist/test/tool-batch-hook.test.js.map +1 -1
- package/dist/test/tool-result-guard.test.js +131 -9
- 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 +907 -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 +411 -66
- package/src/api/mcp-server-core.ts +681 -0
- package/src/api/mcp-server.ts +16 -685
- package/src/api/server.ts +51 -29
- package/src/api/types.ts +9 -1
- package/src/framework.ts +1899 -641
- package/src/index.ts +2 -2
- package/src/mcpl/channel-registry.ts +126 -31
- package/src/mcpl/feature-set-manager.ts +4 -3
- 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/refusal-lines.ts +59 -0
- package/src/mcpl/server-connection.ts +70 -11
- package/src/mcpl/tool-classes.ts +3 -2
- package/src/mcpl/transport.ts +105 -14
- package/src/mcpl/types.ts +5 -1
- package/src/modules/discord/index.ts +4 -2
- package/src/modules/workspace/disk-agreement.ts +74 -18
- package/src/modules/workspace/index.ts +41 -11
- package/src/modules/workspace/observe.ts +265 -39
- package/src/modules/workspace/reconcile.ts +71 -14
- package/src/modules/workspace/types.ts +26 -1
- package/src/recovery/offline-branch.ts +32 -28
- package/src/stored-removal.ts +126 -0
- package/src/types/agent.ts +42 -6
- package/src/types/framework.ts +10 -0
- package/src/types/module.ts +8 -4
- package/src/types/trace.ts +62 -11
package/src/mcpl/transport.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { spawn, type ChildProcess } from 'node:child_process';
|
|
20
20
|
import { createInterface, type Interface as ReadlineInterface } from 'node:readline';
|
|
21
21
|
import { EventEmitter } from 'node:events';
|
|
22
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
22
23
|
import WebSocket from 'ws';
|
|
23
24
|
|
|
24
25
|
import type { McplServerConfig } from './types.js';
|
|
@@ -29,6 +30,9 @@ export interface TransportCloseInfo {
|
|
|
29
30
|
code?: number | null;
|
|
30
31
|
signal?: NodeJS.Signals | null;
|
|
31
32
|
reason?: string;
|
|
33
|
+
/** True when `reason` is the peer's own text, a WebSocket server's close
|
|
34
|
+
* reason, rather than the transport's words. */
|
|
35
|
+
reasonFromPeer?: boolean;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
/**
|
|
@@ -71,6 +75,16 @@ export abstract class McplTransport extends EventEmitter {
|
|
|
71
75
|
return this._closed;
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
/**
|
|
79
|
+
* The last of what the server wrote to its diagnostic stream: at most its
|
|
80
|
+
* last {@link STDERR_TAIL_CHARS} UTF-16 code units, never opening inside a
|
|
81
|
+
* surrogate pair, after a '…' when earlier ones were dropped. A stdio
|
|
82
|
+
* child's stderr, and '' for a transport that has none.
|
|
83
|
+
*/
|
|
84
|
+
get stderrTail(): string {
|
|
85
|
+
return '';
|
|
86
|
+
}
|
|
87
|
+
|
|
74
88
|
/** Deliver an inbound line, buffering it if nobody is listening yet. */
|
|
75
89
|
protected pushLine(line: string): void {
|
|
76
90
|
if (this.listenerCount('line') > 0) this.emit('line', line);
|
|
@@ -167,8 +181,19 @@ export function buildChildEnv(
|
|
|
167
181
|
return merged;
|
|
168
182
|
}
|
|
169
183
|
|
|
184
|
+
/** The most of a stdio child's stderr a transport keeps for {@link McplTransport.stderrTail}. */
|
|
185
|
+
export const STDERR_TAIL_CHARS = 2_048;
|
|
186
|
+
|
|
170
187
|
export class StdioTransport extends McplTransport {
|
|
171
188
|
readonly kind = 'stdio' as const;
|
|
189
|
+
/** The first error on the child's stdin or stdout, named in the close reason. */
|
|
190
|
+
private pipeError: Error | undefined;
|
|
191
|
+
private stderrEnd = '';
|
|
192
|
+
private stderrCut = false;
|
|
193
|
+
|
|
194
|
+
override get stderrTail(): string {
|
|
195
|
+
return (this.stderrCut ? '…' : '') + this.stderrEnd;
|
|
196
|
+
}
|
|
172
197
|
|
|
173
198
|
private constructor(
|
|
174
199
|
private readonly child: ChildProcess,
|
|
@@ -178,26 +203,70 @@ export class StdioTransport extends McplTransport {
|
|
|
178
203
|
|
|
179
204
|
this.rl.on('line', (line: string) => this.pushLine(line));
|
|
180
205
|
|
|
181
|
-
// Stitch stderr chunks into whole lines (a chunk can split a line
|
|
206
|
+
// Stitch stderr chunks into whole lines (a chunk can split a line, and a
|
|
207
|
+
// character's bytes), and keep the stream's last characters for
|
|
208
|
+
// stderrTail.
|
|
182
209
|
let carry = '';
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
210
|
+
const decoder = new StringDecoder('utf8');
|
|
211
|
+
const received = (text: string) => {
|
|
212
|
+
const end = this.stderrEnd + text;
|
|
213
|
+
this.stderrCut ||= end.length > STDERR_TAIL_CHARS;
|
|
214
|
+
let kept = end.slice(-STDERR_TAIL_CHARS);
|
|
215
|
+
// A cut between a surrogate pair's halves drops the low half too.
|
|
216
|
+
if (/^[\uDC00-\uDFFF]/.test(kept) && end.length > STDERR_TAIL_CHARS) kept = kept.slice(1);
|
|
217
|
+
this.stderrEnd = kept;
|
|
218
|
+
const lines = (carry + text).split('\n');
|
|
186
219
|
carry = lines.pop() ?? '';
|
|
187
220
|
for (const l of lines) if (l.length > 0) this.pushStderr(l);
|
|
221
|
+
};
|
|
222
|
+
this.child.stderr?.on('data', (chunk: Buffer) => received(decoder.write(chunk)));
|
|
223
|
+
// A last line with no newline is complete once the stream ends.
|
|
224
|
+
this.child.stderr?.on('end', () => {
|
|
225
|
+
received(decoder.end());
|
|
226
|
+
if (carry.length > 0) this.pushStderr(carry);
|
|
188
227
|
});
|
|
189
228
|
|
|
190
229
|
this.child.on('error', (err) => this.emit('error', err));
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
230
|
+
// The child's stdin and stdout carry the protocol, so after an error on
|
|
231
|
+
// either, a write's EPIPE or a read error, this transport can't reach the
|
|
232
|
+
// child even if it's still running. The handler sends a running child
|
|
233
|
+
// SIGTERM, and the exit's close names the error, so the connection handles
|
|
234
|
+
// it as any other exit. The close is the report: no 'error' is emitted,
|
|
235
|
+
// nothing is thrown. Nothing here escalates to SIGKILL, so a child that
|
|
236
|
+
// ignores SIGTERM keeps running, unreported. A child that stops reading
|
|
237
|
+
// without closing its stdin raises no error: the writes are buffered, and
|
|
238
|
+
// only request timeouts bound it (McplServerConfig.requestTimeoutMs).
|
|
239
|
+
const endOnPipeError = (err: Error) => {
|
|
240
|
+
this.pipeError ??= err;
|
|
241
|
+
if (this.child.exitCode === null && this.child.signalCode === null) this.child.kill();
|
|
242
|
+
};
|
|
243
|
+
this.child.stdin?.on('error', endOnPipeError);
|
|
244
|
+
// On the stream itself, since close() closes readline, which takes its own
|
|
245
|
+
// listener off the stream. While readline is open, its listener, which
|
|
246
|
+
// came first, re-emits the error on the interface, where an empty
|
|
247
|
+
// listener absorbs that copy.
|
|
248
|
+
this.child.stdout?.on('error', endOnPipeError);
|
|
249
|
+
this.rl.on('error', () => { /* readline's copy of a stdout error, handled above */ });
|
|
250
|
+
// stderr carries diagnostics only: a read error on it is reported if
|
|
251
|
+
// anyone listens and never thrown. It ends nothing once the connection
|
|
252
|
+
// is wired; during a handshake the listener is the handshake's, which
|
|
253
|
+
// fails it.
|
|
254
|
+
this.child.stderr?.on('error', (err) => {
|
|
255
|
+
if (this.listenerCount('error') > 0) this.emit('error', err);
|
|
256
|
+
});
|
|
257
|
+
this.child.on('exit', (code, signal) => this.markClosed({
|
|
258
|
+
code,
|
|
259
|
+
signal,
|
|
260
|
+
reason: this.pipeError ? `child process exited, and its pipe failed with ${this.pipeError.message}` : 'child process exited',
|
|
261
|
+
}));
|
|
194
262
|
}
|
|
195
263
|
|
|
196
264
|
/**
|
|
197
|
-
* Spawn `config.command` and
|
|
198
|
-
* command is missing (a config bug) or the
|
|
265
|
+
* Spawn `config.command` and resolve with a ready stdio transport once the
|
|
266
|
+
* child has spawned. Rejects if the command is missing (a config bug) or the
|
|
267
|
+
* process fails to spawn.
|
|
199
268
|
*/
|
|
200
|
-
static spawn(config: McplServerConfig): StdioTransport {
|
|
269
|
+
static async spawn(config: McplServerConfig): Promise<StdioTransport> {
|
|
201
270
|
if (!config.command) {
|
|
202
271
|
throw new Error(
|
|
203
272
|
`MCPL server "${config.id}": stdio transport requires "command" (none provided)`,
|
|
@@ -207,6 +276,22 @@ export class StdioTransport extends McplTransport {
|
|
|
207
276
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
208
277
|
env: buildChildEnv(config),
|
|
209
278
|
});
|
|
279
|
+
// Node reports a failed spawn (a command that doesn't exist, say) as an
|
|
280
|
+
// 'error' on a tick after spawn() returns, and no 'exit' follows. It is
|
|
281
|
+
// settled here, where it is listened for. A caller that listens only once
|
|
282
|
+
// this resolves, as the handshake does, would be too late when reached
|
|
283
|
+
// from a timer callback, as a reconnect is: Node runs that tick before the
|
|
284
|
+
// caller's promise continuation, and throws an 'error' nothing listens for.
|
|
285
|
+
await new Promise<void>((resolve, reject) => {
|
|
286
|
+
const failed = (err: Error) => {
|
|
287
|
+
reject(new Error(`MCPL server "${config.id}" could not be started: ${err.message}`, { cause: err }));
|
|
288
|
+
};
|
|
289
|
+
child.once('error', failed);
|
|
290
|
+
child.once('spawn', () => {
|
|
291
|
+
child.off('error', failed);
|
|
292
|
+
resolve();
|
|
293
|
+
});
|
|
294
|
+
});
|
|
210
295
|
const rl = createInterface({ input: child.stdout! });
|
|
211
296
|
return new StdioTransport(child, rl);
|
|
212
297
|
}
|
|
@@ -223,7 +308,12 @@ export class StdioTransport extends McplTransport {
|
|
|
223
308
|
this.child.kill();
|
|
224
309
|
}
|
|
225
310
|
await new Promise<void>((resolve) => {
|
|
226
|
-
|
|
311
|
+
// Once Node has seen a child exit, it has its exitCode set, or its
|
|
312
|
+
// signalCode when a signal ended it, and its 'exit' event has fired. A
|
|
313
|
+
// signal that didn't come through kill() (an OOM kill, a crash) leaves
|
|
314
|
+
// `killed` false. A child that has exited but isn't reaped yet still
|
|
315
|
+
// takes the kill() above, which sets `killed`.
|
|
316
|
+
if (!this.child || this.child.exitCode !== null || this.child.signalCode !== null || this.child.killed) {
|
|
227
317
|
resolve();
|
|
228
318
|
} else {
|
|
229
319
|
this.child.once('exit', () => resolve());
|
|
@@ -256,9 +346,10 @@ export class WebSocketTransport extends McplTransport {
|
|
|
256
346
|
});
|
|
257
347
|
|
|
258
348
|
this.ws.on('error', (err: Error) => this.emit('error', err));
|
|
259
|
-
this.ws.on('close', (code: number, reason: Buffer) =>
|
|
260
|
-
|
|
261
|
-
|
|
349
|
+
this.ws.on('close', (code: number, reason: Buffer) => {
|
|
350
|
+
const text = reason?.toString('utf8');
|
|
351
|
+
this.markClosed(text ? { code, reason: text, reasonFromPeer: true } : { code, reason: 'websocket closed' });
|
|
352
|
+
});
|
|
262
353
|
}
|
|
263
354
|
|
|
264
355
|
/**
|
package/src/mcpl/types.ts
CHANGED
|
@@ -374,7 +374,11 @@ export interface McplServerConfig {
|
|
|
374
374
|
* accepts a request and never responds would otherwise freeze the awaiting
|
|
375
375
|
* agent turn forever; with the timeout the pending request rejects with a
|
|
376
376
|
* descriptive error, which the framework surfaces as a normal isError
|
|
377
|
-
* tool_result.
|
|
377
|
+
* tool_result. Request timeouts are the only bound for a stdio server that
|
|
378
|
+
* stops reading its stdin without closing it: the host's writes are
|
|
379
|
+
* buffered, no error is raised, and the connection stays up. This is the
|
|
380
|
+
* timeout of every request that names none; the handshake's initialize has
|
|
381
|
+
* its own. Set 0 to disable. Default: 60000 (60 seconds).
|
|
378
382
|
*/
|
|
379
383
|
requestTimeoutMs?: number;
|
|
380
384
|
|
|
@@ -67,9 +67,11 @@ const TYPING_REFRESH_MS = 8_000;
|
|
|
67
67
|
// stuck on forever.
|
|
68
68
|
const TYPING_MAX_MS = 5 * 60_000;
|
|
69
69
|
|
|
70
|
-
// Trace event types that
|
|
71
|
-
// typing keepalive.
|
|
70
|
+
// Trace event types that end an agent's turn (onTrace's contract) — any of
|
|
71
|
+
// these stops the typing keepalive. A turn's tool-call cycles stay inside its
|
|
72
|
+
// stream, so they come after them. inference:completed is the normal end.
|
|
72
73
|
const TURN_TERMINAL_TRACES = new Set([
|
|
74
|
+
'inference:completed',
|
|
73
75
|
'inference:turn_ended',
|
|
74
76
|
'inference:aborted',
|
|
75
77
|
'inference:failed',
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
* that isn't that directory any more (an unmounted drive whose mountpoint is
|
|
16
16
|
* now empty, a replaced directory) is an unavailable mount, never a disk on
|
|
17
17
|
* which every file was deleted; only an explicit acceptance takes it, and
|
|
18
|
-
* that starts its evidence afresh.
|
|
18
|
+
* that starts its evidence afresh. A place, where a walk stopped (`walks`),
|
|
19
|
+
* binds no root: it only says where the next walk starts, in whatever
|
|
20
|
+
* directory the root is by then.
|
|
19
21
|
*
|
|
20
22
|
* Every transition is write-ahead (see reconcile.ts): an `intent` entry
|
|
21
23
|
* (`pending`, with the prior value and the expected one) is durable before the
|
|
@@ -92,16 +94,30 @@ type JournalEntry =
|
|
|
92
94
|
/** The root's identity; with `reset`, an accepted new root, whose evidence starts empty. */
|
|
93
95
|
| { t: 'root'; mount: string; identity: RootIdentity; reset?: true }
|
|
94
96
|
| { t: 'set'; mount: string; path: string; value: Physical }
|
|
95
|
-
| { t: 'forget'; mount: string; path: string }
|
|
97
|
+
| { t: 'forget'; mount: string; path: string }
|
|
98
|
+
/**
|
|
99
|
+
* Where a capped walk of `scope` stopped (the last entry it took), or null
|
|
100
|
+
* once a walk covered the scope. It carries no `path` field: agent-framework
|
|
101
|
+
* 0.22 replays an entry it doesn't know as a `forget` of `entry.path`, and
|
|
102
|
+
* so leaves the evidence alone when there is none.
|
|
103
|
+
*/
|
|
104
|
+
| { t: 'walk'; mount: string; scope: string; after: string | null };
|
|
96
105
|
|
|
97
106
|
interface MountEvidence {
|
|
98
107
|
root: string;
|
|
99
108
|
identity?: RootIdentity;
|
|
100
109
|
paths: Map<string, Physical>;
|
|
110
|
+
/** By scope ('' for the mount root): where the next walk of it resumes. */
|
|
111
|
+
walks: Map<string, string>;
|
|
101
112
|
}
|
|
102
113
|
|
|
103
114
|
interface Snapshot {
|
|
104
|
-
mounts: Record<string, {
|
|
115
|
+
mounts: Record<string, {
|
|
116
|
+
root: string;
|
|
117
|
+
identity?: RootIdentity;
|
|
118
|
+
paths: Record<string, Physical>;
|
|
119
|
+
walks?: Record<string, string>;
|
|
120
|
+
}>;
|
|
105
121
|
}
|
|
106
122
|
|
|
107
123
|
export const DISK_AGREEMENT_RECORD_TYPE = 'workspace/disk-agreement';
|
|
@@ -123,9 +139,10 @@ export class DiskAgreement {
|
|
|
123
139
|
private readonly mounts = new Map<string, MountEvidence>();
|
|
124
140
|
private configured: Array<{ name: string; root: string }> = [];
|
|
125
141
|
/**
|
|
126
|
-
* An append (or a rebuild) not yet made durable. Cleared only
|
|
127
|
-
* barrier that succeeds, so a failed barrier stays owed: a later pass
|
|
128
|
-
* nothing new to record still settles it before deciding anything.
|
|
142
|
+
* An append of evidence (or a rebuild) not yet made durable. Cleared only
|
|
143
|
+
* by a barrier that succeeds, so a failed barrier stays owed: a later pass
|
|
144
|
+
* with nothing new to record still settles it before deciding anything. A
|
|
145
|
+
* place owes none (recordWalk).
|
|
129
146
|
*/
|
|
130
147
|
private unsynced = false;
|
|
131
148
|
|
|
@@ -183,19 +200,19 @@ export class DiskAgreement {
|
|
|
183
200
|
root: mount.root,
|
|
184
201
|
...(mount.identity ? { identity: mount.identity } : {}),
|
|
185
202
|
paths: new Map(Object.entries(mount.paths)),
|
|
203
|
+
walks: new Map(Object.entries(mount.walks ?? {})),
|
|
186
204
|
});
|
|
187
205
|
}
|
|
188
206
|
for (const { entry } of entries) {
|
|
189
207
|
if (entry.t === 'mount') {
|
|
190
208
|
const existing = replayed.get(entry.mount);
|
|
191
|
-
if (!existing || !sameRootPath(existing.root, entry.root))
|
|
209
|
+
if (!existing || !sameRootPath(existing.root, entry.root)) {
|
|
210
|
+
replayed.set(entry.mount, { root: entry.root, paths: new Map(), walks: new Map() });
|
|
211
|
+
}
|
|
192
212
|
continue;
|
|
193
213
|
}
|
|
194
214
|
const mount = replayed.get(entry.mount);
|
|
195
|
-
if (
|
|
196
|
-
if (entry.t === 'root') applyRoot(mount, entry);
|
|
197
|
-
else if (entry.t === 'set') mount.paths.set(entry.path, entry.value);
|
|
198
|
-
else mount.paths.delete(entry.path);
|
|
215
|
+
if (mount) apply(mount, entry);
|
|
199
216
|
}
|
|
200
217
|
this.mounts.clear();
|
|
201
218
|
for (const { name, root } of this.configured) {
|
|
@@ -203,7 +220,7 @@ export class DiskAgreement {
|
|
|
203
220
|
if (found && sameRootPath(found.root, root)) {
|
|
204
221
|
this.mounts.set(name, found);
|
|
205
222
|
} else {
|
|
206
|
-
this.mounts.set(name, { root, paths: new Map() });
|
|
223
|
+
this.mounts.set(name, { root, paths: new Map(), walks: new Map() });
|
|
207
224
|
this.journal.append({ t: 'mount', mount: name, root });
|
|
208
225
|
}
|
|
209
226
|
}
|
|
@@ -250,6 +267,29 @@ export class DiskAgreement {
|
|
|
250
267
|
return out;
|
|
251
268
|
}
|
|
252
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Where the next walk of `scope` ('' for the mount root) resumes: the last
|
|
272
|
+
* entry a walk of it took before its cap stopped it, if one did.
|
|
273
|
+
*/
|
|
274
|
+
walkResumesAfter(mount: string, scope: string): string | undefined {
|
|
275
|
+
this.ensureReconciled();
|
|
276
|
+
return this.mounts.get(mount)?.walks.get(scope);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Record where a walk of `scope` stopped (`after`), or that one covered it
|
|
281
|
+
* (null). Kept with the evidence so a restart resumes there too. A place
|
|
282
|
+
* that's lost costs coverage, never a decision: an append that didn't land
|
|
283
|
+
* loses one, and a checkpoint written by a version that doesn't keep them
|
|
284
|
+
* (0.22) loses them all, and each scope's walks start at its top again.
|
|
285
|
+
* So recording one owes no barrier: a pass that moves only its place makes
|
|
286
|
+
* no store sync, and the place becomes durable with the next one.
|
|
287
|
+
*/
|
|
288
|
+
recordWalk(mount: string, scope: string, after: string | null): void {
|
|
289
|
+
if ((this.walkResumesAfter(mount, scope) ?? null) === after) return;
|
|
290
|
+
this.write({ t: 'walk', mount, scope, after }, {});
|
|
291
|
+
}
|
|
292
|
+
|
|
253
293
|
/**
|
|
254
294
|
* Record an intent. `durable` makes it (and every append before it) stable
|
|
255
295
|
* before the caller performs the effect; batches pass it on their last one.
|
|
@@ -284,14 +324,13 @@ export class DiskAgreement {
|
|
|
284
324
|
|
|
285
325
|
private write(entry: JournalEntry, opts: { durable?: boolean; afterCommittedState?: boolean }): void {
|
|
286
326
|
this.ensureReconciled();
|
|
287
|
-
|
|
327
|
+
// Evidence is owed a barrier until proven durable: a failed append may
|
|
328
|
+
// still have landed. A place isn't (recordWalk).
|
|
329
|
+
if (entry.t !== 'walk') this.unsynced = true;
|
|
288
330
|
this.journal.append(entry, opts);
|
|
289
331
|
if (opts.durable) this.unsynced = false; // synced after this append, and so everything before it
|
|
290
332
|
const mount = this.mounts.get((entry as { mount: string }).mount);
|
|
291
|
-
if (
|
|
292
|
-
if (entry.t === 'root') applyRoot(mount, entry);
|
|
293
|
-
else if (entry.t === 'set') mount.paths.set(entry.path, entry.value);
|
|
294
|
-
else if (entry.t === 'forget') mount.paths.delete(entry.path);
|
|
333
|
+
if (mount) apply(mount, entry);
|
|
295
334
|
}
|
|
296
335
|
|
|
297
336
|
/** Checkpoint when the tail has outgrown the live evidence. */
|
|
@@ -307,6 +346,7 @@ export class DiskAgreement {
|
|
|
307
346
|
root: mount.root,
|
|
308
347
|
...(mount.identity ? { identity: mount.identity } : {}),
|
|
309
348
|
paths: Object.fromEntries(mount.paths),
|
|
349
|
+
...(mount.walks.size > 0 ? { walks: Object.fromEntries(mount.walks) } : {}),
|
|
310
350
|
};
|
|
311
351
|
}
|
|
312
352
|
this.journal.checkpoint(snapshot);
|
|
@@ -319,7 +359,23 @@ export class DiskAgreement {
|
|
|
319
359
|
}
|
|
320
360
|
}
|
|
321
361
|
|
|
362
|
+
/** Apply one entry to a mount's evidence; one of a kind this version doesn't know changes nothing. */
|
|
363
|
+
function apply(mount: MountEvidence, entry: JournalEntry): void {
|
|
364
|
+
if (entry.t === 'root') applyRoot(mount, entry);
|
|
365
|
+
else if (entry.t === 'set') mount.paths.set(entry.path, entry.value);
|
|
366
|
+
else if (entry.t === 'forget') mount.paths.delete(entry.path);
|
|
367
|
+
else if (entry.t === 'walk') {
|
|
368
|
+
if (entry.after === null) mount.walks.delete(entry.scope);
|
|
369
|
+
else mount.walks.set(entry.scope, entry.after);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
322
373
|
function applyRoot(mount: MountEvidence, entry: Extract<JournalEntry, { t: 'root' }>): void {
|
|
323
374
|
mount.identity = entry.identity;
|
|
324
|
-
if (entry.reset)
|
|
375
|
+
if (entry.reset) {
|
|
376
|
+
// Evidence about another directory says nothing about this one, and the
|
|
377
|
+
// accepted root's walks start at its top.
|
|
378
|
+
mount.paths.clear();
|
|
379
|
+
mount.walks.clear();
|
|
380
|
+
}
|
|
325
381
|
}
|
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
type PushResult,
|
|
51
51
|
type Scope,
|
|
52
52
|
type TreeOp,
|
|
53
|
+
WALK_CAP,
|
|
53
54
|
pushPaths,
|
|
54
55
|
reconcilePass,
|
|
55
56
|
settleAdoptionBeforeMutation,
|
|
@@ -651,6 +652,16 @@ export class WorkspaceModule implements Module {
|
|
|
651
652
|
private savedState: WorkspaceModuleState | null = null;
|
|
652
653
|
|
|
653
654
|
constructor(config: WorkspaceConfig) {
|
|
655
|
+
for (const mount of config.mounts) {
|
|
656
|
+
const cap: unknown = mount.walkCap;
|
|
657
|
+
if (cap !== undefined && !(Number.isSafeInteger(cap) && (cap as number) > 0)) {
|
|
658
|
+
// Numbers as JavaScript writes them (JSON would say null for NaN), anything else as JSON.
|
|
659
|
+
const shown = typeof cap === 'number' ? String(cap) : typeof cap === 'bigint' ? `${cap}n` : JSON.stringify(cap) ?? String(cap);
|
|
660
|
+
throw new Error(
|
|
661
|
+
`[workspace] Mount "${mount.name}": walkCap must be a positive integer no larger than ${Number.MAX_SAFE_INTEGER}, not ${shown}`,
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
654
665
|
// Detect overlapping mount paths: if mount A contains mount B,
|
|
655
666
|
// auto-add an ignore rule on A for B's path to prevent syncing
|
|
656
667
|
// the sub-mount's directory through the super-mount.
|
|
@@ -779,6 +790,8 @@ export class WorkspaceModule implements Module {
|
|
|
779
790
|
treeStateId: mount.treeStateId,
|
|
780
791
|
intents: this.intents.get(mount.config.name)!,
|
|
781
792
|
readOnly: mount.config.mode === 'read-only',
|
|
793
|
+
walkCap: mount.config.walkCap ?? WALK_CAP,
|
|
794
|
+
catchUpScan: (mount.config.watch ?? 'always') === 'always',
|
|
782
795
|
};
|
|
783
796
|
}
|
|
784
797
|
|
|
@@ -982,9 +995,12 @@ export class WorkspaceModule implements Module {
|
|
|
982
995
|
this.watchers.set(name, watcher);
|
|
983
996
|
|
|
984
997
|
// Chokidar is started with ignoreInitial:true, so changes made while
|
|
985
|
-
// the session was down would be invisible.
|
|
986
|
-
// under the three-way rule: only real differences produce tree
|
|
987
|
-
// changes and events.
|
|
998
|
+
// the session was down would be invisible. A catch-up pass takes them
|
|
999
|
+
// in under the three-way rule: only real differences produce tree
|
|
1000
|
+
// changes and events. It takes its turn with the mount's bare syncs,
|
|
1001
|
+
// so on a mount over its walkCap it covers the walkCap entries after
|
|
1002
|
+
// where the last turn stopped, and a change elsewhere waits for the
|
|
1003
|
+
// rotation to reach it, or for a sync of its path.
|
|
988
1004
|
void this.initialScan(name);
|
|
989
1005
|
}
|
|
990
1006
|
|
|
@@ -1004,7 +1020,7 @@ export class WorkspaceModule implements Module {
|
|
|
1004
1020
|
try {
|
|
1005
1021
|
// A full pass under the three-way rule: deletions on disk are confirmed
|
|
1006
1022
|
// where nothing newer is pending, and nothing unvisited is touched.
|
|
1007
|
-
await this.withMount(mount, () => this.passUnlocked(mount, { kind: 'dir', dir: '', recursive: true }));
|
|
1023
|
+
await this.withMount(mount, () => this.passUnlocked(mount, { kind: 'dir', dir: '', recursive: true }, { resume: true }));
|
|
1008
1024
|
} catch (err) {
|
|
1009
1025
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1010
1026
|
this.ctx?.pushEvent({
|
|
@@ -1211,7 +1227,12 @@ export class WorkspaceModule implements Module {
|
|
|
1211
1227
|
{
|
|
1212
1228
|
name: 'sync',
|
|
1213
1229
|
description: 'Bring disk changes into the workspace. Without a path, every mount (or the given one) is '
|
|
1214
|
-
+ 'rechecked
|
|
1230
|
+
+ 'rechecked and nothing pending in the workspace is discarded. A sync takes up to the mount\'s walk '
|
|
1231
|
+
+ 'cap of entries, files, links and the like (5000 unless configured). Past it, what this one didn\'t reach is listed under '
|
|
1232
|
+
+ '`incomplete`, and the walk takes turns: the next sync of the same mount or directory picks up where '
|
|
1233
|
+
+ 'the last turn stopped, so repeated syncs cover it all. On a mount watched continuously, its own catch-up '
|
|
1234
|
+
+ 'scan, at start and when its watcher reattaches, takes turns with its whole-mount syncs. With a '
|
|
1235
|
+
+ 'path (file or directory), '
|
|
1215
1236
|
+ 'the workspace takes disk\'s state there explicitly: it restores a workspace-deleted file, resolves '
|
|
1216
1237
|
+ 'conflicts toward disk, replaces a workspace draft, and removes a workspace file disk does not have. '
|
|
1217
1238
|
+ 'Each workspace change it gives up is listed under `discarded`, with the state it had and, as `op`, '
|
|
@@ -2851,7 +2872,8 @@ export class WorkspaceModule implements Module {
|
|
|
2851
2872
|
if (input.acceptRoot && await this.acceptRootUnlocked(mount)) rootsAccepted.push(name);
|
|
2852
2873
|
if (!relativePath) {
|
|
2853
2874
|
// A full recheck: every file rehashed, nothing pending discarded.
|
|
2854
|
-
|
|
2875
|
+
// Past the mount's walkCap, it resumes where the last one stopped.
|
|
2876
|
+
return this.passUnlocked(mount, { kind: 'dir', dir: '', recursive: true }, { rehash: true, resume: true });
|
|
2855
2877
|
}
|
|
2856
2878
|
// An explicit path: the workspace takes disk's state there.
|
|
2857
2879
|
const prefix = relativePath + '/';
|
|
@@ -2864,7 +2886,7 @@ export class WorkspaceModule implements Module {
|
|
|
2864
2886
|
// beneath it (a directory scope's walk; a path scope doesn't walk).
|
|
2865
2887
|
const opts: PassOptions = { rehash: true, adopt: () => true, branchId: askedOn, named: true };
|
|
2866
2888
|
if (!isDir) return this.passUnlocked(mount, { kind: 'paths', paths: [relativePath] }, opts);
|
|
2867
|
-
const beneath = await this.passUnlocked(mount, { kind: 'dir', dir: relativePath, recursive: true }, opts);
|
|
2889
|
+
const beneath = await this.passUnlocked(mount, { kind: 'dir', dir: relativePath, recursive: true }, { ...opts, resume: true });
|
|
2868
2890
|
// Something at the path itself is no part of a directory scope: a file
|
|
2869
2891
|
// disk has where the workspace holds a directory, or a workspace file
|
|
2870
2892
|
// where disk has a directory. Its own pass takes disk's state there
|
|
@@ -2988,10 +3010,14 @@ export class WorkspaceModule implements Module {
|
|
|
2988
3010
|
* After an agent's completed tool batch, before anything continues its turn:
|
|
2989
3011
|
* scan every `watch: 'on-agent-action'` mount, so what the batch's tools did
|
|
2990
3012
|
* on disk (a shell command's files, a deletion) is in the workspace when the
|
|
2991
|
-
* agent next looks.
|
|
2992
|
-
*
|
|
2993
|
-
*
|
|
2994
|
-
*
|
|
3013
|
+
* agent next looks. On a mount over its walkCap, that holds for the first
|
|
3014
|
+
* walkCap entries in the walk's order, which every batch's scan takes; files
|
|
3015
|
+
* past them come in through syncs and the listings that reach them, while a
|
|
3016
|
+
* read takes in only a file the workspace doesn't hold yet. A scan that
|
|
3017
|
+
* outlasts the deadline stops holding the round: the miss is recorded on
|
|
3018
|
+
* the mount's status and pushed as a `workspace:agent-action-scan-incomplete`
|
|
3019
|
+
* event, and the scan continues, deciding on the branch selected when it
|
|
3020
|
+
* applies.
|
|
2995
3021
|
*/
|
|
2996
3022
|
async onToolBatchComplete(_agentName?: string): Promise<void> {
|
|
2997
3023
|
// Every agent's workspace is the store's current branch, so the agent
|
|
@@ -3057,6 +3083,10 @@ export class WorkspaceModule implements Module {
|
|
|
3057
3083
|
const scan: AgentActionScan = { late: false, settled: Promise.resolve() };
|
|
3058
3084
|
const pass = this.withMount(mount, () => {
|
|
3059
3085
|
if (mount.agentActionScanWaiting === scan) mount.agentActionScanWaiting = undefined;
|
|
3086
|
+
// From the mount's start, as a listing walks, keeping no place: on a
|
|
3087
|
+
// mount over its walkCap, every batch's scan takes the first walkCap
|
|
3088
|
+
// entries in the walk's order, and the place that syncs and catch-up
|
|
3089
|
+
// scans resume from stays theirs.
|
|
3060
3090
|
return this.passUnlocked(mount, { kind: 'dir', dir: '', recursive: true });
|
|
3061
3091
|
});
|
|
3062
3092
|
scan.settled = pass.then(
|