@animalabs/agent-framework 0.22.1 → 0.22.3
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 +4 -2
- package/dist/src/agent.d.ts +266 -28
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +539 -104
- 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 +55 -27
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/api/types.d.ts +10 -2
- package/dist/src/api/types.d.ts.map +1 -1
- package/dist/src/branch-names.d.ts +14 -0
- package/dist/src/branch-names.d.ts.map +1 -0
- package/dist/src/branch-names.js +22 -0
- package/dist/src/branch-names.js.map +1 -0
- package/dist/src/closest-names.d.ts +28 -0
- package/dist/src/closest-names.d.ts.map +1 -0
- package/dist/src/closest-names.js +98 -0
- package/dist/src/closest-names.js.map +1 -0
- package/dist/src/code-execution/py-runner.d.ts +16 -5
- package/dist/src/code-execution/py-runner.d.ts.map +1 -1
- package/dist/src/code-execution/py-runner.js +186 -74
- package/dist/src/code-execution/py-runner.js.map +1 -1
- package/dist/src/code-execution/runtime-py.d.ts.map +1 -1
- package/dist/src/code-execution/runtime-py.js +49 -3
- package/dist/src/code-execution/runtime-py.js.map +1 -1
- package/dist/src/code-execution/tool-definition.d.ts +12 -1
- package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
- package/dist/src/code-execution/tool-definition.js +21 -3
- package/dist/src/code-execution/tool-definition.js.map +1 -1
- package/dist/src/config-checks.d.ts +28 -0
- package/dist/src/config-checks.d.ts.map +1 -0
- package/dist/src/config-checks.js +52 -0
- package/dist/src/config-checks.js.map +1 -0
- package/dist/src/deadline-timer.d.ts +21 -0
- package/dist/src/deadline-timer.d.ts.map +1 -0
- package/dist/src/deadline-timer.js +43 -0
- package/dist/src/deadline-timer.js.map +1 -0
- package/dist/src/error-message.d.ts +30 -0
- package/dist/src/error-message.d.ts.map +1 -0
- package/dist/src/error-message.js +50 -0
- package/dist/src/error-message.js.map +1 -0
- package/dist/src/framework.d.ts +1212 -192
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +6686 -1835
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +8 -0
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +27 -3
- package/dist/src/gate/event-gate.js.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 +5 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -3
- 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 +170 -13
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +499 -258
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.d.ts +21 -1
- package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.js +91 -30
- package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts +19 -3
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +23 -8
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/convert-block.d.ts +20 -0
- package/dist/src/mcpl/convert-block.d.ts.map +1 -0
- package/dist/src/mcpl/convert-block.js +89 -0
- package/dist/src/mcpl/convert-block.js.map +1 -0
- package/dist/src/mcpl/errors.d.ts +13 -0
- package/dist/src/mcpl/errors.d.ts.map +1 -1
- package/dist/src/mcpl/errors.js +28 -0
- package/dist/src/mcpl/errors.js.map +1 -1
- package/dist/src/mcpl/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 +23 -0
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.js +83 -72
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +114 -0
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -0
- package/dist/src/mcpl/inbound-source.js +83 -0
- package/dist/src/mcpl/inbound-source.js.map +1 -0
- package/dist/src/mcpl/index.d.ts +3 -1
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js +3 -1
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/inference-router.d.ts +15 -11
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js +153 -84
- package/dist/src/mcpl/inference-router.js.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 +64 -7
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +121 -24
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts +38 -8
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +77 -78
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/reference-fetcher.d.ts +14 -2
- package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
- package/dist/src/mcpl/reference-fetcher.js +72 -30
- package/dist/src/mcpl/reference-fetcher.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +49 -14
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +74 -21
- 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 +107 -12
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +470 -139
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/server-registry.d.ts +10 -1
- package/dist/src/mcpl/server-registry.d.ts.map +1 -1
- package/dist/src/mcpl/server-registry.js +14 -3
- package/dist/src/mcpl/server-registry.js.map +1 -1
- package/dist/src/mcpl/skip-reply-wake.d.ts +36 -0
- package/dist/src/mcpl/skip-reply-wake.d.ts.map +1 -0
- package/dist/src/mcpl/skip-reply-wake.js +55 -0
- package/dist/src/mcpl/skip-reply-wake.js.map +1 -0
- package/dist/src/mcpl/tool-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 +74 -7
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +209 -22
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +43 -28
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/mcpl/visible-content.d.ts +15 -0
- package/dist/src/mcpl/visible-content.d.ts.map +1 -1
- package/dist/src/mcpl/visible-content.js +25 -0
- package/dist/src/mcpl/visible-content.js.map +1 -1
- package/dist/src/module-registry.d.ts +19 -1
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +54 -25
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/api/index.d.ts.map +1 -1
- package/dist/src/modules/api/index.js +1 -1
- package/dist/src/modules/api/index.js.map +1 -1
- package/dist/src/modules/discord/discord-js-client.d.ts +1 -0
- package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
- package/dist/src/modules/discord/discord-js-client.js +25 -9
- package/dist/src/modules/discord/discord-js-client.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts +29 -3
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +383 -145
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/discord/types.d.ts +14 -1
- package/dist/src/modules/discord/types.d.ts.map +1 -1
- package/dist/src/modules/health/index.d.ts.map +1 -1
- package/dist/src/modules/health/index.js +2 -1
- package/dist/src/modules/health/index.js.map +1 -1
- package/dist/src/modules/history/index.d.ts +3 -1
- package/dist/src/modules/history/index.d.ts.map +1 -1
- package/dist/src/modules/history/index.js +359 -127
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/history/semantic.d.ts +28 -1
- package/dist/src/modules/history/semantic.d.ts.map +1 -1
- package/dist/src/modules/history/semantic.js +177 -32
- package/dist/src/modules/history/semantic.js.map +1 -1
- package/dist/src/modules/unknown-tool.d.ts +10 -0
- package/dist/src/modules/unknown-tool.d.ts.map +1 -0
- package/dist/src/modules/unknown-tool.js +17 -0
- package/dist/src/modules/unknown-tool.js.map +1 -0
- package/dist/src/modules/workspace/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 +54 -6
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +93 -27
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/effects.d.ts +64 -6
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +291 -44
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +127 -27
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +821 -248
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +117 -15
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +436 -51
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +75 -8
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +576 -68
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +42 -3
- 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 +7 -2
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +28 -4
- 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/prose-drafts.d.ts +240 -0
- package/dist/src/prose-drafts.d.ts.map +1 -0
- package/dist/src/prose-drafts.js +362 -0
- package/dist/src/prose-drafts.js.map +1 -0
- package/dist/src/prose-segments.d.ts +21 -2
- package/dist/src/prose-segments.d.ts.map +1 -1
- package/dist/src/prose-segments.js +46 -9
- package/dist/src/prose-segments.js.map +1 -1
- package/dist/src/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 +1 -0
- package/dist/src/recovery/offline-branch.d.ts.map +1 -1
- package/dist/src/recovery/offline-branch.js +32 -25
- 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/safe-slice.d.ts +5 -1
- package/dist/src/safe-slice.d.ts.map +1 -1
- package/dist/src/safe-slice.js +9 -1
- package/dist/src/safe-slice.js.map +1 -1
- package/dist/src/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 +87 -19
- package/dist/src/tool-image-ledger.d.ts.map +1 -1
- package/dist/src/tool-image-ledger.js +135 -32
- package/dist/src/tool-image-ledger.js.map +1 -1
- package/dist/src/tool-presentation.d.ts +9 -0
- package/dist/src/tool-presentation.d.ts.map +1 -1
- package/dist/src/tool-presentation.js +257 -60
- package/dist/src/tool-presentation.js.map +1 -1
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +7 -10
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tool-result-history.d.ts +32 -3
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +106 -19
- package/dist/src/tool-result-history.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +25 -1
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +260 -65
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/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 +59 -11
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +17 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +31 -5
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/inference-log.d.ts +1 -1
- package/dist/src/types/inference-log.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +30 -7
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +151 -11
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/src/usage/usage-tracker.d.ts +5 -0
- package/dist/src/usage/usage-tracker.d.ts.map +1 -1
- package/dist/src/usage/usage-tracker.js +35 -8
- package/dist/src/usage/usage-tracker.js.map +1 -1
- package/package.json +10 -9
- package/src/agent.ts +572 -105
- package/src/api/mcp-server-core.ts +681 -0
- package/src/api/mcp-server.ts +16 -685
- package/src/api/server.ts +55 -30
- package/src/api/types.ts +10 -2
- package/src/branch-names.ts +19 -0
- package/src/closest-names.ts +94 -0
- package/src/code-execution/py-runner.ts +200 -76
- package/src/code-execution/runtime-py.ts +49 -3
- package/src/code-execution/tool-definition.ts +28 -4
- package/src/config-checks.ts +48 -0
- package/src/deadline-timer.ts +44 -0
- package/src/error-message.ts +48 -0
- package/src/framework.ts +7158 -1936
- package/src/gate/event-gate.ts +28 -3
- package/src/index.ts +12 -3
- package/src/mcpl/channel-registry.ts +615 -289
- package/src/mcpl/checkpoint-manager.ts +104 -29
- package/src/mcpl/conversation-router.ts +28 -8
- package/src/mcpl/convert-block.ts +97 -0
- package/src/mcpl/errors.ts +28 -0
- package/src/mcpl/feature-set-manager.ts +4 -3
- package/src/mcpl/hook-orchestrator.ts +84 -77
- package/src/mcpl/inbound-source.ts +156 -0
- package/src/mcpl/index.ts +10 -1
- package/src/mcpl/inference-router.ts +174 -99
- package/src/mcpl/log-value.ts +116 -0
- package/src/mcpl/prose-stream-router.ts +6 -6
- package/src/mcpl/push-coalescer.ts +124 -24
- package/src/mcpl/push-handler.ts +86 -84
- package/src/mcpl/reference-fetcher.ts +71 -26
- package/src/mcpl/references.ts +81 -22
- package/src/mcpl/refusal-lines.ts +59 -0
- package/src/mcpl/server-connection.ts +496 -142
- package/src/mcpl/server-registry.ts +15 -4
- package/src/mcpl/skip-reply-wake.ts +73 -0
- package/src/mcpl/tool-classes.ts +3 -2
- package/src/mcpl/transport.ts +219 -26
- package/src/mcpl/types.ts +18 -3
- package/src/mcpl/visible-content.ts +27 -0
- package/src/module-registry.ts +58 -23
- package/src/modules/api/index.ts +1 -1
- package/src/modules/discord/discord-js-client.ts +26 -10
- package/src/modules/discord/index.ts +395 -160
- package/src/modules/discord/types.ts +15 -2
- package/src/modules/health/index.ts +2 -1
- package/src/modules/history/index.ts +410 -120
- package/src/modules/history/semantic.ts +189 -32
- package/src/modules/unknown-tool.ts +16 -0
- package/src/modules/workspace/disk-agreement.ts +125 -21
- package/src/modules/workspace/effects.ts +279 -35
- package/src/modules/workspace/index.ts +841 -261
- package/src/modules/workspace/observe.ts +456 -56
- package/src/modules/workspace/reconcile.ts +688 -76
- package/src/modules/workspace/types.ts +41 -2
- package/src/modules/workspace/watcher.ts +27 -4
- package/src/prose-drafts.ts +481 -0
- package/src/prose-segments.ts +51 -8
- package/src/recovery/offline-branch.ts +36 -29
- package/src/safe-slice.ts +9 -1
- package/src/stored-removal.ts +126 -0
- package/src/tool-image-ledger.ts +153 -34
- package/src/tool-presentation.ts +224 -40
- package/src/tool-result-guard.ts +7 -8
- package/src/tool-result-history.ts +124 -20
- package/src/tune-out/coordinator.ts +259 -65
- package/src/types/agent.ts +59 -13
- package/src/types/events.ts +17 -0
- package/src/types/framework.ts +31 -5
- package/src/types/inference-log.ts +1 -1
- package/src/types/module.ts +31 -7
- package/src/types/trace.ts +158 -12
- package/src/usage/usage-tracker.ts +32 -8
- package/dist/test/activation-request-sanitize.test.d.ts +0 -2
- package/dist/test/activation-request-sanitize.test.d.ts.map +0 -1
- package/dist/test/activation-request-sanitize.test.js +0 -89
- package/dist/test/activation-request-sanitize.test.js.map +0 -1
- package/dist/test/agent-cache-ttl.test.d.ts +0 -2
- package/dist/test/agent-cache-ttl.test.d.ts.map +0 -1
- package/dist/test/agent-cache-ttl.test.js +0 -41
- package/dist/test/agent-cache-ttl.test.js.map +0 -1
- package/dist/test/agent-runtime-settings.test.d.ts +0 -2
- package/dist/test/agent-runtime-settings.test.d.ts.map +0 -1
- package/dist/test/agent-runtime-settings.test.js +0 -462
- package/dist/test/agent-runtime-settings.test.js.map +0 -1
- package/dist/test/api-admin-gate.test.d.ts +0 -2
- package/dist/test/api-admin-gate.test.d.ts.map +0 -1
- package/dist/test/api-admin-gate.test.js +0 -116
- package/dist/test/api-admin-gate.test.js.map +0 -1
- package/dist/test/assistant-persist-refusal.test.d.ts +0 -2
- package/dist/test/assistant-persist-refusal.test.d.ts.map +0 -1
- package/dist/test/assistant-persist-refusal.test.js +0 -63
- package/dist/test/assistant-persist-refusal.test.js.map +0 -1
- package/dist/test/batched-wake-locus.test.d.ts +0 -2
- package/dist/test/batched-wake-locus.test.d.ts.map +0 -1
- package/dist/test/batched-wake-locus.test.js +0 -166
- package/dist/test/batched-wake-locus.test.js.map +0 -1
- package/dist/test/busy-agent-scheduler.test.d.ts +0 -2
- package/dist/test/busy-agent-scheduler.test.d.ts.map +0 -1
- package/dist/test/busy-agent-scheduler.test.js +0 -92
- package/dist/test/busy-agent-scheduler.test.js.map +0 -1
- package/dist/test/channel-incoming-targeting.test.d.ts +0 -2
- package/dist/test/channel-incoming-targeting.test.d.ts.map +0 -1
- package/dist/test/channel-incoming-targeting.test.js +0 -81
- package/dist/test/channel-incoming-targeting.test.js.map +0 -1
- package/dist/test/channel-label-history.test.d.ts +0 -2
- package/dist/test/channel-label-history.test.d.ts.map +0 -1
- package/dist/test/channel-label-history.test.js +0 -424
- package/dist/test/channel-label-history.test.js.map +0 -1
- package/dist/test/channel-lifecycle-provenance.test.d.ts +0 -2
- package/dist/test/channel-lifecycle-provenance.test.d.ts.map +0 -1
- package/dist/test/channel-lifecycle-provenance.test.js +0 -47
- package/dist/test/channel-lifecycle-provenance.test.js.map +0 -1
- package/dist/test/channel-registry-routing.test.d.ts +0 -2
- package/dist/test/channel-registry-routing.test.d.ts.map +0 -1
- package/dist/test/channel-registry-routing.test.js +0 -446
- package/dist/test/channel-registry-routing.test.js.map +0 -1
- package/dist/test/channel-registry-typing.test.d.ts +0 -2
- package/dist/test/channel-registry-typing.test.d.ts.map +0 -1
- package/dist/test/channel-registry-typing.test.js +0 -91
- package/dist/test/channel-registry-typing.test.js.map +0 -1
- package/dist/test/channels-changed-respond-first.test.d.ts +0 -2
- package/dist/test/channels-changed-respond-first.test.d.ts.map +0 -1
- package/dist/test/channels-changed-respond-first.test.js +0 -74
- package/dist/test/channels-changed-respond-first.test.js.map +0 -1
- package/dist/test/checkpoint-attribution.test.d.ts +0 -2
- package/dist/test/checkpoint-attribution.test.d.ts.map +0 -1
- package/dist/test/checkpoint-attribution.test.js +0 -200
- package/dist/test/checkpoint-attribution.test.js.map +0 -1
- package/dist/test/code-execution.test.d.ts +0 -11
- package/dist/test/code-execution.test.d.ts.map +0 -1
- package/dist/test/code-execution.test.js +0 -1272
- package/dist/test/code-execution.test.js.map +0 -1
- package/dist/test/component-defaults.test.d.ts +0 -2
- package/dist/test/component-defaults.test.d.ts.map +0 -1
- package/dist/test/component-defaults.test.js +0 -54
- package/dist/test/component-defaults.test.js.map +0 -1
- package/dist/test/conversation-fork-injection-scoping.test.d.ts +0 -2
- package/dist/test/conversation-fork-injection-scoping.test.d.ts.map +0 -1
- package/dist/test/conversation-fork-injection-scoping.test.js +0 -200
- package/dist/test/conversation-fork-injection-scoping.test.js.map +0 -1
- package/dist/test/conversation-router.test.d.ts +0 -2
- package/dist/test/conversation-router.test.d.ts.map +0 -1
- package/dist/test/conversation-router.test.js +0 -174
- package/dist/test/conversation-router.test.js.map +0 -1
- package/dist/test/conversation-routing.test.d.ts +0 -7
- package/dist/test/conversation-routing.test.d.ts.map +0 -1
- package/dist/test/conversation-routing.test.js +0 -387
- package/dist/test/conversation-routing.test.js.map +0 -1
- package/dist/test/deferred-wake-integrity.test.d.ts +0 -29
- package/dist/test/deferred-wake-integrity.test.d.ts.map +0 -1
- package/dist/test/deferred-wake-integrity.test.js +0 -320
- package/dist/test/deferred-wake-integrity.test.js.map +0 -1
- package/dist/test/discord-awareness-outbox.test.d.ts +0 -2
- package/dist/test/discord-awareness-outbox.test.d.ts.map +0 -1
- package/dist/test/discord-awareness-outbox.test.js +0 -267
- package/dist/test/discord-awareness-outbox.test.js.map +0 -1
- package/dist/test/discord-history-delete-window.test.d.ts +0 -2
- package/dist/test/discord-history-delete-window.test.d.ts.map +0 -1
- package/dist/test/discord-history-delete-window.test.js +0 -81
- package/dist/test/discord-history-delete-window.test.js.map +0 -1
- package/dist/test/discord-history-sync.test.d.ts +0 -2
- package/dist/test/discord-history-sync.test.d.ts.map +0 -1
- package/dist/test/discord-history-sync.test.js +0 -56
- package/dist/test/discord-history-sync.test.js.map +0 -1
- package/dist/test/ephemeral-settle.test.d.ts +0 -2
- package/dist/test/ephemeral-settle.test.d.ts.map +0 -1
- package/dist/test/ephemeral-settle.test.js +0 -354
- package/dist/test/ephemeral-settle.test.js.map +0 -1
- package/dist/test/event-gate-passthrough.test.d.ts +0 -15
- package/dist/test/event-gate-passthrough.test.d.ts.map +0 -1
- package/dist/test/event-gate-passthrough.test.js +0 -289
- package/dist/test/event-gate-passthrough.test.js.map +0 -1
- package/dist/test/event-gate-policy-mutation.test.d.ts +0 -2
- package/dist/test/event-gate-policy-mutation.test.d.ts.map +0 -1
- package/dist/test/event-gate-policy-mutation.test.js +0 -137
- package/dist/test/event-gate-policy-mutation.test.js.map +0 -1
- package/dist/test/event-gate-rate-limit.test.d.ts +0 -2
- package/dist/test/event-gate-rate-limit.test.d.ts.map +0 -1
- package/dist/test/event-gate-rate-limit.test.js +0 -627
- package/dist/test/event-gate-rate-limit.test.js.map +0 -1
- package/dist/test/event-gate-script.test.d.ts +0 -2
- package/dist/test/event-gate-script.test.d.ts.map +0 -1
- package/dist/test/event-gate-script.test.js +0 -95
- package/dist/test/event-gate-script.test.js.map +0 -1
- package/dist/test/event-gate-selfwake.test.d.ts +0 -15
- package/dist/test/event-gate-selfwake.test.d.ts.map +0 -1
- package/dist/test/event-gate-selfwake.test.js +0 -125
- package/dist/test/event-gate-selfwake.test.js.map +0 -1
- package/dist/test/event-gate-tags.test.d.ts +0 -2
- package/dist/test/event-gate-tags.test.d.ts.map +0 -1
- package/dist/test/event-gate-tags.test.js +0 -76
- package/dist/test/event-gate-tags.test.js.map +0 -1
- package/dist/test/event-gate.test.d.ts +0 -2
- package/dist/test/event-gate.test.d.ts.map +0 -1
- package/dist/test/event-gate.test.js +0 -919
- package/dist/test/event-gate.test.js.map +0 -1
- package/dist/test/explicit-prose-routing.test.d.ts +0 -11
- package/dist/test/explicit-prose-routing.test.d.ts.map +0 -1
- package/dist/test/explicit-prose-routing.test.js +0 -1394
- package/dist/test/explicit-prose-routing.test.js.map +0 -1
- package/dist/test/framework.test.d.ts +0 -5
- package/dist/test/framework.test.d.ts.map +0 -1
- package/dist/test/framework.test.js +0 -1048
- package/dist/test/framework.test.js.map +0 -1
- package/dist/test/gather-context-timeout.test.d.ts +0 -2
- package/dist/test/gather-context-timeout.test.d.ts.map +0 -1
- package/dist/test/gather-context-timeout.test.js +0 -120
- package/dist/test/gather-context-timeout.test.js.map +0 -1
- package/dist/test/health-snapshot.test.d.ts +0 -2
- package/dist/test/health-snapshot.test.d.ts.map +0 -1
- package/dist/test/health-snapshot.test.js +0 -74
- package/dist/test/health-snapshot.test.js.map +0 -1
- package/dist/test/helpers/coalescing-fixture.d.ts +0 -72
- package/dist/test/helpers/coalescing-fixture.d.ts.map +0 -1
- package/dist/test/helpers/coalescing-fixture.js +0 -146
- package/dist/test/helpers/coalescing-fixture.js.map +0 -1
- package/dist/test/helpers/coalescing-kill-child.d.ts +0 -2
- package/dist/test/helpers/coalescing-kill-child.d.ts.map +0 -1
- package/dist/test/helpers/coalescing-kill-child.js +0 -24
- package/dist/test/helpers/coalescing-kill-child.js.map +0 -1
- package/dist/test/helpers/mock-membrane.d.ts +0 -53
- package/dist/test/helpers/mock-membrane.d.ts.map +0 -1
- package/dist/test/helpers/mock-membrane.js +0 -168
- package/dist/test/helpers/mock-membrane.js.map +0 -1
- package/dist/test/helpers/pytc-stop-rows.d.ts +0 -11
- package/dist/test/helpers/pytc-stop-rows.d.ts.map +0 -1
- package/dist/test/helpers/pytc-stop-rows.js +0 -143
- package/dist/test/helpers/pytc-stop-rows.js.map +0 -1
- package/dist/test/helpers/quiesce-crash-child.d.ts +0 -2
- package/dist/test/helpers/quiesce-crash-child.d.ts.map +0 -1
- package/dist/test/helpers/quiesce-crash-child.js +0 -156
- package/dist/test/helpers/quiesce-crash-child.js.map +0 -1
- package/dist/test/history-module-overview.test.d.ts +0 -2
- package/dist/test/history-module-overview.test.d.ts.map +0 -1
- package/dist/test/history-module-overview.test.js +0 -711
- package/dist/test/history-module-overview.test.js.map +0 -1
- package/dist/test/history-module-realstore.test.d.ts +0 -2
- package/dist/test/history-module-realstore.test.d.ts.map +0 -1
- package/dist/test/history-module-realstore.test.js +0 -363
- package/dist/test/history-module-realstore.test.js.map +0 -1
- package/dist/test/history-module-semantic.test.d.ts +0 -2
- package/dist/test/history-module-semantic.test.d.ts.map +0 -1
- package/dist/test/history-module-semantic.test.js +0 -213
- package/dist/test/history-module-semantic.test.js.map +0 -1
- package/dist/test/history-module-ux.test.d.ts +0 -2
- package/dist/test/history-module-ux.test.d.ts.map +0 -1
- package/dist/test/history-module-ux.test.js +0 -304
- package/dist/test/history-module-ux.test.js.map +0 -1
- package/dist/test/history-module.test.d.ts +0 -2
- package/dist/test/history-module.test.d.ts.map +0 -1
- package/dist/test/history-module.test.js +0 -619
- package/dist/test/history-module.test.js.map +0 -1
- package/dist/test/history-semantic-review-findings.test.d.ts +0 -2
- package/dist/test/history-semantic-review-findings.test.d.ts.map +0 -1
- package/dist/test/history-semantic-review-findings.test.js +0 -338
- package/dist/test/history-semantic-review-findings.test.js.map +0 -1
- package/dist/test/history-semantic-sync-converges.test.d.ts +0 -2
- package/dist/test/history-semantic-sync-converges.test.d.ts.map +0 -1
- package/dist/test/history-semantic-sync-converges.test.js +0 -109
- package/dist/test/history-semantic-sync-converges.test.js.map +0 -1
- package/dist/test/host-quiesce.test.d.ts +0 -2
- package/dist/test/host-quiesce.test.d.ts.map +0 -1
- package/dist/test/host-quiesce.test.js +0 -863
- package/dist/test/host-quiesce.test.js.map +0 -1
- package/dist/test/inference-exhaustion-rewind.test.d.ts +0 -2
- package/dist/test/inference-exhaustion-rewind.test.d.ts.map +0 -1
- package/dist/test/inference-exhaustion-rewind.test.js +0 -309
- package/dist/test/inference-exhaustion-rewind.test.js.map +0 -1
- package/dist/test/inference-failure-bounds.test.d.ts +0 -2
- package/dist/test/inference-failure-bounds.test.d.ts.map +0 -1
- package/dist/test/inference-failure-bounds.test.js +0 -1201
- package/dist/test/inference-failure-bounds.test.js.map +0 -1
- package/dist/test/inference-failure-observability.test.d.ts +0 -2
- package/dist/test/inference-failure-observability.test.d.ts.map +0 -1
- package/dist/test/inference-failure-observability.test.js +0 -84
- package/dist/test/inference-failure-observability.test.js.map +0 -1
- package/dist/test/journal-tool.test.d.ts +0 -2
- package/dist/test/journal-tool.test.d.ts.map +0 -1
- package/dist/test/journal-tool.test.js +0 -33
- package/dist/test/journal-tool.test.js.map +0 -1
- package/dist/test/kv-unified-open-flight-retry.test.d.ts +0 -2
- package/dist/test/kv-unified-open-flight-retry.test.d.ts.map +0 -1
- package/dist/test/kv-unified-open-flight-retry.test.js +0 -189
- package/dist/test/kv-unified-open-flight-retry.test.js.map +0 -1
- package/dist/test/kv-unified-wiring.test.d.ts +0 -2
- package/dist/test/kv-unified-wiring.test.d.ts.map +0 -1
- package/dist/test/kv-unified-wiring.test.js +0 -95
- package/dist/test/kv-unified-wiring.test.js.map +0 -1
- package/dist/test/late-events-after-stop.test.d.ts +0 -2
- package/dist/test/late-events-after-stop.test.d.ts.map +0 -1
- package/dist/test/late-events-after-stop.test.js +0 -122
- package/dist/test/late-events-after-stop.test.js.map +0 -1
- package/dist/test/liveness-watchdog.test.d.ts +0 -2
- package/dist/test/liveness-watchdog.test.d.ts.map +0 -1
- package/dist/test/liveness-watchdog.test.js +0 -94
- package/dist/test/liveness-watchdog.test.js.map +0 -1
- package/dist/test/machine-close-provenance.test.d.ts +0 -2
- package/dist/test/machine-close-provenance.test.d.ts.map +0 -1
- package/dist/test/machine-close-provenance.test.js +0 -116
- package/dist/test/machine-close-provenance.test.js.map +0 -1
- package/dist/test/maintenance-timer.test.d.ts +0 -2
- package/dist/test/maintenance-timer.test.d.ts.map +0 -1
- package/dist/test/maintenance-timer.test.js +0 -265
- package/dist/test/maintenance-timer.test.js.map +0 -1
- package/dist/test/mcpl-access-provider.test.d.ts +0 -2
- package/dist/test/mcpl-access-provider.test.d.ts.map +0 -1
- package/dist/test/mcpl-access-provider.test.js +0 -20
- package/dist/test/mcpl-access-provider.test.js.map +0 -1
- package/dist/test/mcpl-awareness-barrier.test.d.ts +0 -2
- package/dist/test/mcpl-awareness-barrier.test.d.ts.map +0 -1
- package/dist/test/mcpl-awareness-barrier.test.js +0 -769
- package/dist/test/mcpl-awareness-barrier.test.js.map +0 -1
- package/dist/test/mcpl-capability-grant.test.d.ts +0 -10
- package/dist/test/mcpl-capability-grant.test.d.ts.map +0 -1
- package/dist/test/mcpl-capability-grant.test.js +0 -111
- package/dist/test/mcpl-capability-grant.test.js.map +0 -1
- package/dist/test/mcpl-capability-scoping.test.d.ts +0 -2
- package/dist/test/mcpl-capability-scoping.test.d.ts.map +0 -1
- package/dist/test/mcpl-capability-scoping.test.js +0 -266
- package/dist/test/mcpl-capability-scoping.test.js.map +0 -1
- package/dist/test/mcpl-child-env.test.d.ts +0 -2
- package/dist/test/mcpl-child-env.test.d.ts.map +0 -1
- package/dist/test/mcpl-child-env.test.js +0 -158
- package/dist/test/mcpl-child-env.test.js.map +0 -1
- package/dist/test/mcpl-coalescing.test.d.ts +0 -2
- package/dist/test/mcpl-coalescing.test.d.ts.map +0 -1
- package/dist/test/mcpl-coalescing.test.js +0 -1125
- package/dist/test/mcpl-coalescing.test.js.map +0 -1
- package/dist/test/mcpl-config-diagnostics.test.d.ts +0 -2
- package/dist/test/mcpl-config-diagnostics.test.d.ts.map +0 -1
- package/dist/test/mcpl-config-diagnostics.test.js +0 -452
- package/dist/test/mcpl-config-diagnostics.test.js.map +0 -1
- package/dist/test/mcpl-empty-content.test.d.ts +0 -2
- package/dist/test/mcpl-empty-content.test.d.ts.map +0 -1
- package/dist/test/mcpl-empty-content.test.js +0 -368
- package/dist/test/mcpl-empty-content.test.js.map +0 -1
- package/dist/test/mcpl-gate-roundtrip.test.d.ts +0 -2
- package/dist/test/mcpl-gate-roundtrip.test.d.ts.map +0 -1
- package/dist/test/mcpl-gate-roundtrip.test.js +0 -285
- package/dist/test/mcpl-gate-roundtrip.test.js.map +0 -1
- package/dist/test/mcpl-list-status.test.d.ts +0 -2
- package/dist/test/mcpl-list-status.test.d.ts.map +0 -1
- package/dist/test/mcpl-list-status.test.js +0 -82
- package/dist/test/mcpl-list-status.test.js.map +0 -1
- package/dist/test/mcpl-manifest-changed.test.d.ts +0 -2
- package/dist/test/mcpl-manifest-changed.test.d.ts.map +0 -1
- package/dist/test/mcpl-manifest-changed.test.js +0 -178
- package/dist/test/mcpl-manifest-changed.test.js.map +0 -1
- package/dist/test/mcpl-push-convert.test.d.ts +0 -2
- package/dist/test/mcpl-push-convert.test.d.ts.map +0 -1
- package/dist/test/mcpl-push-convert.test.js +0 -42
- package/dist/test/mcpl-push-convert.test.js.map +0 -1
- package/dist/test/mcpl-reconnect-events.test.d.ts +0 -2
- package/dist/test/mcpl-reconnect-events.test.d.ts.map +0 -1
- package/dist/test/mcpl-reconnect-events.test.js +0 -144
- package/dist/test/mcpl-reconnect-events.test.js.map +0 -1
- package/dist/test/mcpl-reconnect-reregistration.test.d.ts +0 -2
- package/dist/test/mcpl-reconnect-reregistration.test.d.ts.map +0 -1
- package/dist/test/mcpl-reconnect-reregistration.test.js +0 -236
- package/dist/test/mcpl-reconnect-reregistration.test.js.map +0 -1
- package/dist/test/mcpl-reference-fetcher.test.d.ts +0 -2
- package/dist/test/mcpl-reference-fetcher.test.d.ts.map +0 -1
- package/dist/test/mcpl-reference-fetcher.test.js +0 -427
- package/dist/test/mcpl-reference-fetcher.test.js.map +0 -1
- package/dist/test/mcpl-reference-fields.test.d.ts +0 -2
- package/dist/test/mcpl-reference-fields.test.d.ts.map +0 -1
- package/dist/test/mcpl-reference-fields.test.js +0 -149
- package/dist/test/mcpl-reference-fields.test.js.map +0 -1
- package/dist/test/mcpl-request-timeout.test.d.ts +0 -2
- package/dist/test/mcpl-request-timeout.test.d.ts.map +0 -1
- package/dist/test/mcpl-request-timeout.test.js +0 -138
- package/dist/test/mcpl-request-timeout.test.js.map +0 -1
- package/dist/test/mcpl-runtime-lifecycle.test.d.ts +0 -7
- package/dist/test/mcpl-runtime-lifecycle.test.d.ts.map +0 -1
- package/dist/test/mcpl-runtime-lifecycle.test.js +0 -142
- package/dist/test/mcpl-runtime-lifecycle.test.js.map +0 -1
- package/dist/test/mcpl-tool-class-listing.test.d.ts +0 -2
- package/dist/test/mcpl-tool-class-listing.test.d.ts.map +0 -1
- package/dist/test/mcpl-tool-class-listing.test.js +0 -147
- package/dist/test/mcpl-tool-class-listing.test.js.map +0 -1
- package/dist/test/mcpl-tool-classes.test.d.ts +0 -7
- package/dist/test/mcpl-tool-classes.test.d.ts.map +0 -1
- package/dist/test/mcpl-tool-classes.test.js +0 -80
- package/dist/test/mcpl-tool-classes.test.js.map +0 -1
- package/dist/test/mcpl-tool-lifecycle-e2e.test.d.ts +0 -2
- package/dist/test/mcpl-tool-lifecycle-e2e.test.d.ts.map +0 -1
- package/dist/test/mcpl-tool-lifecycle-e2e.test.js +0 -237
- package/dist/test/mcpl-tool-lifecycle-e2e.test.js.map +0 -1
- package/dist/test/mcpl-tool-lifecycle-emitter.test.d.ts +0 -11
- package/dist/test/mcpl-tool-lifecycle-emitter.test.d.ts.map +0 -1
- package/dist/test/mcpl-tool-lifecycle-emitter.test.js +0 -412
- package/dist/test/mcpl-tool-lifecycle-emitter.test.js.map +0 -1
- package/dist/test/mcpl-tool-lifecycle-script.test.d.ts +0 -2
- package/dist/test/mcpl-tool-lifecycle-script.test.d.ts.map +0 -1
- package/dist/test/mcpl-tool-lifecycle-script.test.js +0 -288
- package/dist/test/mcpl-tool-lifecycle-script.test.js.map +0 -1
- package/dist/test/mcpl-tool-lifecycle.test.d.ts +0 -11
- package/dist/test/mcpl-tool-lifecycle.test.d.ts.map +0 -1
- package/dist/test/mcpl-tool-lifecycle.test.js +0 -270
- package/dist/test/mcpl-tool-lifecycle.test.js.map +0 -1
- package/dist/test/mcpl-ws-transport.test.d.ts +0 -2
- package/dist/test/mcpl-ws-transport.test.d.ts.map +0 -1
- package/dist/test/mcpl-ws-transport.test.js +0 -258
- package/dist/test/mcpl-ws-transport.test.js.map +0 -1
- package/dist/test/module-context-integration.test.d.ts +0 -18
- package/dist/test/module-context-integration.test.d.ts.map +0 -1
- package/dist/test/module-context-integration.test.js +0 -181
- package/dist/test/module-context-integration.test.js.map +0 -1
- package/dist/test/offline-recovery-branch.test.d.ts +0 -2
- package/dist/test/offline-recovery-branch.test.d.ts.map +0 -1
- package/dist/test/offline-recovery-branch.test.js +0 -226
- package/dist/test/offline-recovery-branch.test.js.map +0 -1
- package/dist/test/operator-surgery.test.d.ts +0 -2
- package/dist/test/operator-surgery.test.d.ts.map +0 -1
- package/dist/test/operator-surgery.test.js +0 -398
- package/dist/test/operator-surgery.test.js.map +0 -1
- package/dist/test/ops-alert.test.d.ts +0 -2
- package/dist/test/ops-alert.test.d.ts.map +0 -1
- package/dist/test/ops-alert.test.js +0 -153
- package/dist/test/ops-alert.test.js.map +0 -1
- package/dist/test/overbudget-drain-breaker.test.d.ts +0 -2
- package/dist/test/overbudget-drain-breaker.test.d.ts.map +0 -1
- package/dist/test/overbudget-drain-breaker.test.js +0 -197
- package/dist/test/overbudget-drain-breaker.test.js.map +0 -1
- package/dist/test/per-agent-delivery.test.d.ts +0 -2
- package/dist/test/per-agent-delivery.test.d.ts.map +0 -1
- package/dist/test/per-agent-delivery.test.js +0 -108
- package/dist/test/per-agent-delivery.test.js.map +0 -1
- package/dist/test/physical-window-restart.test.d.ts +0 -14
- package/dist/test/physical-window-restart.test.d.ts.map +0 -1
- package/dist/test/physical-window-restart.test.js +0 -222
- package/dist/test/physical-window-restart.test.js.map +0 -1
- package/dist/test/placed-reaction-baseline.test.d.ts +0 -2
- package/dist/test/placed-reaction-baseline.test.d.ts.map +0 -1
- package/dist/test/placed-reaction-baseline.test.js +0 -312
- package/dist/test/placed-reaction-baseline.test.js.map +0 -1
- package/dist/test/present-while-acting.test.d.ts +0 -19
- package/dist/test/present-while-acting.test.d.ts.map +0 -1
- package/dist/test/present-while-acting.test.js +0 -706
- package/dist/test/present-while-acting.test.js.map +0 -1
- package/dist/test/prose-segments.test.d.ts +0 -2
- package/dist/test/prose-segments.test.d.ts.map +0 -1
- package/dist/test/prose-segments.test.js +0 -52
- package/dist/test/prose-segments.test.js.map +0 -1
- package/dist/test/prose-stream-router.test.d.ts +0 -2
- package/dist/test/prose-stream-router.test.d.ts.map +0 -1
- package/dist/test/prose-stream-router.test.js +0 -171
- package/dist/test/prose-stream-router.test.js.map +0 -1
- package/dist/test/provider-acceleration-cooldown.test.d.ts +0 -2
- package/dist/test/provider-acceleration-cooldown.test.d.ts.map +0 -1
- package/dist/test/provider-acceleration-cooldown.test.js +0 -435
- package/dist/test/provider-acceleration-cooldown.test.js.map +0 -1
- package/dist/test/provider-waits.test.d.ts +0 -2
- package/dist/test/provider-waits.test.d.ts.map +0 -1
- package/dist/test/provider-waits.test.js +0 -2196
- package/dist/test/provider-waits.test.js.map +0 -1
- package/dist/test/puppet-tool-call.test.d.ts +0 -2
- package/dist/test/puppet-tool-call.test.d.ts.map +0 -1
- package/dist/test/puppet-tool-call.test.js +0 -224
- package/dist/test/puppet-tool-call.test.js.map +0 -1
- package/dist/test/puppet-turn-reservation.test.d.ts +0 -2
- package/dist/test/puppet-turn-reservation.test.d.ts.map +0 -1
- package/dist/test/puppet-turn-reservation.test.js +0 -139
- package/dist/test/puppet-turn-reservation.test.js.map +0 -1
- package/dist/test/reaction-baseline-platform.test.d.ts +0 -2
- package/dist/test/reaction-baseline-platform.test.d.ts.map +0 -1
- package/dist/test/reaction-baseline-platform.test.js +0 -124
- package/dist/test/reaction-baseline-platform.test.js.map +0 -1
- package/dist/test/record-journal.test.d.ts +0 -6
- package/dist/test/record-journal.test.d.ts.map +0 -1
- package/dist/test/record-journal.test.js +0 -269
- package/dist/test/record-journal.test.js.map +0 -1
- package/dist/test/refusal-reaction-baseline.test.d.ts +0 -2
- package/dist/test/refusal-reaction-baseline.test.d.ts.map +0 -1
- package/dist/test/refusal-reaction-baseline.test.js +0 -81
- package/dist/test/refusal-reaction-baseline.test.js.map +0 -1
- package/dist/test/refusal-rewind.test.d.ts +0 -2
- package/dist/test/refusal-rewind.test.d.ts.map +0 -1
- package/dist/test/refusal-rewind.test.js +0 -108
- package/dist/test/refusal-rewind.test.js.map +0 -1
- package/dist/test/refusal-single-call.test.d.ts +0 -2
- package/dist/test/refusal-single-call.test.d.ts.map +0 -1
- package/dist/test/refusal-single-call.test.js +0 -177
- package/dist/test/refusal-single-call.test.js.map +0 -1
- package/dist/test/release-changelog.test.d.ts +0 -2
- package/dist/test/release-changelog.test.d.ts.map +0 -1
- package/dist/test/release-changelog.test.js +0 -159
- package/dist/test/release-changelog.test.js.map +0 -1
- package/dist/test/same-round-think-policy.test.d.ts +0 -2
- package/dist/test/same-round-think-policy.test.d.ts.map +0 -1
- package/dist/test/same-round-think-policy.test.js +0 -853
- package/dist/test/same-round-think-policy.test.js.map +0 -1
- package/dist/test/save-recent-image.test.d.ts +0 -26
- package/dist/test/save-recent-image.test.d.ts.map +0 -1
- package/dist/test/save-recent-image.test.js +0 -974
- package/dist/test/save-recent-image.test.js.map +0 -1
- package/dist/test/shutdown-errors.test.d.ts +0 -2
- package/dist/test/shutdown-errors.test.d.ts.map +0 -1
- package/dist/test/shutdown-errors.test.js +0 -662
- package/dist/test/shutdown-errors.test.js.map +0 -1
- package/dist/test/silent-heartbeat-rows.test.d.ts +0 -2
- package/dist/test/silent-heartbeat-rows.test.d.ts.map +0 -1
- package/dist/test/silent-heartbeat-rows.test.js +0 -451
- package/dist/test/silent-heartbeat-rows.test.js.map +0 -1
- package/dist/test/silent-heartbeat.test.d.ts +0 -2
- package/dist/test/silent-heartbeat.test.d.ts.map +0 -1
- package/dist/test/silent-heartbeat.test.js +0 -165
- package/dist/test/silent-heartbeat.test.js.map +0 -1
- package/dist/test/state-scaling-e2e.test.d.ts +0 -22
- package/dist/test/state-scaling-e2e.test.d.ts.map +0 -1
- package/dist/test/state-scaling-e2e.test.js +0 -206
- package/dist/test/state-scaling-e2e.test.js.map +0 -1
- package/dist/test/state-scaling.test.d.ts +0 -12
- package/dist/test/state-scaling.test.d.ts.map +0 -1
- package/dist/test/state-scaling.test.js +0 -251
- package/dist/test/state-scaling.test.js.map +0 -1
- package/dist/test/stop-with-work-in-flight.test.d.ts +0 -23
- package/dist/test/stop-with-work-in-flight.test.d.ts.map +0 -1
- package/dist/test/stop-with-work-in-flight.test.js +0 -672
- package/dist/test/stop-with-work-in-flight.test.js.map +0 -1
- package/dist/test/surgery-marks-receipt.test.d.ts +0 -2
- package/dist/test/surgery-marks-receipt.test.d.ts.map +0 -1
- package/dist/test/surgery-marks-receipt.test.js +0 -540
- package/dist/test/surgery-marks-receipt.test.js.map +0 -1
- package/dist/test/thinking-token-stamp.test.d.ts +0 -12
- package/dist/test/thinking-token-stamp.test.d.ts.map +0 -1
- package/dist/test/thinking-token-stamp.test.js +0 -129
- package/dist/test/thinking-token-stamp.test.js.map +0 -1
- package/dist/test/timezone.test.d.ts +0 -2
- package/dist/test/timezone.test.d.ts.map +0 -1
- package/dist/test/timezone.test.js +0 -18
- package/dist/test/timezone.test.js.map +0 -1
- package/dist/test/tool-batch-hook.test.d.ts +0 -8
- package/dist/test/tool-batch-hook.test.d.ts.map +0 -1
- package/dist/test/tool-batch-hook.test.js +0 -275
- package/dist/test/tool-batch-hook.test.js.map +0 -1
- package/dist/test/tool-policy.test.d.ts +0 -2
- package/dist/test/tool-policy.test.d.ts.map +0 -1
- package/dist/test/tool-policy.test.js +0 -51
- package/dist/test/tool-policy.test.js.map +0 -1
- package/dist/test/tool-presentation.test.d.ts +0 -2
- package/dist/test/tool-presentation.test.d.ts.map +0 -1
- package/dist/test/tool-presentation.test.js +0 -268
- package/dist/test/tool-presentation.test.js.map +0 -1
- package/dist/test/tool-result-content.test.d.ts +0 -2
- package/dist/test/tool-result-content.test.d.ts.map +0 -1
- package/dist/test/tool-result-content.test.js +0 -37
- package/dist/test/tool-result-content.test.js.map +0 -1
- package/dist/test/tool-result-guard.test.d.ts +0 -2
- package/dist/test/tool-result-guard.test.d.ts.map +0 -1
- package/dist/test/tool-result-guard.test.js +0 -3607
- package/dist/test/tool-result-guard.test.js.map +0 -1
- package/dist/test/tool-result-history.test.d.ts +0 -7
- package/dist/test/tool-result-history.test.d.ts.map +0 -1
- package/dist/test/tool-result-history.test.js +0 -156
- package/dist/test/tool-result-history.test.js.map +0 -1
- package/dist/test/tool-result-spill.test.d.ts +0 -13
- package/dist/test/tool-result-spill.test.d.ts.map +0 -1
- package/dist/test/tool-result-spill.test.js +0 -586
- package/dist/test/tool-result-spill.test.js.map +0 -1
- package/dist/test/tool-wrapper-prose-guard-integration.test.d.ts +0 -2
- package/dist/test/tool-wrapper-prose-guard-integration.test.d.ts.map +0 -1
- package/dist/test/tool-wrapper-prose-guard-integration.test.js +0 -485
- package/dist/test/tool-wrapper-prose-guard-integration.test.js.map +0 -1
- package/dist/test/tool-wrapper-prose-guard.test.d.ts +0 -2
- package/dist/test/tool-wrapper-prose-guard.test.d.ts.map +0 -1
- package/dist/test/tool-wrapper-prose-guard.test.js +0 -34
- package/dist/test/tool-wrapper-prose-guard.test.js.map +0 -1
- package/dist/test/tools-observe-parser-conformance.test.d.ts +0 -2
- package/dist/test/tools-observe-parser-conformance.test.d.ts.map +0 -1
- package/dist/test/tools-observe-parser-conformance.test.js +0 -129
- package/dist/test/tools-observe-parser-conformance.test.js.map +0 -1
- package/dist/test/trunk-channel-routing.test.d.ts +0 -19
- package/dist/test/trunk-channel-routing.test.d.ts.map +0 -1
- package/dist/test/trunk-channel-routing.test.js +0 -329
- package/dist/test/trunk-channel-routing.test.js.map +0 -1
- package/dist/test/tune-out-e2e.test.d.ts +0 -2
- package/dist/test/tune-out-e2e.test.d.ts.map +0 -1
- package/dist/test/tune-out-e2e.test.js +0 -142
- package/dist/test/tune-out-e2e.test.js.map +0 -1
- package/dist/test/tune-out-gate-composition.test.d.ts +0 -2
- package/dist/test/tune-out-gate-composition.test.d.ts.map +0 -1
- package/dist/test/tune-out-gate-composition.test.js +0 -221
- package/dist/test/tune-out-gate-composition.test.js.map +0 -1
- package/dist/test/tune-out-lifecycle.test.d.ts +0 -2
- package/dist/test/tune-out-lifecycle.test.d.ts.map +0 -1
- package/dist/test/tune-out-lifecycle.test.js +0 -121
- package/dist/test/tune-out-lifecycle.test.js.map +0 -1
- package/dist/test/turn-trigger-provenance.test.d.ts +0 -2
- package/dist/test/turn-trigger-provenance.test.d.ts.map +0 -1
- package/dist/test/turn-trigger-provenance.test.js +0 -137
- package/dist/test/turn-trigger-provenance.test.js.map +0 -1
- package/dist/test/unconvertible-tool-result.test.d.ts +0 -2
- package/dist/test/unconvertible-tool-result.test.d.ts.map +0 -1
- package/dist/test/unconvertible-tool-result.test.js +0 -358
- package/dist/test/unconvertible-tool-result.test.js.map +0 -1
- package/dist/test/usage-accounting.test.d.ts +0 -2
- package/dist/test/usage-accounting.test.d.ts.map +0 -1
- package/dist/test/usage-accounting.test.js +0 -13
- package/dist/test/usage-accounting.test.js.map +0 -1
- package/dist/test/usage-tracker.test.d.ts +0 -2
- package/dist/test/usage-tracker.test.d.ts.map +0 -1
- package/dist/test/usage-tracker.test.js +0 -129
- package/dist/test/usage-tracker.test.js.map +0 -1
- package/dist/test/utils-surface.test.d.ts +0 -2
- package/dist/test/utils-surface.test.d.ts.map +0 -1
- package/dist/test/utils-surface.test.js +0 -145
- package/dist/test/utils-surface.test.js.map +0 -1
- package/dist/test/workspace-branch-continuation.test.d.ts +0 -15
- package/dist/test/workspace-branch-continuation.test.d.ts.map +0 -1
- package/dist/test/workspace-branch-continuation.test.js +0 -183
- package/dist/test/workspace-branch-continuation.test.js.map +0 -1
- package/dist/test/workspace-materialize-freshness.test.d.ts +0 -16
- package/dist/test/workspace-materialize-freshness.test.d.ts.map +0 -1
- package/dist/test/workspace-materialize-freshness.test.js +0 -252
- package/dist/test/workspace-materialize-freshness.test.js.map +0 -1
- package/dist/test/workspace-path-containment.test.d.ts +0 -19
- package/dist/test/workspace-path-containment.test.d.ts.map +0 -1
- package/dist/test/workspace-path-containment.test.js +0 -91
- package/dist/test/workspace-path-containment.test.js.map +0 -1
- package/dist/test/workspace-read-file-from-disk.test.d.ts +0 -17
- package/dist/test/workspace-read-file-from-disk.test.d.ts.map +0 -1
- package/dist/test/workspace-read-file-from-disk.test.js +0 -174
- package/dist/test/workspace-read-file-from-disk.test.js.map +0 -1
- package/dist/test/workspace-read-image.test.d.ts +0 -2
- package/dist/test/workspace-read-image.test.d.ts.map +0 -1
- package/dist/test/workspace-read-image.test.js +0 -597
- package/dist/test/workspace-read-image.test.js.map +0 -1
- package/dist/test/workspace-read-text.test.d.ts +0 -2
- package/dist/test/workspace-read-text.test.d.ts.map +0 -1
- package/dist/test/workspace-read-text.test.js +0 -101
- package/dist/test/workspace-read-text.test.js.map +0 -1
- package/dist/test/workspace-restore-order.test.d.ts +0 -16
- package/dist/test/workspace-restore-order.test.d.ts.map +0 -1
- package/dist/test/workspace-restore-order.test.js +0 -140
- package/dist/test/workspace-restore-order.test.js.map +0 -1
- package/dist/test/workspace-three-way.test.d.ts +0 -12
- package/dist/test/workspace-three-way.test.d.ts.map +0 -1
- package/dist/test/workspace-three-way.test.js +0 -3194
- package/dist/test/workspace-three-way.test.js.map +0 -1
- package/dist/test/workspace-watcher.test.d.ts +0 -2
- package/dist/test/workspace-watcher.test.d.ts.map +0 -1
- package/dist/test/workspace-watcher.test.js +0 -281
- package/dist/test/workspace-watcher.test.js.map +0 -1
- package/dist/test/xml-round-persist.test.d.ts +0 -19
- package/dist/test/xml-round-persist.test.d.ts.map +0 -1
- package/dist/test/xml-round-persist.test.js +0 -235
- package/dist/test/xml-round-persist.test.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
* auto-sync between real filesystem and Chronicle, and manual materialization.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { isUtf8 } from 'node:buffer';
|
|
8
9
|
import { constants as fsConstants } from 'node:fs';
|
|
9
10
|
import type { Stats } from 'node:fs';
|
|
10
|
-
import { open, readFile, stat, lstat, realpath } from 'node:fs/promises';
|
|
11
|
-
import { join, resolve, relative, sep } from 'node:path';
|
|
11
|
+
import { open, readFile, readdir, stat, lstat, realpath } from 'node:fs/promises';
|
|
12
|
+
import { isAbsolute, join, resolve, relative, sep } from 'node:path';
|
|
12
13
|
import type { JsStore } from '@animalabs/chronicle';
|
|
13
14
|
import type { Module, ModuleContext, ProcessState, EventResponse } from '../../types/module.js';
|
|
14
15
|
import type { ProcessEvent, ToolDefinition, ToolCall, ToolResult } from '../../types/events.js';
|
|
@@ -39,8 +40,8 @@ import { MountWatcher, type FsChange } from './watcher.js';
|
|
|
39
40
|
import { hashContent, DEFAULT_MAX_FILE_SIZE } from './sync.js';
|
|
40
41
|
import { DiskAgreement, sameRootIdentity } from './disk-agreement.js';
|
|
41
42
|
import { BranchIntents } from './branch-intent.js';
|
|
42
|
-
import {
|
|
43
|
-
import { TOOL_BATCH_HOOK_TIMEOUT_MS } from '../../module-registry.js';
|
|
43
|
+
import { IGNORED, OTHER_MOUNT, checkDir, filesystemTrustsCtime, inOtherMount, locateRoot, passedOverAt, readPrefix } from './observe.js';
|
|
44
|
+
import { TOOL_BATCH_HOOK_TIMEOUT_MS, ensureStateRegistered } from '../../module-registry.js';
|
|
44
45
|
import {
|
|
45
46
|
type MountRuntime,
|
|
46
47
|
type PassOptions,
|
|
@@ -50,6 +51,9 @@ import {
|
|
|
50
51
|
type PushResult,
|
|
51
52
|
type Scope,
|
|
52
53
|
type TreeOp,
|
|
54
|
+
NOT_PUSHED_IN_OTHER_MOUNT,
|
|
55
|
+
WALK_CAP,
|
|
56
|
+
entryMode,
|
|
53
57
|
pushPaths,
|
|
54
58
|
reconcilePass,
|
|
55
59
|
settleAdoptionBeforeMutation,
|
|
@@ -57,6 +61,14 @@ import {
|
|
|
57
61
|
|
|
58
62
|
/** Default for how long the after-batch scan may hold the agent's next inference. */
|
|
59
63
|
const AGENT_ACTION_SCAN_DEADLINE_MS = 20_000;
|
|
64
|
+
/**
|
|
65
|
+
* How long a mount's catch-up pass waits for its watcher to become ready
|
|
66
|
+
* before it starts anyway (ensureRunning). In a probe on Linux at a load near
|
|
67
|
+
* 40, chokidar's first scan of 10,000 entries took 1.3 s, about a twentieth of
|
|
68
|
+
* this. A scan slower than the bound has the pass started beside it, and the
|
|
69
|
+
* watcher's ready runs the pass again once the scan ends.
|
|
70
|
+
*/
|
|
71
|
+
const CATCH_UP_BOUND_MS = 30_000;
|
|
60
72
|
/** Why a call is refused once the module has stopped. */
|
|
61
73
|
const NOT_RUNNING = 'The workspace has stopped: it takes no more work until it starts again';
|
|
62
74
|
/** The workspace's own deadline must end before the framework's hook bound. */
|
|
@@ -111,11 +123,14 @@ class WorkspaceImageReadError extends Error {
|
|
|
111
123
|
* (`mount_unavailable`), an ordinary missing file (`not_found`), a directory
|
|
112
124
|
* (`directory`), a symlink refused by the mount's `followSymlinks` policy
|
|
113
125
|
* (`symlink`), a symlink whose canonical target lands outside the canonical
|
|
114
|
-
* mount root (`escape`),
|
|
126
|
+
* mount root (`escape`), a file swapped underneath the read (`changed`), and a
|
|
127
|
+
* path in another mount's directory nested in the one named (`other_mount`),
|
|
128
|
+
* whose message names the mount that holds it and the path to use there.
|
|
115
129
|
*/
|
|
116
130
|
export type WorkspaceReadErrorCode =
|
|
117
131
|
| 'unknown_mount'
|
|
118
132
|
| 'traversal'
|
|
133
|
+
| 'other_mount'
|
|
119
134
|
| 'mount_unavailable'
|
|
120
135
|
| 'not_found'
|
|
121
136
|
| 'directory'
|
|
@@ -135,6 +150,17 @@ export type WorkspaceReadStage =
|
|
|
135
150
|
| 'stat'
|
|
136
151
|
| 'read';
|
|
137
152
|
|
|
153
|
+
/**
|
|
154
|
+
* A file operation's path lies in another mount's directory nested in the
|
|
155
|
+
* mount it names. The message names the mount that holds it and the path to
|
|
156
|
+
* use there.
|
|
157
|
+
*/
|
|
158
|
+
class NestedMountError extends Error {
|
|
159
|
+
constructor(path: string, mount: string, holder: string, use: string) {
|
|
160
|
+
super(`"${path}" lies in mount "${holder}", nested in mount "${mount}", which neither reads nor writes it: use "${use}".`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
138
164
|
/**
|
|
139
165
|
* Thrown by `WorkspaceModule.readFileFromDisk()`. Messages carry the
|
|
140
166
|
* mount-prefixed path only — never absolute filesystem paths — so they can be
|
|
@@ -155,13 +181,23 @@ export class WorkspaceReadError extends Error {
|
|
|
155
181
|
|
|
156
182
|
/** Successful `readFileFromDisk()` result. */
|
|
157
183
|
export interface WorkspaceDiskReadResult {
|
|
158
|
-
/**
|
|
184
|
+
/**
|
|
185
|
+
* File bytes: the whole file, or its first `maxBytes` when `truncated`.
|
|
186
|
+
* Without `maxBytes` the file is read with `readFile`, with no limit. In
|
|
187
|
+
* Node, that reads up to the size its own fstat finds, or to the file's end
|
|
188
|
+
* if that size is 0.
|
|
189
|
+
*/
|
|
159
190
|
bytes: Buffer;
|
|
160
|
-
/**
|
|
191
|
+
/**
|
|
192
|
+
* The file's size, agreeing with `bytes` and `truncated`: `bytes.length`,
|
|
193
|
+
* unless `truncated`. Then it's the file's size by an fstat after the read,
|
|
194
|
+
* or `maxBytes + 1` if that's less, since the read found at least that many
|
|
195
|
+
* bytes.
|
|
196
|
+
*/
|
|
161
197
|
size: number;
|
|
162
|
-
/** True when `maxBytes` was set and the file
|
|
198
|
+
/** True when `maxBytes` was set and the read found the file going on past it; `bytes` is a prefix. */
|
|
163
199
|
truncated: boolean;
|
|
164
|
-
/** mtime of the file actually read (for change-detection caches). */
|
|
200
|
+
/** mtime of the file actually read, from its fstat at the open (for change-detection caches). */
|
|
165
201
|
mtimeMs: number;
|
|
166
202
|
/** Canonical (realpath) location of the file read — inside the canonical mount root by construction. */
|
|
167
203
|
realPath: string;
|
|
@@ -170,7 +206,12 @@ export interface WorkspaceDiskReadResult {
|
|
|
170
206
|
}
|
|
171
207
|
|
|
172
208
|
export interface ReadFileFromDiskOptions {
|
|
173
|
-
/**
|
|
209
|
+
/**
|
|
210
|
+
* Keep at most this many bytes (a bounded prefix read): the read loads them
|
|
211
|
+
* and one byte past them, which isn't kept and tells whether the file goes
|
|
212
|
+
* on, and never the rest of the file. It bounds the read itself, so it
|
|
213
|
+
* holds for a file that grows while it's read.
|
|
214
|
+
*/
|
|
174
215
|
maxBytes?: number;
|
|
175
216
|
}
|
|
176
217
|
|
|
@@ -180,6 +221,11 @@ function errnoOf(err: unknown): string | undefined {
|
|
|
180
221
|
: undefined;
|
|
181
222
|
}
|
|
182
223
|
|
|
224
|
+
/** read_image's refusal of a file over the mount's size limit, wherever it's found to be. */
|
|
225
|
+
function imageTooLarge(maxSize: number, mountPrefixedPath: string): WorkspaceImageReadError {
|
|
226
|
+
return new WorkspaceImageReadError('too_large', `Image file exceeds max size (${maxSize} bytes): ${mountPrefixedPath}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
183
229
|
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
184
230
|
const JPEG_SIGNATURE = Buffer.from([0xff, 0xd8, 0xff]);
|
|
185
231
|
const GIF87A_SIGNATURE = Buffer.from('GIF87a', 'ascii');
|
|
@@ -644,6 +690,17 @@ export class WorkspaceModule implements Module {
|
|
|
644
690
|
/** Whether a mount root's filesystem maintains ctime, by canonical root. */
|
|
645
691
|
/** Per canonical root: the device of its filesystem when that maintains ctime, else null. */
|
|
646
692
|
private ctimeTrust = new Map<string, string | null>();
|
|
693
|
+
/**
|
|
694
|
+
* Each mount's directories that are other mounts nested inside it:
|
|
695
|
+
* mount-relative, `/`-separated, with each mount's name and resolved root.
|
|
696
|
+
*/
|
|
697
|
+
private otherMounts = new Map<string, Array<{ dir: string; name: string; root: string }>>();
|
|
698
|
+
/**
|
|
699
|
+
* Whether a file's mode is observed, taken in and applied (MountRuntime's
|
|
700
|
+
* `modesObserved`): not on Windows, which reports no POSIX modes. A test
|
|
701
|
+
* sets it false to run that case anywhere.
|
|
702
|
+
*/
|
|
703
|
+
private modesObserved = process.platform !== 'win32';
|
|
647
704
|
/** Persisted state decoded in start(), held until mounts exist to apply it
|
|
648
705
|
* to — in the Host ordering start() runs before initStore() creates the
|
|
649
706
|
* mounts, so restoration must be second-callback-safe like watcher
|
|
@@ -651,26 +708,41 @@ export class WorkspaceModule implements Module {
|
|
|
651
708
|
private savedState: WorkspaceModuleState | null = null;
|
|
652
709
|
|
|
653
710
|
constructor(config: WorkspaceConfig) {
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
711
|
+
for (const mount of config.mounts) {
|
|
712
|
+
const cap: unknown = mount.walkCap;
|
|
713
|
+
if (cap !== undefined && !(Number.isSafeInteger(cap) && (cap as number) > 0)) {
|
|
714
|
+
// Numbers as JavaScript writes them (JSON would say null for NaN), anything else as JSON.
|
|
715
|
+
const shown = typeof cap === 'number' ? String(cap) : typeof cap === 'bigint' ? `${cap}n` : JSON.stringify(cap) ?? String(cap);
|
|
716
|
+
throw new Error(
|
|
717
|
+
`[workspace] Mount "${mount.name}": walkCap must be a positive integer no larger than ${Number.MAX_SAFE_INTEGER}, not ${shown}`,
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
// Detect overlapping mount paths: a mount nested inside another is that
|
|
722
|
+
// mount's directory, which the outer mount passes over in every pass,
|
|
723
|
+
// named or not, and in its watcher (OTHER_MOUNT). It's kept apart from
|
|
724
|
+
// the outer mount's ignore list, which a sync that names a path sets
|
|
725
|
+
// aside for what it names (#276), so no sync of the outer mount takes in
|
|
726
|
+
// the inner one's files.
|
|
657
727
|
for (const outer of config.mounts) {
|
|
658
728
|
for (const inner of config.mounts) {
|
|
659
729
|
if (outer === inner) continue;
|
|
660
730
|
const outerPath = resolve(outer.path);
|
|
661
731
|
const innerPath = resolve(inner.path);
|
|
662
732
|
const rel = relative(outerPath, innerPath);
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
733
|
+
// Outside the outer mount, `relative` gives a path that climbs with a
|
|
734
|
+
// `..` segment, or an absolute one across Windows drives. A name that
|
|
735
|
+
// only begins with two dots, such as `..cache`, is inside it.
|
|
736
|
+
if (rel && rel !== '..' && !rel.startsWith('..' + sep) && !isAbsolute(rel)) {
|
|
737
|
+
// inner is nested under outer: passed over as exactly that
|
|
738
|
+
// directory, read against `/`-separated paths, whatever the
|
|
739
|
+
// platform's.
|
|
740
|
+
const relPath = rel.split(sep).join('/');
|
|
741
|
+
this.otherMounts.set(outer.name, [...(this.otherMounts.get(outer.name) ?? []), { dir: relPath, name: inner.name, root: innerPath }]);
|
|
742
|
+
console.warn(
|
|
743
|
+
`[workspace] Mount "${outer.name}" contains mount "${inner.name}" ` +
|
|
744
|
+
`at ${relPath}/ — passing over it as another mount's directory`,
|
|
745
|
+
);
|
|
674
746
|
}
|
|
675
747
|
}
|
|
676
748
|
}
|
|
@@ -687,7 +759,11 @@ export class WorkspaceModule implements Module {
|
|
|
687
759
|
* second. `ensureRunning()` gates on `ctx && mounts-populated`.
|
|
688
760
|
*/
|
|
689
761
|
initStore(store: JsStore): void {
|
|
690
|
-
|
|
762
|
+
// Every mount's states are registered before the module takes the store
|
|
763
|
+
// or any mount, so a registration that fails leaves the module as it was
|
|
764
|
+
// before the call, knowing no mount, and a later initStore starts clean.
|
|
765
|
+
const mounts = new Map<string, MountState>();
|
|
766
|
+
const intents = new Map<string, BranchIntents>();
|
|
691
767
|
|
|
692
768
|
// Register tree states for each mount: the workspace tree, and the
|
|
693
769
|
// branch-local intent that qualifies it.
|
|
@@ -695,19 +771,15 @@ export class WorkspaceModule implements Module {
|
|
|
695
771
|
const treeStateId = `workspace/${mount.name}/tree`;
|
|
696
772
|
const intentTreeStateId = `workspace/${mount.name}/intent`;
|
|
697
773
|
for (const id of [treeStateId, intentTreeStateId]) {
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
});
|
|
705
|
-
} catch {
|
|
706
|
-
// State already registered (restart scenario)
|
|
707
|
-
}
|
|
774
|
+
ensureStateRegistered(store, {
|
|
775
|
+
id,
|
|
776
|
+
strategy: 'tree',
|
|
777
|
+
deltaSnapshotEvery: this.config.deltaSnapshotEvery ?? 50,
|
|
778
|
+
fullSnapshotEvery: this.config.fullSnapshotEvery ?? 10,
|
|
779
|
+
});
|
|
708
780
|
}
|
|
709
781
|
|
|
710
|
-
|
|
782
|
+
mounts.set(mount.name, {
|
|
711
783
|
config: mount,
|
|
712
784
|
treeStateId,
|
|
713
785
|
intentTreeStateId,
|
|
@@ -717,14 +789,17 @@ export class WorkspaceModule implements Module {
|
|
|
717
789
|
lastMaterializedBranchId: null,
|
|
718
790
|
watcherReadyAt: null,
|
|
719
791
|
watcherError: null,
|
|
720
|
-
};
|
|
721
|
-
|
|
722
|
-
this.intents.set(mount.name, new BranchIntents(store, intentTreeStateId));
|
|
792
|
+
});
|
|
793
|
+
intents.set(mount.name, new BranchIntents(store, intentTreeStateId));
|
|
723
794
|
}
|
|
724
795
|
|
|
796
|
+
this.store = store;
|
|
797
|
+
for (const [name, state] of mounts) this.mounts.set(name, state);
|
|
798
|
+
for (const [name, intent] of intents) this.intents.set(name, intent);
|
|
799
|
+
|
|
725
800
|
// What disk last agreed with, per mount and path: global records, so a
|
|
726
801
|
// branch switch never rewinds it.
|
|
727
|
-
this.agreement = new DiskAgreement(store);
|
|
802
|
+
this.agreement = new DiskAgreement(store, { modesObserved: () => this.modesObserved });
|
|
728
803
|
this.agreement.open(this.config.mounts.map((m) => ({ name: m.name, root: resolve(m.path) })));
|
|
729
804
|
|
|
730
805
|
this.applySavedState();
|
|
@@ -773,19 +848,61 @@ export class WorkspaceModule implements Module {
|
|
|
773
848
|
followSymlinks: mount.config.followSymlinks === true,
|
|
774
849
|
maxFileSize: mount.config.maxFileSize ?? DEFAULT_MAX_FILE_SIZE,
|
|
775
850
|
ignore: mount.config.ignore ?? [],
|
|
851
|
+
others: this.othersOf(mount.config.name),
|
|
776
852
|
},
|
|
777
853
|
rootReal,
|
|
778
854
|
ctimeTrustedDev: trustedDev,
|
|
779
855
|
treeStateId: mount.treeStateId,
|
|
780
856
|
intents: this.intents.get(mount.config.name)!,
|
|
781
857
|
readOnly: mount.config.mode === 'read-only',
|
|
858
|
+
walkCap: mount.config.walkCap ?? WALK_CAP,
|
|
859
|
+
catchUpScan: (mount.config.watch ?? 'always') === 'always',
|
|
860
|
+
modesObserved: this.modesObserved,
|
|
782
861
|
};
|
|
783
862
|
}
|
|
784
863
|
|
|
864
|
+
/**
|
|
865
|
+
* Where a search's walk starts: `base`, its path as given, then each
|
|
866
|
+
* segment of `beneath` while disk lists it, in the directory reached so
|
|
867
|
+
* far, as a directory with exactly that spelling. The walk names what it
|
|
868
|
+
* finds from its start's spelling, and a case-insensitive disk would let
|
|
869
|
+
* it go on under a spelling disk doesn't list, taking files in under that
|
|
870
|
+
* spelling beside their own. So the start stops before a segment spelled
|
|
871
|
+
* any other way, not there, or not a directory, and before a link, which
|
|
872
|
+
* a walk of the whole mount doesn't go through either. It lists only
|
|
873
|
+
* directories it has checked lie inside the mount, and none the mount
|
|
874
|
+
* passes over by design, which the walk then answers for.
|
|
875
|
+
*/
|
|
876
|
+
private async walkStart(mount: MountState, base: string, beneath: string): Promise<string> {
|
|
877
|
+
if (beneath === '') return base;
|
|
878
|
+
const { view, rootReal } = await this.runtime(mount);
|
|
879
|
+
let start = base;
|
|
880
|
+
for (const segment of beneath.split('/')) {
|
|
881
|
+
if (start !== '' && passedOverAt(start, view.ignore, view.others) !== undefined) break;
|
|
882
|
+
const here = await checkDir(view, rootReal, start);
|
|
883
|
+
if (here.kind !== 'checked') break;
|
|
884
|
+
const entry = (await readdir(here.dir.real, { withFileTypes: true }).catch(() => [])).find((e) => e.name === segment);
|
|
885
|
+
if (entry === undefined || !entry.isDirectory()) break;
|
|
886
|
+
start = start ? `${start}/${segment}` : segment;
|
|
887
|
+
}
|
|
888
|
+
return start;
|
|
889
|
+
}
|
|
890
|
+
|
|
785
891
|
/** One disk→store pass; the caller holds the mount's turn. Emits its events. */
|
|
786
892
|
private async passUnlocked(mount: MountState, scope: Scope, opts: PassOptions = {}): Promise<PassResult> {
|
|
787
893
|
const result = await reconcilePass(this.getStore(), this.agreementOrThrow(), await this.runtime(mount), scope, opts);
|
|
788
894
|
if (scope.kind === 'dir' && scope.dir === '' && scope.recursive) mount.initialSyncDone = true;
|
|
895
|
+
// What a pass says is another mount's directory names that mount and
|
|
896
|
+
// the path to use there, as the file operations' refusal does: a copy's
|
|
897
|
+
// note, and the region of a scope that lies there. The pass's counts
|
|
898
|
+
// keep the bare reason.
|
|
899
|
+
const name = mount.config.name;
|
|
900
|
+
for (const report of result.reports.values()) {
|
|
901
|
+
if (report.note === OTHER_MOUNT) report.note = this.otherMountNote(name, report.path, 'list');
|
|
902
|
+
}
|
|
903
|
+
for (const region of result.incomplete) {
|
|
904
|
+
if (region.reason === OTHER_MOUNT) region.reason = this.otherMountNote(name, region.path, 'list');
|
|
905
|
+
}
|
|
789
906
|
this.emitFsEvents(mount.config.name, result.ops, result.newConflicts);
|
|
790
907
|
return result;
|
|
791
908
|
}
|
|
@@ -948,7 +1065,45 @@ export class WorkspaceModule implements Module {
|
|
|
948
1065
|
|
|
949
1066
|
const watchMode = mount.config.watch ?? 'always';
|
|
950
1067
|
if (watchMode === 'always') {
|
|
951
|
-
|
|
1068
|
+
// Chokidar is started with ignoreInitial:true, so changes made while
|
|
1069
|
+
// the session was down would be invisible. A catch-up pass takes them
|
|
1070
|
+
// in under the three-way rule: only real differences produce tree
|
|
1071
|
+
// changes and events. It takes its turn with the mount's bare syncs,
|
|
1072
|
+
// so on a mount over its walkCap it covers the walkCap entries after
|
|
1073
|
+
// where the last turn stopped, and a change elsewhere waits for the
|
|
1074
|
+
// rotation to reach it, or for a sync of its path.
|
|
1075
|
+
//
|
|
1076
|
+
// The pass runs at each of the watcher's readies, its first and each
|
|
1077
|
+
// re-attach's, when chokidar has called fs.watch on every entry, so a
|
|
1078
|
+
// change made after the pass has looked at a file reaches the watcher.
|
|
1079
|
+
// Started beside chokidar's scan, the pass could finish with a file
|
|
1080
|
+
// before chokidar watched it, and a change between the two reached
|
|
1081
|
+
// neither. A watcher that isn't ready within CATCH_UP_BOUND_MS (from
|
|
1082
|
+
// its start or its re-attach) has the pass started at the bound, and
|
|
1083
|
+
// its ready, if it comes, runs the pass again. Within the mount's
|
|
1084
|
+
// walkCap, that pass covers what the early one missed. Over it, the
|
|
1085
|
+
// pass continues the rotation, and what the early one missed waits for
|
|
1086
|
+
// the rotation to come back, as any change outside a pass's window
|
|
1087
|
+
// does. An error doesn't start the pass: chokidar goes on scanning
|
|
1088
|
+
// after one, and the bound covers a watcher that never becomes ready.
|
|
1089
|
+
// On macOS, a new entry written after ready but before libuv's
|
|
1090
|
+
// FSEventStream exists isn't reported until a later change in its
|
|
1091
|
+
// directory, which this doesn't change.
|
|
1092
|
+
let watcher: MountWatcher | undefined;
|
|
1093
|
+
let bound: ReturnType<typeof setTimeout> | null = null;
|
|
1094
|
+
const catchUp = () => {
|
|
1095
|
+
if (bound) clearTimeout(bound);
|
|
1096
|
+
bound = null;
|
|
1097
|
+
// A watcher the module has since stopped or replaced starts no pass.
|
|
1098
|
+
if (this.watchers.get(name) !== watcher) return;
|
|
1099
|
+
void this.initialScan(name);
|
|
1100
|
+
};
|
|
1101
|
+
const awaitReady = () => {
|
|
1102
|
+
if (bound) clearTimeout(bound);
|
|
1103
|
+
bound = setTimeout(catchUp, CATCH_UP_BOUND_MS);
|
|
1104
|
+
bound.unref?.();
|
|
1105
|
+
};
|
|
1106
|
+
watcher = new MountWatcher(
|
|
952
1107
|
mount.config,
|
|
953
1108
|
(changes) => {
|
|
954
1109
|
this.handleFsChanges(name, changes);
|
|
@@ -957,6 +1112,7 @@ export class WorkspaceModule implements Module {
|
|
|
957
1112
|
onReady: () => {
|
|
958
1113
|
mount.watcherReadyAt = Date.now();
|
|
959
1114
|
mount.watcherError = null;
|
|
1115
|
+
catchUp();
|
|
960
1116
|
},
|
|
961
1117
|
onError: (err) => {
|
|
962
1118
|
mount.watcherError = err.message;
|
|
@@ -969,7 +1125,7 @@ export class WorkspaceModule implements Module {
|
|
|
969
1125
|
},
|
|
970
1126
|
onReattach: () => {
|
|
971
1127
|
mount.watcherError = null;
|
|
972
|
-
|
|
1128
|
+
awaitReady();
|
|
973
1129
|
this.ctx?.pushEvent({
|
|
974
1130
|
type: 'workspace:watcher-reattached',
|
|
975
1131
|
mount: name,
|
|
@@ -977,15 +1133,11 @@ export class WorkspaceModule implements Module {
|
|
|
977
1133
|
} as ProcessEvent);
|
|
978
1134
|
},
|
|
979
1135
|
},
|
|
1136
|
+
this.othersOf(name),
|
|
980
1137
|
);
|
|
1138
|
+
awaitReady();
|
|
981
1139
|
watcher.start();
|
|
982
1140
|
this.watchers.set(name, watcher);
|
|
983
|
-
|
|
984
|
-
// Chokidar is started with ignoreInitial:true, so changes made while
|
|
985
|
-
// the session was down would be invisible. One full pass catches up
|
|
986
|
-
// under the three-way rule: only real differences produce tree
|
|
987
|
-
// changes and events.
|
|
988
|
-
void this.initialScan(name);
|
|
989
1141
|
}
|
|
990
1142
|
|
|
991
1143
|
this.ctx.pushEvent({
|
|
@@ -1004,7 +1156,7 @@ export class WorkspaceModule implements Module {
|
|
|
1004
1156
|
try {
|
|
1005
1157
|
// A full pass under the three-way rule: deletions on disk are confirmed
|
|
1006
1158
|
// where nothing newer is pending, and nothing unvisited is touched.
|
|
1007
|
-
await this.withMount(mount, () => this.passUnlocked(mount, { kind: 'dir', dir: '', recursive: true }));
|
|
1159
|
+
await this.withMount(mount, () => this.passUnlocked(mount, { kind: 'dir', dir: '', recursive: true }, { resume: true }));
|
|
1008
1160
|
} catch (err) {
|
|
1009
1161
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1010
1162
|
this.ctx?.pushEvent({
|
|
@@ -1019,11 +1171,13 @@ export class WorkspaceModule implements Module {
|
|
|
1019
1171
|
// No new watcher-driven passes, then let every pass and write finish
|
|
1020
1172
|
// before the store goes away — including any queued behind the ones in
|
|
1021
1173
|
// flight (a tool call, a scan continuing past its deadline): drained
|
|
1022
|
-
// until no new tail appears.
|
|
1023
|
-
|
|
1174
|
+
// until no new tail appears. The watchers leave this.watchers before they
|
|
1175
|
+
// stop, so a watcher's ready or catch-up bound starts no pass meanwhile.
|
|
1176
|
+
const stopping = [...this.watchers.values()];
|
|
1177
|
+
this.watchers.clear();
|
|
1178
|
+
for (const watcher of stopping) {
|
|
1024
1179
|
await watcher.stop();
|
|
1025
1180
|
}
|
|
1026
|
-
this.watchers.clear();
|
|
1027
1181
|
for (;;) {
|
|
1028
1182
|
const tails = [...this.mountTurns.values()];
|
|
1029
1183
|
await Promise.all(tails);
|
|
@@ -1034,9 +1188,16 @@ export class WorkspaceModule implements Module {
|
|
|
1034
1188
|
// the pins and watermarks as the last work left them.
|
|
1035
1189
|
this.stopped = true;
|
|
1036
1190
|
|
|
1037
|
-
// Persist state
|
|
1038
|
-
|
|
1039
|
-
|
|
1191
|
+
// Persist state, once initStore() has run and the state start() was
|
|
1192
|
+
// handed has been applied (savedState is cleared then). Before that this
|
|
1193
|
+
// session has made no pass or write, so it has nothing to save, and the
|
|
1194
|
+
// stored state stays as it was: saving would put empty or default mounts
|
|
1195
|
+
// over the saved pins. A create() that fails after starting this module
|
|
1196
|
+
// stops it before the host can call initStore() (#389), and an
|
|
1197
|
+
// initStore() that throws after taking the store leaves the state
|
|
1198
|
+
// unapplied.
|
|
1199
|
+
if (this.ctx && this.store && this.savedState === null) {
|
|
1200
|
+
const activeBranchId = this.store.currentBranch().id;
|
|
1040
1201
|
const state: WorkspaceModuleState = { mounts: {}, activeBranchId };
|
|
1041
1202
|
for (const [name, mount] of this.mounts) {
|
|
1042
1203
|
state.mounts[name] = {
|
|
@@ -1077,8 +1238,8 @@ export class WorkspaceModule implements Module {
|
|
|
1077
1238
|
type: 'object' as const,
|
|
1078
1239
|
properties: {
|
|
1079
1240
|
path: { type: 'string', description: 'File path (mount-prefixed, e.g., "project/src/main.ts")' },
|
|
1080
|
-
offset: { type: '
|
|
1081
|
-
limit: { type: '
|
|
1241
|
+
offset: { type: 'integer', description: 'Starting line number (1-indexed)' },
|
|
1242
|
+
limit: { type: 'integer', description: 'Maximum number of lines to return (default 2000)' },
|
|
1082
1243
|
offsetChars: { type: 'integer', description: 'Character paging: zero-based UTF-16 offset (default 0); use nextOffsetChars to continue' },
|
|
1083
1244
|
limitChars: { type: 'integer', description: 'Character paging: code units to return (default 2000), plus at most one to keep a surrogate pair intact. Use the limit suggested by a spill notice.' },
|
|
1084
1245
|
},
|
|
@@ -1142,7 +1303,8 @@ export class WorkspaceModule implements Module {
|
|
|
1142
1303
|
+ 'still on disk); not-in-branch (on disk, absent from this branch\'s workspace); disk-only (binary or over '
|
|
1143
1304
|
+ 'the size limit, never stored — shown with size and image type); conflict, with its kind (disk and the '
|
|
1144
1305
|
+ 'workspace both changed; disk holds a different copy and nothing records which is newer; or a write was '
|
|
1145
|
-
+ 'interrupted) — sync the path to take disk, or materialize that path with force to take the workspace
|
|
1306
|
+
+ 'interrupted) — sync the path to take disk, or materialize that path with force to take the workspace, '
|
|
1307
|
+
+ 'with applyDeletions too where the workspace deleted it; '
|
|
1146
1308
|
+ 'disk-missing-provenance-unknown; unverified (disk could not be checked). `incomplete` names regions '
|
|
1147
1309
|
+ 'the listing could not see.',
|
|
1148
1310
|
inputSchema: {
|
|
@@ -1178,8 +1340,8 @@ export class WorkspaceModule implements Module {
|
|
|
1178
1340
|
pattern: { type: 'string', description: 'Regular expression pattern' },
|
|
1179
1341
|
path: { type: 'string', description: 'File or directory to search (mount-prefixed, optional)' },
|
|
1180
1342
|
glob: { type: 'string', description: 'Glob pattern to filter files' },
|
|
1181
|
-
contextBefore: { type: '
|
|
1182
|
-
contextAfter: { type: '
|
|
1343
|
+
contextBefore: { type: 'integer', description: 'Context lines before match' },
|
|
1344
|
+
contextAfter: { type: 'integer', description: 'Context lines after match' },
|
|
1183
1345
|
},
|
|
1184
1346
|
required: ['pattern'],
|
|
1185
1347
|
},
|
|
@@ -1211,7 +1373,12 @@ export class WorkspaceModule implements Module {
|
|
|
1211
1373
|
{
|
|
1212
1374
|
name: 'sync',
|
|
1213
1375
|
description: 'Bring disk changes into the workspace. Without a path, every mount (or the given one) is '
|
|
1214
|
-
+ 'rechecked
|
|
1376
|
+
+ 'rechecked and nothing pending in the workspace is discarded. A sync takes up to the mount\'s walk '
|
|
1377
|
+
+ 'cap of entries, files, links and the like (5000 unless configured). Past it, what this one didn\'t reach is listed under '
|
|
1378
|
+
+ '`incomplete`, and the walk takes turns: the next sync of the same mount or directory picks up where '
|
|
1379
|
+
+ 'the last turn stopped, so repeated syncs cover it all. On a mount watched continuously, its own catch-up '
|
|
1380
|
+
+ 'scan, at start and when its watcher reattaches, takes turns with its whole-mount syncs. With a '
|
|
1381
|
+
+ 'path (file or directory), '
|
|
1215
1382
|
+ 'the workspace takes disk\'s state there explicitly: it restores a workspace-deleted file, resolves '
|
|
1216
1383
|
+ 'conflicts toward disk, replaces a workspace draft, and removes a workspace file disk does not have. '
|
|
1217
1384
|
+ 'Each workspace change it gives up is listed under `discarded`, with the state it had and, as `op`, '
|
|
@@ -1221,13 +1388,18 @@ export class WorkspaceModule implements Module {
|
|
|
1221
1388
|
+ 'directory) is unavailable and listed under `incomplete`. Reconnect the drive; acceptRoot is for a '
|
|
1222
1389
|
+ 'directory replaced on purpose, or a drive back under a new device number, never for one still '
|
|
1223
1390
|
+ 'unplugged, whose empty mountpoint would become the root. What the scan passed over by design and '
|
|
1224
|
-
+ 'reported nowhere else is counted under `passedOver`, by mount and reason:
|
|
1225
|
-
+ '
|
|
1391
|
+
+ 'reported nowhere else is counted under `passedOver`, by mount and reason: another mount\'s '
|
|
1392
|
+
+ 'directory nested in this one, entries the mount\'s ignore list covers, and entries with an '
|
|
1393
|
+
+ 'editor\'s swap or backup file name (`.name.swp` and `.name.swx` as the whole name, names ending '
|
|
1394
|
+
+ 'in `~` or in `.subl…tmp`), that hold nothing the workspace tracks (such a directory counts once, '
|
|
1395
|
+
+ 'with what lies beneath it). So a '
|
|
1226
1396
|
+ 'total of 0, with no conflicts and nothing under `passedOver`, `incomplete` or `skipped`, means the '
|
|
1227
|
-
+ 'sync looked everywhere and nothing had changed. A path the mount ignores
|
|
1228
|
-
+ 'it, and listed under `ignored`: a sync without a path passes over it.
|
|
1229
|
-
+ '
|
|
1230
|
-
+ '
|
|
1397
|
+
+ 'sync looked everywhere and nothing had changed. A path the mount ignores, or an editor\'s file, is '
|
|
1398
|
+
+ 'synced when you name it, and listed under `ignored`: a sync without a path passes over it. A path '
|
|
1399
|
+
+ 'in another mount\'s directory isn\'t synced even when you name it: use the mount that holds it. Naming '
|
|
1400
|
+
+ 'an ignored directory sets aside the patterns that cover it or a directory above it; one that '
|
|
1401
|
+
+ 'covers only something inside it still applies, and what it covers is passed over, as are editors\' '
|
|
1402
|
+
+ 'files inside it.',
|
|
1231
1403
|
inputSchema: {
|
|
1232
1404
|
type: 'object' as const,
|
|
1233
1405
|
properties: {
|
|
@@ -1260,8 +1432,10 @@ export class WorkspaceModule implements Module {
|
|
|
1260
1432
|
const mountName = slash < 0 ? path : path.slice(0, slash);
|
|
1261
1433
|
const mount = this.config.mounts.find(m => m.name === mountName);
|
|
1262
1434
|
if (!mount) throw new Error(`Unknown mount: "${mountName}"`);
|
|
1263
|
-
|
|
1264
|
-
|
|
1435
|
+
// Against the resolved root, as parsePath judges it.
|
|
1436
|
+
const root = resolve(mount.path);
|
|
1437
|
+
const resolved = resolve(root, slash < 0 ? '' : path.slice(slash + 1));
|
|
1438
|
+
if (!isContainedPath(root, resolved)) throw new Error(`Path traversal detected: "${path}"`);
|
|
1265
1439
|
return resolved;
|
|
1266
1440
|
}
|
|
1267
1441
|
|
|
@@ -1287,6 +1461,13 @@ export class WorkspaceModule implements Module {
|
|
|
1287
1461
|
async handleToolCall(call: ToolCall): Promise<ToolResult> {
|
|
1288
1462
|
if (!this.running()) return { success: false, isError: true, error: NOT_RUNNING };
|
|
1289
1463
|
try {
|
|
1464
|
+
// Every handler reads its input as its schema describes it; a call
|
|
1465
|
+
// that sends something else is refused here, before anything is read.
|
|
1466
|
+
const schema = this.getTools().find((tool) => tool.name === call.name)?.inputSchema;
|
|
1467
|
+
const problem = schema ? inputProblem(schema, call.input, INPUT_MINIMUMS[call.name]) : null;
|
|
1468
|
+
if (problem !== null) {
|
|
1469
|
+
return { success: false, isError: true, error: `The ${call.name} call did nothing: ${problem}.` };
|
|
1470
|
+
}
|
|
1290
1471
|
const input = call.input as Record<string, unknown>;
|
|
1291
1472
|
const generated = typeof input?.path === 'string' ? this.findGeneratedTextFile(input.path) : undefined;
|
|
1292
1473
|
if (generated) {
|
|
@@ -1311,7 +1492,7 @@ export class WorkspaceModule implements Module {
|
|
|
1311
1492
|
return { success: false, error: `Unknown tool: ${call.name}`, isError: true };
|
|
1312
1493
|
}
|
|
1313
1494
|
} catch (err) {
|
|
1314
|
-
return { success: false, error: String(err), isError: true };
|
|
1495
|
+
return { success: false, error: err instanceof Error ? err.message : String(err), isError: true };
|
|
1315
1496
|
}
|
|
1316
1497
|
}
|
|
1317
1498
|
|
|
@@ -1402,7 +1583,7 @@ export class WorkspaceModule implements Module {
|
|
|
1402
1583
|
*/
|
|
1403
1584
|
resolveAbsolutePath(mountPrefixedPath: string): string | null {
|
|
1404
1585
|
try {
|
|
1405
|
-
const { mount, relativePath } = this.
|
|
1586
|
+
const { mount, relativePath } = this.parseFilePath(mountPrefixedPath);
|
|
1406
1587
|
return resolve(mount.config.path, relativePath);
|
|
1407
1588
|
} catch {
|
|
1408
1589
|
return null;
|
|
@@ -1440,7 +1621,7 @@ export class WorkspaceModule implements Module {
|
|
|
1440
1621
|
let mount: MountState;
|
|
1441
1622
|
let relativePath: string;
|
|
1442
1623
|
try {
|
|
1443
|
-
({ mount, relativePath } = this.
|
|
1624
|
+
({ mount, relativePath } = this.parseFilePath(mountPrefixedPath));
|
|
1444
1625
|
} catch (error) {
|
|
1445
1626
|
return {
|
|
1446
1627
|
success: false,
|
|
@@ -1464,13 +1645,17 @@ export class WorkspaceModule implements Module {
|
|
|
1464
1645
|
return { success: false, error: 'the workspace had left the branch this file is for', isError: true, data: { branchLeft: true } };
|
|
1465
1646
|
}
|
|
1466
1647
|
const blobHash = store.storeBlob(data, mimeType);
|
|
1648
|
+
// An overwrite keeps the file's mode, as the write tool's does.
|
|
1649
|
+
const existing = store.treeGet(mount.treeStateId, relativePath);
|
|
1467
1650
|
let committed = false;
|
|
1468
1651
|
let failure: string | null;
|
|
1652
|
+
let modeKept: string | null = null;
|
|
1469
1653
|
try {
|
|
1470
|
-
const
|
|
1471
|
-
{ kind: 'set', blobHash, size: data.byteLength }, () => { committed = true; });
|
|
1472
|
-
|
|
1473
|
-
|
|
1654
|
+
const answer = await this.commitToolChange(mount, relativePath,
|
|
1655
|
+
{ kind: 'set', blobHash, size: data.byteLength, ...(existing ? { mode: existing.mode } : {}) }, () => { committed = true; });
|
|
1656
|
+
modeKept = answer.modeKept;
|
|
1657
|
+
failure = answer.refused === null ? null
|
|
1658
|
+
: `Wrote to Chronicle but failed to materialize "${mountPrefixedPath}" to disk: ${answer.refused}.`;
|
|
1474
1659
|
} catch (error) {
|
|
1475
1660
|
if (!committed) throw error;
|
|
1476
1661
|
failure = `Stored "${mountPrefixedPath}" in Chronicle, but the rest of the write failed: ${error instanceof Error ? error.message : String(error)}.`;
|
|
@@ -1483,7 +1668,7 @@ export class WorkspaceModule implements Module {
|
|
|
1483
1668
|
}
|
|
1484
1669
|
return {
|
|
1485
1670
|
success: true,
|
|
1486
|
-
data: { path: mountPrefixedPath, size: data.byteLength, mimeType },
|
|
1671
|
+
data: { path: mountPrefixedPath, size: data.byteLength, mimeType, ...(modeKept ? { modeKept } : {}) },
|
|
1487
1672
|
};
|
|
1488
1673
|
});
|
|
1489
1674
|
}
|
|
@@ -1500,7 +1685,7 @@ export class WorkspaceModule implements Module {
|
|
|
1500
1685
|
let mount: MountState;
|
|
1501
1686
|
let relativePath: string;
|
|
1502
1687
|
try {
|
|
1503
|
-
({ mount, relativePath } = this.
|
|
1688
|
+
({ mount, relativePath } = this.parseFilePath(mountPrefixedPath));
|
|
1504
1689
|
} catch (error) {
|
|
1505
1690
|
return { error: error instanceof Error ? error.message : String(error) };
|
|
1506
1691
|
}
|
|
@@ -1537,7 +1722,7 @@ export class WorkspaceModule implements Module {
|
|
|
1537
1722
|
let mount: MountState;
|
|
1538
1723
|
let relativePath: string;
|
|
1539
1724
|
try {
|
|
1540
|
-
({ mount, relativePath } = this.
|
|
1725
|
+
({ mount, relativePath } = this.parseFilePath(mountPrefixedPath));
|
|
1541
1726
|
} catch (error) {
|
|
1542
1727
|
return { error: error instanceof Error ? error.message : String(error) };
|
|
1543
1728
|
}
|
|
@@ -1556,10 +1741,66 @@ export class WorkspaceModule implements Module {
|
|
|
1556
1741
|
}
|
|
1557
1742
|
}
|
|
1558
1743
|
|
|
1744
|
+
/** The directories of the mounts nested inside a mount, mount-relative. */
|
|
1745
|
+
private othersOf(mountName: string): string[] {
|
|
1746
|
+
return (this.otherMounts.get(mountName) ?? []).map((o) => o.dir);
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
/**
|
|
1750
|
+
* The innermost mount nested in `mountName` whose directory holds a path
|
|
1751
|
+
* of that mount, judged lexically where the path lands (`.`, `..` and
|
|
1752
|
+
* repeated separators resolved, as for the traversal guard; a link of the
|
|
1753
|
+
* outer mount isn't followed), with the path to use there; undefined when
|
|
1754
|
+
* no nested mount's directory holds it.
|
|
1755
|
+
*/
|
|
1756
|
+
private holderOf(mountName: string, relativePath: string): { name: string; use: string } | undefined {
|
|
1757
|
+
const mount = this.mounts.get(mountName);
|
|
1758
|
+
if (!mount) return undefined;
|
|
1759
|
+
const resolved = resolve(mount.config.path, relativePath);
|
|
1760
|
+
const holder = (this.otherMounts.get(mountName) ?? [])
|
|
1761
|
+
.filter((o) => isContainedPath(o.root, resolved))
|
|
1762
|
+
.sort((a, b) => b.root.length - a.root.length)[0];
|
|
1763
|
+
if (!holder) return undefined;
|
|
1764
|
+
const rest = relative(holder.root, resolved).split(sep).join('/');
|
|
1765
|
+
return { name: holder.name, use: rest ? `${holder.name}/${rest}` : holder.name };
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
/**
|
|
1769
|
+
* Why a mount leaves a path in a nested mount's directory, naming that
|
|
1770
|
+
* mount and the path to use: for a sync or a push, which of them leaves
|
|
1771
|
+
* it; for a listing, whose the path is.
|
|
1772
|
+
*/
|
|
1773
|
+
private otherMountNote(mountName: string, relativePath: string, by: 'sync' | 'push' | 'list'): string {
|
|
1774
|
+
const holder = this.holderOf(mountName, relativePath);
|
|
1775
|
+
const what = by === 'sync' ? ', which no sync of this mount takes in' : by === 'push' ? ', which no push of this mount writes' : '';
|
|
1776
|
+
return holder
|
|
1777
|
+
? `it lies in mount "${holder.name}", nested in this one${what}: use "${holder.use}"`
|
|
1778
|
+
: `it lies in another mount's directory${what}`;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
/**
|
|
1782
|
+
* parsePath for the file operations (read, write, edit, delete, and the
|
|
1783
|
+
* module's binary and disk reads and writes). A path in another mount's
|
|
1784
|
+
* directory nested in this one is that mount's: this mount's passes don't
|
|
1785
|
+
* observe it and its pushes don't write it, so its file operations refuse
|
|
1786
|
+
* it, naming the innermost mount that holds it and the path to use there.
|
|
1787
|
+
*/
|
|
1788
|
+
private parseFilePath(path: string): { mount: MountState; relativePath: string } {
|
|
1789
|
+
const parsed = this.parsePath(path);
|
|
1790
|
+
const holder = this.holderOf(parsed.mount.config.name, parsed.relativePath);
|
|
1791
|
+
if (holder) throw new NestedMountError(path, parsed.mount.config.name, holder.name, holder.use);
|
|
1792
|
+
return parsed;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1559
1795
|
/**
|
|
1560
1796
|
* Parse a mount-prefixed path into (mountName, relativePath).
|
|
1561
1797
|
*/
|
|
1562
1798
|
private parsePath(path: string): { mount: MountState; relativePath: string } {
|
|
1799
|
+
// A file's name on disk is UTF-8, which can't hold a lone surrogate: the
|
|
1800
|
+
// store would keep one name and disk another.
|
|
1801
|
+
if (LONE_SURROGATE.test(path)) {
|
|
1802
|
+
throw new Error(`The path ${JSON.stringify(path)} holds a lone UTF-16 surrogate, which UTF-8 can't store, so no file on disk can have that name. Nothing was done.`);
|
|
1803
|
+
}
|
|
1563
1804
|
const slashIdx = path.indexOf('/');
|
|
1564
1805
|
const mountName = slashIdx >= 0 ? path.slice(0, slashIdx) : path;
|
|
1565
1806
|
const relativePath = slashIdx >= 0 ? path.slice(slashIdx + 1) : '';
|
|
@@ -1573,8 +1814,11 @@ export class WorkspaceModule implements Module {
|
|
|
1573
1814
|
// Containment must compare with the PLATFORM separator (isContainedPath):
|
|
1574
1815
|
// resolve() emits backslashes on Windows, so a '/'-suffixed root never
|
|
1575
1816
|
// prefix-matches there and every in-mount path was rejected as traversal.
|
|
1576
|
-
|
|
1577
|
-
|
|
1817
|
+
// Against the resolved root, so a root configured with a trailing
|
|
1818
|
+
// separator, or relative, contains its own paths, the root included.
|
|
1819
|
+
const root = resolve(mount.config.path);
|
|
1820
|
+
const resolved = resolve(root, relativePath);
|
|
1821
|
+
if (!isContainedPath(root, resolved)) {
|
|
1578
1822
|
throw new Error(`Path traversal detected: "${path}" resolves outside mount "${mountName}"`);
|
|
1579
1823
|
}
|
|
1580
1824
|
|
|
@@ -1606,12 +1850,7 @@ export class WorkspaceModule implements Module {
|
|
|
1606
1850
|
if (bytes.byteLength === 0) {
|
|
1607
1851
|
throw new WorkspaceImageReadError('empty', `Image file is empty: ${mountPrefixedPath}`);
|
|
1608
1852
|
}
|
|
1609
|
-
if (bytes.byteLength > maxSize)
|
|
1610
|
-
throw new WorkspaceImageReadError(
|
|
1611
|
-
'too_large',
|
|
1612
|
-
`Image file exceeds max size (${maxSize} bytes): ${mountPrefixedPath}`,
|
|
1613
|
-
);
|
|
1614
|
-
}
|
|
1853
|
+
if (bytes.byteLength > maxSize) throw imageTooLarge(maxSize, mountPrefixedPath);
|
|
1615
1854
|
return {
|
|
1616
1855
|
bytes,
|
|
1617
1856
|
mimeType: detectImageMimeType(bytes, mountPrefixedPath),
|
|
@@ -1788,11 +2027,16 @@ export class WorkspaceModule implements Module {
|
|
|
1788
2027
|
* - With symlinks allowed, the canonical target must stay beneath the
|
|
1789
2028
|
* canonical mount root; a sibling-prefix root (`/mount-other`) does not count.
|
|
1790
2029
|
* - `maxBytes` performs a bounded prefix read: at most `maxBytes` bytes are
|
|
1791
|
-
*
|
|
2030
|
+
* kept, and one byte past them is read to tell whether the file goes on
|
|
2031
|
+
* (`truncated`). The reads decide where the file ends, not the size fstat
|
|
2032
|
+
* gave at the open, so the bound holds for a file that grows while it's
|
|
2033
|
+
* read.
|
|
1792
2034
|
*
|
|
1793
2035
|
* Throws {@link WorkspaceReadError} with a `code` distinguishing unknown
|
|
1794
|
-
* mount, lexical traversal,
|
|
1795
|
-
*
|
|
2036
|
+
* mount, lexical traversal, a path in another mount's directory nested in
|
|
2037
|
+
* the one named (`other_mount`), unavailable mount, missing file,
|
|
2038
|
+
* directory, policy-denied symlink, outside-mount target, and
|
|
2039
|
+
* file-changed-during-read.
|
|
1796
2040
|
*/
|
|
1797
2041
|
async readFileFromDisk(
|
|
1798
2042
|
mountPrefixedPath: string,
|
|
@@ -1804,10 +2048,11 @@ export class WorkspaceModule implements Module {
|
|
|
1804
2048
|
let mount: MountState;
|
|
1805
2049
|
let relativePath: string;
|
|
1806
2050
|
try {
|
|
1807
|
-
({ mount, relativePath } = this.
|
|
2051
|
+
({ mount, relativePath } = this.parseFilePath(mountPrefixedPath));
|
|
1808
2052
|
} catch (err) {
|
|
1809
2053
|
const message = err instanceof Error ? err.message : String(err);
|
|
1810
|
-
const code: WorkspaceReadErrorCode =
|
|
2054
|
+
const code: WorkspaceReadErrorCode = err instanceof NestedMountError ? 'other_mount'
|
|
2055
|
+
: message.startsWith('Unknown mount') ? 'unknown_mount' : 'traversal';
|
|
1811
2056
|
throw new WorkspaceReadError(code, message, 'parse');
|
|
1812
2057
|
}
|
|
1813
2058
|
if (!relativePath) {
|
|
@@ -1816,23 +2061,17 @@ export class WorkspaceModule implements Module {
|
|
|
1816
2061
|
|
|
1817
2062
|
const { handle, fileStat, realFilePath } = await this.openContainedFile(mount, relativePath, mountPrefixedPath);
|
|
1818
2063
|
try {
|
|
1819
|
-
const size = fileStat.size;
|
|
1820
2064
|
const cap = options.maxBytes;
|
|
1821
2065
|
let bytes: Buffer;
|
|
1822
2066
|
let truncated = false;
|
|
1823
|
-
if (cap
|
|
1824
|
-
truncated = true;
|
|
1825
|
-
bytes = Buffer.alloc(cap);
|
|
1826
|
-
let offset = 0;
|
|
1827
|
-
while (offset < cap) {
|
|
1828
|
-
const { bytesRead } = await handle.read(bytes, offset, cap - offset, offset);
|
|
1829
|
-
if (bytesRead === 0) break;
|
|
1830
|
-
offset += bytesRead;
|
|
1831
|
-
}
|
|
1832
|
-
if (offset < cap) bytes = bytes.subarray(0, offset);
|
|
1833
|
-
} else {
|
|
2067
|
+
if (cap === undefined) {
|
|
1834
2068
|
bytes = await handle.readFile();
|
|
2069
|
+
} else {
|
|
2070
|
+
({ bytes, more: truncated } = await readPrefix(handle, cap, fileStat.size));
|
|
1835
2071
|
}
|
|
2072
|
+
// What the read found, not what the fstat before it said, so `size`,
|
|
2073
|
+
// `bytes` and `truncated` agree even for a file that changed in between.
|
|
2074
|
+
const size = truncated ? Math.max((await handle.stat()).size, bytes.length + 1) : bytes.length;
|
|
1836
2075
|
return { bytes, size, truncated, mtimeMs: fileStat.mtimeMs, realPath: realFilePath, mount: mount.config.name };
|
|
1837
2076
|
} catch (err) {
|
|
1838
2077
|
if (err instanceof WorkspaceReadError) throw err;
|
|
@@ -1883,12 +2122,7 @@ export class WorkspaceModule implements Module {
|
|
|
1883
2122
|
if (fileStat.size === 0) {
|
|
1884
2123
|
throw new WorkspaceImageReadError('empty', `Image file is empty: ${mountPrefixedPath}`);
|
|
1885
2124
|
}
|
|
1886
|
-
if (fileStat.size > maxSize)
|
|
1887
|
-
throw new WorkspaceImageReadError(
|
|
1888
|
-
'too_large',
|
|
1889
|
-
`Image file exceeds max size (${maxSize} bytes): ${mountPrefixedPath}`,
|
|
1890
|
-
);
|
|
1891
|
-
}
|
|
2125
|
+
if (fileStat.size > maxSize) throw imageTooLarge(maxSize, mountPrefixedPath);
|
|
1892
2126
|
});
|
|
1893
2127
|
} catch (err) {
|
|
1894
2128
|
if (err instanceof WorkspaceImageReadError) throw err;
|
|
@@ -1898,9 +2132,12 @@ export class WorkspaceModule implements Module {
|
|
|
1898
2132
|
}
|
|
1899
2133
|
throw new WorkspaceImageReadError('not_found', `Unable to read image file: ${mountPrefixedPath}`);
|
|
1900
2134
|
}
|
|
1901
|
-
const { handle } = opened;
|
|
2135
|
+
const { handle, fileStat } = opened;
|
|
1902
2136
|
try {
|
|
1903
|
-
|
|
2137
|
+
// No more than maxSize is loaded: a file grown past it since the fstat
|
|
2138
|
+
// above is refused here, as one already over it was there.
|
|
2139
|
+
const { bytes, more } = await readPrefix(handle, maxSize, fileStat.size);
|
|
2140
|
+
if (more) throw imageTooLarge(maxSize, mountPrefixedPath);
|
|
1904
2141
|
return this.validateImageBytes(bytes, mountPrefixedPath, maxSize);
|
|
1905
2142
|
} catch (err) {
|
|
1906
2143
|
if (err instanceof WorkspaceImageReadError) throw err;
|
|
@@ -1913,6 +2150,14 @@ export class WorkspaceModule implements Module {
|
|
|
1913
2150
|
}
|
|
1914
2151
|
}
|
|
1915
2152
|
|
|
2153
|
+
/**
|
|
2154
|
+
* Whether a tool's change on this mount goes to disk as it commits: the
|
|
2155
|
+
* push commitToolChange makes, and what an edit's answer says of it.
|
|
2156
|
+
*/
|
|
2157
|
+
private pushesOnCommit(mount: MountState): boolean {
|
|
2158
|
+
return mount.config.autoMaterialize === true && mount.config.mode !== 'read-only';
|
|
2159
|
+
}
|
|
2160
|
+
|
|
1916
2161
|
/**
|
|
1917
2162
|
* Commit a tool's write or delete to the workspace tree, with the branch
|
|
1918
2163
|
* intent it implies (store origin for a path disk never agreed with; a
|
|
@@ -1922,17 +2167,20 @@ export class WorkspaceModule implements Module {
|
|
|
1922
2167
|
* The push follows materialize's rule: a disk copy that changed since it
|
|
1923
2168
|
* last agreed is not overwritten or unlinked, and the refusal says why.
|
|
1924
2169
|
*
|
|
1925
|
-
*
|
|
1926
|
-
*
|
|
1927
|
-
* that disk is the source of truth for
|
|
1928
|
-
*
|
|
2170
|
+
* Answers, so callers surface them in the tool result, the reason when the
|
|
2171
|
+
* change reached the workspace but not disk (`refused`): the
|
|
2172
|
+
* autoMaterialize contract is that disk is the source of truth for
|
|
2173
|
+
* downstream agents. And, apart from it, why a write whose bytes reached
|
|
2174
|
+
* disk left the file's mode as disk had it (`modeKept`). The caller holds
|
|
2175
|
+
* the mount's turn and has settled the path first (settleBeforeMutation, or
|
|
2176
|
+
* a pass over the path, as an edit makes).
|
|
1929
2177
|
*/
|
|
1930
2178
|
private async commitToolChange(
|
|
1931
2179
|
mount: MountState,
|
|
1932
2180
|
relativePath: string,
|
|
1933
2181
|
change: { kind: 'set'; blobHash: string; size: number; mode?: number } | { kind: 'remove'; previousHash: string },
|
|
1934
2182
|
onCommitted?: () => void,
|
|
1935
|
-
): Promise<string | null> {
|
|
2183
|
+
): Promise<{ refused: string | null; modeKept: string | null }> {
|
|
1936
2184
|
const store = this.getStore();
|
|
1937
2185
|
const intents = this.intents.get(mount.config.name)!;
|
|
1938
2186
|
// The mutation's precondition, enforced here, synchronously with the
|
|
@@ -1956,20 +2204,21 @@ export class WorkspaceModule implements Module {
|
|
|
1956
2204
|
});
|
|
1957
2205
|
} else {
|
|
1958
2206
|
store.treeRemove(mount.treeStateId, relativePath);
|
|
2207
|
+
onCommitted?.();
|
|
1959
2208
|
intents.update(relativePath, (cur) => {
|
|
1960
2209
|
const next = { ...cur, tombstone: { hash: change.previousHash } };
|
|
1961
2210
|
delete next.origin;
|
|
1962
2211
|
return next;
|
|
1963
2212
|
});
|
|
1964
2213
|
}
|
|
1965
|
-
if (!mount
|
|
2214
|
+
if (!this.pushesOnCommit(mount)) return { refused: null, modeKept: null };
|
|
1966
2215
|
|
|
1967
2216
|
const pushed = await this.pushUnlocked(mount, [relativePath], {
|
|
1968
2217
|
applyDeletions: change.kind === 'remove',
|
|
1969
2218
|
branchId: store.currentBranch().id,
|
|
1970
2219
|
});
|
|
1971
2220
|
const refused = pushed.skipped.find((s) => s.path === relativePath);
|
|
1972
|
-
if (!refused) return null;
|
|
2221
|
+
if (!refused) return { refused: null, modeKept: pushed.modesKept.find((k) => k.path === relativePath)?.reason ?? null };
|
|
1973
2222
|
this.ctx?.pushEvent({
|
|
1974
2223
|
type: 'workspace:materialize-failed',
|
|
1975
2224
|
mount: mount.config.name,
|
|
@@ -1977,7 +2226,7 @@ export class WorkspaceModule implements Module {
|
|
|
1977
2226
|
op: change.kind === 'set' ? 'write' : 'delete',
|
|
1978
2227
|
error: refused.reason,
|
|
1979
2228
|
} as ProcessEvent);
|
|
1980
|
-
return refused.reason;
|
|
2229
|
+
return { refused: refused.reason, modeKept: null };
|
|
1981
2230
|
}
|
|
1982
2231
|
|
|
1983
2232
|
// ==========================================================================
|
|
@@ -1990,24 +2239,29 @@ export class WorkspaceModule implements Module {
|
|
|
1990
2239
|
* disk now holds (an edit sees a disk change the settled adoption took in).
|
|
1991
2240
|
* The pass may not settle it (a branch that keeps changing, an unreadable
|
|
1992
2241
|
* disk copy); the precondition that matters is enforced again,
|
|
1993
|
-
* synchronously, where the change commits (commitToolChange). The
|
|
1994
|
-
*
|
|
2242
|
+
* synchronously, where the change commits (commitToolChange). The pass
|
|
2243
|
+
* names the path, as a file tool names the one it acts on, so a path the
|
|
2244
|
+
* mount passes over by design is observed there, as a sync naming it would
|
|
2245
|
+
* observe it (#276). The caller holds the mount's turn.
|
|
1995
2246
|
*/
|
|
1996
2247
|
private async settleBeforeMutation(mount: MountState, relativePath: string): Promise<void> {
|
|
1997
2248
|
if (this.agreementOrThrow().get(mount.config.name, relativePath)?.kind !== 'pending') return;
|
|
1998
|
-
await this.passUnlocked(mount, { kind: 'paths', paths: [relativePath] });
|
|
2249
|
+
await this.passUnlocked(mount, { kind: 'paths', paths: [relativePath] }, { named: true });
|
|
1999
2250
|
}
|
|
2000
2251
|
|
|
2001
2252
|
/**
|
|
2002
2253
|
* Before a read: a path the workspace doesn't hold is observed under the
|
|
2003
2254
|
* three-way rule, which ingests a new disk file but never brings back a
|
|
2004
|
-
* workspace deletion or a file absent from this branch. The
|
|
2005
|
-
* the mount
|
|
2255
|
+
* workspace deletion or a file absent from this branch. The pass names the
|
|
2256
|
+
* path, as a file tool names the one it acts on: a path the mount passes
|
|
2257
|
+
* over by design is observed, as a sync naming it would observe it (#276),
|
|
2258
|
+
* while a path in another mount's directory is refused before this, and
|
|
2259
|
+
* passed over by any pass. The caller holds the mount's turn.
|
|
2006
2260
|
*/
|
|
2007
2261
|
private async ensureSynced(mount: MountState, relativePath: string): Promise<PathReport | undefined> {
|
|
2008
2262
|
const store = this.getStore();
|
|
2009
2263
|
if (store.treeGet(mount.treeStateId, relativePath)) return undefined;
|
|
2010
|
-
const pass = await this.passUnlocked(mount, { kind: 'paths', paths: [relativePath] });
|
|
2264
|
+
const pass = await this.passUnlocked(mount, { kind: 'paths', paths: [relativePath] }, { named: true });
|
|
2011
2265
|
return pass.reports.get(relativePath);
|
|
2012
2266
|
}
|
|
2013
2267
|
|
|
@@ -2039,19 +2293,14 @@ export class WorkspaceModule implements Module {
|
|
|
2039
2293
|
const characterPaging = input.offsetChars !== undefined || input.limitChars !== undefined;
|
|
2040
2294
|
const offsetChars = input.offsetChars ?? 0;
|
|
2041
2295
|
const limitChars = input.limitChars ?? 2000;
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
}
|
|
2046
|
-
if (!Number.isSafeInteger(offsetChars) || offsetChars < 0
|
|
2047
|
-
|| !Number.isSafeInteger(limitChars) || limitChars < 1
|
|
2048
|
-
|| input.offsetChars === null || input.limitChars === null) {
|
|
2049
|
-
return { success: false, isError: true, error: 'offsetChars must be a non-negative safe integer and limitChars a positive safe integer.' };
|
|
2050
|
-
}
|
|
2296
|
+
// Each number is a whole number of at least its schema's minimum by now
|
|
2297
|
+
// (inputProblem).
|
|
2298
|
+
if (characterPaging && (input.offset !== undefined || input.limit !== undefined)) {
|
|
2299
|
+
return { success: false, isError: true, error: 'Use either offset/limit (lines) or offsetChars/limitChars (characters), not both.' };
|
|
2051
2300
|
}
|
|
2052
2301
|
let content = generatedContent;
|
|
2053
2302
|
if (content === undefined) {
|
|
2054
|
-
const { mount, relativePath } = this.
|
|
2303
|
+
const { mount, relativePath } = this.parseFilePath(input.path);
|
|
2055
2304
|
const store = this.getStore();
|
|
2056
2305
|
const found = await this.withMount(mount, async (): Promise<{ text: string } | { error: string }> => {
|
|
2057
2306
|
const report = await this.ensureSynced(mount, relativePath);
|
|
@@ -2086,7 +2335,7 @@ export class WorkspaceModule implements Module {
|
|
|
2086
2335
|
},
|
|
2087
2336
|
};
|
|
2088
2337
|
}
|
|
2089
|
-
const lines = content
|
|
2338
|
+
const lines = linesOf(content);
|
|
2090
2339
|
|
|
2091
2340
|
// Apply offset/limit. An unlimited read of a large file would inject the
|
|
2092
2341
|
// whole thing into the turn (a 2MB file ≈ 600k tokens → the request blows
|
|
@@ -2094,6 +2343,22 @@ export class WorkspaceModule implements Module {
|
|
|
2094
2343
|
// default cap; the result reports total/from/to so the agent can page.
|
|
2095
2344
|
const DEFAULT_READ_LINE_LIMIT = 2000;
|
|
2096
2345
|
const startLine = (input.offset ?? 1) - 1; // Convert to 0-indexed
|
|
2346
|
+
if (startLine >= lines.length) {
|
|
2347
|
+
// Nothing to show: an empty file, or an offset past the last line. No
|
|
2348
|
+
// range is claimed, and the line count says where the file ends.
|
|
2349
|
+
return {
|
|
2350
|
+
success: true,
|
|
2351
|
+
data: {
|
|
2352
|
+
path: input.path,
|
|
2353
|
+
totalLines: lines.length,
|
|
2354
|
+
// Reading an empty file from its start is reading it whole.
|
|
2355
|
+
...(startLine > 0
|
|
2356
|
+
? { note: `Line ${startLine + 1} is past the end: the file has ${lines.length} line${lines.length === 1 ? '' : 's'}.` }
|
|
2357
|
+
: {}),
|
|
2358
|
+
content: '',
|
|
2359
|
+
},
|
|
2360
|
+
};
|
|
2361
|
+
}
|
|
2097
2362
|
const effectiveLimit = input.limit ?? DEFAULT_READ_LINE_LIMIT;
|
|
2098
2363
|
const endLine = startLine + effectiveLimit;
|
|
2099
2364
|
const slice = lines.slice(startLine, endLine);
|
|
@@ -2123,7 +2388,7 @@ export class WorkspaceModule implements Module {
|
|
|
2123
2388
|
let mount: MountState;
|
|
2124
2389
|
let relativePath: string;
|
|
2125
2390
|
try {
|
|
2126
|
-
({ mount, relativePath } = this.
|
|
2391
|
+
({ mount, relativePath } = this.parseFilePath(input.path));
|
|
2127
2392
|
} catch (err) {
|
|
2128
2393
|
return {
|
|
2129
2394
|
success: false,
|
|
@@ -2192,7 +2457,10 @@ export class WorkspaceModule implements Module {
|
|
|
2192
2457
|
}
|
|
2193
2458
|
|
|
2194
2459
|
private async handleWrite(input: WriteInput): Promise<ToolResult> {
|
|
2195
|
-
|
|
2460
|
+
if (LONE_SURROGATE.test(input.content)) {
|
|
2461
|
+
return { success: false, isError: true, error: 'The write did nothing: "content" holds a lone UTF-16 surrogate, which UTF-8 can\'t store.' };
|
|
2462
|
+
}
|
|
2463
|
+
const { mount, relativePath } = this.parseFilePath(input.path);
|
|
2196
2464
|
if (mount.config.mode === 'read-only') {
|
|
2197
2465
|
return { success: false, error: `Mount "${mount.config.name}" is read-only`, isError: true };
|
|
2198
2466
|
}
|
|
@@ -2204,126 +2472,211 @@ export class WorkspaceModule implements Module {
|
|
|
2204
2472
|
}
|
|
2205
2473
|
|
|
2206
2474
|
const buffer = Buffer.from(input.content, 'utf-8');
|
|
2207
|
-
const
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2475
|
+
const receipt = { path: input.path, size: buffer.byteLength, hash: hashContent(input.content) };
|
|
2476
|
+
let committed = false;
|
|
2477
|
+
let refused: string | null;
|
|
2478
|
+
let modeKept: string | null;
|
|
2479
|
+
try {
|
|
2480
|
+
({ refused, modeKept } = await this.withMount(mount, async () => {
|
|
2481
|
+
await this.settleBeforeMutation(mount, relativePath);
|
|
2482
|
+
const blobHash = store.storeBlob(buffer, 'text/plain');
|
|
2483
|
+
// An overwrite keeps the file's mode, as an edit does.
|
|
2484
|
+
const existing = store.treeGet(mount.treeStateId, relativePath);
|
|
2485
|
+
return this.commitToolChange(mount, relativePath, { kind: 'set', blobHash, size: buffer.byteLength, ...(existing ? { mode: existing.mode } : {}) },
|
|
2486
|
+
() => { committed = true; });
|
|
2487
|
+
}));
|
|
2488
|
+
} catch (error) {
|
|
2489
|
+
if (!committed) throw error;
|
|
2490
|
+
return storedButUnfinished('write', input.path, { threw: error instanceof Error ? error.message : String(error) });
|
|
2218
2491
|
}
|
|
2492
|
+
if (refused !== null) return storedButUnfinished('write', input.path, { refused });
|
|
2219
2493
|
|
|
2220
|
-
return {
|
|
2221
|
-
success: true,
|
|
2222
|
-
data: {
|
|
2223
|
-
path: input.path,
|
|
2224
|
-
size: Buffer.byteLength(input.content),
|
|
2225
|
-
hash: hashContent(input.content),
|
|
2226
|
-
},
|
|
2227
|
-
};
|
|
2494
|
+
return { success: true, data: { ...receipt, ...(modeKept ? { modeKept } : {}) } };
|
|
2228
2495
|
}
|
|
2229
2496
|
|
|
2230
2497
|
private async handleEdit(input: EditInput): Promise<ToolResult> {
|
|
2231
|
-
|
|
2498
|
+
// Refused before anything is read: an empty oldString matches between
|
|
2499
|
+
// every character, and UTF-8 can't store a lone surrogate.
|
|
2500
|
+
if (input.oldString === '') {
|
|
2501
|
+
return { success: false, isError: true, error: 'The edit did nothing: "oldString" is empty, and an empty string matches between every character. Give the text to replace.' };
|
|
2502
|
+
}
|
|
2503
|
+
for (const field of ['oldString', 'newString'] as const) {
|
|
2504
|
+
if (LONE_SURROGATE.test(input[field])) {
|
|
2505
|
+
return { success: false, isError: true, error: `The edit did nothing: "${field}" holds a lone UTF-16 surrogate, which UTF-8 can't store.` };
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
const { mount, relativePath } = this.parseFilePath(input.path);
|
|
2232
2509
|
if (mount.config.mode === 'read-only') {
|
|
2233
2510
|
return { success: false, error: `Mount "${mount.config.name}" is read-only`, isError: true };
|
|
2234
2511
|
}
|
|
2235
2512
|
|
|
2236
2513
|
const store = this.getStore();
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2514
|
+
let committed = false;
|
|
2515
|
+
let appliedNote: string | null = null;
|
|
2516
|
+
try {
|
|
2517
|
+
return await this.withMount(mount, async (): Promise<ToolResult> => {
|
|
2518
|
+
// The edit applies to the path as disk holds it now: a change on disk
|
|
2519
|
+
// to a file the workspace holds is taken in first, by the rule a
|
|
2520
|
+
// listing uses. The pass settles a pending intent on the path too, as
|
|
2521
|
+
// settleBeforeMutation's would, and the commit enforces the rest
|
|
2522
|
+
// (commitToolChange). A write passes over its path only to settle a
|
|
2523
|
+
// pending intent, since it replaces the whole file. As every file
|
|
2524
|
+
// tool's pass does, it names the path it acts on, so a file the
|
|
2525
|
+
// mount passes over by design is observed (see PassOptions.named).
|
|
2526
|
+
const pass = await this.passUnlocked(mount, { kind: 'paths', paths: [relativePath] }, { named: true });
|
|
2527
|
+
const report = pass.reports.get(relativePath);
|
|
2528
|
+
// A mode taken in alone isn't a change to the text: it brings nothing
|
|
2529
|
+
// new to read, as it makes no event (emitFsEvents), and the edit then
|
|
2530
|
+
// writes at the mode disk has, so it leaves nothing to say.
|
|
2531
|
+
const took = (op: TreeOp) => pass.ops.some((o) => o.path === relativePath && o.op === op && !o.modeOnly);
|
|
2532
|
+
// Which copy the edit used, when it isn't the one the workspace last
|
|
2533
|
+
// agreed on with disk: disk's newer text, or the workspace's copy
|
|
2534
|
+
// because disk's couldn't be read or disk has none. A file the
|
|
2535
|
+
// workspace didn't hold has no older copy, so taking it in needs no
|
|
2536
|
+
// word.
|
|
2537
|
+
const whichCopy = (verb: string): string | null => took('modified')
|
|
2538
|
+
? `Disk's copy had changed since the workspace last agreed with it, so it was taken in first, and the edit ${verb} that text.`
|
|
2539
|
+
: report?.state === 'unverified'
|
|
2540
|
+
? `The path couldn't be checked against disk (${report.note ?? 'not observed'}), so the edit ${verb} the workspace's copy.`
|
|
2541
|
+
: report?.state === 'disk-missing-provenance-unknown'
|
|
2542
|
+
? `Disk has no copy of this file, and nothing records whether it was deleted there, so the edit ${verb} the workspace's copy.`
|
|
2543
|
+
: null;
|
|
2544
|
+
// A conflict the pass found is named when the edit is refused for the
|
|
2545
|
+
// text it found, on either kind of mount, and on a success where the
|
|
2546
|
+
// mount doesn't push, since such a mount keeps the conflict until
|
|
2547
|
+
// materialize or sync settles it. A pushing mount's success met none
|
|
2548
|
+
// (below). A stored edit that couldn't finish leaves it to the push's
|
|
2549
|
+
// reason, which names the conflict, or to ls, which its answer points to.
|
|
2550
|
+
const conflict = report?.state === 'conflict' ? report.conflict?.kind ?? 'both-changed' : null;
|
|
2551
|
+
// Where disk has no copy now, whatever the conflict's kind, a sync of
|
|
2552
|
+
// the path takes disk's deletion and a forced materialize writes the
|
|
2553
|
+
// workspace's copy back. Where it has one, a sync keeps it and gives
|
|
2554
|
+
// up the workspace's, whether or not the workspace can hold disk's
|
|
2555
|
+
// copy: one it can't hold stays on disk alone.
|
|
2556
|
+
const conflictNote = (where: string): string | null => conflict === null ? null
|
|
2557
|
+
: `The path is in conflict with disk (${conflict}): the edit ${where} the workspace's copy. ` + (report?.diskNow === 'absent'
|
|
2558
|
+
? "Sync the path to take disk's deletion, which removes the workspace's copy, or materialize it with force to write the workspace's copy back to disk."
|
|
2559
|
+
: "Sync the path to keep disk's copy and give up the workspace's, or materialize it with force to write the workspace's over disk's.");
|
|
2560
|
+
const suffix = [whichCopy('looked in'), conflictNote('looked in')].filter((n) => n !== null).map((n) => ` ${n}`).join('');
|
|
2561
|
+
appliedNote = whichCopy('applied to');
|
|
2240
2562
|
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2563
|
+
const entry = store.treeGet(mount.treeStateId, relativePath);
|
|
2564
|
+
if (!entry) {
|
|
2565
|
+
// A pass also drops a copy disk made binary or too large, which
|
|
2566
|
+
// the disk-only answer describes; any other drop is disk's deletion.
|
|
2567
|
+
const error = took('deleted') && report?.state !== 'disk-only'
|
|
2568
|
+
? `File not found: ${input.path}. Disk's copy was deleted since the workspace last agreed with it, and that was taken in first.`
|
|
2569
|
+
: this.notInWorkspace(input.path, report);
|
|
2570
|
+
return { success: false, error, isError: true };
|
|
2571
|
+
}
|
|
2245
2572
|
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2573
|
+
const blob = store.getBlob(entry.blobHash);
|
|
2574
|
+
if (!blob) {
|
|
2575
|
+
return { success: false, error: `Blob not found for: ${input.path}`, isError: true };
|
|
2576
|
+
}
|
|
2577
|
+
// Decoding would turn each invalid sequence into U+FFFD, and writing
|
|
2578
|
+
// the text back would change bytes the edit never named.
|
|
2579
|
+
if (!isUtf8(blob)) {
|
|
2580
|
+
return {
|
|
2581
|
+
success: false,
|
|
2582
|
+
isError: true,
|
|
2583
|
+
error: `The edit did nothing: ${input.path} isn't valid UTF-8, so editing it would change bytes outside the text it replaces. `
|
|
2584
|
+
+ `Read shows each invalid byte sequence as U+FFFD, and a write replaces the whole file as UTF-8, so writing back what read shows puts U+FFFD in their place.${suffix}`,
|
|
2585
|
+
};
|
|
2586
|
+
}
|
|
2250
2587
|
|
|
2251
|
-
|
|
2588
|
+
let content = blob.toString('utf-8');
|
|
2252
2589
|
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2590
|
+
// An edit that would replace nothing is refused, replaceAll or not;
|
|
2591
|
+
// without replaceAll, the text must occur once, and the refusal says
|
|
2592
|
+
// how often it does. With replaceAll, whether it occurs is enough.
|
|
2593
|
+
const count = input.replaceAll
|
|
2594
|
+
? Number(content.includes(input.oldString))
|
|
2595
|
+
: content.split(input.oldString).length - 1;
|
|
2256
2596
|
if (count === 0) {
|
|
2257
|
-
return { success: false, error: `String not found in ${input.path}`, isError: true };
|
|
2597
|
+
return { success: false, error: `String not found in ${input.path}.${suffix}`, isError: true };
|
|
2258
2598
|
}
|
|
2259
|
-
if (count > 1) {
|
|
2599
|
+
if (count > 1 && !input.replaceAll) {
|
|
2260
2600
|
return {
|
|
2261
2601
|
success: false,
|
|
2262
|
-
error: `String found ${count} times in ${input.path}. Use replaceAll: true or provide more context
|
|
2602
|
+
error: `String found ${count} times in ${input.path}. Use replaceAll: true or provide more context.${suffix}`,
|
|
2263
2603
|
isError: true,
|
|
2264
2604
|
};
|
|
2265
2605
|
}
|
|
2266
|
-
}
|
|
2267
|
-
|
|
2268
|
-
content = input.replaceAll
|
|
2269
|
-
? content.replaceAll(input.oldString, input.newString)
|
|
2270
|
-
: content.replace(input.oldString, input.newString);
|
|
2271
2606
|
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2607
|
+
// A replacer function, so that the new text goes in as given: in a
|
|
2608
|
+
// replacement string, $&, $$, $' and $` are patterns.
|
|
2609
|
+
content = input.replaceAll
|
|
2610
|
+
? content.replaceAll(input.oldString, () => input.newString)
|
|
2611
|
+
: content.replace(input.oldString, () => input.newString);
|
|
2612
|
+
|
|
2613
|
+
const newBuffer = Buffer.from(content, 'utf-8');
|
|
2614
|
+
const newBlobHash = store.storeBlob(newBuffer, 'text/plain');
|
|
2615
|
+
const { refused, modeKept } = await this.commitToolChange(mount, relativePath, {
|
|
2616
|
+
kind: 'set',
|
|
2617
|
+
blobHash: newBlobHash,
|
|
2618
|
+
size: newBuffer.byteLength,
|
|
2619
|
+
mode: entry.mode,
|
|
2620
|
+
}, () => { committed = true; });
|
|
2621
|
+
if (refused !== null) return storedButUnfinished('edit', input.path, { refused }, appliedNote);
|
|
2622
|
+
|
|
2623
|
+
// The edit makes the path the workspace's own change (commitToolChange),
|
|
2624
|
+
// so a file disk lacked goes back with this edit's push, or with the
|
|
2625
|
+
// next materialize where the mount doesn't push. A push that didn't
|
|
2626
|
+
// complete answers above, with its reason. One that completed met no
|
|
2627
|
+
// conflict, since this push, unforced, stops at one: the edit made the
|
|
2628
|
+
// two copies agree, which settles it, or there was none. So only a
|
|
2629
|
+
// mount that doesn't push still holds the conflict the pass found.
|
|
2630
|
+
const pushes = this.pushesOnCommit(mount);
|
|
2631
|
+
const backOnDisk = report?.state === 'disk-missing-provenance-unknown'
|
|
2632
|
+
? `${pushes ? 'Its push put the file back on disk' : 'A materialize of the path puts it back on disk'}; if it was deleted there on purpose, delete it here too.`
|
|
2633
|
+
: null;
|
|
2634
|
+
const notes = [appliedNote, backOnDisk, pushes ? null : conflictNote('is in')].filter((n): n is string => n !== null);
|
|
2281
2635
|
return {
|
|
2282
|
-
success:
|
|
2283
|
-
|
|
2284
|
-
|
|
2636
|
+
success: true,
|
|
2637
|
+
data: {
|
|
2638
|
+
path: input.path,
|
|
2639
|
+
size: newBuffer.byteLength,
|
|
2640
|
+
...(notes.length > 0 ? { note: notes.join(' ') } : {}),
|
|
2641
|
+
...(modeKept ? { modeKept } : {}),
|
|
2642
|
+
},
|
|
2285
2643
|
};
|
|
2286
|
-
}
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
path: input.path,
|
|
2292
|
-
size: newBuffer.byteLength,
|
|
2293
|
-
},
|
|
2294
|
-
};
|
|
2295
|
-
});
|
|
2644
|
+
});
|
|
2645
|
+
} catch (error) {
|
|
2646
|
+
if (!committed) throw error;
|
|
2647
|
+
return storedButUnfinished('edit', input.path, { threw: error instanceof Error ? error.message : String(error) }, appliedNote);
|
|
2648
|
+
}
|
|
2296
2649
|
}
|
|
2297
2650
|
|
|
2298
2651
|
private async handleDelete(input: DeleteInput): Promise<ToolResult> {
|
|
2299
|
-
const { mount, relativePath } = this.
|
|
2652
|
+
const { mount, relativePath } = this.parseFilePath(input.path);
|
|
2300
2653
|
if (mount.config.mode === 'read-only') {
|
|
2301
2654
|
return { success: false, error: `Mount "${mount.config.name}" is read-only`, isError: true };
|
|
2302
2655
|
}
|
|
2303
2656
|
|
|
2304
2657
|
const store = this.getStore();
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
await this.
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2658
|
+
let committed = false;
|
|
2659
|
+
try {
|
|
2660
|
+
return await this.withMount(mount, async (): Promise<ToolResult> => {
|
|
2661
|
+
const report = await this.ensureSynced(mount, relativePath);
|
|
2662
|
+
await this.settleBeforeMutation(mount, relativePath);
|
|
2663
|
+
const entry = store.treeGet(mount.treeStateId, relativePath);
|
|
2664
|
+
if (!entry) {
|
|
2665
|
+
return { success: false, error: this.notInWorkspace(input.path, report), isError: true };
|
|
2666
|
+
}
|
|
2312
2667
|
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
return {
|
|
2316
|
-
success: false,
|
|
2317
|
-
error: `Removed from Chronicle but did not unlink "${input.path}" from disk: ${materializeError}. ` +
|
|
2318
|
-
'The disk copy stays (ls shows its state) until materialize with applyDeletions removes it.',
|
|
2319
|
-
isError: true,
|
|
2320
|
-
};
|
|
2321
|
-
}
|
|
2668
|
+
const { refused } = await this.commitToolChange(mount, relativePath, { kind: 'remove', previousHash: entry.blobHash },
|
|
2669
|
+
() => { committed = true; });
|
|
2670
|
+
if (refused !== null) return storedButUnfinished('delete', input.path, { refused });
|
|
2322
2671
|
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2672
|
+
// Its whole receipt is what it changed: kept in the stub if the guard
|
|
2673
|
+
// withholds the result (agent-framework #277).
|
|
2674
|
+
return { success: true, data: { path: input.path, deleted: true }, keepWhenWithheld: ['path', 'deleted'] };
|
|
2675
|
+
});
|
|
2676
|
+
} catch (error) {
|
|
2677
|
+
if (!committed) throw error;
|
|
2678
|
+
return storedButUnfinished('delete', input.path, { threw: error instanceof Error ? error.message : String(error) });
|
|
2679
|
+
}
|
|
2327
2680
|
}
|
|
2328
2681
|
|
|
2329
2682
|
private async handleLs(input: LsInput): Promise<ToolResult> {
|
|
@@ -2395,9 +2748,14 @@ export class WorkspaceModule implements Module {
|
|
|
2395
2748
|
? [this.parsePath(input.path)]
|
|
2396
2749
|
: [...this.mounts.values()].map(m => ({ mount: m, relativePath: '' }));
|
|
2397
2750
|
|
|
2751
|
+
// Every match lies beneath the pattern's literal directory, so the walk
|
|
2752
|
+
// starts there: past the walk's cap, a walk of the whole scope can stop
|
|
2753
|
+
// before it gets there.
|
|
2754
|
+
const patternDir = literalDirOf(input.pattern);
|
|
2398
2755
|
for (const { mount, relativePath } of mountsToSearch) {
|
|
2756
|
+
const start = await this.walkStart(mount, relativePath, patternDir);
|
|
2399
2757
|
const pass = await this.withMount(mount, () =>
|
|
2400
|
-
this.passUnlocked(mount, { kind: 'dir', dir:
|
|
2758
|
+
this.passUnlocked(mount, { kind: 'dir', dir: start, recursive: true }));
|
|
2401
2759
|
for (const report of [...pass.reports.values()].sort((a, b) => a.path.localeCompare(b.path))) {
|
|
2402
2760
|
const testPath = relativePath ? report.path.slice(relativePath.length + 1) : report.path;
|
|
2403
2761
|
if (regex.test(testPath)) {
|
|
@@ -2426,10 +2784,15 @@ export class WorkspaceModule implements Module {
|
|
|
2426
2784
|
try {
|
|
2427
2785
|
regex = new RegExp(input.pattern);
|
|
2428
2786
|
} catch (e) {
|
|
2429
|
-
|
|
2787
|
+
// The parser's reason says where the pattern goes wrong.
|
|
2788
|
+
return { success: false, error: `The grep did nothing: ${e instanceof Error ? e.message : String(e)}.`, isError: true };
|
|
2430
2789
|
}
|
|
2431
2790
|
|
|
2432
2791
|
const fileGlob = input.glob ? globToRegex(input.glob) : null;
|
|
2792
|
+
// The filter matches mount-relative paths, so every file it lets through
|
|
2793
|
+
// lies beneath its literal directory, and the walk starts at the deeper of
|
|
2794
|
+
// that and `path`.
|
|
2795
|
+
const filterDir = input.glob ? literalDirOf(input.glob) : '';
|
|
2433
2796
|
const contextBefore = input.contextBefore ?? 0;
|
|
2434
2797
|
const contextAfter = input.contextAfter ?? 0;
|
|
2435
2798
|
|
|
@@ -2443,7 +2806,7 @@ export class WorkspaceModule implements Module {
|
|
|
2443
2806
|
const incomplete: Array<{ path: string; reason: string }> = [];
|
|
2444
2807
|
|
|
2445
2808
|
const search = (content: string): GrepMatch[] => {
|
|
2446
|
-
const lines = content
|
|
2809
|
+
const lines = linesOf(content);
|
|
2447
2810
|
const found: GrepMatch[] = [];
|
|
2448
2811
|
for (let i = 0; i < lines.length; i++) {
|
|
2449
2812
|
if (regex.test(lines[i])) {
|
|
@@ -2460,6 +2823,18 @@ export class WorkspaceModule implements Module {
|
|
|
2460
2823
|
};
|
|
2461
2824
|
|
|
2462
2825
|
for (const { mount, relativePath } of mountsToSearch) {
|
|
2826
|
+
// A path named in another mount's directory, as holderOf resolves it,
|
|
2827
|
+
// is that mount's: grep searches nothing of this mount's there, and
|
|
2828
|
+
// says whose it is.
|
|
2829
|
+
if (this.holderOf(mount.config.name, relativePath)) {
|
|
2830
|
+
incomplete.push({ path: `${mount.config.name}/${relativePath}`, reason: this.otherMountNote(mount.config.name, relativePath, 'list') });
|
|
2831
|
+
continue;
|
|
2832
|
+
}
|
|
2833
|
+
// A `path` and a filter whose directories don't overlap leave no file
|
|
2834
|
+
// the filter can let through, so nothing is searched.
|
|
2835
|
+
const deeper = deeperOf(relativePath, filterDir);
|
|
2836
|
+
if (deeper === null) continue;
|
|
2837
|
+
const start = await this.walkStart(mount, relativePath, deeper === relativePath ? '' : deeper.slice(relativePath ? relativePath.length + 1 : 0));
|
|
2463
2838
|
await this.withMount(mount, async () => {
|
|
2464
2839
|
// A `path` naming a single file greps just that file; otherwise it is a
|
|
2465
2840
|
// directory prefix. (A file path once produced the prefix "notes.md/"
|
|
@@ -2477,7 +2852,7 @@ export class WorkspaceModule implements Module {
|
|
|
2477
2852
|
for (let attempt = 0; attempt < 3 && pass === null; attempt++) {
|
|
2478
2853
|
const next = await this.passUnlocked(mount, fileScope
|
|
2479
2854
|
? { kind: 'paths', paths: [relativePath] }
|
|
2480
|
-
: { kind: 'dir', dir:
|
|
2855
|
+
: { kind: 'dir', dir: start, recursive: true });
|
|
2481
2856
|
if (next.branchId === undefined || next.branchId === store.currentBranch().id) pass = next;
|
|
2482
2857
|
}
|
|
2483
2858
|
if (pass === null) {
|
|
@@ -2504,6 +2879,10 @@ export class WorkspaceModule implements Module {
|
|
|
2504
2879
|
});
|
|
2505
2880
|
continue;
|
|
2506
2881
|
}
|
|
2882
|
+
// A path in another mount's directory is that mount's: grep gives
|
|
2883
|
+
// none of this mount's copy of it, as read refuses it. Beneath a
|
|
2884
|
+
// scope it's passed over by design, like the directory.
|
|
2885
|
+
if (inOtherMount(report.path, this.othersOf(mount.config.name))) continue;
|
|
2507
2886
|
// The workspace's version, labelled with the path's state.
|
|
2508
2887
|
const entry = store.treeGet(mount.treeStateId, report.path);
|
|
2509
2888
|
const blob = entry ? store.getBlob(entry.blobHash) : null;
|
|
@@ -2563,7 +2942,8 @@ export class WorkspaceModule implements Module {
|
|
|
2563
2942
|
if (input.mount !== undefined && name !== input.mount) continue;
|
|
2564
2943
|
const entries = store.treeList(mount.treeStateId);
|
|
2565
2944
|
const currentSeq = store.currentSequence();
|
|
2566
|
-
const
|
|
2945
|
+
const own = this.ownPaths(mount);
|
|
2946
|
+
const intents = this.intents.get(name)!.list().filter(([p]) => own(p));
|
|
2567
2947
|
const conflictPaths = intents.filter(([, bi]) => bi.conflict).map(([p]) => p);
|
|
2568
2948
|
// A workspace deletion is pending until a push confirms it on disk:
|
|
2569
2949
|
// left there without applyDeletions, or unlinked but unconfirmed.
|
|
@@ -2609,12 +2989,13 @@ export class WorkspaceModule implements Module {
|
|
|
2609
2989
|
*/
|
|
2610
2990
|
private materializeSelection(mount: MountState, explicitPath: string): { paths: string[]; inPlace: boolean } {
|
|
2611
2991
|
const store = this.getStore();
|
|
2612
|
-
const
|
|
2992
|
+
const own = this.ownPaths(mount);
|
|
2993
|
+
const intents = new Map(this.intents.get(mount.config.name)!.list(explicitPath).filter(([p]) => own(p)));
|
|
2613
2994
|
const selected = new Set<string>();
|
|
2614
2995
|
|
|
2615
2996
|
if (explicitPath !== '') {
|
|
2616
|
-
if (store.treeGet(mount.treeStateId, explicitPath)) selected.add(explicitPath);
|
|
2617
|
-
for (const e of store.treeList(mount.treeStateId, explicitPath + '/')) selected.add(e.path);
|
|
2997
|
+
if (store.treeGet(mount.treeStateId, explicitPath) && own(explicitPath)) selected.add(explicitPath);
|
|
2998
|
+
for (const e of store.treeList(mount.treeStateId, explicitPath + '/')) if (own(e.path)) selected.add(e.path);
|
|
2618
2999
|
for (const [p, bi] of intents) if (bi.tombstone || bi.conflict) selected.add(p);
|
|
2619
3000
|
return { paths: [...selected], inPlace: false };
|
|
2620
3001
|
}
|
|
@@ -2625,10 +3006,25 @@ export class WorkspaceModule implements Module {
|
|
|
2625
3006
|
return { paths: [...selected], inPlace: agreed > 0 };
|
|
2626
3007
|
}
|
|
2627
3008
|
|
|
3009
|
+
/**
|
|
3010
|
+
* Whether a path of the mount is its own to push and to count as owed:
|
|
3011
|
+
* not in another mount's directory nested in it. Copies the mount's tree
|
|
3012
|
+
* holds there (stored before the nesting) stay in its history and its
|
|
3013
|
+
* listings (and in status's fileCount, which counts the tree), passed over
|
|
3014
|
+
* by its syncs, and left out of what it pushes and of status's pending
|
|
3015
|
+
* changes, conflicts and workspace deletions.
|
|
3016
|
+
*/
|
|
3017
|
+
private ownPaths(mount: MountState): (path: string) => boolean {
|
|
3018
|
+
const others = this.othersOf(mount.config.name);
|
|
3019
|
+
return (path) => !inOtherMount(path, others);
|
|
3020
|
+
}
|
|
3021
|
+
|
|
2628
3022
|
/**
|
|
2629
3023
|
* The workspace entries by what the evidence says of disk. `owed`: P
|
|
2630
|
-
* differs from the entry (a draft, or a push still pending),
|
|
2631
|
-
*
|
|
3024
|
+
* differs from the entry (a draft, or a push still pending), the entry
|
|
3025
|
+
* records a mode other than the one P agreed (a mode owed to disk), or
|
|
3026
|
+
* there is no P and the entry came from the workspace (a draft disk never
|
|
3027
|
+
* had).
|
|
2632
3028
|
* `unchecked`: no P and no workspace origin, an entry from before the
|
|
2633
3029
|
* evidence; a push agrees it, writes it where disk lacks it, or refuses it
|
|
2634
3030
|
* as a conflict. `agreed`: how many have P = S, so disk holds them. Status
|
|
@@ -2643,10 +3039,15 @@ export class WorkspaceModule implements Module {
|
|
|
2643
3039
|
const owed: string[] = [];
|
|
2644
3040
|
const unchecked: string[] = [];
|
|
2645
3041
|
let agreed = 0;
|
|
3042
|
+
const own = this.ownPaths(mount);
|
|
2646
3043
|
for (const e of store.treeList(mount.treeStateId)) {
|
|
3044
|
+
if (!own(e.path)) continue;
|
|
2647
3045
|
const p = agreement.get(name, e.path);
|
|
2648
3046
|
if (p === undefined) (intents.get(e.path)?.origin === 'store' ? owed : unchecked).push(e.path);
|
|
2649
3047
|
else if (p.kind !== 'content' || p.hash !== e.blobHash) owed.push(e.path);
|
|
3048
|
+
// A mode the store records, other than the one disk last agreed with,
|
|
3049
|
+
// where modes are observed: elsewhere no push applies one.
|
|
3050
|
+
else if (this.modesObserved && p.mode !== undefined && (entryMode(e.mode) ?? p.mode) !== p.mode) owed.push(e.path);
|
|
2650
3051
|
else agreed++;
|
|
2651
3052
|
}
|
|
2652
3053
|
return { owed, unchecked, agreed };
|
|
@@ -2658,6 +3059,7 @@ export class WorkspaceModule implements Module {
|
|
|
2658
3059
|
const allWritten: Array<{ mount: string; path: string }> = [];
|
|
2659
3060
|
const allDeleted: Array<{ mount: string; path: string }> = [];
|
|
2660
3061
|
const pendingDeletions: Array<{ mount: string; path: string }> = [];
|
|
3062
|
+
const modesKept: Array<{ mount: string; path: string; reason: string }> = [];
|
|
2661
3063
|
|
|
2662
3064
|
let explicit: { mount: MountState; relativePath: string } | null = null;
|
|
2663
3065
|
if (input.path) explicit = this.parsePath(input.path);
|
|
@@ -2696,6 +3098,13 @@ export class WorkspaceModule implements Module {
|
|
|
2696
3098
|
blocked.push({ mount: name, reason: 'a read-only mount: materialize writes nothing to it' });
|
|
2697
3099
|
continue;
|
|
2698
3100
|
}
|
|
3101
|
+
// A path named in another mount's directory, as holderOf resolves it,
|
|
3102
|
+
// is that mount's: nothing of this mount's is pushed there, and the
|
|
3103
|
+
// caller is told whose it is.
|
|
3104
|
+
if (explicit && this.holderOf(name, explicit.relativePath)) {
|
|
3105
|
+
blocked.push({ mount: name, reason: `${explicit.relativePath}: ${this.otherMountNote(name, explicit.relativePath, 'push')}` });
|
|
3106
|
+
continue;
|
|
3107
|
+
}
|
|
2699
3108
|
const turn = await this.withMount(mount, async (): Promise<{ blocked: string } | { pushed: PushResult; inPlace: boolean }> => {
|
|
2700
3109
|
const branchId = store.currentBranch().id;
|
|
2701
3110
|
const reason = this.mountMaterializeBlockReason(store, mount);
|
|
@@ -2725,8 +3134,12 @@ export class WorkspaceModule implements Module {
|
|
|
2725
3134
|
// Refusals ride the same skipped list as branch blocks: divergence must
|
|
2726
3135
|
// be visible, not resolved silently either way.
|
|
2727
3136
|
for (const s of pushed.skipped) {
|
|
2728
|
-
|
|
3137
|
+
const reason = s.reason === NOT_PUSHED_IN_OTHER_MOUNT ? this.otherMountNote(name, s.path, 'push') : s.reason;
|
|
3138
|
+
blocked.push({ mount: name, reason: `${s.path}: ${reason}` });
|
|
2729
3139
|
}
|
|
3140
|
+
// Written paths whose mode stayed as disk had it: no refusal, so not
|
|
3141
|
+
// among what was skipped.
|
|
3142
|
+
for (const k of pushed.modesKept) modesKept.push({ mount: name, ...k });
|
|
2730
3143
|
|
|
2731
3144
|
// What disk still owes is kept by its evidence, so the watermark and
|
|
2732
3145
|
// the pin only feed the branch guard. Both are the push's own: what it
|
|
@@ -2769,10 +3182,11 @@ export class WorkspaceModule implements Module {
|
|
|
2769
3182
|
}
|
|
2770
3183
|
: {}),
|
|
2771
3184
|
...(blocked.length > 0 ? { skipped: blocked } : {}),
|
|
3185
|
+
...(modesKept.length > 0 ? { modesKept } : {}),
|
|
2772
3186
|
},
|
|
2773
3187
|
// What disk lost, then what it was written: kept in the stub if the
|
|
2774
|
-
// guard withholds the result (agent-framework #277). What was left
|
|
2775
|
-
// skipped, and why, is in the full result.
|
|
3188
|
+
// guard withholds the result (agent-framework #277). What was left,
|
|
3189
|
+
// skipped or written at disk's mode, and why, is in the full result.
|
|
2776
3190
|
keepWhenWithheld: ['deleted', 'materialized'],
|
|
2777
3191
|
};
|
|
2778
3192
|
}
|
|
@@ -2851,7 +3265,8 @@ export class WorkspaceModule implements Module {
|
|
|
2851
3265
|
if (input.acceptRoot && await this.acceptRootUnlocked(mount)) rootsAccepted.push(name);
|
|
2852
3266
|
if (!relativePath) {
|
|
2853
3267
|
// A full recheck: every file rehashed, nothing pending discarded.
|
|
2854
|
-
|
|
3268
|
+
// Past the mount's walkCap, it resumes where the last one stopped.
|
|
3269
|
+
return this.passUnlocked(mount, { kind: 'dir', dir: '', recursive: true }, { rehash: true, resume: true });
|
|
2855
3270
|
}
|
|
2856
3271
|
// An explicit path: the workspace takes disk's state there.
|
|
2857
3272
|
const prefix = relativePath + '/';
|
|
@@ -2864,7 +3279,7 @@ export class WorkspaceModule implements Module {
|
|
|
2864
3279
|
// beneath it (a directory scope's walk; a path scope doesn't walk).
|
|
2865
3280
|
const opts: PassOptions = { rehash: true, adopt: () => true, branchId: askedOn, named: true };
|
|
2866
3281
|
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);
|
|
3282
|
+
const beneath = await this.passUnlocked(mount, { kind: 'dir', dir: relativePath, recursive: true }, { ...opts, resume: true });
|
|
2868
3283
|
// Something at the path itself is no part of a directory scope: a file
|
|
2869
3284
|
// disk has where the workspace holds a directory, or a workspace file
|
|
2870
3285
|
// where disk has a directory. Its own pass takes disk's state there
|
|
@@ -2914,13 +3329,20 @@ export class WorkspaceModule implements Module {
|
|
|
2914
3329
|
for (const region of pass.incomplete) allIncomplete.push({ mount: name, ...region });
|
|
2915
3330
|
for (const [reason, count] of pass.passedOver) allPassedOver.push({ mount: name, reason, count });
|
|
2916
3331
|
// A path named explicitly is synced even where the mount's ignore list
|
|
2917
|
-
// covers it, and the result says so: a sync
|
|
2918
|
-
// over it, so nothing else keeps it current
|
|
2919
|
-
|
|
3332
|
+
// or an editor's file name covers it, and the result says so: a sync
|
|
3333
|
+
// without a path passes over it, so nothing else keeps it current
|
|
3334
|
+
// (agent-framework #276). A path in another mount's directory isn't
|
|
3335
|
+
// synced at all, and the note says so.
|
|
3336
|
+
const passedOver = passedOverAt(relativePath, mount.config.ignore ?? [], this.othersOf(name));
|
|
3337
|
+
if (passedOver !== undefined) {
|
|
2920
3338
|
allIgnored.push({
|
|
2921
3339
|
mount: name,
|
|
2922
3340
|
path: relativePath,
|
|
2923
|
-
note:
|
|
3341
|
+
note: passedOver === OTHER_MOUNT
|
|
3342
|
+
? this.otherMountNote(name, relativePath, 'sync')
|
|
3343
|
+
: passedOver === IGNORED
|
|
3344
|
+
? 'this mount\'s ignore list covers it, so a sync without a path won\'t maintain it'
|
|
3345
|
+
: 'it has an editor\'s swap or backup file name, or lies under one, so a sync without a path won\'t maintain it',
|
|
2924
3346
|
});
|
|
2925
3347
|
}
|
|
2926
3348
|
}
|
|
@@ -2988,10 +3410,14 @@ export class WorkspaceModule implements Module {
|
|
|
2988
3410
|
* After an agent's completed tool batch, before anything continues its turn:
|
|
2989
3411
|
* scan every `watch: 'on-agent-action'` mount, so what the batch's tools did
|
|
2990
3412
|
* on disk (a shell command's files, a deletion) is in the workspace when the
|
|
2991
|
-
* agent next looks.
|
|
2992
|
-
*
|
|
2993
|
-
*
|
|
2994
|
-
*
|
|
3413
|
+
* agent next looks. On a mount over its walkCap, that holds for the first
|
|
3414
|
+
* walkCap entries in the walk's order, which every batch's scan takes; files
|
|
3415
|
+
* past them come in through syncs and the listings that reach them, while a
|
|
3416
|
+
* read takes in only a file the workspace doesn't hold yet. A scan that
|
|
3417
|
+
* outlasts the deadline stops holding the round: the miss is recorded on
|
|
3418
|
+
* the mount's status and pushed as a `workspace:agent-action-scan-incomplete`
|
|
3419
|
+
* event, and the scan continues, deciding on the branch selected when it
|
|
3420
|
+
* applies.
|
|
2995
3421
|
*/
|
|
2996
3422
|
async onToolBatchComplete(_agentName?: string): Promise<void> {
|
|
2997
3423
|
// Every agent's workspace is the store's current branch, so the agent
|
|
@@ -3057,6 +3483,10 @@ export class WorkspaceModule implements Module {
|
|
|
3057
3483
|
const scan: AgentActionScan = { late: false, settled: Promise.resolve() };
|
|
3058
3484
|
const pass = this.withMount(mount, () => {
|
|
3059
3485
|
if (mount.agentActionScanWaiting === scan) mount.agentActionScanWaiting = undefined;
|
|
3486
|
+
// From the mount's root, keeping no place, as a listing keeps none:
|
|
3487
|
+
// on a mount over its walkCap, every batch's scan takes the first
|
|
3488
|
+
// walkCap entries in the walk's order, and the place that syncs and
|
|
3489
|
+
// catch-up scans resume from stays theirs.
|
|
3060
3490
|
return this.passUnlocked(mount, { kind: 'dir', dir: '', recursive: true });
|
|
3061
3491
|
});
|
|
3062
3492
|
scan.settled = pass.then(
|
|
@@ -3101,13 +3531,15 @@ export class WorkspaceModule implements Module {
|
|
|
3101
3531
|
}
|
|
3102
3532
|
|
|
3103
3533
|
/**
|
|
3104
|
-
* Push one ProcessEvent per op for a pass's tree changes and new conflicts
|
|
3105
|
-
*
|
|
3106
|
-
*
|
|
3534
|
+
* Push one ProcessEvent per op for a pass's tree changes and new conflicts,
|
|
3535
|
+
* apart from an op that changes only a file's mode, which brings nothing
|
|
3536
|
+
* new to read and makes none. A conflict rides the event for the disk
|
|
3537
|
+
* change behind it (modified, or deleted for a disk deletion of a file with
|
|
3538
|
+
* newer workspace changes).
|
|
3107
3539
|
*/
|
|
3108
3540
|
private emitFsEvents(
|
|
3109
3541
|
mountName: string,
|
|
3110
|
-
ops: Array<{ path: string; op: TreeOp }>,
|
|
3542
|
+
ops: Array<{ path: string; op: TreeOp; modeOnly?: true }>,
|
|
3111
3543
|
newConflicts: Array<{ path: string; op: TreeOp }> = [],
|
|
3112
3544
|
): void {
|
|
3113
3545
|
if (!this.ctx || (ops.length === 0 && newConflicts.length === 0)) return;
|
|
@@ -3120,7 +3552,10 @@ export class WorkspaceModule implements Module {
|
|
|
3120
3552
|
if (conflict) group.conflicts.push(full);
|
|
3121
3553
|
byOp.set(op, group);
|
|
3122
3554
|
};
|
|
3123
|
-
|
|
3555
|
+
// A path whose mode alone changed, on bytes already held, makes no
|
|
3556
|
+
// event: it brings nothing new to read, and a `chmod -R` over a tree
|
|
3557
|
+
// would otherwise reach every policy that matches its paths.
|
|
3558
|
+
for (const { path, op, modeOnly } of ops) if (!modeOnly) add(op, path, false);
|
|
3124
3559
|
for (const { path, op } of newConflicts) add(op, path, true);
|
|
3125
3560
|
|
|
3126
3561
|
for (const [op, { paths, conflicts }] of byOp) {
|
|
@@ -3151,8 +3586,103 @@ function stateView(report: PathReport): Record<string, unknown> {
|
|
|
3151
3586
|
// Utilities
|
|
3152
3587
|
// ==========================================================================
|
|
3153
3588
|
|
|
3589
|
+
/** A lone UTF-16 surrogate, which UTF-8 can't store: in `u` mode, a pair is one code point. */
|
|
3590
|
+
const LONE_SURROGATE = /\p{Cs}/u;
|
|
3591
|
+
|
|
3592
|
+
/**
|
|
3593
|
+
* The answer to a write, edit or delete that the workspace stored but
|
|
3594
|
+
* couldn't finish: its push to disk didn't complete, or what follows the tree
|
|
3595
|
+
* entry threw. It stays an error, since part of the call failed, but it leads
|
|
3596
|
+
* with what the caller must know before acting on it: the change is stored,
|
|
3597
|
+
* and sending it again could apply it twice. The push's reason says what disk
|
|
3598
|
+
* holds and the way out, as materialize takes it. The text carries all of it,
|
|
3599
|
+
* since an error's data reaches neither the model nor a code_execution
|
|
3600
|
+
* script. An edit's note on which copy it applied to comes last.
|
|
3601
|
+
*/
|
|
3602
|
+
function storedButUnfinished(
|
|
3603
|
+
tool: 'write' | 'edit' | 'delete',
|
|
3604
|
+
path: string,
|
|
3605
|
+
outcome: { refused: string } | { threw: string },
|
|
3606
|
+
note: string | null = null,
|
|
3607
|
+
): ToolResult {
|
|
3608
|
+
const change = tool === 'delete' ? 'deletion' : tool;
|
|
3609
|
+
const stored = `The ${change} of "${path}" is stored in the workspace, so don't send it again`
|
|
3610
|
+
+ (tool === 'edit' ? ': a second edit can apply the change twice.' : '.');
|
|
3611
|
+
const rest = 'refused' in outcome
|
|
3612
|
+
? ` Its push to disk didn't complete: ${outcome.refused}.`
|
|
3613
|
+
: ` The rest of the call failed: ${outcome.threw}. ls shows the path's state.`;
|
|
3614
|
+
return { success: false, isError: true, error: stored + rest + (note !== null ? ` ${note}` : '') };
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
/**
|
|
3618
|
+
* A text's lines, as read and grep number them: a final line end ends the
|
|
3619
|
+
* last line rather than starting an empty one, so "a\nb\n" is two lines,
|
|
3620
|
+
* and an empty text has none.
|
|
3621
|
+
*/
|
|
3622
|
+
function linesOf(content: string): string[] {
|
|
3623
|
+
const lines = content.split('\n');
|
|
3624
|
+
if (lines.at(-1) === '') lines.pop();
|
|
3625
|
+
return lines;
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3628
|
+
/**
|
|
3629
|
+
* The least each tool's whole-number fields may be, beside their schemas,
|
|
3630
|
+
* whose type (membrane's ToolParameter) has no `minimum`.
|
|
3631
|
+
*/
|
|
3632
|
+
const INPUT_MINIMUMS: Record<string, Record<string, number>> = {
|
|
3633
|
+
read: { offset: 1, limit: 1, offsetChars: 0, limitChars: 1 },
|
|
3634
|
+
grep: { contextBefore: 0, contextAfter: 0 },
|
|
3635
|
+
};
|
|
3636
|
+
|
|
3637
|
+
/**
|
|
3638
|
+
* Why a tool call's input doesn't fit its tool's schema, or null when it does:
|
|
3639
|
+
* an object, each required field present, and each field its schema calls a
|
|
3640
|
+
* string, a boolean or an integer of that type, an integer at least its
|
|
3641
|
+
* minimum. The workspace's schemas use no other types. Without it, an edit
|
|
3642
|
+
* missing `newString` wrote the word "undefined", and an offset of 1.5
|
|
3643
|
+
* numbered lines 1.5, 2.5 and on. A field the schema doesn't describe is left
|
|
3644
|
+
* to its handler.
|
|
3645
|
+
*/
|
|
3646
|
+
function inputProblem(
|
|
3647
|
+
schema: { properties?: Record<string, unknown>; required?: string[] },
|
|
3648
|
+
input: unknown,
|
|
3649
|
+
minimums: Record<string, number> = {},
|
|
3650
|
+
): string | null {
|
|
3651
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
3652
|
+
return 'its input must be an object of the fields its schema lists';
|
|
3653
|
+
}
|
|
3654
|
+
const fields = input as Record<string, unknown>;
|
|
3655
|
+
for (const name of schema.required ?? []) {
|
|
3656
|
+
if (fields[name] === undefined) return `"${name}" is required`;
|
|
3657
|
+
}
|
|
3658
|
+
for (const [name, value] of Object.entries(fields)) {
|
|
3659
|
+
const type = (schema.properties?.[name] as { type?: string } | undefined)?.type;
|
|
3660
|
+
if (value === undefined || type === undefined) continue;
|
|
3661
|
+
const minimum = minimums[name];
|
|
3662
|
+
const fits = type === 'string' ? typeof value === 'string'
|
|
3663
|
+
: type === 'boolean' ? typeof value === 'boolean'
|
|
3664
|
+
: type === 'integer'
|
|
3665
|
+
? Number.isSafeInteger(value) && (minimum === undefined || (value as number) >= minimum)
|
|
3666
|
+
: true;
|
|
3667
|
+
if (!fits) {
|
|
3668
|
+
const wanted = type === 'string' ? 'a string'
|
|
3669
|
+
: type === 'boolean' ? 'true or false'
|
|
3670
|
+
: minimum === undefined ? 'a whole number' : `a whole number of at least ${minimum}`;
|
|
3671
|
+
const got = value === null ? 'null'
|
|
3672
|
+
: typeof value === 'number' ? String(value)
|
|
3673
|
+
: Array.isArray(value) ? 'an array'
|
|
3674
|
+
: typeof value === 'object' ? 'an object'
|
|
3675
|
+
: `a ${typeof value}`;
|
|
3676
|
+
return `"${name}" must be ${wanted}, not ${got}`;
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3679
|
+
return null;
|
|
3680
|
+
}
|
|
3681
|
+
|
|
3154
3682
|
/**
|
|
3155
|
-
* Convert a glob pattern to a RegExp.
|
|
3683
|
+
* Convert a glob pattern to a RegExp. Throws, naming the pattern, for a brace
|
|
3684
|
+
* group it can't read: a "{" that never closes, a group inside another, or a
|
|
3685
|
+
* group with no ",".
|
|
3156
3686
|
*/
|
|
3157
3687
|
function globToRegex(pattern: string): RegExp {
|
|
3158
3688
|
// Split pattern into segments, handling {a,b,c} alternation
|
|
@@ -3160,13 +3690,33 @@ function globToRegex(pattern: string): RegExp {
|
|
|
3160
3690
|
let i = 0;
|
|
3161
3691
|
while (i < pattern.length) {
|
|
3162
3692
|
if (pattern[i] === '{') {
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
}
|
|
3693
|
+
// A brace form this can't read is refused. A `{` that never closes, read
|
|
3694
|
+
// as a literal, would almost always answer with no matches, and with it
|
|
3695
|
+
// refused, each pass of this loop moves past at least one character. A
|
|
3696
|
+
// group closes at the `}` that brings the braces open since its `{` back
|
|
3697
|
+
// to none, so a `{` that never closes is named as that, even with a
|
|
3698
|
+
// closed group after it. A group inside another would be split at the
|
|
3699
|
+
// inner `}`, so `{a,{b,c}}` would match `a}` but not `a`, and a group
|
|
3700
|
+
// with no `,` would lose its braces, so `{name}` would match `name` and
|
|
3701
|
+
// never a file named `{name}`, and `{1..3}` would match only `1..3`.
|
|
3702
|
+
let depth = 0;
|
|
3703
|
+
let closeIdx = -1;
|
|
3704
|
+
for (let j = i; j < pattern.length && closeIdx < 0; j++) {
|
|
3705
|
+
if (pattern[j] === '{') depth++;
|
|
3706
|
+
else if (pattern[j] === '}' && --depth === 0) closeIdx = j;
|
|
3707
|
+
}
|
|
3708
|
+
if (closeIdx < 0) throw new Error(braceRefusal(pattern, 'has a "{" with no closing "}"'));
|
|
3709
|
+
const group = pattern.slice(i + 1, closeIdx);
|
|
3710
|
+
if (group.includes('{')) {
|
|
3711
|
+
throw new Error(braceRefusal(pattern, 'has a "{" group inside another; put all its alternatives in one group'));
|
|
3712
|
+
}
|
|
3713
|
+
if (!group.includes(',')) {
|
|
3714
|
+
throw new Error(braceRefusal(pattern, 'has a "{" group with no ","; a group needs two or more alternatives, separated by ","'));
|
|
3715
|
+
}
|
|
3716
|
+
const alternatives = group.split(',');
|
|
3717
|
+
regex += '(?:' + alternatives.map(a => globPartToRegex(a)).join('|') + ')';
|
|
3718
|
+
i = closeIdx + 1;
|
|
3719
|
+
continue;
|
|
3170
3720
|
}
|
|
3171
3721
|
// Accumulate non-brace characters, convert as a chunk
|
|
3172
3722
|
let chunk = '';
|
|
@@ -3181,6 +3731,36 @@ function globToRegex(pattern: string): RegExp {
|
|
|
3181
3731
|
return new RegExp(`^${regex}$`);
|
|
3182
3732
|
}
|
|
3183
3733
|
|
|
3734
|
+
/**
|
|
3735
|
+
* The directory a glob pattern names before its first wildcard, beneath
|
|
3736
|
+
* which every path it matches lies: its leading segments that hold none of
|
|
3737
|
+
* globToRegex's wildcards (`*`, `?`, `{`), short of the last segment, which
|
|
3738
|
+
* names the files. '' when the pattern starts with a wildcard or has a
|
|
3739
|
+
* single segment. A segment that's empty, `.` or `..`, which no path
|
|
3740
|
+
* matched holds, is one no directory listing holds either, so walkStart
|
|
3741
|
+
* stops before it.
|
|
3742
|
+
*/
|
|
3743
|
+
function literalDirOf(pattern: string): string {
|
|
3744
|
+
const dir: string[] = [];
|
|
3745
|
+
for (const segment of pattern.split('/').slice(0, -1)) {
|
|
3746
|
+
if (/[*?{]/.test(segment)) break;
|
|
3747
|
+
dir.push(segment);
|
|
3748
|
+
}
|
|
3749
|
+
return dir.join('/');
|
|
3750
|
+
}
|
|
3751
|
+
|
|
3752
|
+
/** The deeper of two mount-relative directories, '' being the root, or null when neither holds the other. */
|
|
3753
|
+
function deeperOf(a: string, b: string): string | null {
|
|
3754
|
+
if (a === '' || b === a || b.startsWith(a + '/')) return b;
|
|
3755
|
+
if (b === '' || a.startsWith(b + '/')) return a;
|
|
3756
|
+
return null;
|
|
3757
|
+
}
|
|
3758
|
+
|
|
3759
|
+
/** A glob's refusal of a brace form it can't read, with the way to match a literal "{". */
|
|
3760
|
+
function braceRefusal(pattern: string, problem: string): string {
|
|
3761
|
+
return `glob pattern ${JSON.stringify(pattern)} ${problem}. To match a "{" in a name, put "?" in its place.`;
|
|
3762
|
+
}
|
|
3763
|
+
|
|
3184
3764
|
function globPartToRegex(part: string): string {
|
|
3185
3765
|
return part
|
|
3186
3766
|
.replace(/[.+^$()|[\]\\]/g, '\\$&')
|