@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
|
@@ -4,19 +4,28 @@
|
|
|
4
4
|
* Provides a unified workspace with mount-based filesystem access,
|
|
5
5
|
* auto-sync between real filesystem and Chronicle, and manual materialization.
|
|
6
6
|
*/
|
|
7
|
+
import { isUtf8 } from 'node:buffer';
|
|
7
8
|
import { constants as fsConstants } from 'node:fs';
|
|
8
|
-
import { open, readFile, stat, lstat, realpath } from 'node:fs/promises';
|
|
9
|
-
import { join, resolve, relative, sep } from 'node:path';
|
|
9
|
+
import { open, readFile, readdir, stat, lstat, realpath } from 'node:fs/promises';
|
|
10
|
+
import { isAbsolute, join, resolve, relative, sep } from 'node:path';
|
|
10
11
|
import { WORKSPACE_FS_EVENT_TYPES, opToEventType } from './types.js';
|
|
11
12
|
import { MountWatcher } from './watcher.js';
|
|
12
13
|
import { hashContent, DEFAULT_MAX_FILE_SIZE } from './sync.js';
|
|
13
14
|
import { DiskAgreement, sameRootIdentity } from './disk-agreement.js';
|
|
14
15
|
import { BranchIntents } from './branch-intent.js';
|
|
15
|
-
import {
|
|
16
|
-
import { TOOL_BATCH_HOOK_TIMEOUT_MS } from '../../module-registry.js';
|
|
17
|
-
import { pushPaths, reconcilePass, settleAdoptionBeforeMutation, } from './reconcile.js';
|
|
16
|
+
import { IGNORED, OTHER_MOUNT, checkDir, filesystemTrustsCtime, inOtherMount, locateRoot, passedOverAt, readPrefix } from './observe.js';
|
|
17
|
+
import { TOOL_BATCH_HOOK_TIMEOUT_MS, ensureStateRegistered } from '../../module-registry.js';
|
|
18
|
+
import { NOT_PUSHED_IN_OTHER_MOUNT, WALK_CAP, entryMode, pushPaths, reconcilePass, settleAdoptionBeforeMutation, } from './reconcile.js';
|
|
18
19
|
/** Default for how long the after-batch scan may hold the agent's next inference. */
|
|
19
20
|
const AGENT_ACTION_SCAN_DEADLINE_MS = 20_000;
|
|
21
|
+
/**
|
|
22
|
+
* How long a mount's catch-up pass waits for its watcher to become ready
|
|
23
|
+
* before it starts anyway (ensureRunning). In a probe on Linux at a load near
|
|
24
|
+
* 40, chokidar's first scan of 10,000 entries took 1.3 s, about a twentieth of
|
|
25
|
+
* this. A scan slower than the bound has the pass started beside it, and the
|
|
26
|
+
* watcher's ready runs the pass again once the scan ends.
|
|
27
|
+
*/
|
|
28
|
+
const CATCH_UP_BOUND_MS = 30_000;
|
|
20
29
|
/** Why a call is refused once the module has stopped. */
|
|
21
30
|
const NOT_RUNNING = 'The workspace has stopped: it takes no more work until it starts again';
|
|
22
31
|
/** The workspace's own deadline must end before the framework's hook bound. */
|
|
@@ -29,6 +38,16 @@ class WorkspaceImageReadError extends Error {
|
|
|
29
38
|
this.name = 'WorkspaceImageReadError';
|
|
30
39
|
}
|
|
31
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* A file operation's path lies in another mount's directory nested in the
|
|
43
|
+
* mount it names. The message names the mount that holds it and the path to
|
|
44
|
+
* use there.
|
|
45
|
+
*/
|
|
46
|
+
class NestedMountError extends Error {
|
|
47
|
+
constructor(path, mount, holder, use) {
|
|
48
|
+
super(`"${path}" lies in mount "${holder}", nested in mount "${mount}", which neither reads nor writes it: use "${use}".`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
32
51
|
/**
|
|
33
52
|
* Thrown by `WorkspaceModule.readFileFromDisk()`. Messages carry the
|
|
34
53
|
* mount-prefixed path only — never absolute filesystem paths — so they can be
|
|
@@ -53,6 +72,10 @@ function errnoOf(err) {
|
|
|
53
72
|
? err.code
|
|
54
73
|
: undefined;
|
|
55
74
|
}
|
|
75
|
+
/** read_image's refusal of a file over the mount's size limit, wherever it's found to be. */
|
|
76
|
+
function imageTooLarge(maxSize, mountPrefixedPath) {
|
|
77
|
+
return new WorkspaceImageReadError('too_large', `Image file exceeds max size (${maxSize} bytes): ${mountPrefixedPath}`);
|
|
78
|
+
}
|
|
56
79
|
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
57
80
|
const JPEG_SIGNATURE = Buffer.from([0xff, 0xd8, 0xff]);
|
|
58
81
|
const GIF87A_SIGNATURE = Buffer.from('GIF87a', 'ascii');
|
|
@@ -465,15 +488,37 @@ export class WorkspaceModule {
|
|
|
465
488
|
/** Whether a mount root's filesystem maintains ctime, by canonical root. */
|
|
466
489
|
/** Per canonical root: the device of its filesystem when that maintains ctime, else null. */
|
|
467
490
|
ctimeTrust = new Map();
|
|
491
|
+
/**
|
|
492
|
+
* Each mount's directories that are other mounts nested inside it:
|
|
493
|
+
* mount-relative, `/`-separated, with each mount's name and resolved root.
|
|
494
|
+
*/
|
|
495
|
+
otherMounts = new Map();
|
|
496
|
+
/**
|
|
497
|
+
* Whether a file's mode is observed, taken in and applied (MountRuntime's
|
|
498
|
+
* `modesObserved`): not on Windows, which reports no POSIX modes. A test
|
|
499
|
+
* sets it false to run that case anywhere.
|
|
500
|
+
*/
|
|
501
|
+
modesObserved = process.platform !== 'win32';
|
|
468
502
|
/** Persisted state decoded in start(), held until mounts exist to apply it
|
|
469
503
|
* to — in the Host ordering start() runs before initStore() creates the
|
|
470
504
|
* mounts, so restoration must be second-callback-safe like watcher
|
|
471
505
|
* startup already is (issue #72). Cleared after application. */
|
|
472
506
|
savedState = null;
|
|
473
507
|
constructor(config) {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
508
|
+
for (const mount of config.mounts) {
|
|
509
|
+
const cap = mount.walkCap;
|
|
510
|
+
if (cap !== undefined && !(Number.isSafeInteger(cap) && cap > 0)) {
|
|
511
|
+
// Numbers as JavaScript writes them (JSON would say null for NaN), anything else as JSON.
|
|
512
|
+
const shown = typeof cap === 'number' ? String(cap) : typeof cap === 'bigint' ? `${cap}n` : JSON.stringify(cap) ?? String(cap);
|
|
513
|
+
throw new Error(`[workspace] Mount "${mount.name}": walkCap must be a positive integer no larger than ${Number.MAX_SAFE_INTEGER}, not ${shown}`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// Detect overlapping mount paths: a mount nested inside another is that
|
|
517
|
+
// mount's directory, which the outer mount passes over in every pass,
|
|
518
|
+
// named or not, and in its watcher (OTHER_MOUNT). It's kept apart from
|
|
519
|
+
// the outer mount's ignore list, which a sync that names a path sets
|
|
520
|
+
// aside for what it names (#276), so no sync of the outer mount takes in
|
|
521
|
+
// the inner one's files.
|
|
477
522
|
for (const outer of config.mounts) {
|
|
478
523
|
for (const inner of config.mounts) {
|
|
479
524
|
if (outer === inner)
|
|
@@ -481,15 +526,17 @@ export class WorkspaceModule {
|
|
|
481
526
|
const outerPath = resolve(outer.path);
|
|
482
527
|
const innerPath = resolve(inner.path);
|
|
483
528
|
const rel = relative(outerPath, innerPath);
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
}
|
|
529
|
+
// Outside the outer mount, `relative` gives a path that climbs with a
|
|
530
|
+
// `..` segment, or an absolute one across Windows drives. A name that
|
|
531
|
+
// only begins with two dots, such as `..cache`, is inside it.
|
|
532
|
+
if (rel && rel !== '..' && !rel.startsWith('..' + sep) && !isAbsolute(rel)) {
|
|
533
|
+
// inner is nested under outer: passed over as exactly that
|
|
534
|
+
// directory, read against `/`-separated paths, whatever the
|
|
535
|
+
// platform's.
|
|
536
|
+
const relPath = rel.split(sep).join('/');
|
|
537
|
+
this.otherMounts.set(outer.name, [...(this.otherMounts.get(outer.name) ?? []), { dir: relPath, name: inner.name, root: innerPath }]);
|
|
538
|
+
console.warn(`[workspace] Mount "${outer.name}" contains mount "${inner.name}" ` +
|
|
539
|
+
`at ${relPath}/ — passing over it as another mount's directory`);
|
|
493
540
|
}
|
|
494
541
|
}
|
|
495
542
|
}
|
|
@@ -505,26 +552,25 @@ export class WorkspaceModule {
|
|
|
505
552
|
* second. `ensureRunning()` gates on `ctx && mounts-populated`.
|
|
506
553
|
*/
|
|
507
554
|
initStore(store) {
|
|
508
|
-
|
|
555
|
+
// Every mount's states are registered before the module takes the store
|
|
556
|
+
// or any mount, so a registration that fails leaves the module as it was
|
|
557
|
+
// before the call, knowing no mount, and a later initStore starts clean.
|
|
558
|
+
const mounts = new Map();
|
|
559
|
+
const intents = new Map();
|
|
509
560
|
// Register tree states for each mount: the workspace tree, and the
|
|
510
561
|
// branch-local intent that qualifies it.
|
|
511
562
|
for (const mount of this.config.mounts) {
|
|
512
563
|
const treeStateId = `workspace/${mount.name}/tree`;
|
|
513
564
|
const intentTreeStateId = `workspace/${mount.name}/intent`;
|
|
514
565
|
for (const id of [treeStateId, intentTreeStateId]) {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
catch {
|
|
524
|
-
// State already registered (restart scenario)
|
|
525
|
-
}
|
|
566
|
+
ensureStateRegistered(store, {
|
|
567
|
+
id,
|
|
568
|
+
strategy: 'tree',
|
|
569
|
+
deltaSnapshotEvery: this.config.deltaSnapshotEvery ?? 50,
|
|
570
|
+
fullSnapshotEvery: this.config.fullSnapshotEvery ?? 10,
|
|
571
|
+
});
|
|
526
572
|
}
|
|
527
|
-
|
|
573
|
+
mounts.set(mount.name, {
|
|
528
574
|
config: mount,
|
|
529
575
|
treeStateId,
|
|
530
576
|
intentTreeStateId,
|
|
@@ -534,13 +580,17 @@ export class WorkspaceModule {
|
|
|
534
580
|
lastMaterializedBranchId: null,
|
|
535
581
|
watcherReadyAt: null,
|
|
536
582
|
watcherError: null,
|
|
537
|
-
};
|
|
538
|
-
|
|
539
|
-
this.intents.set(mount.name, new BranchIntents(store, intentTreeStateId));
|
|
583
|
+
});
|
|
584
|
+
intents.set(mount.name, new BranchIntents(store, intentTreeStateId));
|
|
540
585
|
}
|
|
586
|
+
this.store = store;
|
|
587
|
+
for (const [name, state] of mounts)
|
|
588
|
+
this.mounts.set(name, state);
|
|
589
|
+
for (const [name, intent] of intents)
|
|
590
|
+
this.intents.set(name, intent);
|
|
541
591
|
// What disk last agreed with, per mount and path: global records, so a
|
|
542
592
|
// branch switch never rewinds it.
|
|
543
|
-
this.agreement = new DiskAgreement(store);
|
|
593
|
+
this.agreement = new DiskAgreement(store, { modesObserved: () => this.modesObserved });
|
|
544
594
|
this.agreement.open(this.config.mounts.map((m) => ({ name: m.name, root: resolve(m.path) })));
|
|
545
595
|
this.applySavedState();
|
|
546
596
|
this.ensureRunning();
|
|
@@ -585,19 +635,66 @@ export class WorkspaceModule {
|
|
|
585
635
|
followSymlinks: mount.config.followSymlinks === true,
|
|
586
636
|
maxFileSize: mount.config.maxFileSize ?? DEFAULT_MAX_FILE_SIZE,
|
|
587
637
|
ignore: mount.config.ignore ?? [],
|
|
638
|
+
others: this.othersOf(mount.config.name),
|
|
588
639
|
},
|
|
589
640
|
rootReal,
|
|
590
641
|
ctimeTrustedDev: trustedDev,
|
|
591
642
|
treeStateId: mount.treeStateId,
|
|
592
643
|
intents: this.intents.get(mount.config.name),
|
|
593
644
|
readOnly: mount.config.mode === 'read-only',
|
|
645
|
+
walkCap: mount.config.walkCap ?? WALK_CAP,
|
|
646
|
+
catchUpScan: (mount.config.watch ?? 'always') === 'always',
|
|
647
|
+
modesObserved: this.modesObserved,
|
|
594
648
|
};
|
|
595
649
|
}
|
|
650
|
+
/**
|
|
651
|
+
* Where a search's walk starts: `base`, its path as given, then each
|
|
652
|
+
* segment of `beneath` while disk lists it, in the directory reached so
|
|
653
|
+
* far, as a directory with exactly that spelling. The walk names what it
|
|
654
|
+
* finds from its start's spelling, and a case-insensitive disk would let
|
|
655
|
+
* it go on under a spelling disk doesn't list, taking files in under that
|
|
656
|
+
* spelling beside their own. So the start stops before a segment spelled
|
|
657
|
+
* any other way, not there, or not a directory, and before a link, which
|
|
658
|
+
* a walk of the whole mount doesn't go through either. It lists only
|
|
659
|
+
* directories it has checked lie inside the mount, and none the mount
|
|
660
|
+
* passes over by design, which the walk then answers for.
|
|
661
|
+
*/
|
|
662
|
+
async walkStart(mount, base, beneath) {
|
|
663
|
+
if (beneath === '')
|
|
664
|
+
return base;
|
|
665
|
+
const { view, rootReal } = await this.runtime(mount);
|
|
666
|
+
let start = base;
|
|
667
|
+
for (const segment of beneath.split('/')) {
|
|
668
|
+
if (start !== '' && passedOverAt(start, view.ignore, view.others) !== undefined)
|
|
669
|
+
break;
|
|
670
|
+
const here = await checkDir(view, rootReal, start);
|
|
671
|
+
if (here.kind !== 'checked')
|
|
672
|
+
break;
|
|
673
|
+
const entry = (await readdir(here.dir.real, { withFileTypes: true }).catch(() => [])).find((e) => e.name === segment);
|
|
674
|
+
if (entry === undefined || !entry.isDirectory())
|
|
675
|
+
break;
|
|
676
|
+
start = start ? `${start}/${segment}` : segment;
|
|
677
|
+
}
|
|
678
|
+
return start;
|
|
679
|
+
}
|
|
596
680
|
/** One disk→store pass; the caller holds the mount's turn. Emits its events. */
|
|
597
681
|
async passUnlocked(mount, scope, opts = {}) {
|
|
598
682
|
const result = await reconcilePass(this.getStore(), this.agreementOrThrow(), await this.runtime(mount), scope, opts);
|
|
599
683
|
if (scope.kind === 'dir' && scope.dir === '' && scope.recursive)
|
|
600
684
|
mount.initialSyncDone = true;
|
|
685
|
+
// What a pass says is another mount's directory names that mount and
|
|
686
|
+
// the path to use there, as the file operations' refusal does: a copy's
|
|
687
|
+
// note, and the region of a scope that lies there. The pass's counts
|
|
688
|
+
// keep the bare reason.
|
|
689
|
+
const name = mount.config.name;
|
|
690
|
+
for (const report of result.reports.values()) {
|
|
691
|
+
if (report.note === OTHER_MOUNT)
|
|
692
|
+
report.note = this.otherMountNote(name, report.path, 'list');
|
|
693
|
+
}
|
|
694
|
+
for (const region of result.incomplete) {
|
|
695
|
+
if (region.reason === OTHER_MOUNT)
|
|
696
|
+
region.reason = this.otherMountNote(name, region.path, 'list');
|
|
697
|
+
}
|
|
601
698
|
this.emitFsEvents(mount.config.name, result.ops, result.newConflicts);
|
|
602
699
|
return result;
|
|
603
700
|
}
|
|
@@ -748,12 +845,54 @@ export class WorkspaceModule {
|
|
|
748
845
|
continue;
|
|
749
846
|
const watchMode = mount.config.watch ?? 'always';
|
|
750
847
|
if (watchMode === 'always') {
|
|
751
|
-
|
|
848
|
+
// Chokidar is started with ignoreInitial:true, so changes made while
|
|
849
|
+
// the session was down would be invisible. A catch-up pass takes them
|
|
850
|
+
// in under the three-way rule: only real differences produce tree
|
|
851
|
+
// changes and events. It takes its turn with the mount's bare syncs,
|
|
852
|
+
// so on a mount over its walkCap it covers the walkCap entries after
|
|
853
|
+
// where the last turn stopped, and a change elsewhere waits for the
|
|
854
|
+
// rotation to reach it, or for a sync of its path.
|
|
855
|
+
//
|
|
856
|
+
// The pass runs at each of the watcher's readies, its first and each
|
|
857
|
+
// re-attach's, when chokidar has called fs.watch on every entry, so a
|
|
858
|
+
// change made after the pass has looked at a file reaches the watcher.
|
|
859
|
+
// Started beside chokidar's scan, the pass could finish with a file
|
|
860
|
+
// before chokidar watched it, and a change between the two reached
|
|
861
|
+
// neither. A watcher that isn't ready within CATCH_UP_BOUND_MS (from
|
|
862
|
+
// its start or its re-attach) has the pass started at the bound, and
|
|
863
|
+
// its ready, if it comes, runs the pass again. Within the mount's
|
|
864
|
+
// walkCap, that pass covers what the early one missed. Over it, the
|
|
865
|
+
// pass continues the rotation, and what the early one missed waits for
|
|
866
|
+
// the rotation to come back, as any change outside a pass's window
|
|
867
|
+
// does. An error doesn't start the pass: chokidar goes on scanning
|
|
868
|
+
// after one, and the bound covers a watcher that never becomes ready.
|
|
869
|
+
// On macOS, a new entry written after ready but before libuv's
|
|
870
|
+
// FSEventStream exists isn't reported until a later change in its
|
|
871
|
+
// directory, which this doesn't change.
|
|
872
|
+
let watcher;
|
|
873
|
+
let bound = null;
|
|
874
|
+
const catchUp = () => {
|
|
875
|
+
if (bound)
|
|
876
|
+
clearTimeout(bound);
|
|
877
|
+
bound = null;
|
|
878
|
+
// A watcher the module has since stopped or replaced starts no pass.
|
|
879
|
+
if (this.watchers.get(name) !== watcher)
|
|
880
|
+
return;
|
|
881
|
+
void this.initialScan(name);
|
|
882
|
+
};
|
|
883
|
+
const awaitReady = () => {
|
|
884
|
+
if (bound)
|
|
885
|
+
clearTimeout(bound);
|
|
886
|
+
bound = setTimeout(catchUp, CATCH_UP_BOUND_MS);
|
|
887
|
+
bound.unref?.();
|
|
888
|
+
};
|
|
889
|
+
watcher = new MountWatcher(mount.config, (changes) => {
|
|
752
890
|
this.handleFsChanges(name, changes);
|
|
753
891
|
}, {
|
|
754
892
|
onReady: () => {
|
|
755
893
|
mount.watcherReadyAt = Date.now();
|
|
756
894
|
mount.watcherError = null;
|
|
895
|
+
catchUp();
|
|
757
896
|
},
|
|
758
897
|
onError: (err) => {
|
|
759
898
|
mount.watcherError = err.message;
|
|
@@ -766,21 +905,17 @@ export class WorkspaceModule {
|
|
|
766
905
|
},
|
|
767
906
|
onReattach: () => {
|
|
768
907
|
mount.watcherError = null;
|
|
769
|
-
|
|
908
|
+
awaitReady();
|
|
770
909
|
this.ctx?.pushEvent({
|
|
771
910
|
type: 'workspace:watcher-reattached',
|
|
772
911
|
mount: name,
|
|
773
912
|
path: mount.config.path,
|
|
774
913
|
});
|
|
775
914
|
},
|
|
776
|
-
});
|
|
915
|
+
}, this.othersOf(name));
|
|
916
|
+
awaitReady();
|
|
777
917
|
watcher.start();
|
|
778
918
|
this.watchers.set(name, watcher);
|
|
779
|
-
// Chokidar is started with ignoreInitial:true, so changes made while
|
|
780
|
-
// the session was down would be invisible. One full pass catches up
|
|
781
|
-
// under the three-way rule: only real differences produce tree
|
|
782
|
-
// changes and events.
|
|
783
|
-
void this.initialScan(name);
|
|
784
919
|
}
|
|
785
920
|
this.ctx.pushEvent({
|
|
786
921
|
type: 'workspace:mounted',
|
|
@@ -797,7 +932,7 @@ export class WorkspaceModule {
|
|
|
797
932
|
try {
|
|
798
933
|
// A full pass under the three-way rule: deletions on disk are confirmed
|
|
799
934
|
// where nothing newer is pending, and nothing unvisited is touched.
|
|
800
|
-
await this.withMount(mount, () => this.passUnlocked(mount, { kind: 'dir', dir: '', recursive: true }));
|
|
935
|
+
await this.withMount(mount, () => this.passUnlocked(mount, { kind: 'dir', dir: '', recursive: true }, { resume: true }));
|
|
801
936
|
}
|
|
802
937
|
catch (err) {
|
|
803
938
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -812,11 +947,13 @@ export class WorkspaceModule {
|
|
|
812
947
|
// No new watcher-driven passes, then let every pass and write finish
|
|
813
948
|
// before the store goes away — including any queued behind the ones in
|
|
814
949
|
// flight (a tool call, a scan continuing past its deadline): drained
|
|
815
|
-
// until no new tail appears.
|
|
816
|
-
|
|
950
|
+
// until no new tail appears. The watchers leave this.watchers before they
|
|
951
|
+
// stop, so a watcher's ready or catch-up bound starts no pass meanwhile.
|
|
952
|
+
const stopping = [...this.watchers.values()];
|
|
953
|
+
this.watchers.clear();
|
|
954
|
+
for (const watcher of stopping) {
|
|
817
955
|
await watcher.stop();
|
|
818
956
|
}
|
|
819
|
-
this.watchers.clear();
|
|
820
957
|
for (;;) {
|
|
821
958
|
const tails = [...this.mountTurns.values()];
|
|
822
959
|
await Promise.all(tails);
|
|
@@ -827,9 +964,16 @@ export class WorkspaceModule {
|
|
|
827
964
|
// Nothing is queued now, and nothing more is taken: what follows saves
|
|
828
965
|
// the pins and watermarks as the last work left them.
|
|
829
966
|
this.stopped = true;
|
|
830
|
-
// Persist state
|
|
831
|
-
|
|
832
|
-
|
|
967
|
+
// Persist state, once initStore() has run and the state start() was
|
|
968
|
+
// handed has been applied (savedState is cleared then). Before that this
|
|
969
|
+
// session has made no pass or write, so it has nothing to save, and the
|
|
970
|
+
// stored state stays as it was: saving would put empty or default mounts
|
|
971
|
+
// over the saved pins. A create() that fails after starting this module
|
|
972
|
+
// stops it before the host can call initStore() (#389), and an
|
|
973
|
+
// initStore() that throws after taking the store leaves the state
|
|
974
|
+
// unapplied.
|
|
975
|
+
if (this.ctx && this.store && this.savedState === null) {
|
|
976
|
+
const activeBranchId = this.store.currentBranch().id;
|
|
833
977
|
const state = { mounts: {}, activeBranchId };
|
|
834
978
|
for (const [name, mount] of this.mounts) {
|
|
835
979
|
state.mounts[name] = {
|
|
@@ -867,8 +1011,8 @@ export class WorkspaceModule {
|
|
|
867
1011
|
type: 'object',
|
|
868
1012
|
properties: {
|
|
869
1013
|
path: { type: 'string', description: 'File path (mount-prefixed, e.g., "project/src/main.ts")' },
|
|
870
|
-
offset: { type: '
|
|
871
|
-
limit: { type: '
|
|
1014
|
+
offset: { type: 'integer', description: 'Starting line number (1-indexed)' },
|
|
1015
|
+
limit: { type: 'integer', description: 'Maximum number of lines to return (default 2000)' },
|
|
872
1016
|
offsetChars: { type: 'integer', description: 'Character paging: zero-based UTF-16 offset (default 0); use nextOffsetChars to continue' },
|
|
873
1017
|
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.' },
|
|
874
1018
|
},
|
|
@@ -932,7 +1076,8 @@ export class WorkspaceModule {
|
|
|
932
1076
|
+ 'still on disk); not-in-branch (on disk, absent from this branch\'s workspace); disk-only (binary or over '
|
|
933
1077
|
+ 'the size limit, never stored — shown with size and image type); conflict, with its kind (disk and the '
|
|
934
1078
|
+ 'workspace both changed; disk holds a different copy and nothing records which is newer; or a write was '
|
|
935
|
-
+ 'interrupted) — sync the path to take disk, or materialize that path with force to take the workspace
|
|
1079
|
+
+ 'interrupted) — sync the path to take disk, or materialize that path with force to take the workspace, '
|
|
1080
|
+
+ 'with applyDeletions too where the workspace deleted it; '
|
|
936
1081
|
+ 'disk-missing-provenance-unknown; unverified (disk could not be checked). `incomplete` names regions '
|
|
937
1082
|
+ 'the listing could not see.',
|
|
938
1083
|
inputSchema: {
|
|
@@ -968,8 +1113,8 @@ export class WorkspaceModule {
|
|
|
968
1113
|
pattern: { type: 'string', description: 'Regular expression pattern' },
|
|
969
1114
|
path: { type: 'string', description: 'File or directory to search (mount-prefixed, optional)' },
|
|
970
1115
|
glob: { type: 'string', description: 'Glob pattern to filter files' },
|
|
971
|
-
contextBefore: { type: '
|
|
972
|
-
contextAfter: { type: '
|
|
1116
|
+
contextBefore: { type: 'integer', description: 'Context lines before match' },
|
|
1117
|
+
contextAfter: { type: 'integer', description: 'Context lines after match' },
|
|
973
1118
|
},
|
|
974
1119
|
required: ['pattern'],
|
|
975
1120
|
},
|
|
@@ -1001,7 +1146,12 @@ export class WorkspaceModule {
|
|
|
1001
1146
|
{
|
|
1002
1147
|
name: 'sync',
|
|
1003
1148
|
description: 'Bring disk changes into the workspace. Without a path, every mount (or the given one) is '
|
|
1004
|
-
+ 'rechecked
|
|
1149
|
+
+ 'rechecked and nothing pending in the workspace is discarded. A sync takes up to the mount\'s walk '
|
|
1150
|
+
+ 'cap of entries, files, links and the like (5000 unless configured). Past it, what this one didn\'t reach is listed under '
|
|
1151
|
+
+ '`incomplete`, and the walk takes turns: the next sync of the same mount or directory picks up where '
|
|
1152
|
+
+ 'the last turn stopped, so repeated syncs cover it all. On a mount watched continuously, its own catch-up '
|
|
1153
|
+
+ 'scan, at start and when its watcher reattaches, takes turns with its whole-mount syncs. With a '
|
|
1154
|
+
+ 'path (file or directory), '
|
|
1005
1155
|
+ 'the workspace takes disk\'s state there explicitly: it restores a workspace-deleted file, resolves '
|
|
1006
1156
|
+ 'conflicts toward disk, replaces a workspace draft, and removes a workspace file disk does not have. '
|
|
1007
1157
|
+ 'Each workspace change it gives up is listed under `discarded`, with the state it had and, as `op`, '
|
|
@@ -1011,13 +1161,18 @@ export class WorkspaceModule {
|
|
|
1011
1161
|
+ 'directory) is unavailable and listed under `incomplete`. Reconnect the drive; acceptRoot is for a '
|
|
1012
1162
|
+ 'directory replaced on purpose, or a drive back under a new device number, never for one still '
|
|
1013
1163
|
+ 'unplugged, whose empty mountpoint would become the root. What the scan passed over by design and '
|
|
1014
|
-
+ 'reported nowhere else is counted under `passedOver`, by mount and reason:
|
|
1015
|
-
+ '
|
|
1164
|
+
+ 'reported nowhere else is counted under `passedOver`, by mount and reason: another mount\'s '
|
|
1165
|
+
+ 'directory nested in this one, entries the mount\'s ignore list covers, and entries with an '
|
|
1166
|
+
+ 'editor\'s swap or backup file name (`.name.swp` and `.name.swx` as the whole name, names ending '
|
|
1167
|
+
+ 'in `~` or in `.subl…tmp`), that hold nothing the workspace tracks (such a directory counts once, '
|
|
1168
|
+
+ 'with what lies beneath it). So a '
|
|
1016
1169
|
+ 'total of 0, with no conflicts and nothing under `passedOver`, `incomplete` or `skipped`, means the '
|
|
1017
|
-
+ 'sync looked everywhere and nothing had changed. A path the mount ignores
|
|
1018
|
-
+ 'it, and listed under `ignored`: a sync without a path passes over it.
|
|
1019
|
-
+ '
|
|
1020
|
-
+ '
|
|
1170
|
+
+ 'sync looked everywhere and nothing had changed. A path the mount ignores, or an editor\'s file, is '
|
|
1171
|
+
+ 'synced when you name it, and listed under `ignored`: a sync without a path passes over it. A path '
|
|
1172
|
+
+ 'in another mount\'s directory isn\'t synced even when you name it: use the mount that holds it. Naming '
|
|
1173
|
+
+ 'an ignored directory sets aside the patterns that cover it or a directory above it; one that '
|
|
1174
|
+
+ 'covers only something inside it still applies, and what it covers is passed over, as are editors\' '
|
|
1175
|
+
+ 'files inside it.',
|
|
1021
1176
|
inputSchema: {
|
|
1022
1177
|
type: 'object',
|
|
1023
1178
|
properties: {
|
|
@@ -1048,8 +1203,10 @@ export class WorkspaceModule {
|
|
|
1048
1203
|
const mount = this.config.mounts.find(m => m.name === mountName);
|
|
1049
1204
|
if (!mount)
|
|
1050
1205
|
throw new Error(`Unknown mount: "${mountName}"`);
|
|
1051
|
-
|
|
1052
|
-
|
|
1206
|
+
// Against the resolved root, as parsePath judges it.
|
|
1207
|
+
const root = resolve(mount.path);
|
|
1208
|
+
const resolved = resolve(root, slash < 0 ? '' : path.slice(slash + 1));
|
|
1209
|
+
if (!isContainedPath(root, resolved))
|
|
1053
1210
|
throw new Error(`Path traversal detected: "${path}"`);
|
|
1054
1211
|
return resolved;
|
|
1055
1212
|
}
|
|
@@ -1077,6 +1234,13 @@ export class WorkspaceModule {
|
|
|
1077
1234
|
if (!this.running())
|
|
1078
1235
|
return { success: false, isError: true, error: NOT_RUNNING };
|
|
1079
1236
|
try {
|
|
1237
|
+
// Every handler reads its input as its schema describes it; a call
|
|
1238
|
+
// that sends something else is refused here, before anything is read.
|
|
1239
|
+
const schema = this.getTools().find((tool) => tool.name === call.name)?.inputSchema;
|
|
1240
|
+
const problem = schema ? inputProblem(schema, call.input, INPUT_MINIMUMS[call.name]) : null;
|
|
1241
|
+
if (problem !== null) {
|
|
1242
|
+
return { success: false, isError: true, error: `The ${call.name} call did nothing: ${problem}.` };
|
|
1243
|
+
}
|
|
1080
1244
|
const input = call.input;
|
|
1081
1245
|
const generated = typeof input?.path === 'string' ? this.findGeneratedTextFile(input.path) : undefined;
|
|
1082
1246
|
if (generated) {
|
|
@@ -1103,7 +1267,7 @@ export class WorkspaceModule {
|
|
|
1103
1267
|
}
|
|
1104
1268
|
}
|
|
1105
1269
|
catch (err) {
|
|
1106
|
-
return { success: false, error: String(err), isError: true };
|
|
1270
|
+
return { success: false, error: err instanceof Error ? err.message : String(err), isError: true };
|
|
1107
1271
|
}
|
|
1108
1272
|
}
|
|
1109
1273
|
async onProcess(event, _state) {
|
|
@@ -1192,7 +1356,7 @@ export class WorkspaceModule {
|
|
|
1192
1356
|
*/
|
|
1193
1357
|
resolveAbsolutePath(mountPrefixedPath) {
|
|
1194
1358
|
try {
|
|
1195
|
-
const { mount, relativePath } = this.
|
|
1359
|
+
const { mount, relativePath } = this.parseFilePath(mountPrefixedPath);
|
|
1196
1360
|
return resolve(mount.config.path, relativePath);
|
|
1197
1361
|
}
|
|
1198
1362
|
catch {
|
|
@@ -1226,7 +1390,7 @@ export class WorkspaceModule {
|
|
|
1226
1390
|
let mount;
|
|
1227
1391
|
let relativePath;
|
|
1228
1392
|
try {
|
|
1229
|
-
({ mount, relativePath } = this.
|
|
1393
|
+
({ mount, relativePath } = this.parseFilePath(mountPrefixedPath));
|
|
1230
1394
|
}
|
|
1231
1395
|
catch (error) {
|
|
1232
1396
|
return {
|
|
@@ -1251,12 +1415,16 @@ export class WorkspaceModule {
|
|
|
1251
1415
|
return { success: false, error: 'the workspace had left the branch this file is for', isError: true, data: { branchLeft: true } };
|
|
1252
1416
|
}
|
|
1253
1417
|
const blobHash = store.storeBlob(data, mimeType);
|
|
1418
|
+
// An overwrite keeps the file's mode, as the write tool's does.
|
|
1419
|
+
const existing = store.treeGet(mount.treeStateId, relativePath);
|
|
1254
1420
|
let committed = false;
|
|
1255
1421
|
let failure;
|
|
1422
|
+
let modeKept = null;
|
|
1256
1423
|
try {
|
|
1257
|
-
const
|
|
1258
|
-
|
|
1259
|
-
|
|
1424
|
+
const answer = await this.commitToolChange(mount, relativePath, { kind: 'set', blobHash, size: data.byteLength, ...(existing ? { mode: existing.mode } : {}) }, () => { committed = true; });
|
|
1425
|
+
modeKept = answer.modeKept;
|
|
1426
|
+
failure = answer.refused === null ? null
|
|
1427
|
+
: `Wrote to Chronicle but failed to materialize "${mountPrefixedPath}" to disk: ${answer.refused}.`;
|
|
1260
1428
|
}
|
|
1261
1429
|
catch (error) {
|
|
1262
1430
|
if (!committed)
|
|
@@ -1271,7 +1439,7 @@ export class WorkspaceModule {
|
|
|
1271
1439
|
}
|
|
1272
1440
|
return {
|
|
1273
1441
|
success: true,
|
|
1274
|
-
data: { path: mountPrefixedPath, size: data.byteLength, mimeType },
|
|
1442
|
+
data: { path: mountPrefixedPath, size: data.byteLength, mimeType, ...(modeKept ? { modeKept } : {}) },
|
|
1275
1443
|
};
|
|
1276
1444
|
});
|
|
1277
1445
|
}
|
|
@@ -1286,7 +1454,7 @@ export class WorkspaceModule {
|
|
|
1286
1454
|
let mount;
|
|
1287
1455
|
let relativePath;
|
|
1288
1456
|
try {
|
|
1289
|
-
({ mount, relativePath } = this.
|
|
1457
|
+
({ mount, relativePath } = this.parseFilePath(mountPrefixedPath));
|
|
1290
1458
|
}
|
|
1291
1459
|
catch (error) {
|
|
1292
1460
|
return { error: error instanceof Error ? error.message : String(error) };
|
|
@@ -1322,7 +1490,7 @@ export class WorkspaceModule {
|
|
|
1322
1490
|
let mount;
|
|
1323
1491
|
let relativePath;
|
|
1324
1492
|
try {
|
|
1325
|
-
({ mount, relativePath } = this.
|
|
1493
|
+
({ mount, relativePath } = this.parseFilePath(mountPrefixedPath));
|
|
1326
1494
|
}
|
|
1327
1495
|
catch (error) {
|
|
1328
1496
|
return { error: error instanceof Error ? error.message : String(error) };
|
|
@@ -1344,10 +1512,65 @@ export class WorkspaceModule {
|
|
|
1344
1512
|
return { error: error instanceof Error ? error.message : String(error) };
|
|
1345
1513
|
}
|
|
1346
1514
|
}
|
|
1515
|
+
/** The directories of the mounts nested inside a mount, mount-relative. */
|
|
1516
|
+
othersOf(mountName) {
|
|
1517
|
+
return (this.otherMounts.get(mountName) ?? []).map((o) => o.dir);
|
|
1518
|
+
}
|
|
1519
|
+
/**
|
|
1520
|
+
* The innermost mount nested in `mountName` whose directory holds a path
|
|
1521
|
+
* of that mount, judged lexically where the path lands (`.`, `..` and
|
|
1522
|
+
* repeated separators resolved, as for the traversal guard; a link of the
|
|
1523
|
+
* outer mount isn't followed), with the path to use there; undefined when
|
|
1524
|
+
* no nested mount's directory holds it.
|
|
1525
|
+
*/
|
|
1526
|
+
holderOf(mountName, relativePath) {
|
|
1527
|
+
const mount = this.mounts.get(mountName);
|
|
1528
|
+
if (!mount)
|
|
1529
|
+
return undefined;
|
|
1530
|
+
const resolved = resolve(mount.config.path, relativePath);
|
|
1531
|
+
const holder = (this.otherMounts.get(mountName) ?? [])
|
|
1532
|
+
.filter((o) => isContainedPath(o.root, resolved))
|
|
1533
|
+
.sort((a, b) => b.root.length - a.root.length)[0];
|
|
1534
|
+
if (!holder)
|
|
1535
|
+
return undefined;
|
|
1536
|
+
const rest = relative(holder.root, resolved).split(sep).join('/');
|
|
1537
|
+
return { name: holder.name, use: rest ? `${holder.name}/${rest}` : holder.name };
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* Why a mount leaves a path in a nested mount's directory, naming that
|
|
1541
|
+
* mount and the path to use: for a sync or a push, which of them leaves
|
|
1542
|
+
* it; for a listing, whose the path is.
|
|
1543
|
+
*/
|
|
1544
|
+
otherMountNote(mountName, relativePath, by) {
|
|
1545
|
+
const holder = this.holderOf(mountName, relativePath);
|
|
1546
|
+
const what = by === 'sync' ? ', which no sync of this mount takes in' : by === 'push' ? ', which no push of this mount writes' : '';
|
|
1547
|
+
return holder
|
|
1548
|
+
? `it lies in mount "${holder.name}", nested in this one${what}: use "${holder.use}"`
|
|
1549
|
+
: `it lies in another mount's directory${what}`;
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* parsePath for the file operations (read, write, edit, delete, and the
|
|
1553
|
+
* module's binary and disk reads and writes). A path in another mount's
|
|
1554
|
+
* directory nested in this one is that mount's: this mount's passes don't
|
|
1555
|
+
* observe it and its pushes don't write it, so its file operations refuse
|
|
1556
|
+
* it, naming the innermost mount that holds it and the path to use there.
|
|
1557
|
+
*/
|
|
1558
|
+
parseFilePath(path) {
|
|
1559
|
+
const parsed = this.parsePath(path);
|
|
1560
|
+
const holder = this.holderOf(parsed.mount.config.name, parsed.relativePath);
|
|
1561
|
+
if (holder)
|
|
1562
|
+
throw new NestedMountError(path, parsed.mount.config.name, holder.name, holder.use);
|
|
1563
|
+
return parsed;
|
|
1564
|
+
}
|
|
1347
1565
|
/**
|
|
1348
1566
|
* Parse a mount-prefixed path into (mountName, relativePath).
|
|
1349
1567
|
*/
|
|
1350
1568
|
parsePath(path) {
|
|
1569
|
+
// A file's name on disk is UTF-8, which can't hold a lone surrogate: the
|
|
1570
|
+
// store would keep one name and disk another.
|
|
1571
|
+
if (LONE_SURROGATE.test(path)) {
|
|
1572
|
+
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.`);
|
|
1573
|
+
}
|
|
1351
1574
|
const slashIdx = path.indexOf('/');
|
|
1352
1575
|
const mountName = slashIdx >= 0 ? path.slice(0, slashIdx) : path;
|
|
1353
1576
|
const relativePath = slashIdx >= 0 ? path.slice(slashIdx + 1) : '';
|
|
@@ -1359,8 +1582,11 @@ export class WorkspaceModule {
|
|
|
1359
1582
|
// Containment must compare with the PLATFORM separator (isContainedPath):
|
|
1360
1583
|
// resolve() emits backslashes on Windows, so a '/'-suffixed root never
|
|
1361
1584
|
// prefix-matches there and every in-mount path was rejected as traversal.
|
|
1362
|
-
|
|
1363
|
-
|
|
1585
|
+
// Against the resolved root, so a root configured with a trailing
|
|
1586
|
+
// separator, or relative, contains its own paths, the root included.
|
|
1587
|
+
const root = resolve(mount.config.path);
|
|
1588
|
+
const resolved = resolve(root, relativePath);
|
|
1589
|
+
if (!isContainedPath(root, resolved)) {
|
|
1364
1590
|
throw new Error(`Path traversal detected: "${path}" resolves outside mount "${mountName}"`);
|
|
1365
1591
|
}
|
|
1366
1592
|
return { mount, relativePath };
|
|
@@ -1385,9 +1611,8 @@ export class WorkspaceModule {
|
|
|
1385
1611
|
if (bytes.byteLength === 0) {
|
|
1386
1612
|
throw new WorkspaceImageReadError('empty', `Image file is empty: ${mountPrefixedPath}`);
|
|
1387
1613
|
}
|
|
1388
|
-
if (bytes.byteLength > maxSize)
|
|
1389
|
-
throw
|
|
1390
|
-
}
|
|
1614
|
+
if (bytes.byteLength > maxSize)
|
|
1615
|
+
throw imageTooLarge(maxSize, mountPrefixedPath);
|
|
1391
1616
|
return {
|
|
1392
1617
|
bytes,
|
|
1393
1618
|
mimeType: detectImageMimeType(bytes, mountPrefixedPath),
|
|
@@ -1544,11 +1769,16 @@ export class WorkspaceModule {
|
|
|
1544
1769
|
* - With symlinks allowed, the canonical target must stay beneath the
|
|
1545
1770
|
* canonical mount root; a sibling-prefix root (`/mount-other`) does not count.
|
|
1546
1771
|
* - `maxBytes` performs a bounded prefix read: at most `maxBytes` bytes are
|
|
1547
|
-
*
|
|
1772
|
+
* kept, and one byte past them is read to tell whether the file goes on
|
|
1773
|
+
* (`truncated`). The reads decide where the file ends, not the size fstat
|
|
1774
|
+
* gave at the open, so the bound holds for a file that grows while it's
|
|
1775
|
+
* read.
|
|
1548
1776
|
*
|
|
1549
1777
|
* Throws {@link WorkspaceReadError} with a `code` distinguishing unknown
|
|
1550
|
-
* mount, lexical traversal,
|
|
1551
|
-
*
|
|
1778
|
+
* mount, lexical traversal, a path in another mount's directory nested in
|
|
1779
|
+
* the one named (`other_mount`), unavailable mount, missing file,
|
|
1780
|
+
* directory, policy-denied symlink, outside-mount target, and
|
|
1781
|
+
* file-changed-during-read.
|
|
1552
1782
|
*/
|
|
1553
1783
|
async readFileFromDisk(mountPrefixedPath, options = {}) {
|
|
1554
1784
|
if (options.maxBytes !== undefined && !(Number.isInteger(options.maxBytes) && options.maxBytes >= 0)) {
|
|
@@ -1557,11 +1787,12 @@ export class WorkspaceModule {
|
|
|
1557
1787
|
let mount;
|
|
1558
1788
|
let relativePath;
|
|
1559
1789
|
try {
|
|
1560
|
-
({ mount, relativePath } = this.
|
|
1790
|
+
({ mount, relativePath } = this.parseFilePath(mountPrefixedPath));
|
|
1561
1791
|
}
|
|
1562
1792
|
catch (err) {
|
|
1563
1793
|
const message = err instanceof Error ? err.message : String(err);
|
|
1564
|
-
const code =
|
|
1794
|
+
const code = err instanceof NestedMountError ? 'other_mount'
|
|
1795
|
+
: message.startsWith('Unknown mount') ? 'unknown_mount' : 'traversal';
|
|
1565
1796
|
throw new WorkspaceReadError(code, message, 'parse');
|
|
1566
1797
|
}
|
|
1567
1798
|
if (!relativePath) {
|
|
@@ -1569,26 +1800,18 @@ export class WorkspaceModule {
|
|
|
1569
1800
|
}
|
|
1570
1801
|
const { handle, fileStat, realFilePath } = await this.openContainedFile(mount, relativePath, mountPrefixedPath);
|
|
1571
1802
|
try {
|
|
1572
|
-
const size = fileStat.size;
|
|
1573
1803
|
const cap = options.maxBytes;
|
|
1574
1804
|
let bytes;
|
|
1575
1805
|
let truncated = false;
|
|
1576
|
-
if (cap
|
|
1577
|
-
|
|
1578
|
-
bytes = Buffer.alloc(cap);
|
|
1579
|
-
let offset = 0;
|
|
1580
|
-
while (offset < cap) {
|
|
1581
|
-
const { bytesRead } = await handle.read(bytes, offset, cap - offset, offset);
|
|
1582
|
-
if (bytesRead === 0)
|
|
1583
|
-
break;
|
|
1584
|
-
offset += bytesRead;
|
|
1585
|
-
}
|
|
1586
|
-
if (offset < cap)
|
|
1587
|
-
bytes = bytes.subarray(0, offset);
|
|
1806
|
+
if (cap === undefined) {
|
|
1807
|
+
bytes = await handle.readFile();
|
|
1588
1808
|
}
|
|
1589
1809
|
else {
|
|
1590
|
-
bytes = await handle.
|
|
1810
|
+
({ bytes, more: truncated } = await readPrefix(handle, cap, fileStat.size));
|
|
1591
1811
|
}
|
|
1812
|
+
// What the read found, not what the fstat before it said, so `size`,
|
|
1813
|
+
// `bytes` and `truncated` agree even for a file that changed in between.
|
|
1814
|
+
const size = truncated ? Math.max((await handle.stat()).size, bytes.length + 1) : bytes.length;
|
|
1592
1815
|
return { bytes, size, truncated, mtimeMs: fileStat.mtimeMs, realPath: realFilePath, mount: mount.config.name };
|
|
1593
1816
|
}
|
|
1594
1817
|
catch (err) {
|
|
@@ -1635,9 +1858,8 @@ export class WorkspaceModule {
|
|
|
1635
1858
|
if (fileStat.size === 0) {
|
|
1636
1859
|
throw new WorkspaceImageReadError('empty', `Image file is empty: ${mountPrefixedPath}`);
|
|
1637
1860
|
}
|
|
1638
|
-
if (fileStat.size > maxSize)
|
|
1639
|
-
throw
|
|
1640
|
-
}
|
|
1861
|
+
if (fileStat.size > maxSize)
|
|
1862
|
+
throw imageTooLarge(maxSize, mountPrefixedPath);
|
|
1641
1863
|
});
|
|
1642
1864
|
}
|
|
1643
1865
|
catch (err) {
|
|
@@ -1650,9 +1872,13 @@ export class WorkspaceModule {
|
|
|
1650
1872
|
}
|
|
1651
1873
|
throw new WorkspaceImageReadError('not_found', `Unable to read image file: ${mountPrefixedPath}`);
|
|
1652
1874
|
}
|
|
1653
|
-
const { handle } = opened;
|
|
1875
|
+
const { handle, fileStat } = opened;
|
|
1654
1876
|
try {
|
|
1655
|
-
|
|
1877
|
+
// No more than maxSize is loaded: a file grown past it since the fstat
|
|
1878
|
+
// above is refused here, as one already over it was there.
|
|
1879
|
+
const { bytes, more } = await readPrefix(handle, maxSize, fileStat.size);
|
|
1880
|
+
if (more)
|
|
1881
|
+
throw imageTooLarge(maxSize, mountPrefixedPath);
|
|
1656
1882
|
return this.validateImageBytes(bytes, mountPrefixedPath, maxSize);
|
|
1657
1883
|
}
|
|
1658
1884
|
catch (err) {
|
|
@@ -1667,6 +1893,13 @@ export class WorkspaceModule {
|
|
|
1667
1893
|
await handle.close();
|
|
1668
1894
|
}
|
|
1669
1895
|
}
|
|
1896
|
+
/**
|
|
1897
|
+
* Whether a tool's change on this mount goes to disk as it commits: the
|
|
1898
|
+
* push commitToolChange makes, and what an edit's answer says of it.
|
|
1899
|
+
*/
|
|
1900
|
+
pushesOnCommit(mount) {
|
|
1901
|
+
return mount.config.autoMaterialize === true && mount.config.mode !== 'read-only';
|
|
1902
|
+
}
|
|
1670
1903
|
/**
|
|
1671
1904
|
* Commit a tool's write or delete to the workspace tree, with the branch
|
|
1672
1905
|
* intent it implies (store origin for a path disk never agreed with; a
|
|
@@ -1676,10 +1909,13 @@ export class WorkspaceModule {
|
|
|
1676
1909
|
* The push follows materialize's rule: a disk copy that changed since it
|
|
1677
1910
|
* last agreed is not overwritten or unlinked, and the refusal says why.
|
|
1678
1911
|
*
|
|
1679
|
-
*
|
|
1680
|
-
*
|
|
1681
|
-
* that disk is the source of truth for
|
|
1682
|
-
*
|
|
1912
|
+
* Answers, so callers surface them in the tool result, the reason when the
|
|
1913
|
+
* change reached the workspace but not disk (`refused`): the
|
|
1914
|
+
* autoMaterialize contract is that disk is the source of truth for
|
|
1915
|
+
* downstream agents. And, apart from it, why a write whose bytes reached
|
|
1916
|
+
* disk left the file's mode as disk had it (`modeKept`). The caller holds
|
|
1917
|
+
* the mount's turn and has settled the path first (settleBeforeMutation, or
|
|
1918
|
+
* a pass over the path, as an edit makes).
|
|
1683
1919
|
*/
|
|
1684
1920
|
async commitToolChange(mount, relativePath, change, onCommitted) {
|
|
1685
1921
|
const store = this.getStore();
|
|
@@ -1707,21 +1943,22 @@ export class WorkspaceModule {
|
|
|
1707
1943
|
}
|
|
1708
1944
|
else {
|
|
1709
1945
|
store.treeRemove(mount.treeStateId, relativePath);
|
|
1946
|
+
onCommitted?.();
|
|
1710
1947
|
intents.update(relativePath, (cur) => {
|
|
1711
1948
|
const next = { ...cur, tombstone: { hash: change.previousHash } };
|
|
1712
1949
|
delete next.origin;
|
|
1713
1950
|
return next;
|
|
1714
1951
|
});
|
|
1715
1952
|
}
|
|
1716
|
-
if (!
|
|
1717
|
-
return null;
|
|
1953
|
+
if (!this.pushesOnCommit(mount))
|
|
1954
|
+
return { refused: null, modeKept: null };
|
|
1718
1955
|
const pushed = await this.pushUnlocked(mount, [relativePath], {
|
|
1719
1956
|
applyDeletions: change.kind === 'remove',
|
|
1720
1957
|
branchId: store.currentBranch().id,
|
|
1721
1958
|
});
|
|
1722
1959
|
const refused = pushed.skipped.find((s) => s.path === relativePath);
|
|
1723
1960
|
if (!refused)
|
|
1724
|
-
return null;
|
|
1961
|
+
return { refused: null, modeKept: pushed.modesKept.find((k) => k.path === relativePath)?.reason ?? null };
|
|
1725
1962
|
this.ctx?.pushEvent({
|
|
1726
1963
|
type: 'workspace:materialize-failed',
|
|
1727
1964
|
mount: mount.config.name,
|
|
@@ -1729,7 +1966,7 @@ export class WorkspaceModule {
|
|
|
1729
1966
|
op: change.kind === 'set' ? 'write' : 'delete',
|
|
1730
1967
|
error: refused.reason,
|
|
1731
1968
|
});
|
|
1732
|
-
return refused.reason;
|
|
1969
|
+
return { refused: refused.reason, modeKept: null };
|
|
1733
1970
|
}
|
|
1734
1971
|
// ==========================================================================
|
|
1735
1972
|
// Lazy Sync
|
|
@@ -1740,25 +1977,30 @@ export class WorkspaceModule {
|
|
|
1740
1977
|
* disk now holds (an edit sees a disk change the settled adoption took in).
|
|
1741
1978
|
* The pass may not settle it (a branch that keeps changing, an unreadable
|
|
1742
1979
|
* disk copy); the precondition that matters is enforced again,
|
|
1743
|
-
* synchronously, where the change commits (commitToolChange). The
|
|
1744
|
-
*
|
|
1980
|
+
* synchronously, where the change commits (commitToolChange). The pass
|
|
1981
|
+
* names the path, as a file tool names the one it acts on, so a path the
|
|
1982
|
+
* mount passes over by design is observed there, as a sync naming it would
|
|
1983
|
+
* observe it (#276). The caller holds the mount's turn.
|
|
1745
1984
|
*/
|
|
1746
1985
|
async settleBeforeMutation(mount, relativePath) {
|
|
1747
1986
|
if (this.agreementOrThrow().get(mount.config.name, relativePath)?.kind !== 'pending')
|
|
1748
1987
|
return;
|
|
1749
|
-
await this.passUnlocked(mount, { kind: 'paths', paths: [relativePath] });
|
|
1988
|
+
await this.passUnlocked(mount, { kind: 'paths', paths: [relativePath] }, { named: true });
|
|
1750
1989
|
}
|
|
1751
1990
|
/**
|
|
1752
1991
|
* Before a read: a path the workspace doesn't hold is observed under the
|
|
1753
1992
|
* three-way rule, which ingests a new disk file but never brings back a
|
|
1754
|
-
* workspace deletion or a file absent from this branch. The
|
|
1755
|
-
* the mount
|
|
1993
|
+
* workspace deletion or a file absent from this branch. The pass names the
|
|
1994
|
+
* path, as a file tool names the one it acts on: a path the mount passes
|
|
1995
|
+
* over by design is observed, as a sync naming it would observe it (#276),
|
|
1996
|
+
* while a path in another mount's directory is refused before this, and
|
|
1997
|
+
* passed over by any pass. The caller holds the mount's turn.
|
|
1756
1998
|
*/
|
|
1757
1999
|
async ensureSynced(mount, relativePath) {
|
|
1758
2000
|
const store = this.getStore();
|
|
1759
2001
|
if (store.treeGet(mount.treeStateId, relativePath))
|
|
1760
2002
|
return undefined;
|
|
1761
|
-
const pass = await this.passUnlocked(mount, { kind: 'paths', paths: [relativePath] });
|
|
2003
|
+
const pass = await this.passUnlocked(mount, { kind: 'paths', paths: [relativePath] }, { named: true });
|
|
1762
2004
|
return pass.reports.get(relativePath);
|
|
1763
2005
|
}
|
|
1764
2006
|
/** Why the workspace holds no entry at a path, when disk explains it. */
|
|
@@ -1787,19 +2029,14 @@ export class WorkspaceModule {
|
|
|
1787
2029
|
const characterPaging = input.offsetChars !== undefined || input.limitChars !== undefined;
|
|
1788
2030
|
const offsetChars = input.offsetChars ?? 0;
|
|
1789
2031
|
const limitChars = input.limitChars ?? 2000;
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
}
|
|
1794
|
-
if (!Number.isSafeInteger(offsetChars) || offsetChars < 0
|
|
1795
|
-
|| !Number.isSafeInteger(limitChars) || limitChars < 1
|
|
1796
|
-
|| input.offsetChars === null || input.limitChars === null) {
|
|
1797
|
-
return { success: false, isError: true, error: 'offsetChars must be a non-negative safe integer and limitChars a positive safe integer.' };
|
|
1798
|
-
}
|
|
2032
|
+
// Each number is a whole number of at least its schema's minimum by now
|
|
2033
|
+
// (inputProblem).
|
|
2034
|
+
if (characterPaging && (input.offset !== undefined || input.limit !== undefined)) {
|
|
2035
|
+
return { success: false, isError: true, error: 'Use either offset/limit (lines) or offsetChars/limitChars (characters), not both.' };
|
|
1799
2036
|
}
|
|
1800
2037
|
let content = generatedContent;
|
|
1801
2038
|
if (content === undefined) {
|
|
1802
|
-
const { mount, relativePath } = this.
|
|
2039
|
+
const { mount, relativePath } = this.parseFilePath(input.path);
|
|
1803
2040
|
const store = this.getStore();
|
|
1804
2041
|
const found = await this.withMount(mount, async () => {
|
|
1805
2042
|
const report = await this.ensureSynced(mount, relativePath);
|
|
@@ -1837,13 +2074,29 @@ export class WorkspaceModule {
|
|
|
1837
2074
|
},
|
|
1838
2075
|
};
|
|
1839
2076
|
}
|
|
1840
|
-
const lines = content
|
|
2077
|
+
const lines = linesOf(content);
|
|
1841
2078
|
// Apply offset/limit. An unlimited read of a large file would inject the
|
|
1842
2079
|
// whole thing into the turn (a 2MB file ≈ 600k tokens → the request blows
|
|
1843
2080
|
// past the model context and 400s), so an omitted limit falls back to a
|
|
1844
2081
|
// default cap; the result reports total/from/to so the agent can page.
|
|
1845
2082
|
const DEFAULT_READ_LINE_LIMIT = 2000;
|
|
1846
2083
|
const startLine = (input.offset ?? 1) - 1; // Convert to 0-indexed
|
|
2084
|
+
if (startLine >= lines.length) {
|
|
2085
|
+
// Nothing to show: an empty file, or an offset past the last line. No
|
|
2086
|
+
// range is claimed, and the line count says where the file ends.
|
|
2087
|
+
return {
|
|
2088
|
+
success: true,
|
|
2089
|
+
data: {
|
|
2090
|
+
path: input.path,
|
|
2091
|
+
totalLines: lines.length,
|
|
2092
|
+
// Reading an empty file from its start is reading it whole.
|
|
2093
|
+
...(startLine > 0
|
|
2094
|
+
? { note: `Line ${startLine + 1} is past the end: the file has ${lines.length} line${lines.length === 1 ? '' : 's'}.` }
|
|
2095
|
+
: {}),
|
|
2096
|
+
content: '',
|
|
2097
|
+
},
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
1847
2100
|
const effectiveLimit = input.limit ?? DEFAULT_READ_LINE_LIMIT;
|
|
1848
2101
|
const endLine = startLine + effectiveLimit;
|
|
1849
2102
|
const slice = lines.slice(startLine, endLine);
|
|
@@ -1870,7 +2123,7 @@ export class WorkspaceModule {
|
|
|
1870
2123
|
let mount;
|
|
1871
2124
|
let relativePath;
|
|
1872
2125
|
try {
|
|
1873
|
-
({ mount, relativePath } = this.
|
|
2126
|
+
({ mount, relativePath } = this.parseFilePath(input.path));
|
|
1874
2127
|
}
|
|
1875
2128
|
catch (err) {
|
|
1876
2129
|
return {
|
|
@@ -1939,7 +2192,10 @@ export class WorkspaceModule {
|
|
|
1939
2192
|
}
|
|
1940
2193
|
}
|
|
1941
2194
|
async handleWrite(input) {
|
|
1942
|
-
|
|
2195
|
+
if (LONE_SURROGATE.test(input.content)) {
|
|
2196
|
+
return { success: false, isError: true, error: 'The write did nothing: "content" holds a lone UTF-16 surrogate, which UTF-8 can\'t store.' };
|
|
2197
|
+
}
|
|
2198
|
+
const { mount, relativePath } = this.parseFilePath(input.path);
|
|
1943
2199
|
if (mount.config.mode === 'read-only') {
|
|
1944
2200
|
return { success: false, error: `Mount "${mount.config.name}" is read-only`, isError: true };
|
|
1945
2201
|
}
|
|
@@ -1949,112 +2205,204 @@ export class WorkspaceModule {
|
|
|
1949
2205
|
return { success: false, error: `Content exceeds max file size (${maxSize} bytes)`, isError: true };
|
|
1950
2206
|
}
|
|
1951
2207
|
const buffer = Buffer.from(input.content, 'utf-8');
|
|
1952
|
-
const
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
2208
|
+
const receipt = { path: input.path, size: buffer.byteLength, hash: hashContent(input.content) };
|
|
2209
|
+
let committed = false;
|
|
2210
|
+
let refused;
|
|
2211
|
+
let modeKept;
|
|
2212
|
+
try {
|
|
2213
|
+
({ refused, modeKept } = await this.withMount(mount, async () => {
|
|
2214
|
+
await this.settleBeforeMutation(mount, relativePath);
|
|
2215
|
+
const blobHash = store.storeBlob(buffer, 'text/plain');
|
|
2216
|
+
// An overwrite keeps the file's mode, as an edit does.
|
|
2217
|
+
const existing = store.treeGet(mount.treeStateId, relativePath);
|
|
2218
|
+
return this.commitToolChange(mount, relativePath, { kind: 'set', blobHash, size: buffer.byteLength, ...(existing ? { mode: existing.mode } : {}) }, () => { committed = true; });
|
|
2219
|
+
}));
|
|
1963
2220
|
}
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
}
|
|
1971
|
-
};
|
|
2221
|
+
catch (error) {
|
|
2222
|
+
if (!committed)
|
|
2223
|
+
throw error;
|
|
2224
|
+
return storedButUnfinished('write', input.path, { threw: error instanceof Error ? error.message : String(error) });
|
|
2225
|
+
}
|
|
2226
|
+
if (refused !== null)
|
|
2227
|
+
return storedButUnfinished('write', input.path, { refused });
|
|
2228
|
+
return { success: true, data: { ...receipt, ...(modeKept ? { modeKept } : {}) } };
|
|
1972
2229
|
}
|
|
1973
2230
|
async handleEdit(input) {
|
|
1974
|
-
|
|
2231
|
+
// Refused before anything is read: an empty oldString matches between
|
|
2232
|
+
// every character, and UTF-8 can't store a lone surrogate.
|
|
2233
|
+
if (input.oldString === '') {
|
|
2234
|
+
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.' };
|
|
2235
|
+
}
|
|
2236
|
+
for (const field of ['oldString', 'newString']) {
|
|
2237
|
+
if (LONE_SURROGATE.test(input[field])) {
|
|
2238
|
+
return { success: false, isError: true, error: `The edit did nothing: "${field}" holds a lone UTF-16 surrogate, which UTF-8 can't store.` };
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
const { mount, relativePath } = this.parseFilePath(input.path);
|
|
1975
2242
|
if (mount.config.mode === 'read-only') {
|
|
1976
2243
|
return { success: false, error: `Mount "${mount.config.name}" is read-only`, isError: true };
|
|
1977
2244
|
}
|
|
1978
2245
|
const store = this.getStore();
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
2246
|
+
let committed = false;
|
|
2247
|
+
let appliedNote = null;
|
|
2248
|
+
try {
|
|
2249
|
+
return await this.withMount(mount, async () => {
|
|
2250
|
+
// The edit applies to the path as disk holds it now: a change on disk
|
|
2251
|
+
// to a file the workspace holds is taken in first, by the rule a
|
|
2252
|
+
// listing uses. The pass settles a pending intent on the path too, as
|
|
2253
|
+
// settleBeforeMutation's would, and the commit enforces the rest
|
|
2254
|
+
// (commitToolChange). A write passes over its path only to settle a
|
|
2255
|
+
// pending intent, since it replaces the whole file. As every file
|
|
2256
|
+
// tool's pass does, it names the path it acts on, so a file the
|
|
2257
|
+
// mount passes over by design is observed (see PassOptions.named).
|
|
2258
|
+
const pass = await this.passUnlocked(mount, { kind: 'paths', paths: [relativePath] }, { named: true });
|
|
2259
|
+
const report = pass.reports.get(relativePath);
|
|
2260
|
+
// A mode taken in alone isn't a change to the text: it brings nothing
|
|
2261
|
+
// new to read, as it makes no event (emitFsEvents), and the edit then
|
|
2262
|
+
// writes at the mode disk has, so it leaves nothing to say.
|
|
2263
|
+
const took = (op) => pass.ops.some((o) => o.path === relativePath && o.op === op && !o.modeOnly);
|
|
2264
|
+
// Which copy the edit used, when it isn't the one the workspace last
|
|
2265
|
+
// agreed on with disk: disk's newer text, or the workspace's copy
|
|
2266
|
+
// because disk's couldn't be read or disk has none. A file the
|
|
2267
|
+
// workspace didn't hold has no older copy, so taking it in needs no
|
|
2268
|
+
// word.
|
|
2269
|
+
const whichCopy = (verb) => took('modified')
|
|
2270
|
+
? `Disk's copy had changed since the workspace last agreed with it, so it was taken in first, and the edit ${verb} that text.`
|
|
2271
|
+
: report?.state === 'unverified'
|
|
2272
|
+
? `The path couldn't be checked against disk (${report.note ?? 'not observed'}), so the edit ${verb} the workspace's copy.`
|
|
2273
|
+
: report?.state === 'disk-missing-provenance-unknown'
|
|
2274
|
+
? `Disk has no copy of this file, and nothing records whether it was deleted there, so the edit ${verb} the workspace's copy.`
|
|
2275
|
+
: null;
|
|
2276
|
+
// A conflict the pass found is named when the edit is refused for the
|
|
2277
|
+
// text it found, on either kind of mount, and on a success where the
|
|
2278
|
+
// mount doesn't push, since such a mount keeps the conflict until
|
|
2279
|
+
// materialize or sync settles it. A pushing mount's success met none
|
|
2280
|
+
// (below). A stored edit that couldn't finish leaves it to the push's
|
|
2281
|
+
// reason, which names the conflict, or to ls, which its answer points to.
|
|
2282
|
+
const conflict = report?.state === 'conflict' ? report.conflict?.kind ?? 'both-changed' : null;
|
|
2283
|
+
// Where disk has no copy now, whatever the conflict's kind, a sync of
|
|
2284
|
+
// the path takes disk's deletion and a forced materialize writes the
|
|
2285
|
+
// workspace's copy back. Where it has one, a sync keeps it and gives
|
|
2286
|
+
// up the workspace's, whether or not the workspace can hold disk's
|
|
2287
|
+
// copy: one it can't hold stays on disk alone.
|
|
2288
|
+
const conflictNote = (where) => conflict === null ? null
|
|
2289
|
+
: `The path is in conflict with disk (${conflict}): the edit ${where} the workspace's copy. ` + (report?.diskNow === 'absent'
|
|
2290
|
+
? "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."
|
|
2291
|
+
: "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.");
|
|
2292
|
+
const suffix = [whichCopy('looked in'), conflictNote('looked in')].filter((n) => n !== null).map((n) => ` ${n}`).join('');
|
|
2293
|
+
appliedNote = whichCopy('applied to');
|
|
2294
|
+
const entry = store.treeGet(mount.treeStateId, relativePath);
|
|
2295
|
+
if (!entry) {
|
|
2296
|
+
// A pass also drops a copy disk made binary or too large, which
|
|
2297
|
+
// the disk-only answer describes; any other drop is disk's deletion.
|
|
2298
|
+
const error = took('deleted') && report?.state !== 'disk-only'
|
|
2299
|
+
? `File not found: ${input.path}. Disk's copy was deleted since the workspace last agreed with it, and that was taken in first.`
|
|
2300
|
+
: this.notInWorkspace(input.path, report);
|
|
2301
|
+
return { success: false, error, isError: true };
|
|
2302
|
+
}
|
|
2303
|
+
const blob = store.getBlob(entry.blobHash);
|
|
2304
|
+
if (!blob) {
|
|
2305
|
+
return { success: false, error: `Blob not found for: ${input.path}`, isError: true };
|
|
2306
|
+
}
|
|
2307
|
+
// Decoding would turn each invalid sequence into U+FFFD, and writing
|
|
2308
|
+
// the text back would change bytes the edit never named.
|
|
2309
|
+
if (!isUtf8(blob)) {
|
|
2310
|
+
return {
|
|
2311
|
+
success: false,
|
|
2312
|
+
isError: true,
|
|
2313
|
+
error: `The edit did nothing: ${input.path} isn't valid UTF-8, so editing it would change bytes outside the text it replaces. `
|
|
2314
|
+
+ `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}`,
|
|
2315
|
+
};
|
|
2316
|
+
}
|
|
2317
|
+
let content = blob.toString('utf-8');
|
|
2318
|
+
// An edit that would replace nothing is refused, replaceAll or not;
|
|
2319
|
+
// without replaceAll, the text must occur once, and the refusal says
|
|
2320
|
+
// how often it does. With replaceAll, whether it occurs is enough.
|
|
2321
|
+
const count = input.replaceAll
|
|
2322
|
+
? Number(content.includes(input.oldString))
|
|
2323
|
+
: content.split(input.oldString).length - 1;
|
|
1994
2324
|
if (count === 0) {
|
|
1995
|
-
return { success: false, error: `String not found in ${input.path}`, isError: true };
|
|
2325
|
+
return { success: false, error: `String not found in ${input.path}.${suffix}`, isError: true };
|
|
1996
2326
|
}
|
|
1997
|
-
if (count > 1) {
|
|
2327
|
+
if (count > 1 && !input.replaceAll) {
|
|
1998
2328
|
return {
|
|
1999
2329
|
success: false,
|
|
2000
|
-
error: `String found ${count} times in ${input.path}. Use replaceAll: true or provide more context
|
|
2330
|
+
error: `String found ${count} times in ${input.path}. Use replaceAll: true or provide more context.${suffix}`,
|
|
2001
2331
|
isError: true,
|
|
2002
2332
|
};
|
|
2003
2333
|
}
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2334
|
+
// A replacer function, so that the new text goes in as given: in a
|
|
2335
|
+
// replacement string, $&, $$, $' and $` are patterns.
|
|
2336
|
+
content = input.replaceAll
|
|
2337
|
+
? content.replaceAll(input.oldString, () => input.newString)
|
|
2338
|
+
: content.replace(input.oldString, () => input.newString);
|
|
2339
|
+
const newBuffer = Buffer.from(content, 'utf-8');
|
|
2340
|
+
const newBlobHash = store.storeBlob(newBuffer, 'text/plain');
|
|
2341
|
+
const { refused, modeKept } = await this.commitToolChange(mount, relativePath, {
|
|
2342
|
+
kind: 'set',
|
|
2343
|
+
blobHash: newBlobHash,
|
|
2344
|
+
size: newBuffer.byteLength,
|
|
2345
|
+
mode: entry.mode,
|
|
2346
|
+
}, () => { committed = true; });
|
|
2347
|
+
if (refused !== null)
|
|
2348
|
+
return storedButUnfinished('edit', input.path, { refused }, appliedNote);
|
|
2349
|
+
// The edit makes the path the workspace's own change (commitToolChange),
|
|
2350
|
+
// so a file disk lacked goes back with this edit's push, or with the
|
|
2351
|
+
// next materialize where the mount doesn't push. A push that didn't
|
|
2352
|
+
// complete answers above, with its reason. One that completed met no
|
|
2353
|
+
// conflict, since this push, unforced, stops at one: the edit made the
|
|
2354
|
+
// two copies agree, which settles it, or there was none. So only a
|
|
2355
|
+
// mount that doesn't push still holds the conflict the pass found.
|
|
2356
|
+
const pushes = this.pushesOnCommit(mount);
|
|
2357
|
+
const backOnDisk = report?.state === 'disk-missing-provenance-unknown'
|
|
2358
|
+
? `${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.`
|
|
2359
|
+
: null;
|
|
2360
|
+
const notes = [appliedNote, backOnDisk, pushes ? null : conflictNote('is in')].filter((n) => n !== null);
|
|
2017
2361
|
return {
|
|
2018
|
-
success:
|
|
2019
|
-
|
|
2020
|
-
|
|
2362
|
+
success: true,
|
|
2363
|
+
data: {
|
|
2364
|
+
path: input.path,
|
|
2365
|
+
size: newBuffer.byteLength,
|
|
2366
|
+
...(notes.length > 0 ? { note: notes.join(' ') } : {}),
|
|
2367
|
+
...(modeKept ? { modeKept } : {}),
|
|
2368
|
+
},
|
|
2021
2369
|
};
|
|
2022
|
-
}
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
};
|
|
2030
|
-
});
|
|
2370
|
+
});
|
|
2371
|
+
}
|
|
2372
|
+
catch (error) {
|
|
2373
|
+
if (!committed)
|
|
2374
|
+
throw error;
|
|
2375
|
+
return storedButUnfinished('edit', input.path, { threw: error instanceof Error ? error.message : String(error) }, appliedNote);
|
|
2376
|
+
}
|
|
2031
2377
|
}
|
|
2032
2378
|
async handleDelete(input) {
|
|
2033
|
-
const { mount, relativePath } = this.
|
|
2379
|
+
const { mount, relativePath } = this.parseFilePath(input.path);
|
|
2034
2380
|
if (mount.config.mode === 'read-only') {
|
|
2035
2381
|
return { success: false, error: `Mount "${mount.config.name}" is read-only`, isError: true };
|
|
2036
2382
|
}
|
|
2037
2383
|
const store = this.getStore();
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
await this.
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
};
|
|
2053
|
-
}
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2384
|
+
let committed = false;
|
|
2385
|
+
try {
|
|
2386
|
+
return await this.withMount(mount, async () => {
|
|
2387
|
+
const report = await this.ensureSynced(mount, relativePath);
|
|
2388
|
+
await this.settleBeforeMutation(mount, relativePath);
|
|
2389
|
+
const entry = store.treeGet(mount.treeStateId, relativePath);
|
|
2390
|
+
if (!entry) {
|
|
2391
|
+
return { success: false, error: this.notInWorkspace(input.path, report), isError: true };
|
|
2392
|
+
}
|
|
2393
|
+
const { refused } = await this.commitToolChange(mount, relativePath, { kind: 'remove', previousHash: entry.blobHash }, () => { committed = true; });
|
|
2394
|
+
if (refused !== null)
|
|
2395
|
+
return storedButUnfinished('delete', input.path, { refused });
|
|
2396
|
+
// Its whole receipt is what it changed: kept in the stub if the guard
|
|
2397
|
+
// withholds the result (agent-framework #277).
|
|
2398
|
+
return { success: true, data: { path: input.path, deleted: true }, keepWhenWithheld: ['path', 'deleted'] };
|
|
2399
|
+
});
|
|
2400
|
+
}
|
|
2401
|
+
catch (error) {
|
|
2402
|
+
if (!committed)
|
|
2403
|
+
throw error;
|
|
2404
|
+
return storedButUnfinished('delete', input.path, { threw: error instanceof Error ? error.message : String(error) });
|
|
2405
|
+
}
|
|
2058
2406
|
}
|
|
2059
2407
|
async handleLs(input) {
|
|
2060
2408
|
const store = this.getStore();
|
|
@@ -2118,8 +2466,13 @@ export class WorkspaceModule {
|
|
|
2118
2466
|
const mountsToSearch = input.path
|
|
2119
2467
|
? [this.parsePath(input.path)]
|
|
2120
2468
|
: [...this.mounts.values()].map(m => ({ mount: m, relativePath: '' }));
|
|
2469
|
+
// Every match lies beneath the pattern's literal directory, so the walk
|
|
2470
|
+
// starts there: past the walk's cap, a walk of the whole scope can stop
|
|
2471
|
+
// before it gets there.
|
|
2472
|
+
const patternDir = literalDirOf(input.pattern);
|
|
2121
2473
|
for (const { mount, relativePath } of mountsToSearch) {
|
|
2122
|
-
const
|
|
2474
|
+
const start = await this.walkStart(mount, relativePath, patternDir);
|
|
2475
|
+
const pass = await this.withMount(mount, () => this.passUnlocked(mount, { kind: 'dir', dir: start, recursive: true }));
|
|
2123
2476
|
for (const report of [...pass.reports.values()].sort((a, b) => a.path.localeCompare(b.path))) {
|
|
2124
2477
|
const testPath = relativePath ? report.path.slice(relativePath.length + 1) : report.path;
|
|
2125
2478
|
if (regex.test(testPath)) {
|
|
@@ -2147,9 +2500,14 @@ export class WorkspaceModule {
|
|
|
2147
2500
|
regex = new RegExp(input.pattern);
|
|
2148
2501
|
}
|
|
2149
2502
|
catch (e) {
|
|
2150
|
-
|
|
2503
|
+
// The parser's reason says where the pattern goes wrong.
|
|
2504
|
+
return { success: false, error: `The grep did nothing: ${e instanceof Error ? e.message : String(e)}.`, isError: true };
|
|
2151
2505
|
}
|
|
2152
2506
|
const fileGlob = input.glob ? globToRegex(input.glob) : null;
|
|
2507
|
+
// The filter matches mount-relative paths, so every file it lets through
|
|
2508
|
+
// lies beneath its literal directory, and the walk starts at the deeper of
|
|
2509
|
+
// that and `path`.
|
|
2510
|
+
const filterDir = input.glob ? literalDirOf(input.glob) : '';
|
|
2153
2511
|
const contextBefore = input.contextBefore ?? 0;
|
|
2154
2512
|
const contextAfter = input.contextAfter ?? 0;
|
|
2155
2513
|
const mountsToSearch = input.path
|
|
@@ -2159,7 +2517,7 @@ export class WorkspaceModule {
|
|
|
2159
2517
|
const skipped = [];
|
|
2160
2518
|
const incomplete = [];
|
|
2161
2519
|
const search = (content) => {
|
|
2162
|
-
const lines = content
|
|
2520
|
+
const lines = linesOf(content);
|
|
2163
2521
|
const found = [];
|
|
2164
2522
|
for (let i = 0; i < lines.length; i++) {
|
|
2165
2523
|
if (regex.test(lines[i])) {
|
|
@@ -2175,6 +2533,19 @@ export class WorkspaceModule {
|
|
|
2175
2533
|
return found;
|
|
2176
2534
|
};
|
|
2177
2535
|
for (const { mount, relativePath } of mountsToSearch) {
|
|
2536
|
+
// A path named in another mount's directory, as holderOf resolves it,
|
|
2537
|
+
// is that mount's: grep searches nothing of this mount's there, and
|
|
2538
|
+
// says whose it is.
|
|
2539
|
+
if (this.holderOf(mount.config.name, relativePath)) {
|
|
2540
|
+
incomplete.push({ path: `${mount.config.name}/${relativePath}`, reason: this.otherMountNote(mount.config.name, relativePath, 'list') });
|
|
2541
|
+
continue;
|
|
2542
|
+
}
|
|
2543
|
+
// A `path` and a filter whose directories don't overlap leave no file
|
|
2544
|
+
// the filter can let through, so nothing is searched.
|
|
2545
|
+
const deeper = deeperOf(relativePath, filterDir);
|
|
2546
|
+
if (deeper === null)
|
|
2547
|
+
continue;
|
|
2548
|
+
const start = await this.walkStart(mount, relativePath, deeper === relativePath ? '' : deeper.slice(relativePath ? relativePath.length + 1 : 0));
|
|
2178
2549
|
await this.withMount(mount, async () => {
|
|
2179
2550
|
// A `path` naming a single file greps just that file; otherwise it is a
|
|
2180
2551
|
// directory prefix. (A file path once produced the prefix "notes.md/"
|
|
@@ -2190,7 +2561,7 @@ export class WorkspaceModule {
|
|
|
2190
2561
|
for (let attempt = 0; attempt < 3 && pass === null; attempt++) {
|
|
2191
2562
|
const next = await this.passUnlocked(mount, fileScope
|
|
2192
2563
|
? { kind: 'paths', paths: [relativePath] }
|
|
2193
|
-
: { kind: 'dir', dir:
|
|
2564
|
+
: { kind: 'dir', dir: start, recursive: true });
|
|
2194
2565
|
if (next.branchId === undefined || next.branchId === store.currentBranch().id)
|
|
2195
2566
|
pass = next;
|
|
2196
2567
|
}
|
|
@@ -2218,6 +2589,11 @@ export class WorkspaceModule {
|
|
|
2218
2589
|
});
|
|
2219
2590
|
continue;
|
|
2220
2591
|
}
|
|
2592
|
+
// A path in another mount's directory is that mount's: grep gives
|
|
2593
|
+
// none of this mount's copy of it, as read refuses it. Beneath a
|
|
2594
|
+
// scope it's passed over by design, like the directory.
|
|
2595
|
+
if (inOtherMount(report.path, this.othersOf(mount.config.name)))
|
|
2596
|
+
continue;
|
|
2221
2597
|
// The workspace's version, labelled with the path's state.
|
|
2222
2598
|
const entry = store.treeGet(mount.treeStateId, report.path);
|
|
2223
2599
|
const blob = entry ? store.getBlob(entry.blobHash) : null;
|
|
@@ -2280,7 +2656,8 @@ export class WorkspaceModule {
|
|
|
2280
2656
|
continue;
|
|
2281
2657
|
const entries = store.treeList(mount.treeStateId);
|
|
2282
2658
|
const currentSeq = store.currentSequence();
|
|
2283
|
-
const
|
|
2659
|
+
const own = this.ownPaths(mount);
|
|
2660
|
+
const intents = this.intents.get(name).list().filter(([p]) => own(p));
|
|
2284
2661
|
const conflictPaths = intents.filter(([, bi]) => bi.conflict).map(([p]) => p);
|
|
2285
2662
|
// A workspace deletion is pending until a push confirms it on disk:
|
|
2286
2663
|
// left there without applyDeletions, or unlinked but unconfirmed.
|
|
@@ -2323,13 +2700,15 @@ export class WorkspaceModule {
|
|
|
2323
2700
|
*/
|
|
2324
2701
|
materializeSelection(mount, explicitPath) {
|
|
2325
2702
|
const store = this.getStore();
|
|
2326
|
-
const
|
|
2703
|
+
const own = this.ownPaths(mount);
|
|
2704
|
+
const intents = new Map(this.intents.get(mount.config.name).list(explicitPath).filter(([p]) => own(p)));
|
|
2327
2705
|
const selected = new Set();
|
|
2328
2706
|
if (explicitPath !== '') {
|
|
2329
|
-
if (store.treeGet(mount.treeStateId, explicitPath))
|
|
2707
|
+
if (store.treeGet(mount.treeStateId, explicitPath) && own(explicitPath))
|
|
2330
2708
|
selected.add(explicitPath);
|
|
2331
2709
|
for (const e of store.treeList(mount.treeStateId, explicitPath + '/'))
|
|
2332
|
-
|
|
2710
|
+
if (own(e.path))
|
|
2711
|
+
selected.add(e.path);
|
|
2333
2712
|
for (const [p, bi] of intents)
|
|
2334
2713
|
if (bi.tombstone || bi.conflict)
|
|
2335
2714
|
selected.add(p);
|
|
@@ -2343,10 +2722,24 @@ export class WorkspaceModule {
|
|
|
2343
2722
|
selected.add(p);
|
|
2344
2723
|
return { paths: [...selected], inPlace: agreed > 0 };
|
|
2345
2724
|
}
|
|
2725
|
+
/**
|
|
2726
|
+
* Whether a path of the mount is its own to push and to count as owed:
|
|
2727
|
+
* not in another mount's directory nested in it. Copies the mount's tree
|
|
2728
|
+
* holds there (stored before the nesting) stay in its history and its
|
|
2729
|
+
* listings (and in status's fileCount, which counts the tree), passed over
|
|
2730
|
+
* by its syncs, and left out of what it pushes and of status's pending
|
|
2731
|
+
* changes, conflicts and workspace deletions.
|
|
2732
|
+
*/
|
|
2733
|
+
ownPaths(mount) {
|
|
2734
|
+
const others = this.othersOf(mount.config.name);
|
|
2735
|
+
return (path) => !inOtherMount(path, others);
|
|
2736
|
+
}
|
|
2346
2737
|
/**
|
|
2347
2738
|
* The workspace entries by what the evidence says of disk. `owed`: P
|
|
2348
|
-
* differs from the entry (a draft, or a push still pending),
|
|
2349
|
-
*
|
|
2739
|
+
* differs from the entry (a draft, or a push still pending), the entry
|
|
2740
|
+
* records a mode other than the one P agreed (a mode owed to disk), or
|
|
2741
|
+
* there is no P and the entry came from the workspace (a draft disk never
|
|
2742
|
+
* had).
|
|
2350
2743
|
* `unchecked`: no P and no workspace origin, an entry from before the
|
|
2351
2744
|
* evidence; a push agrees it, writes it where disk lacks it, or refuses it
|
|
2352
2745
|
* as a conflict. `agreed`: how many have P = S, so disk holds them. Status
|
|
@@ -2361,12 +2754,19 @@ export class WorkspaceModule {
|
|
|
2361
2754
|
const owed = [];
|
|
2362
2755
|
const unchecked = [];
|
|
2363
2756
|
let agreed = 0;
|
|
2757
|
+
const own = this.ownPaths(mount);
|
|
2364
2758
|
for (const e of store.treeList(mount.treeStateId)) {
|
|
2759
|
+
if (!own(e.path))
|
|
2760
|
+
continue;
|
|
2365
2761
|
const p = agreement.get(name, e.path);
|
|
2366
2762
|
if (p === undefined)
|
|
2367
2763
|
(intents.get(e.path)?.origin === 'store' ? owed : unchecked).push(e.path);
|
|
2368
2764
|
else if (p.kind !== 'content' || p.hash !== e.blobHash)
|
|
2369
2765
|
owed.push(e.path);
|
|
2766
|
+
// A mode the store records, other than the one disk last agreed with,
|
|
2767
|
+
// where modes are observed: elsewhere no push applies one.
|
|
2768
|
+
else if (this.modesObserved && p.mode !== undefined && (entryMode(e.mode) ?? p.mode) !== p.mode)
|
|
2769
|
+
owed.push(e.path);
|
|
2370
2770
|
else
|
|
2371
2771
|
agreed++;
|
|
2372
2772
|
}
|
|
@@ -2377,6 +2777,7 @@ export class WorkspaceModule {
|
|
|
2377
2777
|
const allWritten = [];
|
|
2378
2778
|
const allDeleted = [];
|
|
2379
2779
|
const pendingDeletions = [];
|
|
2780
|
+
const modesKept = [];
|
|
2380
2781
|
let explicit = null;
|
|
2381
2782
|
if (input.path)
|
|
2382
2783
|
explicit = this.parsePath(input.path);
|
|
@@ -2414,6 +2815,13 @@ export class WorkspaceModule {
|
|
|
2414
2815
|
blocked.push({ mount: name, reason: 'a read-only mount: materialize writes nothing to it' });
|
|
2415
2816
|
continue;
|
|
2416
2817
|
}
|
|
2818
|
+
// A path named in another mount's directory, as holderOf resolves it,
|
|
2819
|
+
// is that mount's: nothing of this mount's is pushed there, and the
|
|
2820
|
+
// caller is told whose it is.
|
|
2821
|
+
if (explicit && this.holderOf(name, explicit.relativePath)) {
|
|
2822
|
+
blocked.push({ mount: name, reason: `${explicit.relativePath}: ${this.otherMountNote(name, explicit.relativePath, 'push')}` });
|
|
2823
|
+
continue;
|
|
2824
|
+
}
|
|
2417
2825
|
const turn = await this.withMount(mount, async () => {
|
|
2418
2826
|
const branchId = store.currentBranch().id;
|
|
2419
2827
|
const reason = this.mountMaterializeBlockReason(store, mount);
|
|
@@ -2446,8 +2854,13 @@ export class WorkspaceModule {
|
|
|
2446
2854
|
// Refusals ride the same skipped list as branch blocks: divergence must
|
|
2447
2855
|
// be visible, not resolved silently either way.
|
|
2448
2856
|
for (const s of pushed.skipped) {
|
|
2449
|
-
|
|
2857
|
+
const reason = s.reason === NOT_PUSHED_IN_OTHER_MOUNT ? this.otherMountNote(name, s.path, 'push') : s.reason;
|
|
2858
|
+
blocked.push({ mount: name, reason: `${s.path}: ${reason}` });
|
|
2450
2859
|
}
|
|
2860
|
+
// Written paths whose mode stayed as disk had it: no refusal, so not
|
|
2861
|
+
// among what was skipped.
|
|
2862
|
+
for (const k of pushed.modesKept)
|
|
2863
|
+
modesKept.push({ mount: name, ...k });
|
|
2451
2864
|
// What disk still owes is kept by its evidence, so the watermark and
|
|
2452
2865
|
// the pin only feed the branch guard. Both are the push's own: what it
|
|
2453
2866
|
// planned from is what disk now reflects, whatever branch was selected
|
|
@@ -2488,10 +2901,11 @@ export class WorkspaceModule {
|
|
|
2488
2901
|
}
|
|
2489
2902
|
: {}),
|
|
2490
2903
|
...(blocked.length > 0 ? { skipped: blocked } : {}),
|
|
2904
|
+
...(modesKept.length > 0 ? { modesKept } : {}),
|
|
2491
2905
|
},
|
|
2492
2906
|
// What disk lost, then what it was written: kept in the stub if the
|
|
2493
|
-
// guard withholds the result (agent-framework #277). What was left
|
|
2494
|
-
// skipped, and why, is in the full result.
|
|
2907
|
+
// guard withholds the result (agent-framework #277). What was left,
|
|
2908
|
+
// skipped or written at disk's mode, and why, is in the full result.
|
|
2495
2909
|
keepWhenWithheld: ['deleted', 'materialized'],
|
|
2496
2910
|
};
|
|
2497
2911
|
}
|
|
@@ -2562,7 +2976,8 @@ export class WorkspaceModule {
|
|
|
2562
2976
|
rootsAccepted.push(name);
|
|
2563
2977
|
if (!relativePath) {
|
|
2564
2978
|
// A full recheck: every file rehashed, nothing pending discarded.
|
|
2565
|
-
|
|
2979
|
+
// Past the mount's walkCap, it resumes where the last one stopped.
|
|
2980
|
+
return this.passUnlocked(mount, { kind: 'dir', dir: '', recursive: true }, { rehash: true, resume: true });
|
|
2566
2981
|
}
|
|
2567
2982
|
// An explicit path: the workspace takes disk's state there.
|
|
2568
2983
|
const prefix = relativePath + '/';
|
|
@@ -2575,7 +2990,7 @@ export class WorkspaceModule {
|
|
|
2575
2990
|
const opts = { rehash: true, adopt: () => true, branchId: askedOn, named: true };
|
|
2576
2991
|
if (!isDir)
|
|
2577
2992
|
return this.passUnlocked(mount, { kind: 'paths', paths: [relativePath] }, opts);
|
|
2578
|
-
const beneath = await this.passUnlocked(mount, { kind: 'dir', dir: relativePath, recursive: true }, opts);
|
|
2993
|
+
const beneath = await this.passUnlocked(mount, { kind: 'dir', dir: relativePath, recursive: true }, { ...opts, resume: true });
|
|
2579
2994
|
// Something at the path itself is no part of a directory scope: a file
|
|
2580
2995
|
// disk has where the workspace holds a directory, or a workspace file
|
|
2581
2996
|
// where disk has a directory. Its own pass takes disk's state there
|
|
@@ -2629,13 +3044,20 @@ export class WorkspaceModule {
|
|
|
2629
3044
|
for (const [reason, count] of pass.passedOver)
|
|
2630
3045
|
allPassedOver.push({ mount: name, reason, count });
|
|
2631
3046
|
// A path named explicitly is synced even where the mount's ignore list
|
|
2632
|
-
// covers it, and the result says so: a sync
|
|
2633
|
-
// over it, so nothing else keeps it current
|
|
2634
|
-
|
|
3047
|
+
// or an editor's file name covers it, and the result says so: a sync
|
|
3048
|
+
// without a path passes over it, so nothing else keeps it current
|
|
3049
|
+
// (agent-framework #276). A path in another mount's directory isn't
|
|
3050
|
+
// synced at all, and the note says so.
|
|
3051
|
+
const passedOver = passedOverAt(relativePath, mount.config.ignore ?? [], this.othersOf(name));
|
|
3052
|
+
if (passedOver !== undefined) {
|
|
2635
3053
|
allIgnored.push({
|
|
2636
3054
|
mount: name,
|
|
2637
3055
|
path: relativePath,
|
|
2638
|
-
note:
|
|
3056
|
+
note: passedOver === OTHER_MOUNT
|
|
3057
|
+
? this.otherMountNote(name, relativePath, 'sync')
|
|
3058
|
+
: passedOver === IGNORED
|
|
3059
|
+
? 'this mount\'s ignore list covers it, so a sync without a path won\'t maintain it'
|
|
3060
|
+
: 'it has an editor\'s swap or backup file name, or lies under one, so a sync without a path won\'t maintain it',
|
|
2639
3061
|
});
|
|
2640
3062
|
}
|
|
2641
3063
|
}
|
|
@@ -2704,10 +3126,14 @@ export class WorkspaceModule {
|
|
|
2704
3126
|
* After an agent's completed tool batch, before anything continues its turn:
|
|
2705
3127
|
* scan every `watch: 'on-agent-action'` mount, so what the batch's tools did
|
|
2706
3128
|
* on disk (a shell command's files, a deletion) is in the workspace when the
|
|
2707
|
-
* agent next looks.
|
|
2708
|
-
*
|
|
2709
|
-
*
|
|
2710
|
-
*
|
|
3129
|
+
* agent next looks. On a mount over its walkCap, that holds for the first
|
|
3130
|
+
* walkCap entries in the walk's order, which every batch's scan takes; files
|
|
3131
|
+
* past them come in through syncs and the listings that reach them, while a
|
|
3132
|
+
* read takes in only a file the workspace doesn't hold yet. A scan that
|
|
3133
|
+
* outlasts the deadline stops holding the round: the miss is recorded on
|
|
3134
|
+
* the mount's status and pushed as a `workspace:agent-action-scan-incomplete`
|
|
3135
|
+
* event, and the scan continues, deciding on the branch selected when it
|
|
3136
|
+
* applies.
|
|
2711
3137
|
*/
|
|
2712
3138
|
async onToolBatchComplete(_agentName) {
|
|
2713
3139
|
// Every agent's workspace is the store's current branch, so the agent
|
|
@@ -2774,6 +3200,10 @@ export class WorkspaceModule {
|
|
|
2774
3200
|
const pass = this.withMount(mount, () => {
|
|
2775
3201
|
if (mount.agentActionScanWaiting === scan)
|
|
2776
3202
|
mount.agentActionScanWaiting = undefined;
|
|
3203
|
+
// From the mount's root, keeping no place, as a listing keeps none:
|
|
3204
|
+
// on a mount over its walkCap, every batch's scan takes the first
|
|
3205
|
+
// walkCap entries in the walk's order, and the place that syncs and
|
|
3206
|
+
// catch-up scans resume from stays theirs.
|
|
2777
3207
|
return this.passUnlocked(mount, { kind: 'dir', dir: '', recursive: true });
|
|
2778
3208
|
});
|
|
2779
3209
|
scan.settled = pass.then((result) => {
|
|
@@ -2815,9 +3245,11 @@ export class WorkspaceModule {
|
|
|
2815
3245
|
return AGENT_ACTION_SCAN_DEADLINE_MAX_MS;
|
|
2816
3246
|
}
|
|
2817
3247
|
/**
|
|
2818
|
-
* Push one ProcessEvent per op for a pass's tree changes and new conflicts
|
|
2819
|
-
*
|
|
2820
|
-
*
|
|
3248
|
+
* Push one ProcessEvent per op for a pass's tree changes and new conflicts,
|
|
3249
|
+
* apart from an op that changes only a file's mode, which brings nothing
|
|
3250
|
+
* new to read and makes none. A conflict rides the event for the disk
|
|
3251
|
+
* change behind it (modified, or deleted for a disk deletion of a file with
|
|
3252
|
+
* newer workspace changes).
|
|
2821
3253
|
*/
|
|
2822
3254
|
emitFsEvents(mountName, ops, newConflicts = []) {
|
|
2823
3255
|
if (!this.ctx || (ops.length === 0 && newConflicts.length === 0))
|
|
@@ -2832,8 +3264,12 @@ export class WorkspaceModule {
|
|
|
2832
3264
|
group.conflicts.push(full);
|
|
2833
3265
|
byOp.set(op, group);
|
|
2834
3266
|
};
|
|
2835
|
-
|
|
2836
|
-
|
|
3267
|
+
// A path whose mode alone changed, on bytes already held, makes no
|
|
3268
|
+
// event: it brings nothing new to read, and a `chmod -R` over a tree
|
|
3269
|
+
// would otherwise reach every policy that matches its paths.
|
|
3270
|
+
for (const { path, op, modeOnly } of ops)
|
|
3271
|
+
if (!modeOnly)
|
|
3272
|
+
add(op, path, false);
|
|
2837
3273
|
for (const { path, op } of newConflicts)
|
|
2838
3274
|
add(op, path, true);
|
|
2839
3275
|
for (const [op, { paths, conflicts }] of byOp) {
|
|
@@ -2860,8 +3296,92 @@ function stateView(report) {
|
|
|
2860
3296
|
// ==========================================================================
|
|
2861
3297
|
// Utilities
|
|
2862
3298
|
// ==========================================================================
|
|
3299
|
+
/** A lone UTF-16 surrogate, which UTF-8 can't store: in `u` mode, a pair is one code point. */
|
|
3300
|
+
const LONE_SURROGATE = /\p{Cs}/u;
|
|
3301
|
+
/**
|
|
3302
|
+
* The answer to a write, edit or delete that the workspace stored but
|
|
3303
|
+
* couldn't finish: its push to disk didn't complete, or what follows the tree
|
|
3304
|
+
* entry threw. It stays an error, since part of the call failed, but it leads
|
|
3305
|
+
* with what the caller must know before acting on it: the change is stored,
|
|
3306
|
+
* and sending it again could apply it twice. The push's reason says what disk
|
|
3307
|
+
* holds and the way out, as materialize takes it. The text carries all of it,
|
|
3308
|
+
* since an error's data reaches neither the model nor a code_execution
|
|
3309
|
+
* script. An edit's note on which copy it applied to comes last.
|
|
3310
|
+
*/
|
|
3311
|
+
function storedButUnfinished(tool, path, outcome, note = null) {
|
|
3312
|
+
const change = tool === 'delete' ? 'deletion' : tool;
|
|
3313
|
+
const stored = `The ${change} of "${path}" is stored in the workspace, so don't send it again`
|
|
3314
|
+
+ (tool === 'edit' ? ': a second edit can apply the change twice.' : '.');
|
|
3315
|
+
const rest = 'refused' in outcome
|
|
3316
|
+
? ` Its push to disk didn't complete: ${outcome.refused}.`
|
|
3317
|
+
: ` The rest of the call failed: ${outcome.threw}. ls shows the path's state.`;
|
|
3318
|
+
return { success: false, isError: true, error: stored + rest + (note !== null ? ` ${note}` : '') };
|
|
3319
|
+
}
|
|
2863
3320
|
/**
|
|
2864
|
-
*
|
|
3321
|
+
* A text's lines, as read and grep number them: a final line end ends the
|
|
3322
|
+
* last line rather than starting an empty one, so "a\nb\n" is two lines,
|
|
3323
|
+
* and an empty text has none.
|
|
3324
|
+
*/
|
|
3325
|
+
function linesOf(content) {
|
|
3326
|
+
const lines = content.split('\n');
|
|
3327
|
+
if (lines.at(-1) === '')
|
|
3328
|
+
lines.pop();
|
|
3329
|
+
return lines;
|
|
3330
|
+
}
|
|
3331
|
+
/**
|
|
3332
|
+
* The least each tool's whole-number fields may be, beside their schemas,
|
|
3333
|
+
* whose type (membrane's ToolParameter) has no `minimum`.
|
|
3334
|
+
*/
|
|
3335
|
+
const INPUT_MINIMUMS = {
|
|
3336
|
+
read: { offset: 1, limit: 1, offsetChars: 0, limitChars: 1 },
|
|
3337
|
+
grep: { contextBefore: 0, contextAfter: 0 },
|
|
3338
|
+
};
|
|
3339
|
+
/**
|
|
3340
|
+
* Why a tool call's input doesn't fit its tool's schema, or null when it does:
|
|
3341
|
+
* an object, each required field present, and each field its schema calls a
|
|
3342
|
+
* string, a boolean or an integer of that type, an integer at least its
|
|
3343
|
+
* minimum. The workspace's schemas use no other types. Without it, an edit
|
|
3344
|
+
* missing `newString` wrote the word "undefined", and an offset of 1.5
|
|
3345
|
+
* numbered lines 1.5, 2.5 and on. A field the schema doesn't describe is left
|
|
3346
|
+
* to its handler.
|
|
3347
|
+
*/
|
|
3348
|
+
function inputProblem(schema, input, minimums = {}) {
|
|
3349
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
3350
|
+
return 'its input must be an object of the fields its schema lists';
|
|
3351
|
+
}
|
|
3352
|
+
const fields = input;
|
|
3353
|
+
for (const name of schema.required ?? []) {
|
|
3354
|
+
if (fields[name] === undefined)
|
|
3355
|
+
return `"${name}" is required`;
|
|
3356
|
+
}
|
|
3357
|
+
for (const [name, value] of Object.entries(fields)) {
|
|
3358
|
+
const type = schema.properties?.[name]?.type;
|
|
3359
|
+
if (value === undefined || type === undefined)
|
|
3360
|
+
continue;
|
|
3361
|
+
const minimum = minimums[name];
|
|
3362
|
+
const fits = type === 'string' ? typeof value === 'string'
|
|
3363
|
+
: type === 'boolean' ? typeof value === 'boolean'
|
|
3364
|
+
: type === 'integer'
|
|
3365
|
+
? Number.isSafeInteger(value) && (minimum === undefined || value >= minimum)
|
|
3366
|
+
: true;
|
|
3367
|
+
if (!fits) {
|
|
3368
|
+
const wanted = type === 'string' ? 'a string'
|
|
3369
|
+
: type === 'boolean' ? 'true or false'
|
|
3370
|
+
: minimum === undefined ? 'a whole number' : `a whole number of at least ${minimum}`;
|
|
3371
|
+
const got = value === null ? 'null'
|
|
3372
|
+
: typeof value === 'number' ? String(value)
|
|
3373
|
+
: Array.isArray(value) ? 'an array'
|
|
3374
|
+
: typeof value === 'object' ? 'an object'
|
|
3375
|
+
: `a ${typeof value}`;
|
|
3376
|
+
return `"${name}" must be ${wanted}, not ${got}`;
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
return null;
|
|
3380
|
+
}
|
|
3381
|
+
/**
|
|
3382
|
+
* Convert a glob pattern to a RegExp. Throws, naming the pattern, for a brace
|
|
3383
|
+
* group it can't read: a "{" that never closes, a group inside another, or a
|
|
3384
|
+
* group with no ",".
|
|
2865
3385
|
*/
|
|
2866
3386
|
function globToRegex(pattern) {
|
|
2867
3387
|
// Split pattern into segments, handling {a,b,c} alternation
|
|
@@ -2869,13 +3389,36 @@ function globToRegex(pattern) {
|
|
|
2869
3389
|
let i = 0;
|
|
2870
3390
|
while (i < pattern.length) {
|
|
2871
3391
|
if (pattern[i] === '{') {
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
}
|
|
3392
|
+
// A brace form this can't read is refused. A `{` that never closes, read
|
|
3393
|
+
// as a literal, would almost always answer with no matches, and with it
|
|
3394
|
+
// refused, each pass of this loop moves past at least one character. A
|
|
3395
|
+
// group closes at the `}` that brings the braces open since its `{` back
|
|
3396
|
+
// to none, so a `{` that never closes is named as that, even with a
|
|
3397
|
+
// closed group after it. A group inside another would be split at the
|
|
3398
|
+
// inner `}`, so `{a,{b,c}}` would match `a}` but not `a`, and a group
|
|
3399
|
+
// with no `,` would lose its braces, so `{name}` would match `name` and
|
|
3400
|
+
// never a file named `{name}`, and `{1..3}` would match only `1..3`.
|
|
3401
|
+
let depth = 0;
|
|
3402
|
+
let closeIdx = -1;
|
|
3403
|
+
for (let j = i; j < pattern.length && closeIdx < 0; j++) {
|
|
3404
|
+
if (pattern[j] === '{')
|
|
3405
|
+
depth++;
|
|
3406
|
+
else if (pattern[j] === '}' && --depth === 0)
|
|
3407
|
+
closeIdx = j;
|
|
3408
|
+
}
|
|
3409
|
+
if (closeIdx < 0)
|
|
3410
|
+
throw new Error(braceRefusal(pattern, 'has a "{" with no closing "}"'));
|
|
3411
|
+
const group = pattern.slice(i + 1, closeIdx);
|
|
3412
|
+
if (group.includes('{')) {
|
|
3413
|
+
throw new Error(braceRefusal(pattern, 'has a "{" group inside another; put all its alternatives in one group'));
|
|
3414
|
+
}
|
|
3415
|
+
if (!group.includes(',')) {
|
|
3416
|
+
throw new Error(braceRefusal(pattern, 'has a "{" group with no ","; a group needs two or more alternatives, separated by ","'));
|
|
3417
|
+
}
|
|
3418
|
+
const alternatives = group.split(',');
|
|
3419
|
+
regex += '(?:' + alternatives.map(a => globPartToRegex(a)).join('|') + ')';
|
|
3420
|
+
i = closeIdx + 1;
|
|
3421
|
+
continue;
|
|
2879
3422
|
}
|
|
2880
3423
|
// Accumulate non-brace characters, convert as a chunk
|
|
2881
3424
|
let chunk = '';
|
|
@@ -2889,6 +3432,36 @@ function globToRegex(pattern) {
|
|
|
2889
3432
|
}
|
|
2890
3433
|
return new RegExp(`^${regex}$`);
|
|
2891
3434
|
}
|
|
3435
|
+
/**
|
|
3436
|
+
* The directory a glob pattern names before its first wildcard, beneath
|
|
3437
|
+
* which every path it matches lies: its leading segments that hold none of
|
|
3438
|
+
* globToRegex's wildcards (`*`, `?`, `{`), short of the last segment, which
|
|
3439
|
+
* names the files. '' when the pattern starts with a wildcard or has a
|
|
3440
|
+
* single segment. A segment that's empty, `.` or `..`, which no path
|
|
3441
|
+
* matched holds, is one no directory listing holds either, so walkStart
|
|
3442
|
+
* stops before it.
|
|
3443
|
+
*/
|
|
3444
|
+
function literalDirOf(pattern) {
|
|
3445
|
+
const dir = [];
|
|
3446
|
+
for (const segment of pattern.split('/').slice(0, -1)) {
|
|
3447
|
+
if (/[*?{]/.test(segment))
|
|
3448
|
+
break;
|
|
3449
|
+
dir.push(segment);
|
|
3450
|
+
}
|
|
3451
|
+
return dir.join('/');
|
|
3452
|
+
}
|
|
3453
|
+
/** The deeper of two mount-relative directories, '' being the root, or null when neither holds the other. */
|
|
3454
|
+
function deeperOf(a, b) {
|
|
3455
|
+
if (a === '' || b === a || b.startsWith(a + '/'))
|
|
3456
|
+
return b;
|
|
3457
|
+
if (b === '' || a.startsWith(b + '/'))
|
|
3458
|
+
return a;
|
|
3459
|
+
return null;
|
|
3460
|
+
}
|
|
3461
|
+
/** A glob's refusal of a brace form it can't read, with the way to match a literal "{". */
|
|
3462
|
+
function braceRefusal(pattern, problem) {
|
|
3463
|
+
return `glob pattern ${JSON.stringify(pattern)} ${problem}. To match a "{" in a name, put "?" in its place.`;
|
|
3464
|
+
}
|
|
2892
3465
|
function globPartToRegex(part) {
|
|
2893
3466
|
return part
|
|
2894
3467
|
.replace(/[.+^$()|[\]\\]/g, '\\$&')
|