mixdog 1.2.3 → 1.2.4
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/package.json +1 -1
- package/scripts/gate-local.mjs +6 -4
- package/scripts/lib/gate-scheduler.mjs +29 -11
- package/scripts/probe-provider-catalog.mjs +31 -0
- package/scripts/provider-pingpong-live.mjs +128 -38
- package/scripts/session-sweep.mjs +5 -25
- package/src/agents/maintainer/AGENT.md +1 -1
- package/src/agents/maintainer/agent.json +1 -1
- package/src/agents/reviewer/AGENT.md +2 -4
- package/src/agents/security/AGENT.md +1 -2
- package/src/agents/writer/AGENT.md +2 -3
- package/src/defaults/shipped-history.json +6 -1
- package/src/defaults/skills/image/SKILL.md +21 -23
- package/src/lib/keychain-cjs.cjs +13 -11
- package/src/rules/agent/AGENT.md +7 -5
- package/src/rules/lead/LEAD.md +7 -9
- package/src/rules/shared/45-destructive.md +4 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +1 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +28 -7
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -11
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +101 -23
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-api-key-fetch.mjs +36 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-error-contract.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/gzip-post.mjs +26 -44
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +2 -5
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +41 -12
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +13 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +81 -10
- package/src/runtime/agent/orchestrator/providers/antigravity-retry-policy.mjs +109 -5
- package/src/runtime/agent/orchestrator/providers/antigravity-stream.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +107 -37
- package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/cursor-native-context.mjs +48 -0
- package/src/runtime/agent/orchestrator/providers/cursor-wire-exec.mjs +144 -34
- package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +28 -4
- package/src/runtime/agent/orchestrator/providers/cursor-wire-request.mjs +8 -4
- package/src/runtime/agent/orchestrator/providers/cursor-wire-stream-bridge.mjs +41 -11
- package/src/runtime/agent/orchestrator/providers/cursor-wire-stream-messages.mjs +67 -5
- package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +14 -4
- package/src/runtime/agent/orchestrator/providers/cursor.mjs +14 -1
- package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +9 -6
- package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +7 -2
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +26 -4
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +12 -6
- package/src/runtime/agent/orchestrator/providers/gemini-error-contract.mjs +14 -6
- package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +4 -4
- package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/gemini-rest-stream/watchdogs.mjs +16 -5
- package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +22 -10
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +4 -2
- package/src/runtime/agent/orchestrator/providers/gemini-thinking.mjs +33 -7
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +11 -2
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +9 -2
- package/src/runtime/agent/orchestrator/providers/lib/claude-bridge-schema.mjs +9 -5
- package/src/runtime/agent/orchestrator/providers/lib/request-compression.mjs +86 -0
- package/src/runtime/agent/orchestrator/providers/lib/wire-pairing.mjs +4 -2
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +16 -5
- package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +60 -16
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +11 -13
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +30 -19
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +5 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +42 -5
- package/src/runtime/agent/orchestrator/providers/openai-compat-reasoning-field.mjs +13 -9
- package/src/runtime/agent/orchestrator/providers/openai-compat-response-normalization.mjs +1 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses-parity.fixtures.mjs +18 -7
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +36 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +51 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +11 -1
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-request.mjs +64 -52
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/terminal-frames.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +1 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-response-state/tool-calls.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +7 -6
- package/src/runtime/agent/orchestrator/providers/opencode-go-gemini.mjs +13 -2
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/openrouter.mjs +1 -2
- package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +5 -4
- package/src/runtime/agent/orchestrator/providers/registry.mjs +9 -1
- package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/wire-gateway.mjs +11 -4
- package/src/runtime/agent/orchestrator/providers/wire-route.mjs +14 -4
- package/src/runtime/agent/orchestrator/providers/xai-tool-search.fixtures.mjs +15 -3
- package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +6 -2
- package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +8 -6
- package/src/runtime/agent/orchestrator/runtime-core/deferred-tool-availability.mjs +4 -1
- package/src/runtime/agent/orchestrator/runtime-core/goal-store.mjs +30 -2
- package/src/runtime/agent/orchestrator/runtime-core/goal-turns.mjs +3 -2
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +8 -3
- package/src/runtime/agent/orchestrator/runtime-core/skill-tool-loading.mjs +3 -1
- package/src/runtime/agent/orchestrator/runtime-core/tool-bridge-contract.mjs +6 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +10 -10
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +3 -1
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +46 -8
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +10 -8
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +1 -2
- package/src/runtime/agent/orchestrator/session/agent-pool-progress.mjs +24 -0
- package/src/runtime/agent/orchestrator/session/cache/tool-result-reuse.mjs +10 -2
- package/src/runtime/agent/orchestrator/session/catalog-db/index.mjs +2 -3
- package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +3 -11
- package/src/runtime/agent/orchestrator/session/catalog-db/worker.mjs +6 -22
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +16 -2
- package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +0 -6
- package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +0 -6
- package/src/runtime/agent/orchestrator/session/eager-dispatch/admission.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/historical-session-closure.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/loop/assistant-commit.mjs +2 -5
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +2 -4
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +0 -27
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/output-limit.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +4 -10
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +2 -9
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/loop/provider-tool-exclusions.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/loop/request-boundary.mjs +1 -4
- package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +76 -0
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +8 -9
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +1 -4
- package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +3 -4
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +33 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-support.mjs +0 -4
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +21 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +38 -13
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-failure.mjs +33 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +19 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-state.mjs +57 -13
- package/src/runtime/agent/orchestrator/session/manager/failed-turn-rewind.mjs +16 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +6 -120
- package/src/runtime/agent/orchestrator/session/manager/pending-persist-queue.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +29 -8
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +42 -49
- package/src/runtime/agent/orchestrator/session/manager/runtime-state.mjs +50 -0
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +32 -14
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +17 -33
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +29 -13
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-recovery.mjs +45 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +2 -9
- package/src/runtime/agent/orchestrator/session/send-instrumentation.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -2
- package/src/runtime/agent/orchestrator/session/side-question.mjs +12 -7
- package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +5 -12
- package/src/runtime/agent/orchestrator/session/store/lifecycle-read.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +2 -6
- package/src/runtime/agent/orchestrator/session/store/live-cache-eviction.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store/liveness-veto.mjs +46 -37
- package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store/load-session.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +5 -11
- package/src/runtime/agent/orchestrator/session/store/session-paths.mjs +59 -0
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store/sweep/stale-sweep.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-finish.mjs +27 -32
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-plan.mjs +0 -8
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-record.mjs +6 -18
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +33 -106
- package/src/runtime/agent/orchestrator/session/store/write-admission.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store-agent-worker-pool.mjs +5 -8
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +136 -196
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +50 -116
- package/src/runtime/agent/orchestrator/session/store-summary-locations.mjs +3 -29
- package/src/runtime/agent/orchestrator/session/store-summary-reader-catalog-fixture.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +7 -8
- package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +9 -20
- package/src/runtime/agent/orchestrator/session/store.mjs +3 -16
- package/src/runtime/agent/orchestrator/session/tool-batch/execute-call.mjs +11 -12
- package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +37 -13
- package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +15 -4
- package/src/runtime/agent/orchestrator/session/tool-schema-rejection.mjs +4 -2
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +72 -15
- package/src/runtime/agent/orchestrator/session/turn-failure.mjs +45 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +50 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +5 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/inline-hoist.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +204 -10
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +160 -39
- package/src/runtime/agent/orchestrator/tools/builtin/git-partial-stage.mjs +7 -5
- package/src/runtime/agent/orchestrator/tools/builtin/grep-formatting.mjs +14 -5
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +4 -3
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-output.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-path-fanout.mjs +48 -44
- package/src/runtime/agent/orchestrator/tools/builtin/lib/search-input-helpers.mjs +10 -3
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +200 -17
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-task-output.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +20 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +101 -2
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +67 -0
- package/src/runtime/agent/orchestrator/tools/lib/process-table-fixture.mjs +50 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +303 -63
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +108 -18
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +77 -11
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +81 -5
- package/src/runtime/attachments/store.d.mts +9 -0
- package/src/runtime/attachments/store.mjs +80 -9
- package/src/runtime/browser-bridge/browser-action-contract.d.mts +0 -1
- package/src/runtime/browser-bridge/browser-action-contract.mjs +0 -2
- package/src/runtime/computer-bridge/action-schema.mjs +178 -178
- package/src/runtime/computer-bridge/linux-readiness.d.mts +0 -2
- package/src/runtime/computer-bridge/linux-readiness.mjs +0 -16
- package/src/runtime/effort-judge/judge-client.mjs +21 -4
- package/src/runtime/effort-judge/judge-worker.mjs +4 -1
- package/src/runtime/github/tool.mjs +118 -17
- package/src/runtime/media/adapters/codex-image.mjs +140 -21
- package/src/runtime/media/catalog.mjs +4 -4
- package/src/runtime/media/chatgpt-usage.mjs +55 -0
- package/src/runtime/media/defaults.mjs +37 -0
- package/src/runtime/media/jobs.mjs +12 -2
- package/src/runtime/media/lanes.mjs +12 -0
- package/src/runtime/media/tool-defs.mjs +15 -4
- package/src/runtime/media/tool.mjs +41 -12
- package/src/runtime/media/upstream-error.mjs +72 -6
- package/src/runtime/memory/lib/ingest/transcript-rows.mjs +1 -0
- package/src/runtime/memory/lib/pg/process.mjs +5 -7
- package/src/runtime/memory/lib/pg/supervisor.mjs +4 -1
- package/src/runtime/office/authoring/docx-html-runner.mjs +1 -1
- package/src/runtime/office/authoring/html-browser.mjs +18 -8
- package/src/runtime/office/authoring/pdf-html-runner.mjs +1 -1
- package/src/runtime/office/authoring/pptx-html-measure.mjs +1 -1
- package/src/runtime/office/authoring/xlsx-html-runner.mjs +1 -1
- package/src/runtime/office/core/office-write-ledger.mjs +1 -5
- package/src/runtime/office/index.mjs +2 -3
- package/src/runtime/office/pdf/pdf-analysis.mjs +0 -68
- package/src/runtime/session-search/session-search-ingest.mjs +1 -1
- package/src/runtime/shared/agent-reap-state.mjs +13 -0
- package/src/runtime/shared/atomic-file.d.mts +1 -1
- package/src/runtime/shared/background-tasks.mjs +6 -1
- package/src/runtime/shared/bounded-line-diff.mjs +11 -2
- package/src/runtime/shared/browser-launch.mjs +15 -0
- package/src/runtime/shared/err-text.mjs +42 -4
- package/src/runtime/shared/file-lock.mjs +6 -5
- package/src/runtime/shared/llm/anthropic-betas.mjs +1 -2
- package/src/runtime/shared/llm/provider-model-identities.mjs +1 -0
- package/src/runtime/shared/model-route-chain.d.mts +8 -0
- package/src/runtime/shared/model-route-chain.mjs +48 -0
- package/src/runtime/shared/model-route-fallback.mjs +22 -0
- package/src/runtime/shared/pid-liveness.mjs +12 -0
- package/src/runtime/shared/provider-api-key.mjs +6 -0
- package/src/runtime/shared/provider-timeout-phase.mjs +17 -0
- package/src/runtime/shared/quiet-progress.d.mts +35 -0
- package/src/runtime/shared/quiet-progress.mjs +101 -0
- package/src/runtime/shared/quota-resume-store.mjs +32 -1
- package/src/runtime/shared/read-row-numbers.mjs +44 -0
- package/src/runtime/shared/remote-intent.mjs +2 -42
- package/src/runtime/shared/slow-span.mjs +8 -1
- package/src/runtime/shared/statusline/statusline-segments.mjs +24 -2
- package/src/runtime/shared/tool-card-model/card-header.mjs +2 -1
- package/src/runtime/shared/tool-output-budget.mjs +2 -0
- package/src/runtime/shared/tool-result-summary.mjs +16 -2
- package/src/runtime/shared/tool-status.mjs +3 -2
- package/src/runtime/shared/tool-surface.mjs +2 -3
- package/src/runtime/shared/tool-work-units.mjs +22 -15
- package/src/runtime/shared/transcript-metadata.mjs +49 -1
- package/src/runtime/shared/transcript-turn-keys.mjs +33 -0
- package/src/runtime/shared/transcript-writer/transcript-file.mjs +10 -1
- package/src/runtime/shared/transcript-writer.mjs +14 -4
- package/src/runtime/shared/turn-changes.mjs +2 -1
- package/src/runtime/shared/turn-snapshot.mjs +11 -3
- package/src/runtime/shared/worktrees/activity-gate.mjs +175 -0
- package/src/runtime/shared/worktrees/branch-management.mjs +179 -0
- package/src/runtime/shared/worktrees/carry.mjs +6 -4
- package/src/runtime/shared/worktrees/contract.mjs +27 -1
- package/src/runtime/shared/worktrees/dependencies.mjs +456 -0
- package/src/runtime/shared/worktrees/file-recovery.mjs +29 -13
- package/src/runtime/shared/worktrees/git-head.mjs +22 -13
- package/src/runtime/shared/worktrees/include.mjs +125 -7
- package/src/runtime/shared/worktrees/lock.mjs +1 -1
- package/src/runtime/shared/worktrees/maintenance.mjs +20 -2
- package/src/runtime/shared/worktrees/manager.mjs +378 -32
- package/src/runtime/shared/worktrees/owner.mjs +2 -1
- package/src/runtime/shared/worktrees/preparation-report.mjs +79 -0
- package/src/runtime/shared/worktrees/setup-commands.mjs +232 -0
- package/src/runtime/shared/worktrees/workspace-visibility.mjs +17 -0
- package/src/runtime/terminal-bridge/client.mjs +14 -10
- package/src/session-runtime/boot/apis.mjs +7 -1
- package/src/session-runtime/boot/config.mjs +2 -0
- package/src/session-runtime/boot/facade.mjs +3 -0
- package/src/session-runtime/boot/session.mjs +3 -2
- package/src/session-runtime/boot/tools.mjs +3 -0
- package/src/session-runtime/conversation-import/service.mjs +1 -1
- package/src/session-runtime/goal-facade-api.mjs +2 -2
- package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +44 -18
- package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +4 -1
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +22 -8
- package/src/session-runtime/internal-tool-executor.mjs +2 -0
- package/src/session-runtime/lifecycle/cold-heir.mjs +0 -1
- package/src/session-runtime/lifecycle/session-catalog.mjs +14 -16
- package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +18 -53
- package/src/session-runtime/lifecycle/session-switch.mjs +1 -1
- package/src/session-runtime/lifecycle/shared.mjs +5 -6
- package/src/session-runtime/lifecycle/teardown/session-close.mjs +6 -5
- package/src/session-runtime/lifecycle/teardown.mjs +7 -16
- package/src/session-runtime/media-api.mjs +8 -0
- package/src/session-runtime/model-route/route-ownership.mjs +60 -0
- package/src/session-runtime/model-route/route-persist.mjs +3 -2
- package/src/session-runtime/model-route/set-route.mjs +114 -34
- package/src/session-runtime/model-route/web-search-route.mjs +7 -2
- package/src/session-runtime/model-route-api.mjs +4 -2
- package/src/session-runtime/native-web-search/routes.mjs +37 -24
- package/src/session-runtime/native-web-search.mjs +9 -8
- package/src/session-runtime/provider-auth/setup.mjs +4 -0
- package/src/session-runtime/provider-models/catalog-load.mjs +9 -0
- package/src/session-runtime/provider-models/web-search-catalog.mjs +23 -3
- package/src/session-runtime/provider-models.mjs +1 -0
- package/src/session-runtime/provider-usage.mjs +4 -0
- package/src/session-runtime/route-resolution.mjs +3 -1
- package/src/session-runtime/runtime-core.mjs +4 -0
- package/src/session-runtime/runtime-facade.mjs +6 -0
- package/src/session-runtime/services/agent-task-status.mjs +2 -0
- package/src/session-runtime/services/agent-tool/close-flow/close-all.mjs +0 -3
- package/src/session-runtime/services/agent-tool/close-flow.mjs +6 -7
- package/src/session-runtime/services/agent-tool/execute-send.mjs +2 -2
- package/src/session-runtime/services/agent-tool/execute-spawn.mjs +36 -3
- package/src/session-runtime/services/agent-tool/execute.mjs +9 -1
- package/src/session-runtime/services/agent-tool/helpers.mjs +30 -7
- package/src/session-runtime/services/agent-tool/job-task-reconcile.mjs +7 -2
- package/src/session-runtime/services/agent-tool/job-views/session-progress.mjs +6 -3
- package/src/session-runtime/services/agent-tool/lead-worker-index/lead-rows.mjs +3 -4
- package/src/session-runtime/services/agent-tool/lead-worker-index/reap-timers.mjs +9 -3
- package/src/session-runtime/services/agent-tool/lead-worker-index/row-settlement.mjs +7 -18
- package/src/session-runtime/services/agent-tool/lead-worker-index.mjs +9 -6
- package/src/session-runtime/services/agent-tool/model-select.mjs +82 -0
- package/src/session-runtime/services/agent-tool/send-flow.mjs +24 -6
- package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +3 -1
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +19 -5
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +5 -4
- package/src/session-runtime/services/agent-tool/spawn-flow.mjs +2 -1
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +3 -0
- package/src/session-runtime/services/agent-tool/tag-registry/tag-lookup.mjs +1 -3
- package/src/session-runtime/services/agent-tool/tag-registry/tag-refresh.mjs +1 -3
- package/src/session-runtime/services/agent-tool/tag-registry.mjs +3 -9
- package/src/session-runtime/services/agent-tool/terminal-reaper/scan-admission.mjs +8 -41
- package/src/session-runtime/services/agent-tool/terminal-reaper/stale-rows.mjs +3 -3
- package/src/session-runtime/services/agent-tool/terminal-reaper.mjs +6 -23
- package/src/session-runtime/services/agent-tool/terminal-trace.mjs +0 -1
- package/src/session-runtime/services/agent-tool/tool-def.mjs +7 -0
- package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +32 -20
- package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +5 -2
- package/src/session-runtime/services/agent-tool/worker-index.mjs +28 -8
- package/src/session-runtime/services/agent-tool.mjs +4 -0
- package/src/session-runtime/services/agent-watchdog-registry.mjs +2 -0
- package/src/session-runtime/services/provider-admin.mjs +77 -0
- package/src/session-runtime/session-create/session-options.mjs +7 -5
- package/src/session-runtime/session-create/session-start-hook.mjs +2 -1
- package/src/session-runtime/session-transcript.mjs +4 -5
- package/src/session-runtime/session-turn-api.mjs +3 -9
- package/src/session-runtime/settings-builtin-tools-api.mjs +9 -6
- package/src/session-runtime/setup-tool/executor.mjs +23 -5
- package/src/session-runtime/setup-tool/extended-actions.mjs +6 -6
- package/src/session-runtime/setup-tool/settings-contract.mjs +7 -2
- package/src/session-runtime/setup-tool/tool-defs.mjs +13 -1
- package/src/session-runtime/tool-policy-refresh.mjs +83 -32
- package/src/session-runtime/turn/agent-ops.mjs +49 -2
- package/src/session-runtime/turn/side-question-ops.mjs +1 -4
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +2 -0
- package/src/session-runtime/turn/turn-run/turn-prepare.mjs +4 -0
- package/src/session-runtime/turn/turn-run/turn-settle.mjs +8 -2
- package/src/session-runtime/turn/turn-run.mjs +1 -36
- package/src/session-runtime/usage-stats-api.mjs +4 -1
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +16 -1
- package/src/session-runtime/workspace/admission-gate.mjs +44 -15
- package/src/session-runtime/workspace/project-branches.mjs +28 -36
- package/src/session-runtime/workspace/transition-steps.mjs +96 -95
- package/src/session-runtime/workspace/transitions.mjs +248 -68
- package/src/standalone/channel-transport.mjs +3 -4
- package/src/standalone/daemon-desktop-runtime.mjs +32 -1
- package/src/standalone/daemon-pg-stop.mjs +1 -3
- package/src/standalone/daemon-session-control.mjs +88 -0
- package/src/standalone/daemon-stored-session-views.mjs +23 -3
- package/src/standalone/daemon.mjs +2 -0
- package/src/standalone/local-session-runtime.mjs +11 -12
- package/src/standalone/session-protocol.mjs +7 -0
- package/src/standalone/session-runtime-inline-host.mjs +2 -0
- package/src/standalone/session-service/agent-tree/agent-cancel.mjs +91 -49
- package/src/standalone/session-service/agent-tree/agent-child.mjs +19 -1
- package/src/standalone/session-service/agent-tree/agent-registry.mjs +25 -0
- package/src/standalone/session-service/agent-tree/agent-turns.mjs +9 -1
- package/src/standalone/session-service/agent-tree.mjs +28 -2
- package/src/standalone/session-service/entries/session-loads.mjs +16 -5
- package/src/standalone/session-service/entries.mjs +1 -0
- package/src/standalone/session-service/projection/transcript-window.mjs +39 -0
- package/src/standalone/session-service/quota-resume.mjs +28 -13
- package/src/standalone/session-service/session-calls/session-turns.mjs +17 -6
- package/src/standalone/session-service/session-calls/session-views.mjs +6 -0
- package/src/standalone/session-tool-output.mjs +82 -0
- package/src/tui/app/app-view.jsx +2 -1
- package/src/tui/app/provider-setup/provider-items.mjs +8 -0
- package/src/tui/app/provider-setup-picker.mjs +15 -3
- package/src/tui/app/slash-commands.mjs +6 -0
- package/src/tui/app/slash-dispatch/session-commands.mjs +49 -0
- package/src/tui/components/Spinner.jsx +4 -0
- package/src/tui/components/SpinnerByline.jsx +21 -2
- package/src/tui/components/status-line/local-line.mjs +10 -2
- package/src/tui/dist/index.mjs +478 -251
- package/src/tui/session/active-tool-summary.mjs +1 -1
- package/src/tui/session/context-state.mjs +0 -3
- package/src/tui/session/goal-continuation/continuation-queue.mjs +2 -3
- package/src/tui/session/goal-continuation/deadline-delivery.mjs +4 -2
- package/src/tui/session/goal-continuation/session-boundary.mjs +21 -0
- package/src/tui/session/quota-resume.mjs +140 -61
- package/src/tui/session/session-api/commands.mjs +4 -10
- package/src/tui/session/session-api/controls.mjs +12 -0
- package/src/tui/session/session-api/lifecycle/dispose.mjs +2 -9
- package/src/tui/session/session-api/lifecycle/resume.mjs +2 -7
- package/src/tui/session/session-api/lifecycle.mjs +13 -11
- package/src/tui/session/session-api/media.mjs +2 -0
- package/src/tui/session/session-api/routes.mjs +81 -57
- package/src/tui/session/session-api/settings.mjs +3 -3
- package/src/tui/session/session-api/worktree.mjs +3 -2
- package/src/tui/session/session-flow/auto-clear.mjs +1 -3
- package/src/tui/session/session-flow/drain.mjs +6 -4
- package/src/tui/session/session-flow/queue/entry-shape.mjs +2 -0
- package/src/tui/session/session-flow/queue/take-entries.mjs +2 -1
- package/src/tui/session/session-flow/queue.mjs +1 -1
- package/src/tui/session/tool-approval.mjs +17 -8
- package/src/tui/session/transcript-store/streaming-tail.mjs +1 -1
- package/src/tui/session/tui-steering-persist.mjs +3 -5
- package/src/tui/session/turn-quiet.mjs +53 -0
- package/src/tui/session/turn-stream.mjs +5 -1
- package/src/tui/session/turn.mjs +38 -31
- package/src/tui/session/worktree-setup-notice.mjs +38 -0
- package/src/tui/spinner-meta.mjs +10 -0
- package/src/tui/spinner-verbs.mjs +3 -0
- package/src/ui/statusline.mjs +13 -3
- package/src/runtime/agent/orchestrator/providers/model-pricing-rates.mjs +0 -1
- package/src/runtime/agent/orchestrator/session/manager/foreign-pending-messages.mjs +0 -346
- package/src/runtime/agent/orchestrator/session/manager/session-owner-liveness.mjs +0 -117
- package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +0 -302
- package/src/session-runtime/services/agent-tool/terminal-reaper/reap-timers.mjs +0 -142
- package/src/session-runtime/turn/share-ops.mjs +0 -77
- package/src/tui/session/cross-surface-share.mjs +0 -406
- package/src/tui/session/live-share/delta.mjs +0 -153
- package/src/tui/session/live-share/mirror.mjs +0 -106
- package/src/tui/session/live-share/owner.mjs +0 -254
- package/src/tui/session/live-share/retry.mjs +0 -54
- package/src/tui/session/live-share/viewer/frame-router.mjs +0 -32
- package/src/tui/session/live-share/viewer/pipe-client.mjs +0 -108
- package/src/tui/session/live-share/viewer/submit-acks.mjs +0 -65
- package/src/tui/session/live-share/viewer/sync-waiters.mjs +0 -53
- package/src/tui/session/live-share/viewer.mjs +0 -99
- package/src/tui/session/live-share/wire.mjs +0 -44
- package/src/tui/session/live-share.mjs +0 -154
- package/src/tui/session/shared-dir-watch.mjs +0 -69
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Managed git worktrees for Lead sessions.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Automatic worktrees are created lazily from the project's current branch
|
|
5
|
+
* on "<branchPrefix><slug>" at managedWorktreePath(). Explicit branch selection
|
|
6
|
+
* reuses its checkout or creates one for that exact branch. Creator retention
|
|
7
|
+
* and additional sessions' durable references protect shared checkouts. Every
|
|
6
8
|
* mutation runs under withRepoLock (in-process git lock keyed by the common
|
|
7
9
|
* git dir + cross-process file lock) and is recorded in the durable registry
|
|
8
10
|
* (registry.mjs) before it touches anything:
|
|
@@ -26,8 +28,13 @@
|
|
|
26
28
|
* -d` forgets it).
|
|
27
29
|
*/
|
|
28
30
|
import { existsSync } from 'node:fs';
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
+
import {
|
|
32
|
+
assertRecoveryStoresClear,
|
|
33
|
+
recoveryScopes,
|
|
34
|
+
retainedRecoveryScopes,
|
|
35
|
+
validateRecoveryScope,
|
|
36
|
+
} from './file-recovery.mjs';
|
|
37
|
+
import { lstat, mkdir, readdir, readFile, rm, rmdir, stat } from 'node:fs/promises';
|
|
31
38
|
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
32
39
|
import {
|
|
33
40
|
managedWorktreePath,
|
|
@@ -64,21 +71,34 @@ import {
|
|
|
64
71
|
applyIncludes,
|
|
65
72
|
coveredByIncludes,
|
|
66
73
|
modifiedIncludeCopies,
|
|
74
|
+
untrackedEnvFiles,
|
|
67
75
|
planIncludes,
|
|
68
76
|
readIncludeEntries,
|
|
69
77
|
removeIncludeLinks,
|
|
78
|
+
restoreIncludeLinks,
|
|
70
79
|
} from './include.mjs';
|
|
80
|
+
import { linkWorktreeDependencies } from './dependencies.mjs';
|
|
71
81
|
import { withRepoLock as withRepoMutationLock } from './lock.mjs';
|
|
72
|
-
import { mergeBranchIntoCheckout } from './merge.mjs';
|
|
82
|
+
import { mergeBranchIntoCheckout, operationInProgress } from './merge.mjs';
|
|
73
83
|
import { readOwnerLease, removeOwnerLease, sameOwner, writeOwnerLease } from './owner.mjs';
|
|
74
84
|
import { canonicalPath, checkoutFolder, isWithin, pathKey, projectSubdirOf, samePath } from './paths.mjs';
|
|
75
|
-
import {
|
|
85
|
+
import {
|
|
86
|
+
deleteRecord,
|
|
87
|
+
entryView,
|
|
88
|
+
getRecord,
|
|
89
|
+
insertRecord,
|
|
90
|
+
putRecord,
|
|
91
|
+
readRegistry,
|
|
92
|
+
registryPath,
|
|
93
|
+
} from './registry.mjs';
|
|
76
94
|
import { normalizeWorktreeSettings } from './settings.mjs';
|
|
77
95
|
import { slugFromHint, uniqueSlug } from './slug.mjs';
|
|
78
96
|
|
|
79
97
|
const WORKTREE_ADD_TIMEOUT_MS = 30 * 60_000;
|
|
80
98
|
const REMOVE_RETRIES = 6;
|
|
81
99
|
const REMOVE_RETRY_DELAY_MS = 150;
|
|
100
|
+
// Entries a folder-only removal checks for unrecorded directory links.
|
|
101
|
+
const LINK_SCAN_LIMIT = 250_000;
|
|
82
102
|
// The session copies the project identity into each managed worktree; it is
|
|
83
103
|
// Mixdog metadata, not user data, so it alone never makes a worktree dirty.
|
|
84
104
|
const SESSION_PROJECT_ID_PATH = '.mixdog/project.id';
|
|
@@ -324,6 +344,7 @@ async function dirtyPaths(record) {
|
|
|
324
344
|
}
|
|
325
345
|
|
|
326
346
|
async function deleteBranchIfFree(record) {
|
|
347
|
+
if (record.existingBranch === true) return { deleted: false, reason: 'pre-existing' };
|
|
327
348
|
const cwd = repoCwd(record);
|
|
328
349
|
if (!(await branchCommit(cwd, record.branch))) return { deleted: false, reason: 'missing' };
|
|
329
350
|
const holder = (await listWorktrees(cwd)).find((worktree) => worktree.branch === record.branch);
|
|
@@ -339,6 +360,10 @@ async function deleteBranchIfFree(record) {
|
|
|
339
360
|
async function removeCheckout(record) {
|
|
340
361
|
await assertRecoveryStoresClear(record);
|
|
341
362
|
await assertManagedCheckoutPath(record);
|
|
363
|
+
// `git worktree remove --force` can descend through a junction on Windows
|
|
364
|
+
// (fs.rm below does not: it unlinks links), so an unknown directory link
|
|
365
|
+
// refuses this removal as it does the folder-only one.
|
|
366
|
+
await assertNoUnrecordedDirectoryLinks(record);
|
|
342
367
|
const remaining = [];
|
|
343
368
|
for (const path of await removeIncludeLinks(record.resources?.includes)) remaining.push({ kind: 'link', path });
|
|
344
369
|
if (remaining.length) return remaining;
|
|
@@ -365,6 +390,128 @@ async function removeCheckout(record) {
|
|
|
365
390
|
return remaining;
|
|
366
391
|
}
|
|
367
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Refuse a checkout that holds a directory link the ledger does not know (a
|
|
395
|
+
* junction or directory symlink made by the user or a build tool): `git
|
|
396
|
+
* worktree remove` can descend through it on Windows and delete the target's
|
|
397
|
+
* files. Refusing is chosen over unlinking: nothing the manager did not make
|
|
398
|
+
* is ever touched, and the user removes the link (never its target) and
|
|
399
|
+
* retries. The walk never follows a link, skips `.git` entries (the back link
|
|
400
|
+
* file, nested repositories' internals) and is bounded: a checkout too large
|
|
401
|
+
* to verify is refused as well. Recorded links are skipped (unlinked next).
|
|
402
|
+
*/
|
|
403
|
+
async function assertNoUnrecordedDirectoryLinks(record) {
|
|
404
|
+
const key = (path) => (process.platform === 'win32' ? resolve(path).toLowerCase() : resolve(path));
|
|
405
|
+
const recorded = new Set(
|
|
406
|
+
(record.resources?.includes || []).filter((item) => item.mode === 'link').map((item) => key(item.path))
|
|
407
|
+
);
|
|
408
|
+
const refuse = (message, details) =>
|
|
409
|
+
worktreeError('WORKTREE_PATH_UNSAFE', message, { entryId: record.id, path: record.path, ...details });
|
|
410
|
+
const found = [];
|
|
411
|
+
const pending = [record.path];
|
|
412
|
+
let scanned = 0;
|
|
413
|
+
while (pending.length) {
|
|
414
|
+
const dir = pending.pop();
|
|
415
|
+
let entries;
|
|
416
|
+
try {
|
|
417
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
418
|
+
} catch (error) {
|
|
419
|
+
if (error?.code === 'ENOENT') continue;
|
|
420
|
+
throw refuse(
|
|
421
|
+
`The worktree folder could not be checked for links (${error?.code || error?.message}); nothing was deleted.`,
|
|
422
|
+
{ reason: 'unreadable' }
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
for (const entry of entries) {
|
|
426
|
+
if (++scanned > LINK_SCAN_LIMIT) {
|
|
427
|
+
throw refuse(
|
|
428
|
+
`The worktree folder has more than ${LINK_SCAN_LIMIT} entries to check for links; nothing was deleted.`,
|
|
429
|
+
{ reason: 'too-many-entries' }
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
if (entry.name.toLowerCase() === '.git') continue;
|
|
433
|
+
const path = join(dir, entry.name);
|
|
434
|
+
// A junction is a link here too (a reparse point), so it is never descended.
|
|
435
|
+
if (entry.isDirectory()) pending.push(path);
|
|
436
|
+
else if (entry.isSymbolicLink() && !recorded.has(key(path)) && (await linksToDirectory(path))) {
|
|
437
|
+
found.push(relative(record.path, path).split(sep).join('/'));
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (found.length) {
|
|
442
|
+
throw refuse(
|
|
443
|
+
`The worktree contains linked folders it did not create (${found.slice(0, 10).join(', ')}${found.length > 10 ? ', …' : ''}). ` +
|
|
444
|
+
'Removing the folder could delete their targets: remove those links (not their targets) first; nothing was deleted.',
|
|
445
|
+
{ reason: 'unrecorded-links', links: found.slice(0, 50), count: found.length }
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** True when the link at `path` resolves to a directory (or cannot be ruled out). */
|
|
451
|
+
async function linksToDirectory(path) {
|
|
452
|
+
try {
|
|
453
|
+
return (await stat(path)).isDirectory();
|
|
454
|
+
} catch (error) {
|
|
455
|
+
// Dangling: nothing to descend into.
|
|
456
|
+
return error?.code !== 'ENOENT' && error?.code !== 'ENOTDIR';
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** Explicit folder-only cleanup never force-removes a checkout or user files. */
|
|
461
|
+
async function removeCheckoutSafely(record) {
|
|
462
|
+
const registration = await registrationFor(record);
|
|
463
|
+
if (!registration && !existsSync(record.path)) return [];
|
|
464
|
+
await assertManagedCheckoutPath(record);
|
|
465
|
+
await assertRecoveryStoresClear(record);
|
|
466
|
+
await assertRemovable(record);
|
|
467
|
+
if (await operationInProgress(gitStatusRunner, record.path)) {
|
|
468
|
+
throw worktreeError('WORKTREE_DIRTY', 'Finish the Git operation before removing this worktree.');
|
|
469
|
+
}
|
|
470
|
+
const ownedLock = `mixdog session=${record.ownerSessionId}`;
|
|
471
|
+
if (registration?.locked !== null && registration?.locked !== ownedLock) {
|
|
472
|
+
throw worktreeError('WORKTREE_BUSY_JOBS', 'The worktree has an external Git lock.');
|
|
473
|
+
}
|
|
474
|
+
// `git worktree remove` descends through a directory junction on Windows
|
|
475
|
+
// and would empty its target (a linked node_modules in the main checkout):
|
|
476
|
+
// an unknown link refuses the removal, the recorded links go first, by unlink only.
|
|
477
|
+
await assertNoUnrecordedDirectoryLinks(record);
|
|
478
|
+
const includes = record.resources?.includes;
|
|
479
|
+
if ((await removeIncludeLinks(includes)).length) {
|
|
480
|
+
const failures = await restoreIncludeLinks(includes);
|
|
481
|
+
throw withRollbackFailures(
|
|
482
|
+
worktreeError('WORKTREE_BUSY_JOBS', 'A linked folder in the worktree could not be unlinked.'),
|
|
483
|
+
failures.map((failure) => `${failure.path} (${failure.error})`)
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
let unlocked = false;
|
|
487
|
+
try {
|
|
488
|
+
if (registration?.locked === ownedLock) {
|
|
489
|
+
await runGit(repoCwd(record), ['worktree', 'unlock', record.path]);
|
|
490
|
+
unlocked = true;
|
|
491
|
+
}
|
|
492
|
+
await runGit(repoCwd(record), ['worktree', 'remove', record.path]);
|
|
493
|
+
} catch (error) {
|
|
494
|
+
const failures = (await restoreIncludeLinks(includes)).map((failure) => `${failure.path} (${failure.error})`);
|
|
495
|
+
if (unlocked && (await registrationFor(record))) {
|
|
496
|
+
try {
|
|
497
|
+
await runGit(repoCwd(record), ['worktree', 'lock', '--reason', ownedLock, record.path]);
|
|
498
|
+
} catch (lockError) {
|
|
499
|
+
failures.push(`worktree lock (${firstLine(lockError?.message) || lockError})`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
throw withRollbackFailures(error, failures);
|
|
503
|
+
}
|
|
504
|
+
return [];
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** Report what a failed safe removal could not put back on its error (never swallowed). */
|
|
508
|
+
function withRollbackFailures(error, failures) {
|
|
509
|
+
if (!failures.length) return error;
|
|
510
|
+
error.rollbackFailures = failures;
|
|
511
|
+
error.message = `${error.message} Not restored: ${failures.join(', ')}.`;
|
|
512
|
+
return error;
|
|
513
|
+
}
|
|
514
|
+
|
|
368
515
|
/**
|
|
369
516
|
* After the last worktree of a repository is gone, drop its now-empty store
|
|
370
517
|
* folder (<dataDir>/worktrees-v1/<repoHash>/ and its .owners/). Runs under the
|
|
@@ -410,7 +557,11 @@ async function rollbackCreation(record, cause) {
|
|
|
410
557
|
if (resources.worktree) {
|
|
411
558
|
const left = await removeCheckout(record);
|
|
412
559
|
remaining.push(...left);
|
|
413
|
-
if (
|
|
560
|
+
if (
|
|
561
|
+
!left.length &&
|
|
562
|
+
record.existingBranch !== true &&
|
|
563
|
+
!(record.exactBranch === true && resources.worktree !== 'created')
|
|
564
|
+
) {
|
|
414
565
|
// The slug was free when this transaction reserved it, so a branch
|
|
415
566
|
// still at the base commit is the one `worktree add -b` made. A
|
|
416
567
|
// branch that moved holds someone's commits and is kept.
|
|
@@ -460,7 +611,7 @@ async function finishDeletion(record) {
|
|
|
460
611
|
for (const result of results) {
|
|
461
612
|
if (!result.ok) remaining.push({ kind: 'cleanup', name: result.name, error: result.error.message });
|
|
462
613
|
}
|
|
463
|
-
remaining.push(...(await removeCheckout(record)));
|
|
614
|
+
remaining.push(...(await (record.deleting?.safeOnly ? removeCheckoutSafely(record) : removeCheckout(record))));
|
|
464
615
|
} catch (error) {
|
|
465
616
|
remaining.push({ kind: 'error', error: String(error?.message || error) });
|
|
466
617
|
}
|
|
@@ -472,7 +623,7 @@ async function finishDeletion(record) {
|
|
|
472
623
|
let branch = { deleted: false, reason: 'kept' };
|
|
473
624
|
let kept = { tip: null, recoveryRef: null };
|
|
474
625
|
const merged = (await unmergedCommitCount(record)) === 0;
|
|
475
|
-
if (record.deleting?.force || merged) {
|
|
626
|
+
if (!record.deleting?.preserveBranch && (record.deleting?.force || merged)) {
|
|
476
627
|
// Forced past unmerged commits: the tip stays reachable before `branch -D`.
|
|
477
628
|
if (!merged) kept = await keepRemovedTip(record);
|
|
478
629
|
else kept.tip = await branchCommit(repoCwd(record), record.branch);
|
|
@@ -521,6 +672,100 @@ export async function getWorktreeByPath(path) {
|
|
|
521
672
|
return record ? entryView(record) : null;
|
|
522
673
|
}
|
|
523
674
|
|
|
675
|
+
/** Resolve a branch to its checkout without switching any existing folder. */
|
|
676
|
+
export async function resolveBranchWorkspace({
|
|
677
|
+
projectPath,
|
|
678
|
+
branch,
|
|
679
|
+
create = false,
|
|
680
|
+
sessionId,
|
|
681
|
+
runtimeToken,
|
|
682
|
+
txId,
|
|
683
|
+
confirmLarge = false,
|
|
684
|
+
signal,
|
|
685
|
+
} = {}) {
|
|
686
|
+
requireText(branch, 'branch');
|
|
687
|
+
const repo = await inspectRepo(projectPath, { signal });
|
|
688
|
+
return withRepoLock(repo.repoCommonDir, async () => {
|
|
689
|
+
if (!(await validBranchName(repo.projectPath, branch))) throw new TypeError('Invalid branch name.');
|
|
690
|
+
const records = Object.values((await readRegistry()).entries);
|
|
691
|
+
const transaction = records.find((record) => record.txId === txId);
|
|
692
|
+
if (
|
|
693
|
+
transaction &&
|
|
694
|
+
(transaction.ownerSessionId !== sessionId ||
|
|
695
|
+
transaction.branch !== branch ||
|
|
696
|
+
!samePath(transaction.repoCommonDir, repo.repoCommonDir))
|
|
697
|
+
) {
|
|
698
|
+
throw worktreeError('WORKTREE_REVISION_CONFLICT', 'This transaction belongs to another workspace.');
|
|
699
|
+
}
|
|
700
|
+
const retry =
|
|
701
|
+
transaction ??
|
|
702
|
+
(create
|
|
703
|
+
? records.find(
|
|
704
|
+
(record) =>
|
|
705
|
+
record.ownerSessionId === sessionId &&
|
|
706
|
+
record.branch === branch &&
|
|
707
|
+
record.exactBranch === true &&
|
|
708
|
+
samePath(record.repoCommonDir, repo.repoCommonDir)
|
|
709
|
+
)
|
|
710
|
+
: null);
|
|
711
|
+
if (retry?.state === 'ready') {
|
|
712
|
+
if (!existsSync(retry.path)) throw worktreeError('WORKTREE_MISSING');
|
|
713
|
+
await claimWorktreeOwner(retry.id, { sessionId, runtimeToken, pid: process.pid });
|
|
714
|
+
return { path: retry.path, branch, entry: entryView(retry), repoCommonDir: repo.repoCommonDir };
|
|
715
|
+
}
|
|
716
|
+
const branches = await localBranches(repo.projectPath, { signal });
|
|
717
|
+
if (create && branches.includes(branch))
|
|
718
|
+
throw worktreeError('WORKTREE_REVISION_CONFLICT', 'Branch already exists.');
|
|
719
|
+
if (!create && !branches.includes(branch)) throw worktreeError('WORKTREE_MISSING', 'Branch does not exist.');
|
|
720
|
+
const holder = (await listWorktrees(repo.projectPath)).find((item) => item.branch === branch && !item.prunable);
|
|
721
|
+
if (holder) {
|
|
722
|
+
if (!existsSync(holder.path)) throw worktreeError('WORKTREE_MISSING');
|
|
723
|
+
const entry = await getWorktreeByPath(holder.path);
|
|
724
|
+
const claim = entry ? await claimWorktreeOwner(entry.id, { sessionId, runtimeToken, pid: process.pid }) : null;
|
|
725
|
+
return {
|
|
726
|
+
path: holder.path,
|
|
727
|
+
branch,
|
|
728
|
+
entry,
|
|
729
|
+
referenceAdded: claim?.referenceAdded === true,
|
|
730
|
+
repoCommonDir: repo.repoCommonDir,
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
const entry = await createManagedWorktree({
|
|
734
|
+
projectPath,
|
|
735
|
+
sessionId,
|
|
736
|
+
runtimeToken,
|
|
737
|
+
txId,
|
|
738
|
+
branch,
|
|
739
|
+
existingBranch: !create,
|
|
740
|
+
baseBranch: create ? repo.currentBranch : branch,
|
|
741
|
+
nameHint: branch,
|
|
742
|
+
confirmLarge,
|
|
743
|
+
signal,
|
|
744
|
+
});
|
|
745
|
+
const record = await getRecord(entry.id);
|
|
746
|
+
record.mergeTarget = repo.currentBranch;
|
|
747
|
+
await putRecord(record);
|
|
748
|
+
entry.mergeTarget = repo.currentBranch;
|
|
749
|
+
return { path: entry.path, branch, entry, created: true, repoCommonDir: repo.repoCommonDir };
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/** Remove only a deleted session's shared references, never its neighbours'. */
|
|
754
|
+
export async function forgetSharedWorktreeSession(sessionId) {
|
|
755
|
+
for (const record of Object.values((await readRegistry()).entries)) {
|
|
756
|
+
if (!record.sharedSessionIds?.includes(sessionId)) continue;
|
|
757
|
+
await withRepoLock(record.repoCommonDir, async () => {
|
|
758
|
+
const current = await getRecord(record.id);
|
|
759
|
+
if (!current) return;
|
|
760
|
+
current.sharedSessionIds = (current.sharedSessionIds ?? []).filter((id) => id !== sessionId);
|
|
761
|
+
const leases = { ...current.sharedLeases };
|
|
762
|
+
delete leases[sessionId];
|
|
763
|
+
current.sharedLeases = leases;
|
|
764
|
+
await putRecord(current);
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
524
769
|
/** The managed worktree whose checkout is `path` or contains it (a subfolder Project's session root), or null. */
|
|
525
770
|
export async function findWorktreeContaining(path) {
|
|
526
771
|
if (typeof path !== 'string' || !path.trim()) return null;
|
|
@@ -545,10 +790,14 @@ export async function withWorktreeRecoveryStore(root, directory, action, run) {
|
|
|
545
790
|
// lookup. Its reserved namespace must never become an ordinary project.
|
|
546
791
|
// Retain a lexical check as well, so a replacement link cannot escape it.
|
|
547
792
|
const store = dirname(registryPath());
|
|
548
|
-
const lexicalKey = (path) => process.platform === 'win32' ? resolve(path).toLowerCase() : resolve(path);
|
|
793
|
+
const lexicalKey = (path) => (process.platform === 'win32' ? resolve(path).toLowerCase() : resolve(path));
|
|
549
794
|
const requested = lexicalKey(root);
|
|
550
|
-
if (
|
|
551
|
-
|
|
795
|
+
if (
|
|
796
|
+
isWithin(store, root) ||
|
|
797
|
+
[store, canonicalPath(store)].some(
|
|
798
|
+
(path) => requested === lexicalKey(path) || requested.startsWith(lexicalKey(path) + sep)
|
|
799
|
+
)
|
|
800
|
+
)
|
|
552
801
|
throw worktreeError('WORKTREE_MISSING', undefined, { path: root });
|
|
553
802
|
return run();
|
|
554
803
|
}
|
|
@@ -588,7 +837,7 @@ export async function markWorktreeDisposable(entryId, disposable = true) {
|
|
|
588
837
|
* WORKTREE_REVISION_CONFLICT for another session's transaction, a ready
|
|
589
838
|
* worktree of another repository, or an attempt that is still being cleaned up.
|
|
590
839
|
*/
|
|
591
|
-
async function settleSessionAttempts({ txId, owner, repoCommonDir }) {
|
|
840
|
+
async function settleSessionAttempts({ txId, owner, repoCommonDir, multiple = false }) {
|
|
592
841
|
const doc = await readRegistry();
|
|
593
842
|
const sameRepo = (record) => samePath(record.repoCommonDir, repoCommonDir);
|
|
594
843
|
|
|
@@ -602,6 +851,7 @@ async function settleSessionAttempts({ txId, owner, repoCommonDir }) {
|
|
|
602
851
|
}
|
|
603
852
|
|
|
604
853
|
for (const record of Object.values(doc.entries).filter((item) => item.ownerSessionId === owner.sessionId)) {
|
|
854
|
+
if (multiple && record.txId !== txId) continue;
|
|
605
855
|
if (record.state === 'ready') {
|
|
606
856
|
if (!sameRepo(record)) {
|
|
607
857
|
throw worktreeError(
|
|
@@ -677,6 +927,8 @@ export async function createManagedWorktree({
|
|
|
677
927
|
txId,
|
|
678
928
|
signal,
|
|
679
929
|
carryChanges = null,
|
|
930
|
+
branch: requestedBranch = null,
|
|
931
|
+
existingBranch = false,
|
|
680
932
|
} = {}) {
|
|
681
933
|
requireText(projectPath, 'projectPath');
|
|
682
934
|
requireText(txId, 'txId');
|
|
@@ -687,7 +939,12 @@ export async function createManagedWorktree({
|
|
|
687
939
|
throwIfAborted(signal);
|
|
688
940
|
const repo = await inspectRepo(projectPath, { signal });
|
|
689
941
|
return withRepoLock(repo.repoCommonDir, async () => {
|
|
690
|
-
const existing = await settleSessionAttempts({
|
|
942
|
+
const existing = await settleSessionAttempts({
|
|
943
|
+
txId,
|
|
944
|
+
owner,
|
|
945
|
+
repoCommonDir: repo.repoCommonDir,
|
|
946
|
+
multiple: requestedBranch !== null,
|
|
947
|
+
});
|
|
691
948
|
if (existing) return existing;
|
|
692
949
|
throwIfAborted(signal);
|
|
693
950
|
|
|
@@ -714,7 +971,11 @@ export async function createManagedWorktree({
|
|
|
714
971
|
mergeTarget = baseBranch;
|
|
715
972
|
}
|
|
716
973
|
|
|
717
|
-
|
|
974
|
+
// The main checkout's local env files come along on their own;
|
|
975
|
+
// `.worktreeinclude` applies on top.
|
|
976
|
+
const includePlan = await planIncludes(repo.topLevel, await readIncludeEntries(repo.topLevel), {
|
|
977
|
+
envFiles: await untrackedEnvFiles(repo.topLevel),
|
|
978
|
+
});
|
|
718
979
|
const tracked = await trackedCheckoutBytes(repo.topLevel, baseCommit, { signal });
|
|
719
980
|
const contentBytes = tracked.bytes + includePlan.copyBytes;
|
|
720
981
|
const root = managedWorktreeRoot(repo.repoCommonDir);
|
|
@@ -750,10 +1011,16 @@ export async function createManagedWorktree({
|
|
|
750
1011
|
return reserved;
|
|
751
1012
|
};
|
|
752
1013
|
const addCheckout = (target) =>
|
|
753
|
-
runGit(
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
1014
|
+
runGit(
|
|
1015
|
+
repo.projectPath,
|
|
1016
|
+
existingBranch
|
|
1017
|
+
? ['worktree', 'add', target.path, target.branch]
|
|
1018
|
+
: ['worktree', 'add', '-b', target.branch, target.path, baseCommit],
|
|
1019
|
+
{
|
|
1020
|
+
signal,
|
|
1021
|
+
timeoutMs: WORKTREE_ADD_TIMEOUT_MS,
|
|
1022
|
+
}
|
|
1023
|
+
);
|
|
757
1024
|
let record = await reserve();
|
|
758
1025
|
let carried = null;
|
|
759
1026
|
|
|
@@ -763,6 +1030,7 @@ export async function createManagedWorktree({
|
|
|
763
1030
|
try {
|
|
764
1031
|
await addCheckout(record);
|
|
765
1032
|
} catch (error) {
|
|
1033
|
+
if (requestedBranch) throw error;
|
|
766
1034
|
// Another process took the branch name (or folder) after the
|
|
767
1035
|
// reservation: git refused before making anything, so reserve the
|
|
768
1036
|
// next free name once and try again.
|
|
@@ -807,10 +1075,29 @@ export async function createManagedWorktree({
|
|
|
807
1075
|
}
|
|
808
1076
|
const applied = await applyIncludes(includePlan, record.path, (ledger) => recordInclude(record, ledger));
|
|
809
1077
|
throwIfAborted(signal);
|
|
1078
|
+
// After the includes: one that already brought a node_modules wins.
|
|
1079
|
+
const linkedDependencies = await linkWorktreeDependencies({
|
|
1080
|
+
mainDir: repo.projectPath,
|
|
1081
|
+
worktreeDir: projectSubdir ? join(record.path, ...projectSubdir.split('/')) : record.path,
|
|
1082
|
+
worktreeRel: projectSubdir,
|
|
1083
|
+
signal,
|
|
1084
|
+
record: (ledger) => recordInclude(record, ledger),
|
|
1085
|
+
});
|
|
1086
|
+
throwIfAborted(signal);
|
|
810
1087
|
record.warnings = [...record.warnings, ...applied.warnings];
|
|
811
1088
|
record.state = 'ready';
|
|
812
1089
|
await putRecord(record);
|
|
813
|
-
|
|
1090
|
+
const made = (record.resources.includes || []).filter((item) => item.status === 'created');
|
|
1091
|
+
const rels = (predicate) => made.filter(predicate).map((item) => item.rel);
|
|
1092
|
+
// What creation did, for the agent's note at the first turn (transitions.mjs).
|
|
1093
|
+
const prepared = {
|
|
1094
|
+
root: repo.topLevel,
|
|
1095
|
+
copied: rels((item) => item.mode === 'copy'),
|
|
1096
|
+
linked: rels((item) => item.mode === 'link' && !item.dependency),
|
|
1097
|
+
dependencies: linkedDependencies,
|
|
1098
|
+
skipped: [...includePlan.warnings, ...applied.warnings],
|
|
1099
|
+
};
|
|
1100
|
+
return { ...entryView(record), prepared, ...(changes ? { changes } : {}) };
|
|
814
1101
|
} catch (error) {
|
|
815
1102
|
// Carried already: the changes go back to the project folder before the
|
|
816
1103
|
// worktree that holds them is rolled back (the recovery ref keeps them
|
|
@@ -841,7 +1128,7 @@ export async function createManagedWorktree({
|
|
|
841
1128
|
branches.has(`${settings.branchPrefix}${candidate}`.toLowerCase()) ||
|
|
842
1129
|
existsSync(join(root, candidate))
|
|
843
1130
|
);
|
|
844
|
-
const branch = worktreeBranchName(slug, settings.branchPrefix);
|
|
1131
|
+
const branch = requestedBranch ?? worktreeBranchName(slug, settings.branchPrefix);
|
|
845
1132
|
if (!(await validBranchName(repo.projectPath, branch))) {
|
|
846
1133
|
throw worktreeError('WORKTREE_UNSUPPORTED_REPO', `Branch name ${branch} is not valid.`, { branch });
|
|
847
1134
|
}
|
|
@@ -857,6 +1144,8 @@ export async function createManagedWorktree({
|
|
|
857
1144
|
mergeTarget,
|
|
858
1145
|
createdAt: Date.now(),
|
|
859
1146
|
ownerSessionId: owner.sessionId,
|
|
1147
|
+
existingBranch,
|
|
1148
|
+
exactBranch: requestedBranch !== null,
|
|
860
1149
|
state: 'creating',
|
|
861
1150
|
disposable: false,
|
|
862
1151
|
warnings: [...(tracked.submodules ? ['submodules'] : []), ...includePlan.warnings],
|
|
@@ -906,10 +1195,21 @@ export async function claimWorktreeOwner(entryId, owner) {
|
|
|
906
1195
|
const current = await getRecord(entryId);
|
|
907
1196
|
if (current?.state !== 'ready') throw worktreeError('WORKTREE_MISSING', undefined, { entryId });
|
|
908
1197
|
if (current.ownerSessionId && current.ownerSessionId !== claimant.sessionId) {
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1198
|
+
const references = new Set(current.sharedSessionIds ?? []);
|
|
1199
|
+
const referenceAdded = !references.has(claimant.sessionId);
|
|
1200
|
+
references.add(claimant.sessionId);
|
|
1201
|
+
current.sharedSessionIds = [...references];
|
|
1202
|
+
const previous = current.sharedLeases?.[claimant.sessionId] ?? null;
|
|
1203
|
+
const lease = { ...claimant, entryId, claimedAt: Date.now() };
|
|
1204
|
+
current.sharedLeases = { ...current.sharedLeases, [claimant.sessionId]: lease };
|
|
1205
|
+
current.disposable = false;
|
|
1206
|
+
await putRecord(current);
|
|
1207
|
+
return {
|
|
1208
|
+
entry: entryView(current),
|
|
1209
|
+
lease,
|
|
1210
|
+
referenceAdded,
|
|
1211
|
+
replaced: previous && !sameOwner(previous, claimant) ? previous : null,
|
|
1212
|
+
};
|
|
913
1213
|
}
|
|
914
1214
|
if (!current.ownerSessionId) {
|
|
915
1215
|
const other = findRecord(
|
|
@@ -934,13 +1234,23 @@ export async function claimWorktreeOwner(entryId, owner) {
|
|
|
934
1234
|
* Release the live lease when (and only when) it is held by exactly `owner`.
|
|
935
1235
|
* Durable ownership (the session retaining the worktree) is unchanged.
|
|
936
1236
|
*/
|
|
937
|
-
export async function releaseWorktreeOwner(entryId, owner) {
|
|
1237
|
+
export async function releaseWorktreeOwner(entryId, owner, { forgetReference = false } = {}) {
|
|
938
1238
|
const claimant = normalizeWorktreeOwner(owner);
|
|
939
1239
|
const record = await getRecord(entryId);
|
|
940
1240
|
if (!record) return { released: false };
|
|
941
1241
|
return withRepoLock(record.repoCommonDir, async () => {
|
|
942
1242
|
const current = await getRecord(entryId);
|
|
943
1243
|
if (!current) return { released: false };
|
|
1244
|
+
const shared = current.sharedLeases?.[claimant.sessionId];
|
|
1245
|
+
if (sameOwner(shared, claimant)) {
|
|
1246
|
+
const leases = { ...current.sharedLeases };
|
|
1247
|
+
delete leases[claimant.sessionId];
|
|
1248
|
+
current.sharedLeases = leases;
|
|
1249
|
+
if (forgetReference)
|
|
1250
|
+
current.sharedSessionIds = (current.sharedSessionIds ?? []).filter((id) => id !== claimant.sessionId);
|
|
1251
|
+
await putRecord(current);
|
|
1252
|
+
return { released: true };
|
|
1253
|
+
}
|
|
944
1254
|
const lease = await readOwnerLease(current);
|
|
945
1255
|
if (!sameOwner(lease, claimant)) return { released: false };
|
|
946
1256
|
await removeOwnerLease(current);
|
|
@@ -1186,6 +1496,27 @@ async function assertRemovable(record) {
|
|
|
1186
1496
|
}
|
|
1187
1497
|
}
|
|
1188
1498
|
|
|
1499
|
+
/** User changes, excluding the manager's own project identity and unchanged include copies. */
|
|
1500
|
+
export async function managedWorktreeDirtyPaths(entryId) {
|
|
1501
|
+
const record = await getRecord(entryId);
|
|
1502
|
+
if (!record) throw worktreeError('WORKTREE_MISSING');
|
|
1503
|
+
return dirtyPaths(record);
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// Durable references protect sleeping sessions as well as running runtimes.
|
|
1507
|
+
// Force may discard files, never another session's workspace.
|
|
1508
|
+
function assertNotShared(record, sessionId = record.ownerSessionId) {
|
|
1509
|
+
const others = new Set(record.sharedSessionIds ?? []);
|
|
1510
|
+
if (record.ownerSessionId) others.add(record.ownerSessionId);
|
|
1511
|
+
if (sessionId) others.delete(sessionId);
|
|
1512
|
+
if (others.size) {
|
|
1513
|
+
throw worktreeError('WORKTREE_BUSY_JOBS', 'Other sessions retain this worktree; its folder must be kept.', {
|
|
1514
|
+
reason: 'workspace-shared',
|
|
1515
|
+
sessions: [...others],
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1189
1520
|
/**
|
|
1190
1521
|
* The refusal removeManagedWorktree would raise, checked before anything
|
|
1191
1522
|
* moves (a session leaves its worktree only for a removal that can proceed).
|
|
@@ -1193,9 +1524,10 @@ async function assertRemovable(record) {
|
|
|
1193
1524
|
* and non-ready entries; ordinary dirty/unmerged checks apply only to ready ones.
|
|
1194
1525
|
* @param {{ entryId: string, force?: boolean }} input
|
|
1195
1526
|
*/
|
|
1196
|
-
export async function assertWorktreeRemovable({ entryId, force = false } = {}) {
|
|
1527
|
+
export async function assertWorktreeRemovable({ entryId, force = false, sessionId } = {}) {
|
|
1197
1528
|
const record = await getRecord(entryId);
|
|
1198
1529
|
if (!record) throw worktreeError('WORKTREE_MISSING', undefined, { entryId });
|
|
1530
|
+
assertNotShared(record, sessionId);
|
|
1199
1531
|
await assertRecoveryStoresClear(record);
|
|
1200
1532
|
if (record.state !== 'ready') return;
|
|
1201
1533
|
await assertManagedCheckoutPath(record);
|
|
@@ -1209,12 +1541,19 @@ export async function assertWorktreeRemovable({ entryId, force = false } = {}) {
|
|
|
1209
1541
|
* @param {{ entryId: string, force?: boolean }} input
|
|
1210
1542
|
* @returns {Promise<{ removed: boolean, retryable?: boolean, remaining?: object[], branchDeleted?: boolean }>}
|
|
1211
1543
|
*/
|
|
1212
|
-
export async function removeManagedWorktree({
|
|
1544
|
+
export async function removeManagedWorktree({
|
|
1545
|
+
entryId,
|
|
1546
|
+
force = false,
|
|
1547
|
+
sessionId,
|
|
1548
|
+
preserveBranch = false,
|
|
1549
|
+
safeOnly = false,
|
|
1550
|
+
} = {}) {
|
|
1213
1551
|
const record = await getRecord(entryId);
|
|
1214
1552
|
if (!record) throw worktreeError('WORKTREE_MISSING', undefined, { entryId });
|
|
1215
1553
|
return withRepoLock(record.repoCommonDir, async () => {
|
|
1216
1554
|
const current = await getRecord(entryId);
|
|
1217
1555
|
if (!current) throw worktreeError('WORKTREE_MISSING', undefined, { entryId });
|
|
1556
|
+
assertNotShared(current, sessionId);
|
|
1218
1557
|
if (current.state === 'creating' || current.state === 'recovery') {
|
|
1219
1558
|
const rollback = await rollbackCreation(current);
|
|
1220
1559
|
return rollback.complete ? { removed: true } : { removed: false, retryable: true, remaining: rollback.remaining };
|
|
@@ -1224,7 +1563,7 @@ export async function removeManagedWorktree({ entryId, force = false } = {}) {
|
|
|
1224
1563
|
await assertManagedCheckoutPath(current);
|
|
1225
1564
|
if (!force) await assertRemovable(current);
|
|
1226
1565
|
current.state = 'deleting';
|
|
1227
|
-
current.deleting = { force: force === true, startedAt: Date.now(), remaining: [] };
|
|
1566
|
+
current.deleting = { force: force === true, preserveBranch, safeOnly, startedAt: Date.now(), remaining: [] };
|
|
1228
1567
|
await putRecord(current);
|
|
1229
1568
|
}
|
|
1230
1569
|
return finishDeletion(current);
|
|
@@ -1251,10 +1590,13 @@ export async function listManagedWorktrees({ liveSessionIds = [], retainedSessio
|
|
|
1251
1590
|
const ownerLive =
|
|
1252
1591
|
(!!record.ownerSessionId && live.has(record.ownerSessionId)) || leaseProcessAlive(await readOwnerLease(record));
|
|
1253
1592
|
const isRetained = !!record.ownerSessionId && retained.has(record.ownerSessionId);
|
|
1254
|
-
let removable =
|
|
1593
|
+
let removable =
|
|
1594
|
+
!record.sharedSessionIds?.length &&
|
|
1595
|
+
(record.state !== 'ready' || (!ownerLive && (record.disposable === true || !isRetained)));
|
|
1255
1596
|
if (removable) {
|
|
1256
|
-
try {
|
|
1257
|
-
|
|
1597
|
+
try {
|
|
1598
|
+
await assertRecoveryStoresClear(record);
|
|
1599
|
+
} catch (error) {
|
|
1258
1600
|
if (error?.code !== 'WORKTREE_FILE_RECOVERY') throw error;
|
|
1259
1601
|
removable = false;
|
|
1260
1602
|
}
|
|
@@ -1311,6 +1653,10 @@ export async function reconcileManagedWorktrees({
|
|
|
1311
1653
|
for (const record of records) {
|
|
1312
1654
|
const isRetained = !!record.ownerSessionId && retained.has(record.ownerSessionId);
|
|
1313
1655
|
try {
|
|
1656
|
+
if (record.sharedSessionIds?.length) {
|
|
1657
|
+
report.kept.push({ id: record.id, reason: 'shared' });
|
|
1658
|
+
continue;
|
|
1659
|
+
}
|
|
1314
1660
|
if (record.state === 'creating' || record.state === 'recovery') {
|
|
1315
1661
|
if (isRetained) {
|
|
1316
1662
|
report.kept.push({ id: record.id, reason: 'retained' });
|
|
@@ -17,7 +17,7 @@ function ownerLeasePath(record) {
|
|
|
17
17
|
return join(managedWorktreeRoot(record.repoCommonDir), '.owners', `${record.id}.json`);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export async function readOwnerLease(record) {
|
|
20
|
+
export async function readOwnerLease(record, { strict = false } = {}) {
|
|
21
21
|
try {
|
|
22
22
|
const value = parseJsonText(await readFile(ownerLeasePath(record), 'utf8'));
|
|
23
23
|
return {
|
|
@@ -26,6 +26,7 @@ export async function readOwnerLease(record) {
|
|
|
26
26
|
claimedAt: value.claimedAt ?? null,
|
|
27
27
|
};
|
|
28
28
|
} catch (error) {
|
|
29
|
+
if (strict && error?.code !== 'ENOENT') throw error;
|
|
29
30
|
if (error?.code === 'ENOENT' || error instanceof SyntaxError || error instanceof TypeError) return null;
|
|
30
31
|
throw error;
|
|
31
32
|
}
|