mixdog 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/package.json +2 -2
- package/scripts/lib/run-node-tests.mjs +9 -0
- package/scripts/prune-desktop-runtime.mjs +3 -0
- package/scripts/release-paths.mjs +39 -2
- package/scripts/test-direct.mjs +4 -2
- package/src/defaults/shipped-history.json +6 -0
- package/src/defaults/skills/setup/references/actions.md +5 -2
- package/src/defaults/skills/visualize/SKILL.md +157 -0
- package/src/defaults/skills/visualize/references/craft.md +88 -0
- package/src/headless-command.mjs +56 -3
- package/src/headless-exec.mjs +17 -1
- package/src/headless-role.mjs +2 -2
- package/src/help.mjs +2 -0
- package/src/lib/rules-builder.cjs +0 -23
- package/src/rules/lead/LEAD.md +2 -2
- package/src/rules/shared/70-delivery.md +2 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +1 -0
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +1 -0
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -0
- package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
- package/src/runtime/agent/orchestrator/config-providers-merge.mjs +17 -11
- package/src/runtime/agent/orchestrator/config-storage.mjs +1 -0
- package/src/runtime/agent/orchestrator/config.mjs +9 -1
- package/src/runtime/agent/orchestrator/mcp/client-config.mjs +47 -7
- package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +51 -8
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +25 -5
- package/src/runtime/agent/orchestrator/mcp/client.mjs +348 -122
- package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +13 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +12 -4
- package/src/runtime/agent/orchestrator/providers/effort-rejection.mjs +244 -0
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +8 -40
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +51 -12
- package/src/runtime/agent/orchestrator/providers/model-cache.mjs +37 -2
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +67 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +521 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +251 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +69 -102
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/registry.mjs +15 -1
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +10 -0
- package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +36 -3
- package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +12 -11
- package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +16 -1
- package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +11 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +11 -8
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +9 -5
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +18 -8
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +18 -11
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +13 -5
- package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +362 -0
- package/src/runtime/agent/orchestrator/session/compaction-archive.mjs +253 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +12 -14
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/diagnostic.mjs +1 -7
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/refusal.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +17 -7
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +5 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +5 -2
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +37 -16
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +90 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +22 -12
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +32 -3
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +27 -0
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -1
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +1 -5
- package/src/runtime/agent/orchestrator/session/message-ids.mjs +123 -0
- package/src/runtime/agent/orchestrator/session/message-ids.test-support.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +29 -19
- package/src/runtime/agent/orchestrator/session/read-dedup.mjs +0 -1
- package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-finish.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +11 -0
- package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +23 -4
- package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +20 -3
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +28 -8
- package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +140 -0
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +52 -2
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +32 -22
- package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +10 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-cache-policy.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +31 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-fast-paths.mjs +32 -35
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +11 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +9 -6
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -66
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +8 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +5 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +16 -7
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +137 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +11 -10
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +21 -11
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +56 -13
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +58 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +25 -1
- package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +78 -5
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +0 -10
- package/src/runtime/attachments/store.mjs +38 -0
- package/src/runtime/channels/index.mjs +1 -5
- package/src/runtime/channels/lib/runtime-paths.mjs +8 -23
- package/src/runtime/channels/lib/tool-dispatch.mjs +1 -13
- package/src/runtime/channels/lib/worker-ipc.mjs +2 -2
- package/src/runtime/channels/lib/worker-main.mjs +4 -34
- package/src/runtime/shared/atomic-file.mjs +8 -2
- package/src/runtime/shared/best-effort-stderr.mjs +8 -0
- package/src/runtime/shared/code-graph-state.mjs +12 -0
- package/src/runtime/shared/defaults-separation.mjs +2 -0
- package/src/runtime/shared/git-runner-worker.mjs +3 -2
- package/src/runtime/shared/git-runner.mjs +4 -3
- package/src/runtime/shared/llm/default-context-window.mjs +6 -0
- package/src/runtime/shared/llm/model-catalog-projection.mjs +5 -4
- package/src/runtime/shared/llm/model-catalog.mjs +240 -118
- package/src/runtime/shared/llm/provider-catalog-cache.mjs +36 -28
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +156 -0
- package/src/runtime/shared/llm/provider-seed-rosters.mjs +130 -0
- package/src/runtime/shared/model-proxy-mode.d.mts +3 -0
- package/src/runtime/shared/model-proxy-mode.mjs +19 -0
- package/src/runtime/shared/provider-accounts.mjs +2 -2
- package/src/runtime/shared/provider-api-key.mjs +21 -1
- package/src/runtime/shared/schedule-model-ref.mjs +11 -2
- package/src/runtime/shared/semver.d.mts +1 -0
- package/src/runtime/shared/semver.mjs +57 -0
- package/src/runtime/shared/session-usage-totals.mjs +19 -0
- package/src/runtime/shared/sharp-electron-linux.mjs +25 -0
- package/src/runtime/shared/singleton-owner.mjs +36 -6
- package/src/runtime/shared/statusline/statusline-segments.mjs +68 -14
- package/src/runtime/shared/tool-surface.mjs +9 -2
- package/src/runtime/shared/tool-work-units.mjs +14 -0
- package/src/runtime/shared/transcript-metadata.mjs +17 -1
- package/src/runtime/shared/turn-changes.mjs +791 -0
- package/src/runtime/shared/turn-snapshot-store.mjs +346 -12
- package/src/runtime/shared/turn-snapshot.mjs +690 -81
- package/src/runtime/shared/turn-worktree-snapshot.mjs +161 -11
- package/src/runtime/shared/update-checker.mjs +2 -31
- package/src/runtime/shared/worktrees/cache-cleanups.mjs +52 -0
- package/src/runtime/shared/worktrees/contract.mjs +551 -0
- package/src/runtime/shared/worktrees/disk.mjs +57 -0
- package/src/runtime/shared/worktrees/git.mjs +247 -0
- package/src/runtime/shared/worktrees/include.mjs +480 -0
- package/src/runtime/shared/worktrees/lock.mjs +46 -0
- package/src/runtime/shared/worktrees/maintenance.mjs +96 -0
- package/src/runtime/shared/worktrees/manager.mjs +1215 -0
- package/src/runtime/shared/worktrees/merge.mjs +182 -0
- package/src/runtime/shared/worktrees/owner.mjs +46 -0
- package/src/runtime/shared/worktrees/paths.mjs +62 -0
- package/src/runtime/shared/worktrees/registry.mjs +110 -0
- package/src/runtime/shared/worktrees/settings.mjs +84 -0
- package/src/runtime/shared/worktrees/slug.mjs +62 -0
- package/src/runtime/shared/worktrees/test-support.mjs +96 -0
- package/src/runtime/visualize-bridge/client.mjs +181 -0
- package/src/runtime/visualize-bridge/tool-defs.mjs +97 -0
- package/src/session-runtime/boot/apis.mjs +35 -0
- package/src/session-runtime/boot/begin.mjs +0 -2
- package/src/session-runtime/boot/config.mjs +2 -0
- package/src/session-runtime/boot/facade.mjs +20 -0
- package/src/session-runtime/boot/infrastructure.mjs +1 -0
- package/src/session-runtime/boot/session.mjs +1 -0
- package/src/session-runtime/boot/tools.mjs +9 -4
- package/src/session-runtime/config-lifecycle.mjs +10 -1
- package/src/session-runtime/cwd-plugins/cwd-apply.mjs +11 -1
- package/src/session-runtime/global-extensions.mjs +2 -2
- package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +27 -2
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +32 -1
- package/src/session-runtime/internal-tool-executor.mjs +9 -1
- package/src/session-runtime/lifecycle/session-catalog.mjs +8 -1
- package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +5 -0
- package/src/session-runtime/lifecycle/session-switch/resume.mjs +21 -3
- package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +16 -0
- package/src/session-runtime/mcp-glue/connect.mjs +11 -6
- package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -3
- package/src/session-runtime/mcp-glue/server-input.mjs +29 -0
- package/src/session-runtime/model-route/route-persist.mjs +3 -0
- package/src/session-runtime/model-route/set-route.mjs +15 -2
- package/src/session-runtime/notification-bus.mjs +9 -1
- package/src/session-runtime/provider-models/row-hydration.mjs +2 -0
- package/src/session-runtime/resource-extension-sync.mjs +30 -7
- package/src/session-runtime/resource-mcp-api.mjs +27 -10
- package/src/session-runtime/route-resolution.mjs +10 -1
- package/src/session-runtime/route-state.mjs +16 -2
- package/src/session-runtime/runtime-bootstrap.mjs +0 -4
- package/src/session-runtime/runtime-core.mjs +5 -0
- package/src/session-runtime/runtime-facade.mjs +8 -0
- package/src/session-runtime/runtime-paths.mjs +0 -1
- package/src/session-runtime/runtime-review-api.mjs +9 -0
- package/src/session-runtime/services/agent-tool/helpers.mjs +5 -0
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +3 -0
- package/src/session-runtime/services/agent-tool/job-views/job-lookup.mjs +1 -0
- package/src/session-runtime/services/agent-tool/job-views/spawn-meta.mjs +1 -0
- package/src/session-runtime/services/agent-tool/lead-worker-index.mjs +15 -0
- package/src/session-runtime/services/agent-tool/send-flow.mjs +1 -0
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +10 -2
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +19 -7
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +3 -0
- package/src/session-runtime/services/agent-tool/tag-registry.mjs +2 -1
- package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +5 -0
- package/src/session-runtime/services/agent-tool/worker-rows.mjs +1 -0
- package/src/session-runtime/services/agent-tool.mjs +1 -0
- package/src/session-runtime/services/channel-worker.mjs +1 -1
- package/src/session-runtime/services/daemon-candidate.mjs +3 -2
- package/src/session-runtime/services/daemon-crash-capture/spawn-capture.mjs +55 -0
- package/src/session-runtime/session-create/session-options.mjs +10 -1
- package/src/session-runtime/session-turn-api.mjs +1 -0
- package/src/session-runtime/settings-api.mjs +4 -2
- package/src/session-runtime/settings-builtin-tools-api.mjs +9 -2
- package/src/session-runtime/settings-profile-api.mjs +27 -2
- package/src/session-runtime/settings-worktree-api.mjs +17 -0
- package/src/session-runtime/setup-tool/executor.mjs +11 -4
- package/src/session-runtime/setup-tool/settings-contract.mjs +3 -1
- package/src/session-runtime/setup-tool/tool-defs.mjs +31 -3
- package/src/session-runtime/starter-guide-state.mjs +62 -0
- package/src/session-runtime/turn/agent-ops.mjs +12 -1
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +13 -3
- package/src/session-runtime/turn/turn-run/snapshot-tracker.mjs +20 -15
- package/src/session-runtime/turn/turn-run/turn-open.mjs +4 -5
- package/src/session-runtime/turn/turn-run/turn-prepare.mjs +29 -1
- package/src/session-runtime/turn/turn-run/turn-settle.mjs +6 -1
- package/src/session-runtime/turn/turn-run.mjs +6 -7
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +15 -3
- package/src/session-runtime/workflow-agents-api/onboarding.mjs +3 -2
- package/src/session-runtime/workspace/admission-gate.mjs +197 -0
- package/src/session-runtime/workspace/project-branches.mjs +136 -0
- package/src/session-runtime/workspace/transition-steps.mjs +364 -0
- package/src/session-runtime/workspace/transitions.mjs +651 -0
- package/src/session-runtime/workspace/workspace-state.mjs +112 -0
- package/src/standalone/channel-binding.mjs +1 -2
- package/src/standalone/channel-transport/call-dispatch.mjs +4 -10
- package/src/standalone/channel-transport/http-routes.mjs +17 -0
- package/src/standalone/channel-transport.mjs +13 -1
- package/src/standalone/daemon-channels-loader.mjs +2 -3
- package/src/standalone/daemon-desktop-runtime.mjs +1 -0
- package/src/standalone/daemon-owner.mjs +372 -0
- package/src/standalone/daemon-stored-session-views.mjs +4 -1
- package/src/standalone/daemon.mjs +139 -5
- package/src/standalone/desktop-service-registry.mjs +20 -0
- package/src/standalone/local-session-runtime.mjs +13 -78
- package/src/standalone/session-client/attachment-calls.mjs +3 -1
- package/src/standalone/session-client/daemon-unavailable.mjs +62 -0
- package/src/standalone/session-client/lease-heartbeat.mjs +47 -0
- package/src/standalone/session-client.mjs +104 -13
- package/src/standalone/session-protocol.mjs +17 -0
- package/src/standalone/session-proxy.mjs +3 -0
- package/src/standalone/session-service/agent-tree/agent-child.mjs +4 -0
- package/src/standalone/session-service/agent-tree/agent-rehydrate.mjs +1 -0
- package/src/standalone/session-service/entries/lifecycle.mjs +6 -0
- package/src/standalone/session-service/projection/revisions.mjs +2 -0
- package/src/standalone/session-service/projection/transcript-window.mjs +149 -19
- package/src/standalone/session-service/projection/workspace.mjs +14 -0
- package/src/standalone/session-service/session-calls/session-action.mjs +11 -3
- package/src/standalone/session-transport/call-route.mjs +13 -0
- package/src/standalone/session-transport/client-registry/client-grace.mjs +45 -3
- package/src/standalone/session-transport/client-registry/registration-replays.mjs +1 -0
- package/src/standalone/session-transport/client-registry.mjs +20 -3
- package/src/standalone/session-transport/routes.mjs +79 -2
- package/src/standalone/session-transport/stack.mjs +2 -0
- package/src/standalone/session-transport.mjs +24 -1
- package/src/tui/app/message-selector.mjs +2 -1
- package/src/tui/app/model-options.mjs +35 -5
- package/src/tui/app/model-picker/provider-models-list.mjs +2 -2
- package/src/tui/app/model-picker.mjs +4 -2
- package/src/tui/dist/index.mjs +332 -36
- package/src/tui/index.jsx +10 -2
- package/src/tui/session/compaction-view.mjs +147 -0
- package/src/tui/session/context-state.mjs +6 -0
- package/src/tui/session/initial-state.mjs +3 -0
- package/src/tui/session/quota-resume.mjs +2 -1
- package/src/tui/session/session-api/commands.mjs +10 -2
- package/src/tui/session/session-api/controls.mjs +4 -0
- package/src/tui/session/session-api/extensions.mjs +12 -8
- package/src/tui/session/session-api/intake/rewind.mjs +2 -1
- package/src/tui/session/session-api/integrations.mjs +18 -0
- package/src/tui/session/session-api/lifecycle/dispose.mjs +6 -1
- package/src/tui/session/session-api/lifecycle/inheritance.mjs +2 -0
- package/src/tui/session/session-api/lifecycle/resume.mjs +3 -0
- package/src/tui/session/session-api/routes.mjs +10 -1
- package/src/tui/session/session-api/settings.mjs +2 -0
- package/src/tui/session/session-api/shared.mjs +10 -6
- package/src/tui/session/session-api/worktree.mjs +60 -0
- package/src/tui/session/session-api.mjs +2 -0
- package/src/tui/session/session-flow/auto-clear.mjs +31 -12
- package/src/tui/session/session-flow/drain.mjs +15 -1
- package/src/tui/session/session-flow/session-reset.mjs +4 -3
- package/src/tui/session/session-flow.mjs +2 -0
- package/src/tui/session/transcript-intake.mjs +14 -5
- package/src/tui/session/turn-stream.mjs +32 -4
- package/src/tui/session/turn-tool-cards.mjs +10 -2
- package/src/tui/session/turn.mjs +39 -10
- package/src/runtime/channels/tool-defs.mjs +0 -17
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-wide admission gate shared by Lead workspace transitions, turn
|
|
3
|
+
* admission and worker spawning.
|
|
4
|
+
*
|
|
5
|
+
* - A transition and a worker spawn of the same Lead never overlap: a
|
|
6
|
+
* transition refuses while a spawn is being prepared, a spawn refuses while
|
|
7
|
+
* a transition is in flight.
|
|
8
|
+
* - Turns and transitions are mutually exclusive: an admitted turn is
|
|
9
|
+
* registered until it settles; a turn that arrives while its own session
|
|
10
|
+
* transitions, or while another session changes the checkout its cwd lies
|
|
11
|
+
* in (a held checkout folder), waits for that change to settle. A checkout
|
|
12
|
+
* folder can only be held while no other session's admitted turn runs in it.
|
|
13
|
+
* - Each worktree-bound Lead's execution root, which every worker of that Lead
|
|
14
|
+
* runs in.
|
|
15
|
+
*/
|
|
16
|
+
import { worktreeError } from '../../runtime/shared/worktrees/contract.mjs';
|
|
17
|
+
import { throwIfAborted } from '../../runtime/shared/worktrees/git.mjs';
|
|
18
|
+
import { isWithin } from '../../runtime/shared/worktrees/paths.mjs';
|
|
19
|
+
|
|
20
|
+
/** @type {Map<string, { txId: string, settled: Promise<void> }>} Lead session id → in-flight transition. */
|
|
21
|
+
const transitions = new Map();
|
|
22
|
+
/** @type {Map<string, number>} Lead session id → spawns being prepared. */
|
|
23
|
+
const spawns = new Map();
|
|
24
|
+
/** @type {Map<string, string>} Lead session id → bound worktree execution root. */
|
|
25
|
+
const cwdLocks = new Map();
|
|
26
|
+
/** @type {Map<symbol, { sessionId: string, cwd: () => string }>} Admitted turns (live cwd). */
|
|
27
|
+
const turns = new Map();
|
|
28
|
+
/** @type {Map<symbol, { sessionId: string, folder: string, settled: Promise<void> }>} Checkouts being changed. */
|
|
29
|
+
const folderHolds = new Map();
|
|
30
|
+
|
|
31
|
+
const key = (sessionId) => String(sessionId || '').trim();
|
|
32
|
+
|
|
33
|
+
function latch() {
|
|
34
|
+
let resolve;
|
|
35
|
+
const settled = new Promise((done) => {
|
|
36
|
+
resolve = done;
|
|
37
|
+
});
|
|
38
|
+
return { settled, resolve };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function once(fn) {
|
|
42
|
+
let done = false;
|
|
43
|
+
return () => {
|
|
44
|
+
if (done) return;
|
|
45
|
+
done = true;
|
|
46
|
+
fn();
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const within = (folder, cwd) => Boolean(folder && cwd) && isWithin(folder, cwd);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Enter a transition for `sessionId`; the returned function leaves it.
|
|
54
|
+
* @returns {() => void}
|
|
55
|
+
*/
|
|
56
|
+
export function beginWorkspaceTransition(sessionId, txId) {
|
|
57
|
+
const id = key(sessionId);
|
|
58
|
+
if (!id) throw new TypeError('workspace transition requires a session id.');
|
|
59
|
+
if (transitions.has(id)) {
|
|
60
|
+
throw worktreeError('WORKTREE_BUSY_JOBS', 'Another workspace change is in progress for this session.', {
|
|
61
|
+
reason: 'transition',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if ((spawns.get(id) || 0) > 0) {
|
|
65
|
+
throw worktreeError('WORKTREE_BUSY_JOBS', undefined, { reason: 'workers', starting: spawns.get(id) });
|
|
66
|
+
}
|
|
67
|
+
const gate = latch();
|
|
68
|
+
transitions.set(id, { txId: String(txId || 'tx'), settled: gate.settled });
|
|
69
|
+
return once(() => {
|
|
70
|
+
transitions.delete(id);
|
|
71
|
+
gate.resolve();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Hold `folder` (a checkout root) while its branch changes or a merge lands
|
|
77
|
+
* in it. Refuses (WORKTREE_BUSY_JOBS, `folder-in-use`) while another session's
|
|
78
|
+
* admitted turn runs inside it, and (`transition`) while another change holds
|
|
79
|
+
* an overlapping checkout; turns arriving meanwhile wait (admitTurn). The
|
|
80
|
+
* holder's own turns are its own business (the Lead busy check).
|
|
81
|
+
* @param {string|null} sessionId The changing session; '' / null for a session-less change.
|
|
82
|
+
* @returns {() => void} Release.
|
|
83
|
+
*/
|
|
84
|
+
export function holdCheckoutFolder(sessionId, folder) {
|
|
85
|
+
const id = key(sessionId);
|
|
86
|
+
const users = new Set();
|
|
87
|
+
for (const turn of turns.values()) {
|
|
88
|
+
if (turn.sessionId !== id && within(folder, turn.cwd())) users.add(turn.sessionId);
|
|
89
|
+
}
|
|
90
|
+
if (users.size) {
|
|
91
|
+
throw worktreeError('WORKTREE_BUSY_JOBS', 'Other sessions are working in the project folder.', {
|
|
92
|
+
reason: 'folder-in-use',
|
|
93
|
+
sessions: [...users],
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if ([...folderHolds.values()].some((hold) => within(hold.folder, folder) || within(folder, hold.folder))) {
|
|
97
|
+
throw worktreeError('WORKTREE_BUSY_JOBS', 'Another branch change is in progress in the project folder.', {
|
|
98
|
+
reason: 'transition',
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const token = Symbol('checkout-folder');
|
|
102
|
+
const gate = latch();
|
|
103
|
+
folderHolds.set(token, { sessionId: id, folder, settled: gate.settled });
|
|
104
|
+
return once(() => {
|
|
105
|
+
folderHolds.delete(token);
|
|
106
|
+
gate.resolve();
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function admissionBlocker(id, cwd) {
|
|
111
|
+
const own = transitions.get(id);
|
|
112
|
+
if (own) return own.settled;
|
|
113
|
+
const path = cwd();
|
|
114
|
+
for (const hold of folderHolds.values()) {
|
|
115
|
+
if (hold.sessionId !== id && within(hold.folder, path)) return hold.settled;
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function settledOrAborted(promise, signal) {
|
|
121
|
+
if (!signal) return promise;
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
const abort = () => {
|
|
124
|
+
try {
|
|
125
|
+
throwIfAborted(signal);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
reject(error);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
131
|
+
promise.then(() => {
|
|
132
|
+
signal.removeEventListener('abort', abort);
|
|
133
|
+
resolve();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Admit one turn of `sessionId`, whose live cwd is `cwd()`: waits while the
|
|
140
|
+
* session itself transitions or another session holds the checkout the cwd
|
|
141
|
+
* lies in, then registers the turn until the returned release is called.
|
|
142
|
+
* An abort while waiting rejects and registers nothing.
|
|
143
|
+
* @param {{ sessionId: string, cwd: () => string, signal?: AbortSignal }} input
|
|
144
|
+
* @returns {Promise<() => void>}
|
|
145
|
+
*/
|
|
146
|
+
export async function admitTurn({ sessionId, cwd, signal }) {
|
|
147
|
+
const id = key(sessionId);
|
|
148
|
+
for (let blocker = admissionBlocker(id, cwd); blocker; blocker = admissionBlocker(id, cwd)) {
|
|
149
|
+
throwIfAborted(signal);
|
|
150
|
+
await settledOrAborted(blocker, signal);
|
|
151
|
+
}
|
|
152
|
+
throwIfAborted(signal);
|
|
153
|
+
const token = Symbol('turn');
|
|
154
|
+
turns.set(token, { sessionId: id, cwd });
|
|
155
|
+
return once(() => turns.delete(token));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Admit one worker spawn preparation for the Lead(s) in `sessionIds`.
|
|
160
|
+
* @param {Array<string|null|undefined>} sessionIds Owner and caller session ids.
|
|
161
|
+
* @returns {() => void}
|
|
162
|
+
*/
|
|
163
|
+
export function enterWorkerSpawn(sessionIds) {
|
|
164
|
+
const ids = [...new Set(sessionIds.map(key).filter(Boolean))];
|
|
165
|
+
if (ids.some((id) => transitions.has(id))) {
|
|
166
|
+
throw worktreeError('WORKTREE_BUSY_JOBS', 'The Lead workspace is changing; spawn the worker after it settles.', {
|
|
167
|
+
reason: 'transition',
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
for (const id of ids) spawns.set(id, (spawns.get(id) || 0) + 1);
|
|
171
|
+
let left = false;
|
|
172
|
+
return () => {
|
|
173
|
+
if (left) return;
|
|
174
|
+
left = true;
|
|
175
|
+
for (const id of ids) {
|
|
176
|
+
const next = (spawns.get(id) || 1) - 1;
|
|
177
|
+
if (next > 0) spawns.set(id, next);
|
|
178
|
+
else spawns.delete(id);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Record (or clear with null) the worktree root a Lead's workers must run in. */
|
|
184
|
+
export function setWorkspaceCwdLock(sessionId, cwd) {
|
|
185
|
+
const id = key(sessionId);
|
|
186
|
+
if (!id) return;
|
|
187
|
+
if (cwd) cwdLocks.set(id, String(cwd));
|
|
188
|
+
else cwdLocks.delete(id);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The locked worker cwd for the first Lead id that has one, or null. */
|
|
192
|
+
export function workspaceCwdLock(sessionIds) {
|
|
193
|
+
for (const id of sessionIds.map(key)) {
|
|
194
|
+
if (id && cwdLocks.has(id)) return cwdLocks.get(id);
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-less branch operations on a registered Project's folder (a New
|
|
3
|
+
* Task draft, the source-control dock): its branch menu, a guarded checkout
|
|
4
|
+
* and a guarded merge. They act only on a registered Project (a repository
|
|
5
|
+
* subfolder Project included) and hold the checkout (withProjectFolder) while
|
|
6
|
+
* git changes it.
|
|
7
|
+
*/
|
|
8
|
+
import { branchMenuEntries, projectWorkspace, worktreeError } from '../../runtime/shared/worktrees/contract.mjs';
|
|
9
|
+
|
|
10
|
+
function requireText(value, name) {
|
|
11
|
+
if (typeof value !== 'string' || !value.trim()) throw new TypeError(`${name} must be a non-empty string.`);
|
|
12
|
+
return value.trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* WORKTREE_UNSUPPORTED_REPO when the project folder has no checked-out branch
|
|
17
|
+
* to merge into; WORKTREE_TARGET_MISMATCH when the confirmed `target` (null:
|
|
18
|
+
* none confirmed) is no longer the checked-out branch.
|
|
19
|
+
*/
|
|
20
|
+
export function assertMergeTarget(currentBranch, target) {
|
|
21
|
+
if (!currentBranch) {
|
|
22
|
+
throw worktreeError('WORKTREE_UNSUPPORTED_REPO', 'The project folder has no checked-out branch to merge into.');
|
|
23
|
+
}
|
|
24
|
+
if (target !== null && target !== currentBranch) {
|
|
25
|
+
throw worktreeError(
|
|
26
|
+
'WORKTREE_TARGET_MISMATCH',
|
|
27
|
+
`The project folder is now on "${currentBranch}"; confirm the merge again.`,
|
|
28
|
+
{ target, current: currentBranch }
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {object} deps
|
|
35
|
+
* @param {() => Promise<any>} deps.backend The managed-worktree backend.
|
|
36
|
+
* @param {(path: string) => Promise<any>} deps.inspectProject
|
|
37
|
+
* @param {(path: string) => Promise<string|null>} deps.findRegisteredProject
|
|
38
|
+
* @param {<T>(sessionId: string|null, folder: string, fn: () => Promise<T>) => Promise<T>} deps.withProjectFolder
|
|
39
|
+
*/
|
|
40
|
+
export function createProjectBranchOps({ backend, inspectProject, findRegisteredProject, withProjectFolder }) {
|
|
41
|
+
/** WORKTREE_PROJECT_UNKNOWN unless `projectPath` is a registered Project. */
|
|
42
|
+
async function registeredProject(projectPath) {
|
|
43
|
+
const path = requireText(projectPath, 'projectPath');
|
|
44
|
+
const registered = await findRegisteredProject(path);
|
|
45
|
+
if (!registered) throw worktreeError('WORKTREE_PROJECT_UNKNOWN', undefined, { projectPath: path });
|
|
46
|
+
return registered;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A Project's branch menu with no session: the project folder's workspace
|
|
51
|
+
* and the real occupancy (current / session / external / free).
|
|
52
|
+
*/
|
|
53
|
+
async function getProjectBranchMenu({ projectPath } = {}) {
|
|
54
|
+
const path = await registeredProject(projectPath);
|
|
55
|
+
const project = await inspectProject(path);
|
|
56
|
+
const menu = await (await backend()).listBranchOccupancy({ projectPath: path, sessionId: null });
|
|
57
|
+
return {
|
|
58
|
+
workspace: projectWorkspace({
|
|
59
|
+
projectPath: path,
|
|
60
|
+
repoCommonDir: project.repoCommonDir,
|
|
61
|
+
branch: project.currentBranch,
|
|
62
|
+
}),
|
|
63
|
+
branches: branchMenuEntries(menu.branches, menu.occupancy),
|
|
64
|
+
occupancy: menu.occupancy,
|
|
65
|
+
retainedWorktree: null,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check out (or create) `branch` in a Project folder with no session: the
|
|
71
|
+
* same guarded path a session's branch change takes — refused while another
|
|
72
|
+
* session works in the checkout, which is held until the switch is done;
|
|
73
|
+
* the manager's repository lock, occupancy and no-carry (dirty) checks.
|
|
74
|
+
*/
|
|
75
|
+
async function selectProjectBranch({
|
|
76
|
+
projectPath,
|
|
77
|
+
branch,
|
|
78
|
+
create = false,
|
|
79
|
+
remote = false,
|
|
80
|
+
startPoint = null,
|
|
81
|
+
detach = null,
|
|
82
|
+
} = {}) {
|
|
83
|
+
const path = await registeredProject(projectPath);
|
|
84
|
+
if (detach !== null) {
|
|
85
|
+
// A history entry checked out detached: the same guard, no branch.
|
|
86
|
+
if (branch !== undefined || create === true || remote === true || startPoint !== null) {
|
|
87
|
+
throw new TypeError('detach excludes branch, create, remote and startPoint.');
|
|
88
|
+
}
|
|
89
|
+
const commit = requireText(detach, 'detach');
|
|
90
|
+
const project = await inspectProject(path);
|
|
91
|
+
return withProjectFolder(null, project.topLevel || path, async () =>
|
|
92
|
+
(await backend()).checkoutProjectBranch({ projectPath: path, detach: commit })
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
const name = requireText(branch, 'branch');
|
|
96
|
+
if (create === true && remote === true) throw new TypeError('create and remote are exclusive.');
|
|
97
|
+
if (startPoint !== null && create !== true) throw new TypeError('startPoint needs create.');
|
|
98
|
+
const project = await inspectProject(path);
|
|
99
|
+
if (create !== true && remote !== true && project.currentBranch === name) return { branch: name, changed: false };
|
|
100
|
+
return withProjectFolder(null, project.topLevel || path, async () => {
|
|
101
|
+
const backendApi = await backend();
|
|
102
|
+
return backendApi.checkoutProjectBranch({
|
|
103
|
+
projectPath: path,
|
|
104
|
+
branch: name,
|
|
105
|
+
create: create === true,
|
|
106
|
+
remote: remote === true,
|
|
107
|
+
...(startPoint !== null ? { startPoint: requireText(startPoint, 'startPoint') } : {}),
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Merge a local branch into a Project folder's checked-out branch with no
|
|
114
|
+
* session: `target` is the branch the user confirmed and must still be
|
|
115
|
+
* checked out (WORKTREE_TARGET_MISMATCH); refused while another session
|
|
116
|
+
* works in that checkout, which is held (turn admission waits) until the
|
|
117
|
+
* merge is done.
|
|
118
|
+
*/
|
|
119
|
+
async function mergeProjectBranch({ projectPath, branch, target } = {}) {
|
|
120
|
+
const path = await registeredProject(projectPath);
|
|
121
|
+
const source = requireText(branch, 'branch');
|
|
122
|
+
const confirmedTarget = requireText(target, 'target');
|
|
123
|
+
const project = await inspectProject(path);
|
|
124
|
+
assertMergeTarget(project.currentBranch, confirmedTarget);
|
|
125
|
+
return withProjectFolder(null, project.topLevel || path, async () =>
|
|
126
|
+
(await backend()).mergeBranchIntoProject({
|
|
127
|
+
projectPath: path,
|
|
128
|
+
source,
|
|
129
|
+
target: confirmedTarget,
|
|
130
|
+
confirmed: true,
|
|
131
|
+
})
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { getProjectBranchMenu, selectProjectBranch, mergeProjectBranch };
|
|
136
|
+
}
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The steps of one Lead workspace transaction (transitions.mjs runs them
|
|
3
|
+
* serialized): commit a new workspace, enter the retained or a new worktree,
|
|
4
|
+
* move to a project-folder branch, remove the retained worktree. Every step
|
|
5
|
+
* pushes the exact undo of what it changed onto the transaction `journal`;
|
|
6
|
+
* the caller runs it (undoAll) when the transaction fails.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
9
|
+
import {
|
|
10
|
+
isWorktreeBound,
|
|
11
|
+
isWorktreeError,
|
|
12
|
+
nextRevision,
|
|
13
|
+
normalizeSessionWorkspace,
|
|
14
|
+
worktreeError,
|
|
15
|
+
} from '../../runtime/shared/worktrees/contract.mjs';
|
|
16
|
+
import { throwIfAborted } from '../../runtime/shared/worktrees/git.mjs';
|
|
17
|
+
import { checkoutFolder, projectSubdirOf, samePath } from '../../runtime/shared/worktrees/paths.mjs';
|
|
18
|
+
import { setWorkspaceCwdLock, workspaceCwdLock } from './admission-gate.mjs';
|
|
19
|
+
import { copyProjectIdentity } from './workspace-state.mjs';
|
|
20
|
+
|
|
21
|
+
const PROMPT_FIELDS = ['bp3EnvironmentContext', 'bp3CoreContext', 'sessionStartMetaInjected'];
|
|
22
|
+
|
|
23
|
+
/** Everything a prompt refresh rewrites on `target`, restorable exactly. */
|
|
24
|
+
function snapshotPrompt(target) {
|
|
25
|
+
const fields = PROMPT_FIELDS.map((name) => [name, Object.hasOwn(target, name), target[name]]);
|
|
26
|
+
const messages = Array.isArray(target.messages) ? [...target.messages] : null;
|
|
27
|
+
return () => {
|
|
28
|
+
for (const [name, had, value] of fields) {
|
|
29
|
+
if (had) target[name] = value;
|
|
30
|
+
else delete target[name];
|
|
31
|
+
}
|
|
32
|
+
if (!messages) return;
|
|
33
|
+
if (Array.isArray(target.messages)) target.messages.splice(0, target.messages.length, ...messages);
|
|
34
|
+
else target.messages = messages;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Run a failed transaction's undo steps newest first; every step runs. */
|
|
39
|
+
export async function undoAll(journal, error) {
|
|
40
|
+
const failures = [];
|
|
41
|
+
for (const undo of journal.reverse()) {
|
|
42
|
+
try {
|
|
43
|
+
await undo();
|
|
44
|
+
} catch (cause) {
|
|
45
|
+
failures.push(cause);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (failures.length && error && typeof error === 'object') error.rollbackErrors = failures;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* WORKTREE_PROJECT_MISMATCH unless the retained `entry` was created for the
|
|
53
|
+
* same repository and the same Project folder inside it (a Project switch may
|
|
54
|
+
* have left it behind).
|
|
55
|
+
*/
|
|
56
|
+
export function assertRetainedFits(entry, project, projectPath) {
|
|
57
|
+
const subdir = projectSubdirOf(project.topLevel, project.projectPath || projectPath);
|
|
58
|
+
if (samePath(entry.repoCommonDir, project.repoCommonDir) && (entry.projectSubdir || '') === subdir) return;
|
|
59
|
+
throw worktreeError('WORKTREE_PROJECT_MISMATCH', undefined, {
|
|
60
|
+
worktreeId: entry.id,
|
|
61
|
+
worktreeProject: entry.projectPath,
|
|
62
|
+
projectPath,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* WORKTREE_REMOVAL_INCOMPLETE: the deletion started (the record stays
|
|
68
|
+
* `deleting`) but something in the folder is still held. The session keeps
|
|
69
|
+
* the project folder — never a cwd that may already be deleted — and
|
|
70
|
+
* removeWorktree again resumes the deletion.
|
|
71
|
+
*/
|
|
72
|
+
function incompleteRemoval(journal, entry, result, outcome) {
|
|
73
|
+
journal.length = 0;
|
|
74
|
+
return worktreeError('WORKTREE_REMOVAL_INCOMPLETE', undefined, {
|
|
75
|
+
entryId: entry.id,
|
|
76
|
+
path: entry.path,
|
|
77
|
+
retryable: outcome?.retryable !== false,
|
|
78
|
+
remaining: Array.isArray(outcome?.remaining) ? outcome.remaining : [],
|
|
79
|
+
...(outcome?.reason ? { reason: outcome.reason } : {}),
|
|
80
|
+
revision: result.revision,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {object} deps The host hooks of createWorkspaceTransitions plus its
|
|
86
|
+
* `backend()` loader, `workersOf(session)` and `withProjectFolder`.
|
|
87
|
+
*/
|
|
88
|
+
export function createTransitionSteps({
|
|
89
|
+
backend,
|
|
90
|
+
inspectProject,
|
|
91
|
+
runtimeToken,
|
|
92
|
+
getCurrentCwd,
|
|
93
|
+
applyResolvedCwd,
|
|
94
|
+
persistSession,
|
|
95
|
+
refreshWorkspaceContext,
|
|
96
|
+
invalidateCaches,
|
|
97
|
+
updateLeadWorkerCwd,
|
|
98
|
+
workersOf,
|
|
99
|
+
withProjectFolder,
|
|
100
|
+
}) {
|
|
101
|
+
const owner = (sessionId) => ({ sessionId, runtimeToken, pid: process.pid });
|
|
102
|
+
|
|
103
|
+
/** Point this Lead's idle workers at the new root so a reused worker never keeps an old workspace. */
|
|
104
|
+
async function retargetWorkers(session, cwd, journal) {
|
|
105
|
+
for (const worker of workersOf(session)) {
|
|
106
|
+
if (samePath(worker.cwd || '', cwd)) continue;
|
|
107
|
+
const priorCwd = worker.cwd;
|
|
108
|
+
const restorePrompt = snapshotPrompt(worker);
|
|
109
|
+
journal.push(() => {
|
|
110
|
+
worker.cwd = priorCwd;
|
|
111
|
+
restorePrompt();
|
|
112
|
+
worker.updatedAt = Date.now();
|
|
113
|
+
persistSession(worker);
|
|
114
|
+
});
|
|
115
|
+
worker.cwd = cwd;
|
|
116
|
+
await refreshWorkspaceContext(worker, cwd);
|
|
117
|
+
worker.updatedAt = Date.now();
|
|
118
|
+
persistSession(worker);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Apply the new workspace: runtime cwd, record, worker lock, prompt context
|
|
124
|
+
* of the Lead and its idle workers, persistence. Journals the exact undo
|
|
125
|
+
* (cwd, record, lock, prompts, worker roots) for a transaction that fails
|
|
126
|
+
* after this point.
|
|
127
|
+
*/
|
|
128
|
+
async function commit(session, previous, fields, journal) {
|
|
129
|
+
const moveLeadRow = (cwd) => {
|
|
130
|
+
try {
|
|
131
|
+
updateLeadWorkerCwd(session.id, cwd);
|
|
132
|
+
} catch {
|
|
133
|
+
/* the Lead pool must never break a transition */
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
const next = normalizeSessionWorkspace({
|
|
137
|
+
...fields,
|
|
138
|
+
revision: nextRevision(previous),
|
|
139
|
+
transition: { state: 'idle', txId: null },
|
|
140
|
+
});
|
|
141
|
+
const prior = {
|
|
142
|
+
cwd: getCurrentCwd(),
|
|
143
|
+
sessionCwd: session.cwd,
|
|
144
|
+
workspace: session.workspace,
|
|
145
|
+
lock: workspaceCwdLock([session.id]),
|
|
146
|
+
prompt: snapshotPrompt(session),
|
|
147
|
+
};
|
|
148
|
+
journal.push(() => {
|
|
149
|
+
applyResolvedCwd(prior.cwd);
|
|
150
|
+
session.cwd = prior.sessionCwd;
|
|
151
|
+
session.workspace = prior.workspace;
|
|
152
|
+
setWorkspaceCwdLock(session.id, prior.lock);
|
|
153
|
+
prior.prompt();
|
|
154
|
+
session.updatedAt = Date.now();
|
|
155
|
+
persistSession(session);
|
|
156
|
+
moveLeadRow(prior.sessionCwd || prior.cwd);
|
|
157
|
+
invalidateCaches();
|
|
158
|
+
});
|
|
159
|
+
applyResolvedCwd(next.cwd);
|
|
160
|
+
session.cwd = next.cwd;
|
|
161
|
+
session.workspace = next;
|
|
162
|
+
moveLeadRow(next.cwd);
|
|
163
|
+
setWorkspaceCwdLock(session.id, isWorktreeBound(next) ? next.cwd : null);
|
|
164
|
+
await refreshWorkspaceContext(session, next.cwd);
|
|
165
|
+
await retargetWorkers(session, next.cwd, journal);
|
|
166
|
+
session.updatedAt = Date.now();
|
|
167
|
+
persistSession(session);
|
|
168
|
+
invalidateCaches();
|
|
169
|
+
return { revision: next.revision, workspace: next };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function releaseOwner(backendApi, entryId, sessionId) {
|
|
173
|
+
try {
|
|
174
|
+
await backendApi.releaseWorktreeOwner(entryId, owner(sessionId));
|
|
175
|
+
} catch (error) {
|
|
176
|
+
// A worktree that vanished has no owner left to release.
|
|
177
|
+
if (!isWorktreeError(error, 'WORKTREE_MISSING')) throw error;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function enterWorktree(session, previous, { name, nameHint, confirmLarge, signal, txId, journal }) {
|
|
182
|
+
const backendApi = await backend();
|
|
183
|
+
const projectPath = previous.projectPath;
|
|
184
|
+
const project = await inspectProject(projectPath, { signal });
|
|
185
|
+
let entry = await backendApi.getSessionWorktree(session.id);
|
|
186
|
+
if (entry) {
|
|
187
|
+
if (!existsSync(entry.path)) {
|
|
188
|
+
throw worktreeError('WORKTREE_MISSING', undefined, { worktreeId: entry.id, path: entry.path });
|
|
189
|
+
}
|
|
190
|
+
if (isWorktreeBound(previous) && previous.worktreeId === entry.id) {
|
|
191
|
+
return { revision: previous.revision, workspace: previous };
|
|
192
|
+
}
|
|
193
|
+
assertRetainedFits(entry, project, projectPath);
|
|
194
|
+
await backendApi.claimWorktreeOwner(entry.id, owner(session.id));
|
|
195
|
+
journal.push(() => releaseOwner(backendApi, entry.id, session.id));
|
|
196
|
+
} else {
|
|
197
|
+
// The backend resolves the base branch commit (from the project
|
|
198
|
+
// folder's checked-out branch), sizes and creates the checkout; the
|
|
199
|
+
// txId makes a retried or duplicate send return the same worktree.
|
|
200
|
+
entry = await backendApi.createManagedWorktree({
|
|
201
|
+
projectPath,
|
|
202
|
+
sessionId: session.id,
|
|
203
|
+
runtimeToken,
|
|
204
|
+
baseBranch: project.currentBranch,
|
|
205
|
+
nameHint: name ?? nameHint ?? '',
|
|
206
|
+
confirmLarge: confirmLarge === true,
|
|
207
|
+
txId,
|
|
208
|
+
signal,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
// Cancelled after the worktree exists: it stays retained (same txId on
|
|
212
|
+
// retry), the session does not move.
|
|
213
|
+
throwIfAborted(signal);
|
|
214
|
+
// A Project registered below the repository root runs in the same
|
|
215
|
+
// subfolder of the worktree (and returns to its own folder on the way back).
|
|
216
|
+
const cwd = checkoutFolder(entry.path, projectSubdirOf(project.topLevel, project.projectPath || projectPath));
|
|
217
|
+
if (!existsSync(cwd)) mkdirSync(cwd, { recursive: true });
|
|
218
|
+
copyProjectIdentity(projectPath, cwd);
|
|
219
|
+
const entered = await commit(
|
|
220
|
+
session,
|
|
221
|
+
previous,
|
|
222
|
+
{
|
|
223
|
+
projectPath,
|
|
224
|
+
repoCommonDir: entry.repoCommonDir || project.repoCommonDir,
|
|
225
|
+
cwd,
|
|
226
|
+
worktreeId: entry.id,
|
|
227
|
+
branch: entry.branch,
|
|
228
|
+
baseCommit: entry.baseCommit,
|
|
229
|
+
mergeTarget: project.currentBranch,
|
|
230
|
+
},
|
|
231
|
+
journal
|
|
232
|
+
);
|
|
233
|
+
// Creation notes (submodules, include entries missing or already present).
|
|
234
|
+
return { ...entered, warnings: Array.isArray(entry.warnings) ? [...entry.warnings] : [] };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function enterProjectBranch(session, previous, { branch, create, signal, journal }) {
|
|
238
|
+
const backendApi = await backend();
|
|
239
|
+
const projectPath = previous.projectPath;
|
|
240
|
+
const project = await inspectProject(projectPath, { signal });
|
|
241
|
+
const bound = isWorktreeBound(previous);
|
|
242
|
+
const finalize = async () => {
|
|
243
|
+
const result = await commit(
|
|
244
|
+
session,
|
|
245
|
+
previous,
|
|
246
|
+
{ projectPath, repoCommonDir: project.repoCommonDir, cwd: projectPath, branch },
|
|
247
|
+
journal
|
|
248
|
+
);
|
|
249
|
+
if (bound) await releaseOwner(backendApi, previous.worktreeId, session.id);
|
|
250
|
+
return result;
|
|
251
|
+
};
|
|
252
|
+
if (create !== true && project.currentBranch === branch) {
|
|
253
|
+
if (!bound && previous.branch === branch && samePath(previous.cwd, projectPath)) {
|
|
254
|
+
return { revision: previous.revision, workspace: previous };
|
|
255
|
+
}
|
|
256
|
+
return finalize();
|
|
257
|
+
}
|
|
258
|
+
// The checkout stays held until the session is finalized on it, or put
|
|
259
|
+
// back: no other session's turn starts on a half-switched folder.
|
|
260
|
+
return withProjectFolder(session.id, project.topLevel || projectPath, async () => {
|
|
261
|
+
await backendApi.checkoutProjectBranch({ projectPath, branch, create: create === true });
|
|
262
|
+
try {
|
|
263
|
+
return await finalize();
|
|
264
|
+
} catch (error) {
|
|
265
|
+
await undoAll(journal, error);
|
|
266
|
+
journal.length = 0;
|
|
267
|
+
const context = { backendApi, projectPath, project, branch, bound, error };
|
|
268
|
+
throw await revertProjectCheckout(session, previous, context);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* A branch switch whose finalization failed after git switched: switch the
|
|
275
|
+
* project folder back when that is safe (the manager refuses a dirty tree
|
|
276
|
+
* or a vanished branch) and rethrow the cause. Otherwise the session record
|
|
277
|
+
* is made to match the real checkout and the failure becomes
|
|
278
|
+
* WORKTREE_PARTIAL_TRANSITION naming the branch actually checked out.
|
|
279
|
+
*/
|
|
280
|
+
async function revertProjectCheckout(session, previous, context) {
|
|
281
|
+
const { backendApi, projectPath, project, branch, bound, error } = context;
|
|
282
|
+
const previousBranch = project.currentBranch;
|
|
283
|
+
let revertError = null;
|
|
284
|
+
if (previousBranch) {
|
|
285
|
+
try {
|
|
286
|
+
await backendApi.checkoutProjectBranch({ projectPath, branch: previousBranch });
|
|
287
|
+
return error;
|
|
288
|
+
} catch (cause) {
|
|
289
|
+
revertError = cause;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (!bound) {
|
|
293
|
+
// The session runs in the project folder, which is now on `branch`.
|
|
294
|
+
session.workspace = normalizeSessionWorkspace({
|
|
295
|
+
...previous,
|
|
296
|
+
repoCommonDir: project.repoCommonDir,
|
|
297
|
+
branch,
|
|
298
|
+
revision: nextRevision(previous),
|
|
299
|
+
transition: { state: 'idle', txId: null },
|
|
300
|
+
});
|
|
301
|
+
session.updatedAt = Date.now();
|
|
302
|
+
persistSession(session);
|
|
303
|
+
invalidateCaches();
|
|
304
|
+
}
|
|
305
|
+
return worktreeError(
|
|
306
|
+
'WORKTREE_PARTIAL_TRANSITION',
|
|
307
|
+
`The branch change failed (${error?.message || error}) and the project folder could not be switched back` +
|
|
308
|
+
`${previousBranch ? ` to "${previousBranch}"` : ''}; it is now on "${branch}".`,
|
|
309
|
+
{
|
|
310
|
+
checkout: branch,
|
|
311
|
+
previousBranch: previousBranch ?? null,
|
|
312
|
+
reason: String(error?.message || error),
|
|
313
|
+
revertReason: revertError ? String(revertError.message || revertError) : 'detached',
|
|
314
|
+
}
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Validate the removal first (a dirty or unmerged worktree without `force`
|
|
320
|
+
* refuses with the session untouched), then return the session to its
|
|
321
|
+
* project folder, release the live lease and remove. A removal that still
|
|
322
|
+
* fails puts the session back exactly (the transaction journal).
|
|
323
|
+
*/
|
|
324
|
+
async function removeRetainedWorktree(session, previous, { force, signal, journal }) {
|
|
325
|
+
const backendApi = await backend();
|
|
326
|
+
const entry = await backendApi.getSessionWorktree(session.id);
|
|
327
|
+
if (entry) await backendApi.assertWorktreeRemovable({ entryId: entry.id, force: force === true });
|
|
328
|
+
let result = { revision: previous.revision, workspace: previous };
|
|
329
|
+
if (isWorktreeBound(previous)) {
|
|
330
|
+
const project = await inspectProject(previous.projectPath, { signal });
|
|
331
|
+
result = await commit(
|
|
332
|
+
session,
|
|
333
|
+
previous,
|
|
334
|
+
{
|
|
335
|
+
projectPath: previous.projectPath,
|
|
336
|
+
repoCommonDir: project.repoCommonDir,
|
|
337
|
+
cwd: previous.projectPath,
|
|
338
|
+
branch: project.currentBranch,
|
|
339
|
+
},
|
|
340
|
+
journal
|
|
341
|
+
);
|
|
342
|
+
if (entry) {
|
|
343
|
+
await releaseOwner(backendApi, entry.id, session.id);
|
|
344
|
+
journal.push(() => backendApi.claimWorktreeOwner(entry.id, owner(session.id)));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (!entry) return { ...result, removed: null };
|
|
348
|
+
let removed;
|
|
349
|
+
try {
|
|
350
|
+
removed = await backendApi.removeManagedWorktree({ entryId: entry.id, force: force === true });
|
|
351
|
+
} catch (error) {
|
|
352
|
+
// Refused before anything was deleted: the journal puts the session back.
|
|
353
|
+
// Once the checkout may be (partly) gone, the session stays in the
|
|
354
|
+
// project folder and the removal is reported incomplete.
|
|
355
|
+
const now = await backendApi.getSessionWorktree(session.id).catch(() => entry);
|
|
356
|
+
if (now?.id === entry.id && now.state === 'ready' && existsSync(entry.path)) throw error;
|
|
357
|
+
throw incompleteRemoval(journal, entry, result, { reason: String(error?.message || error) });
|
|
358
|
+
}
|
|
359
|
+
if (removed?.removed !== true) throw incompleteRemoval(journal, entry, result, removed);
|
|
360
|
+
return { ...result, removed };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return { enterWorktree, enterProjectBranch, removeRetainedWorktree };
|
|
364
|
+
}
|