@adhdev/daemon-core 0.9.82-rc.49 → 0.9.82-rc.490
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/dist/boot/daemon-lifecycle.d.ts +9 -0
- package/dist/boot/process-hardening.d.ts +50 -0
- package/dist/build-info.d.ts +37 -0
- package/dist/chat/source-machine.d.ts +166 -0
- package/dist/chat/source-resolver.d.ts +104 -0
- package/dist/chat/subscription-updates.d.ts +1 -0
- package/dist/cli-adapter-types.d.ts +68 -2
- package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +220 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +137 -64
- package/dist/cli-adapters/provider-cli-parse.d.ts +5 -0
- package/dist/cli-adapters/provider-cli-runtime.d.ts +34 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +74 -0
- package/dist/cli-adapters/pty-write-chunking.d.ts +34 -0
- package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
- package/dist/cli-adapters/resolve-executable.d.ts +69 -0
- package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +6 -3
- package/dist/cli-adapters/terminal-backends/types.d.ts +10 -2
- package/dist/cli-adapters/terminal-screen.d.ts +4 -11
- package/dist/commands/chat-commands-debug-bundle.d.ts +14 -0
- package/dist/commands/chat-commands-read.d.ts +23 -0
- package/dist/commands/chat-commands-scope.d.ts +39 -0
- package/dist/commands/chat-commands-shared.d.ts +33 -0
- package/dist/commands/chat-commands-write.d.ts +14 -0
- package/dist/commands/chat-commands.d.ts +10 -23
- package/dist/commands/cli-manager.d.ts +71 -1
- package/dist/commands/handler.d.ts +125 -0
- package/dist/commands/high-family/index.d.ts +3 -0
- package/dist/commands/high-family/mesh-coordinator-launch.d.ts +2 -0
- package/dist/commands/high-family/mesh-events.d.ts +2 -0
- package/dist/commands/high-family/mesh-status.d.ts +2 -0
- package/dist/commands/high-family/types.d.ts +64 -0
- package/dist/commands/low-family/coordinator-prompt.d.ts +9 -0
- package/dist/commands/low-family/daemon-lifecycle.d.ts +2 -0
- package/dist/commands/low-family/diagnostics.d.ts +2 -0
- package/dist/commands/low-family/index.d.ts +3 -0
- package/dist/commands/low-family/mesh-ledger.d.ts +10 -0
- package/dist/commands/low-family/mesh-node-logs.d.ts +2 -0
- package/dist/commands/low-family/notification.d.ts +2 -0
- package/dist/commands/low-family/refine-config.d.ts +2 -0
- package/dist/commands/low-family/session-host.d.ts +2 -0
- package/dist/commands/low-family/spec-providerdev.d.ts +11 -0
- package/dist/commands/low-family/status-meta.d.ts +2 -0
- package/dist/commands/low-family/types.d.ts +33 -0
- package/dist/commands/med-family/cli-agent.d.ts +2 -0
- package/dist/commands/med-family/fast-forward.d.ts +2 -0
- package/dist/commands/med-family/ide.d.ts +10 -0
- package/dist/commands/med-family/index.d.ts +3 -0
- package/dist/commands/med-family/mesh-crud.d.ts +48 -0
- package/dist/commands/med-family/mesh-host-pairing.d.ts +2 -0
- package/dist/commands/med-family/mesh-queue.d.ts +2 -0
- package/dist/commands/med-family/mesh-restart.d.ts +2 -0
- package/dist/commands/med-family/types.d.ts +152 -0
- package/dist/commands/mesh-coordinator.d.ts +85 -1
- package/dist/commands/router-aggregate-status.d.ts +24 -0
- package/dist/commands/router-mesh-session-owner.d.ts +59 -0
- package/dist/commands/router-refine.d.ts +151 -0
- package/dist/commands/router-worktree-cleanup.d.ts +133 -0
- package/dist/commands/router.d.ts +203 -15
- package/dist/commands/upgrade-helper.d.ts +41 -1
- package/dist/config/chat-history.d.ts +11 -0
- package/dist/config/config.d.ts +19 -0
- package/dist/config/mesh-config.d.ts +65 -0
- package/dist/config/mesh-json-config.d.ts +199 -0
- package/dist/config/registry-resolver.d.ts +54 -0
- package/dist/config/repo-settings.d.ts +77 -0
- package/dist/config/state-store.d.ts +30 -0
- package/dist/daemon/dev-server.d.ts +0 -2
- package/dist/detection/cli-detector.d.ts +17 -0
- package/dist/detection/ide-detector.d.ts +13 -0
- package/dist/detection/win32-ide-version.d.ts +37 -0
- package/dist/git/change-impact-config.d.ts +159 -0
- package/dist/git/git-commands.d.ts +26 -2
- package/dist/git/git-diff.d.ts +6 -0
- package/dist/git/git-executor.d.ts +11 -0
- package/dist/git/git-status.d.ts +80 -0
- package/dist/git/git-types.d.ts +2 -50
- package/dist/git/git-worktree.d.ts +71 -1
- package/dist/git/index.d.ts +3 -1
- package/dist/index.d.ts +70 -17
- package/dist/index.js +53532 -19073
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +53283 -18989
- package/dist/index.mjs.map +1 -1
- package/dist/installer.d.ts +1 -4
- package/dist/ipc/local-ipc-server.d.ts +91 -0
- package/dist/launch.d.ts +1 -1
- package/dist/logging/async-batch-writer.d.ts +10 -0
- package/dist/logging/debug-config.d.ts +16 -0
- package/dist/logging/log-redactor.d.ts +24 -0
- package/dist/logging/log-tail-reader.d.ts +81 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/mesh/contracts.d.ts +256 -0
- package/dist/mesh/coordinator-prompt.d.ts +76 -0
- package/dist/mesh/coordinator-registry.d.ts +59 -0
- package/dist/mesh/mesh-active-work.d.ts +203 -0
- package/dist/mesh/mesh-clone-grace.d.ts +8 -0
- package/dist/mesh/mesh-completion-synthesis.d.ts +4 -0
- package/dist/mesh/mesh-coordinator-config.d.ts +21 -0
- package/dist/mesh/mesh-delivery-policy.d.ts +104 -0
- package/dist/mesh/mesh-event-classify.d.ts +7 -0
- package/dist/mesh/mesh-event-forwarding.d.ts +58 -0
- package/dist/mesh/mesh-event-trace.d.ts +21 -0
- package/dist/mesh/mesh-events-coordinator.d.ts +4 -0
- package/dist/mesh/mesh-events-pending.d.ts +258 -0
- package/dist/mesh/mesh-events-stale.d.ts +61 -0
- package/dist/mesh/mesh-events-utils.d.ts +75 -0
- package/dist/mesh/mesh-events.d.ts +6 -49
- package/dist/mesh/mesh-fast-forward.d.ts +81 -0
- package/dist/mesh/mesh-host-ownership.d.ts +21 -1
- package/dist/mesh/mesh-init.d.ts +128 -0
- package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -4
- package/dist/mesh/mesh-ledger.d.ts +139 -1
- package/dist/mesh/mesh-magi-status.d.ts +63 -0
- package/dist/mesh/mesh-missions.d.ts +281 -0
- package/dist/mesh/mesh-node-identity.d.ts +310 -0
- package/dist/mesh/mesh-queue-assignment.d.ts +162 -0
- package/dist/mesh/mesh-reconcile-acked-hold.d.ts +17 -0
- package/dist/mesh/mesh-reconcile-config.d.ts +6 -0
- package/dist/mesh/mesh-reconcile-identity.d.ts +6 -0
- package/dist/mesh/mesh-reconcile-loop.d.ts +84 -0
- package/dist/mesh/mesh-reconcile-v2-backstop.d.ts +16 -0
- package/dist/mesh/mesh-refine-batch.d.ts +68 -0
- package/dist/mesh/mesh-refine-gates.d.ts +457 -0
- package/dist/mesh/mesh-refine-status.d.ts +62 -0
- package/dist/mesh/mesh-remote-event-pull.d.ts +15 -0
- package/dist/mesh/mesh-review-inbox.d.ts +90 -0
- package/dist/mesh/mesh-routing.d.ts +70 -0
- package/dist/mesh/mesh-runtime-store.d.ts +567 -0
- package/dist/mesh/mesh-scheduling-runtime.d.ts +78 -0
- package/dist/mesh/mesh-task-inflight.d.ts +46 -0
- package/dist/mesh/mesh-task-stats.d.ts +49 -0
- package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +34 -0
- package/dist/mesh/mesh-warmup-deadline.d.ts +68 -0
- package/dist/mesh/mesh-work-queue.d.ts +392 -1
- package/dist/mesh/preview-freshness.d.ts +28 -0
- package/dist/mesh/refine-config.d.ts +97 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +219 -0
- package/dist/providers/acp-provider-instance.d.ts +5 -0
- package/dist/providers/approval-utils.d.ts +46 -0
- package/dist/providers/chat-message-normalization.d.ts +58 -1
- package/dist/providers/cli-provider-effect-format.d.ts +30 -0
- package/dist/providers/cli-provider-history-dedup.d.ts +17 -0
- package/dist/providers/cli-provider-input-prompt.d.ts +12 -0
- package/dist/providers/cli-provider-instance-types.d.ts +45 -0
- package/dist/providers/cli-provider-instance.d.ts +426 -30
- package/dist/providers/cli-provider-status-helpers.d.ts +46 -0
- package/dist/providers/cli-provider-transcript-merge.d.ts +7 -0
- package/dist/providers/contracts.d.ts +196 -6
- package/dist/providers/external-sources.d.ts +71 -0
- package/dist/providers/manual-attendance.d.ts +79 -0
- package/dist/providers/native-history/antigravity-claim-registry.d.ts +39 -0
- package/dist/providers/native-history/antigravity-cli-transcript.d.ts +137 -0
- package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
- package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
- package/dist/providers/native-history/constants.d.ts +12 -0
- package/dist/providers/native-history/dispatcher.d.ts +44 -0
- package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
- package/dist/providers/native-history/index.d.ts +13 -0
- package/dist/providers/provider-instance-manager.d.ts +30 -0
- package/dist/providers/provider-instance.d.ts +31 -1
- package/dist/providers/provider-loader.d.ts +42 -5
- package/dist/providers/provider-trust.d.ts +31 -0
- package/dist/providers/read-chat-contract.d.ts +29 -0
- package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
- package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +86 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
- package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
- package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
- package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
- package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
- package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
- package/dist/providers/sdk/v1/index.d.ts +30 -0
- package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
- package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
- package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
- package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
- package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
- package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
- package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
- package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
- package/dist/providers/spec/adapter.d.ts +98 -0
- package/dist/providers/spec/cli-adapter.d.ts +223 -0
- package/dist/providers/spec/evaluator.d.ts +45 -0
- package/dist/providers/spec/fsm-driver.d.ts +449 -0
- package/dist/providers/spec/fsm-evaluator.d.ts +93 -0
- package/dist/providers/spec/fsm-loader.d.ts +14 -0
- package/dist/providers/spec/fsm-types.d.ts +202 -0
- package/dist/providers/spec/native-history-executor.d.ts +41 -0
- package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
- package/dist/providers/spec/route.d.ts +4 -0
- package/dist/providers/spec/types.d.ts +244 -0
- package/dist/providers/status-monitor.d.ts +7 -7
- package/dist/providers/transcript-v2.d.ts +176 -0
- package/dist/providers/types/interactive-prompt.d.ts +84 -0
- package/dist/providers/working-dir.d.ts +17 -0
- package/dist/repo-mesh-types.d.ts +616 -12
- package/dist/runtime-defaults.d.ts +2 -0
- package/dist/session-host/managed-host.d.ts +64 -0
- package/dist/session-host/runtime-surface.d.ts +10 -16
- package/dist/sessions/registry.d.ts +26 -0
- package/dist/shared-types-extra.d.ts +2 -4
- package/dist/shared-types.d.ts +110 -55
- package/dist/status/chat-tail-hot-sessions.d.ts +40 -0
- package/dist/status/normalize.d.ts +1 -1
- package/dist/status/normalize.js +1 -0
- package/dist/status/normalize.js.map +1 -1
- package/dist/status/normalize.mjs +1 -0
- package/dist/status/normalize.mjs.map +1 -1
- package/dist/status/reporter.d.ts +2 -0
- package/dist/status/snapshot.d.ts +27 -0
- package/dist/system/hash.d.ts +8 -0
- package/dist/system/load-better-sqlite3.d.ts +21 -0
- package/dist/types.d.ts +20 -6
- package/package.json +11 -4
- package/src/agent-stream/poller.ts +2 -3
- package/src/agent-stream/provider-adapter.ts +1 -1
- package/src/boot/daemon-lifecycle.ts +80 -14
- package/src/boot/process-hardening.ts +89 -0
- package/src/build-info.ts +73 -0
- package/src/chat/source-machine.ts +534 -0
- package/src/chat/source-resolver.ts +0 -0
- package/src/chat/subscription-updates.ts +20 -1
- package/src/cli-adapter-types.d.ts +3 -1
- package/src/cli-adapter-types.ts +68 -2
- package/src/cli-adapters/cli-script-runner.ts +421 -0
- package/src/cli-adapters/cli-state-engine.ts +1282 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +1048 -1143
- package/src/cli-adapters/provider-cli-parse.d.ts +2 -0
- package/src/cli-adapters/provider-cli-parse.ts +19 -0
- package/src/cli-adapters/provider-cli-runtime.ts +56 -7
- package/src/cli-adapters/provider-cli-shared.d.ts +4 -0
- package/src/cli-adapters/provider-cli-shared.ts +121 -11
- package/src/cli-adapters/pty-transport.ts +2 -1
- package/src/cli-adapters/pty-write-chunking.ts +106 -0
- package/src/cli-adapters/raw-terminal-io.ts +252 -0
- package/src/cli-adapters/resolve-executable.ts +204 -0
- package/src/cli-adapters/session-host-transport.ts +2 -1
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +63 -29
- package/src/cli-adapters/terminal-backends/types.ts +10 -3
- package/src/cli-adapters/terminal-screen.ts +16 -81
- package/src/commands/chat-commands-debug-bundle.ts +398 -0
- package/src/commands/chat-commands-read.ts +2788 -0
- package/src/commands/chat-commands-scope.ts +54 -0
- package/src/commands/chat-commands-shared.ts +114 -0
- package/src/commands/chat-commands-write.ts +891 -0
- package/src/commands/chat-commands.ts +19 -1841
- package/src/commands/cli-manager.d.ts +2 -0
- package/src/commands/cli-manager.ts +724 -27
- package/src/commands/handler.ts +865 -2
- package/src/commands/high-family/index.ts +28 -0
- package/src/commands/high-family/mesh-coordinator-launch.ts +696 -0
- package/src/commands/high-family/mesh-events.ts +102 -0
- package/src/commands/high-family/mesh-status.ts +857 -0
- package/src/commands/high-family/types.ts +80 -0
- package/src/commands/low-family/coordinator-prompt.ts +125 -0
- package/src/commands/low-family/daemon-lifecycle.ts +120 -0
- package/src/commands/low-family/diagnostics.ts +57 -0
- package/src/commands/low-family/index.ts +37 -0
- package/src/commands/low-family/mesh-ledger.ts +62 -0
- package/src/commands/low-family/mesh-node-logs.ts +87 -0
- package/src/commands/low-family/notification.ts +116 -0
- package/src/commands/low-family/refine-config.ts +106 -0
- package/src/commands/low-family/session-host.ts +282 -0
- package/src/commands/low-family/spec-providerdev.ts +217 -0
- package/src/commands/low-family/status-meta.ts +112 -0
- package/src/commands/low-family/types.ts +39 -0
- package/src/commands/med-family/cli-agent.ts +270 -0
- package/src/commands/med-family/fast-forward.ts +230 -0
- package/src/commands/med-family/ide.ts +163 -0
- package/src/commands/med-family/index.ts +37 -0
- package/src/commands/med-family/mesh-crud.ts +1268 -0
- package/src/commands/med-family/mesh-host-pairing.ts +234 -0
- package/src/commands/med-family/mesh-queue.ts +167 -0
- package/src/commands/med-family/mesh-restart.ts +92 -0
- package/src/commands/med-family/types.ts +153 -0
- package/src/commands/mesh-coordinator.ts +334 -124
- package/src/commands/router-aggregate-status.ts +209 -0
- package/src/commands/router-mesh-session-owner.ts +114 -0
- package/src/commands/router-refine.ts +1794 -0
- package/src/commands/router-worktree-cleanup.ts +870 -0
- package/src/commands/router.ts +567 -4149
- package/src/commands/stream-commands.ts +8 -0
- package/src/commands/upgrade-helper.ts +310 -45
- package/src/config/chat-history.ts +517 -24
- package/src/config/config.ts +30 -0
- package/src/config/mesh-config.ts +300 -24
- package/src/config/mesh-json-config.ts +376 -0
- package/src/config/recent-activity.ts +8 -2
- package/src/config/registry-resolver.ts +100 -0
- package/src/config/repo-settings.ts +111 -0
- package/src/config/state-store.ts +55 -0
- package/src/daemon/dev-auto-implement.ts +3 -2
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/daemon/dev-server.ts +0 -541
- package/src/detection/cli-detector.ts +94 -9
- package/src/detection/ide-detector.ts +55 -16
- package/src/detection/win32-ide-version.ts +106 -0
- package/src/git/change-impact-config.ts +354 -0
- package/src/git/git-commands.ts +112 -16
- package/src/git/git-diff.ts +81 -11
- package/src/git/git-executor.ts +12 -0
- package/src/git/git-status.ts +822 -48
- package/src/git/git-types.ts +14 -62
- package/src/git/git-worktree.ts +261 -4
- package/src/git/index.ts +17 -0
- package/src/index.ts +190 -13
- package/src/installer.d.ts +1 -1
- package/src/installer.ts +8 -6
- package/src/ipc/local-ipc-server.ts +278 -0
- package/src/launch.d.ts +1 -1
- package/src/launch.ts +37 -28
- package/src/logging/async-batch-writer.ts +55 -0
- package/src/logging/command-log.ts +7 -5
- package/src/logging/debug-config.ts +25 -0
- package/src/logging/debug-trace.ts +7 -2
- package/src/logging/log-redactor.ts +100 -0
- package/src/logging/log-tail-reader.ts +341 -0
- package/src/logging/logger.ts +14 -7
- package/src/mesh/contracts.ts +510 -0
- package/src/mesh/coordinator-prompt.ts +627 -34
- package/src/mesh/coordinator-registry.ts +121 -0
- package/src/mesh/mesh-active-work.ts +706 -0
- package/src/mesh/mesh-clone-grace.ts +68 -0
- package/src/mesh/mesh-completion-synthesis.ts +416 -0
- package/src/mesh/mesh-coordinator-config.ts +97 -0
- package/src/mesh/mesh-delivery-policy.ts +284 -0
- package/src/mesh/mesh-event-classify.ts +78 -0
- package/src/mesh/mesh-event-forwarding.ts +2030 -0
- package/src/mesh/mesh-event-trace.ts +67 -0
- package/src/mesh/mesh-events-coordinator.ts +32 -0
- package/src/mesh/mesh-events-pending.ts +1548 -0
- package/src/mesh/mesh-events-stale.ts +452 -0
- package/src/mesh/mesh-events-utils.ts +443 -0
- package/src/mesh/mesh-events.ts +43 -1035
- package/src/mesh/mesh-fast-forward.ts +856 -0
- package/src/mesh/mesh-host-ownership.ts +41 -3
- package/src/mesh/mesh-init.ts +350 -0
- package/src/mesh/mesh-ledger-reconciliation.ts +12 -7
- package/src/mesh/mesh-ledger.ts +897 -102
- package/src/mesh/mesh-magi-status.ts +223 -0
- package/src/mesh/mesh-missions.ts +695 -0
- package/src/mesh/mesh-node-identity.ts +2065 -0
- package/src/mesh/mesh-queue-assignment.ts +2498 -0
- package/src/mesh/mesh-reconcile-acked-hold.ts +230 -0
- package/src/mesh/mesh-reconcile-config.ts +66 -0
- package/src/mesh/mesh-reconcile-identity.ts +103 -0
- package/src/mesh/mesh-reconcile-loop.ts +1696 -0
- package/src/mesh/mesh-reconcile-v2-backstop.ts +62 -0
- package/src/mesh/mesh-refine-batch.ts +205 -0
- package/src/mesh/mesh-refine-gates.ts +1792 -0
- package/src/mesh/mesh-refine-status.ts +231 -0
- package/src/mesh/mesh-remote-event-pull.ts +279 -0
- package/src/mesh/mesh-review-inbox.ts +308 -0
- package/src/mesh/mesh-routing.ts +291 -0
- package/src/mesh/mesh-runtime-store.ts +2292 -0
- package/src/mesh/mesh-scheduling-runtime.ts +198 -0
- package/src/mesh/mesh-task-inflight.ts +70 -0
- package/src/mesh/mesh-task-stats.ts +161 -0
- package/src/mesh/mesh-unresolved-forward-outbox.ts +215 -0
- package/src/mesh/mesh-warmup-deadline.ts +152 -0
- package/src/mesh/mesh-work-queue.ts +1489 -145
- package/src/mesh/preview-freshness.ts +163 -0
- package/src/mesh/refine-config.ts +129 -12
- package/src/mesh/worktree-bootstrap-config.ts +599 -0
- package/src/providers/acp-provider-instance.ts +43 -10
- package/src/providers/approval-utils.d.ts +5 -0
- package/src/providers/approval-utils.ts +100 -6
- package/src/providers/chat-message-normalization.ts +126 -4
- package/src/providers/cli-provider-effect-format.ts +53 -0
- package/src/providers/cli-provider-history-dedup.ts +75 -0
- package/src/providers/cli-provider-input-prompt.ts +133 -0
- package/src/providers/cli-provider-instance-types.ts +131 -0
- package/src/providers/cli-provider-instance.ts +2771 -499
- package/src/providers/cli-provider-status-helpers.ts +123 -0
- package/src/providers/cli-provider-transcript-merge.ts +114 -0
- package/src/providers/contracts.d.ts +55 -0
- package/src/providers/contracts.ts +207 -6
- package/src/providers/extension-provider-instance.ts +12 -7
- package/src/providers/external-sources.ts +218 -0
- package/src/providers/ide-provider-instance.ts +35 -12
- package/src/providers/manual-attendance.ts +105 -0
- package/src/providers/native-history/antigravity-claim-registry.ts +138 -0
- package/src/providers/native-history/antigravity-cli-transcript.ts +1275 -0
- package/src/providers/native-history/claude-cli-transcript.ts +396 -0
- package/src/providers/native-history/codex-cli-transcript.ts +479 -0
- package/src/providers/native-history/constants.ts +19 -0
- package/src/providers/native-history/dispatcher.ts +617 -0
- package/src/providers/native-history/hermes-cli-transcript.ts +312 -0
- package/src/providers/native-history/index.ts +30 -0
- package/src/providers/provider-instance-manager.ts +71 -0
- package/src/providers/provider-instance.ts +24 -1
- package/src/providers/provider-loader.ts +700 -55
- package/src/providers/provider-schema.ts +93 -14
- package/src/providers/provider-trust.ts +114 -0
- package/src/providers/read-chat-contract.ts +76 -16
- package/src/providers/sdk/README.md +49 -0
- package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
- package/src/providers/sdk/v1/builders/cli/detect-status.ts +313 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval.ts +261 -0
- package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
- package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
- package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
- package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
- package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
- package/src/providers/sdk/v1/index.ts +152 -0
- package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
- package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
- package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +538 -0
- package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
- package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
- package/src/providers/sdk/v1/types/cli/index.ts +365 -0
- package/src/providers/sdk/v1/types/common/index.ts +229 -0
- package/src/providers/sdk/v1/validators/index.ts +19 -0
- package/src/providers/sdk/v1/validators/manifest.ts +110 -0
- package/src/providers/sdk/v1/validators/taint.ts +309 -0
- package/src/providers/spec/adapter.ts +246 -0
- package/src/providers/spec/cli-adapter.ts +1064 -0
- package/src/providers/spec/evaluator.ts +407 -0
- package/src/providers/spec/fsm-driver.ts +1528 -0
- package/src/providers/spec/fsm-evaluator.ts +290 -0
- package/src/providers/spec/fsm-loader.ts +128 -0
- package/src/providers/spec/fsm-types.ts +301 -0
- package/src/providers/spec/native-history-executor.ts +1241 -0
- package/src/providers/spec/pre-launch-trust.ts +104 -0
- package/src/providers/spec/route.ts +51 -0
- package/src/providers/spec/types.ts +284 -0
- package/src/providers/status-monitor.d.ts +7 -7
- package/src/providers/status-monitor.ts +37 -22
- package/src/providers/transcript-v2.ts +567 -0
- package/src/providers/types/interactive-prompt.ts +536 -0
- package/src/providers/version-archive.ts +64 -24
- package/src/providers/working-dir.ts +23 -0
- package/src/repo-mesh-types.ts +861 -14
- package/src/runtime-defaults.ts +39 -0
- package/src/session-host/managed-host.ts +218 -0
- package/src/session-host/runtime-surface.ts +20 -80
- package/src/sessions/registry.ts +44 -0
- package/src/shared-types-extra.ts +2 -4
- package/src/shared-types.d.ts +8 -0
- package/src/shared-types.ts +148 -55
- package/src/status/builders.ts +69 -6
- package/src/status/chat-tail-hot-sessions.ts +117 -2
- package/src/status/normalize.ts +2 -0
- package/src/status/reporter.ts +19 -1
- package/src/status/snapshot.ts +100 -27
- package/src/system/hash.ts +23 -0
- package/src/system/host-memory.ts +29 -12
- package/src/system/load-better-sqlite3.ts +68 -0
- package/src/types.ts +26 -6
- package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
- package/dist/mesh/mesh-sync.d.ts +0 -53
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
- package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
- package/src/mesh/mesh-sync.ts +0 -111
|
@@ -0,0 +1,870 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worktree / mesh-session cleanup — extracted from router.ts (behavior-preserving code move).
|
|
3
|
+
*
|
|
4
|
+
* These functions were `DaemonCommandRouter` methods; they now take the router
|
|
5
|
+
* instance as `self`. The class keeps thin delegating wrappers (cleanupMeshSessions /
|
|
6
|
+
* cleanupLocalWorktreeNode / precheckLocalWorktreeRemovable are bound into
|
|
7
|
+
* MedFamilyContext; bestEffortRemoveWorktreeDir is overridden on the instance by a
|
|
8
|
+
* unit test — intra-cluster calls therefore go through `self.` so instance dispatch
|
|
9
|
+
* (and that test override) is preserved). No log string, error message, refusal
|
|
10
|
+
* condition, or result shape changed — only physical location + `this.` → `self.`.
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import { execFileSync } from 'node:child_process';
|
|
14
|
+
import { resolve as pathResolve } from 'path';
|
|
15
|
+
import type { DaemonCommandRouter } from './router.js';
|
|
16
|
+
import { LOG } from '../logging/logger.js';
|
|
17
|
+
import { meshNodeIdMatches, daemonIdsEquivalent } from '@adhdev/mesh-shared';
|
|
18
|
+
import { readStringValue, readObjectRecord, readBooleanValue } from '../mesh/mesh-node-identity.js';
|
|
19
|
+
import { checkWorktreeChangesPatchEquivalentInRef, MeshWorktreePatchContainmentSummary } from '../mesh/mesh-refine-gates.js';
|
|
20
|
+
import { getSessionHostSurfaceKind } from '../session-host/runtime-surface.js';
|
|
21
|
+
import type { RepoMeshSessionCleanupMode } from '../repo-mesh-types.js';
|
|
22
|
+
import { DEFAULT_MESH_POLICY, magiAutoLaunchedSessionCleanupDecision } from '../repo-mesh-types.js';
|
|
23
|
+
|
|
24
|
+
export function sessionMatchesMeshNode(self: DaemonCommandRouter, record: any, node: any, nodeId: string, sessionIds?: Set<string>): boolean {
|
|
25
|
+
const sessionId = typeof record?.sessionId === 'string' ? record.sessionId : '';
|
|
26
|
+
if (!sessionId) return false;
|
|
27
|
+
if (sessionIds?.size) return sessionIds.has(sessionId);
|
|
28
|
+
const workspace = typeof node?.workspace === 'string' ? node.workspace : '';
|
|
29
|
+
if (workspace && record?.workspace === workspace) return true;
|
|
30
|
+
if (record?.meta?.meshNodeId === nodeId) return true;
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Best-effort recursive removal of a managed worktree directory.
|
|
36
|
+
*
|
|
37
|
+
* The git-registry de-registration is the safety-critical step of worktree
|
|
38
|
+
* teardown; a leftover directory must never gate dropping the node from the
|
|
39
|
+
* mesh. On Windows, `fs.rmSync` can throw EINVAL/EPERM/EBUSY on submodule
|
|
40
|
+
* gitlink (`.git`) files, long paths, junctions, or while a just-stopped
|
|
41
|
+
* delegate session is still releasing a handle/cwd on the directory. This
|
|
42
|
+
* helper absorbs those errors (never throws), with bounded retries + backoff
|
|
43
|
+
* to give handles time to release, and reports whether residue remains.
|
|
44
|
+
*/
|
|
45
|
+
export async function bestEffortRemoveWorktreeDir(self: DaemonCommandRouter, dir: string): Promise<{ removed: boolean; residue: boolean; error?: string }> {
|
|
46
|
+
if (!dir || !fs.existsSync(dir)) return { removed: true, residue: false };
|
|
47
|
+
const sleep = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms));
|
|
48
|
+
// EINVAL is the Windows symptom for submodule gitlink residue; the rest are
|
|
49
|
+
// transient lock/permission classes. None should escape as a throw here.
|
|
50
|
+
const ABSORB = new Set(['EINVAL', 'EPERM', 'EBUSY', 'ENOTEMPTY', 'EACCES', 'EMFILE', 'ENFILE']);
|
|
51
|
+
let lastErr: any;
|
|
52
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
53
|
+
try {
|
|
54
|
+
// maxRetries/retryDelay give fs.rmSync its own internal backoff for
|
|
55
|
+
// EBUSY/EPERM/ENOTEMPTY; the outer loop extends tolerance to EINVAL.
|
|
56
|
+
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
57
|
+
if (!fs.existsSync(dir)) return { removed: true, residue: false };
|
|
58
|
+
lastErr = new Error('directory still present after rmSync');
|
|
59
|
+
} catch (e: any) {
|
|
60
|
+
lastErr = e;
|
|
61
|
+
const code = typeof e?.code === 'string' ? e.code : '';
|
|
62
|
+
if (code && !ABSORB.has(code)) {
|
|
63
|
+
// Unexpected error class — stay best-effort (no throw) but stop retrying.
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
await sleep(150 * (attempt + 1));
|
|
68
|
+
}
|
|
69
|
+
return fs.existsSync(dir)
|
|
70
|
+
? { removed: false, residue: true, error: String(lastErr?.message || lastErr || 'unknown rm error') }
|
|
71
|
+
: { removed: true, residue: false };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Non-destructive precheck mirroring every REFUSAL condition in
|
|
76
|
+
* {@link cleanupLocalWorktreeNode} — missing workspace / source-repo / branch
|
|
77
|
+
* metadata, unexpected (non-managed) path, branch mismatch — PLUS the
|
|
78
|
+
* dirty-worktree guard that `removeWorktree(requireClean)` enforces
|
|
79
|
+
* (`git status --porcelain`). It performs ZERO destructive actions: no
|
|
80
|
+
* `git worktree remove`, no `git worktree prune`, no directory deletion.
|
|
81
|
+
*
|
|
82
|
+
* remove_mesh_node calls this BEFORE any session cleanup so that a refusal
|
|
83
|
+
* (the common one being a dirty worktree) does not first stop/delete the
|
|
84
|
+
* delegated session and orphan it — the original ordering bug. Success/skip
|
|
85
|
+
* cases that the real cleanup handles idempotently (worktree path already
|
|
86
|
+
* gone, git-de-registered residue) are NOT refusals and return `{ ok: true }`.
|
|
87
|
+
*
|
|
88
|
+
* `force:true` skips the dirty guard, preserving `removeWorktree`'s
|
|
89
|
+
* `requireClean: !force` semantics. This is a read-only superset check; the
|
|
90
|
+
* authoritative `requireClean` guard inside `removeWorktree` is intentionally
|
|
91
|
+
* kept as a second line of defense against a precheck→execute race.
|
|
92
|
+
*/
|
|
93
|
+
export async function precheckLocalWorktreeRemovable(self: DaemonCommandRouter, args: {
|
|
94
|
+
mesh: any;
|
|
95
|
+
node: any;
|
|
96
|
+
nodeId: string;
|
|
97
|
+
force?: boolean;
|
|
98
|
+
}): Promise<{ ok: true } | { ok: false; code: string; error: string; recoveryHint: string }> {
|
|
99
|
+
const sessionPreservedNote = ' The delegated session was left running (not stopped) — resolve the issue and retry mesh_remove_node.';
|
|
100
|
+
const workspace = typeof args.node?.workspace === 'string' ? args.node.workspace.trim() : '';
|
|
101
|
+
if (!workspace) {
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
code: 'mesh_worktree_cleanup_missing_workspace',
|
|
105
|
+
error: `Worktree node '${args.nodeId}' is missing workspace metadata`,
|
|
106
|
+
recoveryHint: 'Inspect the mesh node record before removing it, or remove stale metadata manually only after confirming no managed worktree remains.' + sessionPreservedNote,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Worktree path already gone → not a refusal; the real cleanup returns a
|
|
111
|
+
// skipped:true success and the node is dropped from the registry.
|
|
112
|
+
if (!fs.existsSync(workspace)) return { ok: true };
|
|
113
|
+
|
|
114
|
+
const sourceNode = args.node?.clonedFromNodeId
|
|
115
|
+
? args.mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, args.node.clonedFromNodeId))
|
|
116
|
+
: args.mesh?.nodes?.find((n: any) => !n.isLocalWorktree);
|
|
117
|
+
const repoRoot = typeof sourceNode?.repoRoot === 'string' && sourceNode.repoRoot.trim()
|
|
118
|
+
? sourceNode.repoRoot.trim()
|
|
119
|
+
: typeof sourceNode?.workspace === 'string' && sourceNode.workspace.trim()
|
|
120
|
+
? sourceNode.workspace.trim()
|
|
121
|
+
: '';
|
|
122
|
+
if (!repoRoot || !fs.existsSync(repoRoot)) {
|
|
123
|
+
return {
|
|
124
|
+
ok: false,
|
|
125
|
+
code: 'mesh_worktree_cleanup_missing_source_repo',
|
|
126
|
+
error: `Refusing to remove worktree '${workspace}' because the source repo root is unavailable`,
|
|
127
|
+
recoveryHint: 'Run mesh_remove_node from the machine that owns the source repo, or verify the source node metadata before retrying.' + sessionPreservedNote,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (typeof args.node?.worktreeBranch !== 'string' || !args.node.worktreeBranch.trim()) {
|
|
131
|
+
return {
|
|
132
|
+
ok: false,
|
|
133
|
+
code: 'mesh_worktree_cleanup_missing_branch',
|
|
134
|
+
error: `Refusing to remove worktree '${workspace}' because worktreeBranch metadata is missing`,
|
|
135
|
+
recoveryHint: 'Confirm this is an ADHDev-managed worktree before removing it manually; managed worktree nodes include worktreeBranch metadata.' + sessionPreservedNote,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const { resolveWorktreePath, listWorktrees } = await import('../git/git-worktree.js');
|
|
140
|
+
const normalizePath = (value: string) => {
|
|
141
|
+
const resolved = pathResolve(value);
|
|
142
|
+
try { return fs.realpathSync(resolved); } catch { return resolved; }
|
|
143
|
+
};
|
|
144
|
+
const expectedPath = normalizePath(resolveWorktreePath(repoRoot, String(args.mesh?.name || args.mesh?.id || 'mesh'), args.node.worktreeBranch));
|
|
145
|
+
const actualPath = normalizePath(workspace);
|
|
146
|
+
if (actualPath !== expectedPath) {
|
|
147
|
+
return {
|
|
148
|
+
ok: false,
|
|
149
|
+
code: 'mesh_worktree_cleanup_unexpected_path',
|
|
150
|
+
error: `Refusing to remove worktree '${workspace}' because it is not at the expected managed path '${expectedPath}'`,
|
|
151
|
+
recoveryHint: 'Use git worktree list/status to inspect the path. Retry only after confirming the mesh node metadata points to an ADHDev-managed worktree.' + sessionPreservedNote,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const entries = await listWorktrees(repoRoot);
|
|
156
|
+
const managedEntry = entries.find(entry => normalizePath(entry.path) === actualPath);
|
|
157
|
+
// De-registered residue (git no longer lists it as a worktree) is an
|
|
158
|
+
// idempotent recovery path in the real cleanup, NOT a refusal — neither the
|
|
159
|
+
// branch-mismatch nor the dirty check applies, so let the removal proceed.
|
|
160
|
+
if (!managedEntry) return { ok: true };
|
|
161
|
+
|
|
162
|
+
if (managedEntry.branch && managedEntry.branch !== args.node.worktreeBranch) {
|
|
163
|
+
return {
|
|
164
|
+
ok: false,
|
|
165
|
+
code: 'mesh_worktree_cleanup_branch_mismatch',
|
|
166
|
+
error: `Refusing to remove '${workspace}' because git reports branch '${managedEntry.branch}', expected '${args.node.worktreeBranch}'`,
|
|
167
|
+
recoveryHint: 'Inspect the worktree branch and mesh metadata before retrying cleanup.' + sessionPreservedNote,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Dirty-worktree guard — a read-only mirror of removeWorktree(requireClean)
|
|
172
|
+
// (`git status --porcelain` run inside the worktree). `force:true` skips it,
|
|
173
|
+
// preserving the requireClean:!force semantics.
|
|
174
|
+
if (args.force !== true) {
|
|
175
|
+
const { execFile } = await import('node:child_process');
|
|
176
|
+
const { promisify } = await import('node:util');
|
|
177
|
+
const execFileAsync = promisify(execFile);
|
|
178
|
+
try {
|
|
179
|
+
const { stdout } = await execFileAsync('git', ['status', '--porcelain'], {
|
|
180
|
+
cwd: workspace, encoding: 'utf8', timeout: 30_000, maxBuffer: 4 * 1024 * 1024, windowsHide: true,
|
|
181
|
+
});
|
|
182
|
+
if (stdout.trim()) {
|
|
183
|
+
return {
|
|
184
|
+
ok: false,
|
|
185
|
+
code: 'mesh_worktree_cleanup_dirty',
|
|
186
|
+
error: `Refusing to remove dirty worktree: ${workspace}`,
|
|
187
|
+
recoveryHint: 'Commit, stash, or intentionally discard the worktree changes before retrying mesh_remove_node. The mesh registry entry is preserved until cleanup is safe.' + sessionPreservedNote,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
} catch {
|
|
191
|
+
// A status probe failure is not itself proof of dirtiness; defer to
|
|
192
|
+
// the authoritative removeWorktree(requireClean) guard rather than
|
|
193
|
+
// refusing here (which would block an otherwise-clean removal).
|
|
194
|
+
return { ok: true };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { ok: true };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function cleanupLocalWorktreeNode(self: DaemonCommandRouter, args: {
|
|
202
|
+
mesh: any;
|
|
203
|
+
node: any;
|
|
204
|
+
nodeId: string;
|
|
205
|
+
force?: boolean;
|
|
206
|
+
}): Promise<{ success: true; skipped?: boolean; removedPath?: string; repoRoot?: string; reason?: string; fallback?: string; forced?: boolean; convergence?: Record<string, unknown>; recovered?: boolean; residue?: boolean; residueWarning?: string; residueError?: string; branchRefDeleted?: boolean; branchRefReason?: string; branchRefForced?: boolean; branchRefWarning?: string } | { success: false; code: string; error: string; recoveryHint: string; convergence?: Record<string, unknown> }> {
|
|
207
|
+
const workspace = typeof args.node?.workspace === 'string' ? args.node.workspace.trim() : '';
|
|
208
|
+
if (!workspace) {
|
|
209
|
+
return {
|
|
210
|
+
success: false,
|
|
211
|
+
code: 'mesh_worktree_cleanup_missing_workspace',
|
|
212
|
+
error: `Worktree node '${args.nodeId}' is missing workspace metadata`,
|
|
213
|
+
recoveryHint: 'Inspect the mesh node record before removing it, or remove stale metadata manually only after confirming no managed worktree remains.',
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const worktreeExists = fs.existsSync(workspace);
|
|
218
|
+
const sourceNode = args.node?.clonedFromNodeId
|
|
219
|
+
? args.mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, args.node.clonedFromNodeId))
|
|
220
|
+
: args.mesh?.nodes?.find((n: any) => !n.isLocalWorktree);
|
|
221
|
+
const repoRoot = typeof sourceNode?.repoRoot === 'string' && sourceNode.repoRoot.trim()
|
|
222
|
+
? sourceNode.repoRoot.trim()
|
|
223
|
+
: typeof sourceNode?.workspace === 'string' && sourceNode.workspace.trim()
|
|
224
|
+
? sourceNode.workspace.trim()
|
|
225
|
+
: '';
|
|
226
|
+
|
|
227
|
+
if (!worktreeExists) {
|
|
228
|
+
return { success: true, skipped: true, removedPath: workspace, repoRoot: repoRoot || undefined, reason: 'worktree_path_missing' };
|
|
229
|
+
}
|
|
230
|
+
if (!repoRoot || !fs.existsSync(repoRoot)) {
|
|
231
|
+
return {
|
|
232
|
+
success: false,
|
|
233
|
+
code: 'mesh_worktree_cleanup_missing_source_repo',
|
|
234
|
+
error: `Refusing to remove worktree '${workspace}' because the source repo root is unavailable`,
|
|
235
|
+
recoveryHint: 'Run mesh_remove_node from the machine that owns the source repo, or verify the source node metadata before retrying.',
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
if (typeof args.node?.worktreeBranch !== 'string' || !args.node.worktreeBranch.trim()) {
|
|
239
|
+
return {
|
|
240
|
+
success: false,
|
|
241
|
+
code: 'mesh_worktree_cleanup_missing_branch',
|
|
242
|
+
error: `Refusing to remove worktree '${workspace}' because worktreeBranch metadata is missing`,
|
|
243
|
+
recoveryHint: 'Confirm this is an ADHDev-managed worktree before removing it manually; managed worktree nodes include worktreeBranch metadata.',
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const { resolveWorktreePath, listWorktrees, removeWorktree } = await import('../git/git-worktree.js');
|
|
248
|
+
const normalizePath = (value: string) => {
|
|
249
|
+
const resolved = pathResolve(value);
|
|
250
|
+
try { return fs.realpathSync(resolved); } catch { return resolved; }
|
|
251
|
+
};
|
|
252
|
+
const expectedPath = normalizePath(resolveWorktreePath(repoRoot, String(args.mesh?.name || args.mesh?.id || 'mesh'), args.node.worktreeBranch));
|
|
253
|
+
const actualPath = normalizePath(workspace);
|
|
254
|
+
if (actualPath !== expectedPath) {
|
|
255
|
+
return {
|
|
256
|
+
success: false,
|
|
257
|
+
code: 'mesh_worktree_cleanup_unexpected_path',
|
|
258
|
+
error: `Refusing to remove worktree '${workspace}' because it is not at the expected managed path '${expectedPath}'`,
|
|
259
|
+
recoveryHint: 'Use git worktree list/status to inspect the path. Retry only after confirming the mesh node metadata points to an ADHDev-managed worktree.',
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const entries = await listWorktrees(repoRoot);
|
|
264
|
+
const managedEntry = entries.find(entry => normalizePath(entry.path) === actualPath);
|
|
265
|
+
if (!managedEntry) {
|
|
266
|
+
// Idempotent residue recovery (NOT a refusal). By this point the path is
|
|
267
|
+
// already proven ADHDev-managed: worktreeBranch metadata is present and
|
|
268
|
+
// actualPath === expectedPath. Git nonetheless no longer lists it as a
|
|
269
|
+
// worktree. This is the post-force-fallback re-entry state — an earlier
|
|
270
|
+
// removal de-registered the worktree from git but left the directory
|
|
271
|
+
// behind (commonly Windows EINVAL on submodule gitlink files). Refusing
|
|
272
|
+
// here would strand the node in mesh membership forever, so prune any
|
|
273
|
+
// stale registration, best-effort remove the leftover directory, and
|
|
274
|
+
// report success so the caller drops the node from the mesh registry.
|
|
275
|
+
try {
|
|
276
|
+
const { execFile } = await import('node:child_process');
|
|
277
|
+
const { promisify } = await import('node:util');
|
|
278
|
+
const execFileAsync = promisify(execFile);
|
|
279
|
+
await execFileAsync('git', ['worktree', 'prune'], {
|
|
280
|
+
cwd: repoRoot, encoding: 'utf8', timeout: 30_000, maxBuffer: 4 * 1024 * 1024, windowsHide: true,
|
|
281
|
+
});
|
|
282
|
+
} catch { /* prune is best-effort */ }
|
|
283
|
+
const rm = await self.bestEffortRemoveWorktreeDir(workspace);
|
|
284
|
+
return {
|
|
285
|
+
success: true,
|
|
286
|
+
removedPath: workspace,
|
|
287
|
+
repoRoot,
|
|
288
|
+
reason: 'worktree_unregistered_residue_recovered',
|
|
289
|
+
recovered: true,
|
|
290
|
+
...(rm.residue ? {
|
|
291
|
+
residue: true,
|
|
292
|
+
residueWarning: `Worktree was already de-registered from git but the directory could not be fully removed (leftover residue at '${workspace}'): ${rm.error || 'unknown error'}. The node will be dropped from the mesh; remove the directory manually if needed.`,
|
|
293
|
+
residueError: rm.error,
|
|
294
|
+
} : {}),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
if (managedEntry.branch && managedEntry.branch !== args.node.worktreeBranch) {
|
|
298
|
+
return {
|
|
299
|
+
success: false,
|
|
300
|
+
code: 'mesh_worktree_cleanup_branch_mismatch',
|
|
301
|
+
error: `Refusing to remove '${workspace}' because git reports branch '${managedEntry.branch}', expected '${args.node.worktreeBranch}'`,
|
|
302
|
+
recoveryHint: 'Inspect the worktree branch and mesh metadata before retrying cleanup.',
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Always evaluate real merge convergence so we can decide whether the
|
|
307
|
+
// branch ref is safe to delete after removal — even on the force path,
|
|
308
|
+
// where `force_override` only authorizes the *worktree* removal and must
|
|
309
|
+
// NOT be taken as proof the branch is merged (that would risk work loss).
|
|
310
|
+
const mergeConvergence = await self.getWorktreeForceCleanupConvergence({ repoRoot, workspace, node: args.node });
|
|
311
|
+
const forceFallbackConvergence = args.force
|
|
312
|
+
? { allow: true, status: 'force_override', source: 'caller_force_flag' }
|
|
313
|
+
: mergeConvergence;
|
|
314
|
+
|
|
315
|
+
// After the worktree is removed, delete the branch ref iff the branch is
|
|
316
|
+
// fully merged into the default ref (no work loss). Otherwise preserve it
|
|
317
|
+
// and surface a warning. `mergeConvergence` (NOT the force override) is the
|
|
318
|
+
// authority on merged-ness.
|
|
319
|
+
const deleteBranchIfMerged = async () => {
|
|
320
|
+
const branch = String(args.node.worktreeBranch).trim();
|
|
321
|
+
const status = mergeConvergence.allow ? (mergeConvergence.status || '') : '';
|
|
322
|
+
const MERGED_STATUSES = new Set([
|
|
323
|
+
'merged_to_main', 'merged_pushed', 'merged_to_default_ref', 'cleanup_candidate',
|
|
324
|
+
]);
|
|
325
|
+
const PATCH_EQUIV_STATUS = 'patch_equivalent_to_default_ref';
|
|
326
|
+
if (!branch) {
|
|
327
|
+
return { branchRefDeleted: false, branchRefReason: 'empty_branch_name' };
|
|
328
|
+
}
|
|
329
|
+
if (!mergeConvergence.allow || (!MERGED_STATUSES.has(status) && status !== PATCH_EQUIV_STATUS)) {
|
|
330
|
+
return {
|
|
331
|
+
branchRefDeleted: false,
|
|
332
|
+
branchRefReason: `branch_not_merged_preserved: ${mergeConvergence.error || mergeConvergence.status || 'convergence_unverified'}`,
|
|
333
|
+
branchRefWarning: `Branch ref '${branch}' was preserved (not deleted) because it is not confirmed merged into the default ref — no work was lost. Merge it (or pass a verified branchConvergence final state) and re-run cleanup, or delete it manually after confirming.`,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
const { deleteBranchRef } = await import('../git/git-worktree.js');
|
|
337
|
+
// `-d` can detect a true fast-forward/merge; patch-equivalent landings
|
|
338
|
+
// (squash/cherry-pick) are invisible to `-d`, so allow the verified `-D`
|
|
339
|
+
// fallback only for the patch-equivalence status.
|
|
340
|
+
const res = await deleteBranchRef(repoRoot, branch, { safeDeleteOnly: status !== PATCH_EQUIV_STATUS });
|
|
341
|
+
return {
|
|
342
|
+
branchRefDeleted: res.deleted,
|
|
343
|
+
branchRefReason: res.reason,
|
|
344
|
+
...(res.forced ? { branchRefForced: true } : {}),
|
|
345
|
+
...(res.deleted ? {} : {
|
|
346
|
+
branchRefWarning: `Branch ref '${branch}' could not be deleted (${res.reason}); it was preserved so no work is lost.`,
|
|
347
|
+
}),
|
|
348
|
+
};
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
const result = await removeWorktree(repoRoot, workspace, {
|
|
353
|
+
requireClean: !args.force,
|
|
354
|
+
allowSubmoduleForceFallback: forceFallbackConvergence.allow,
|
|
355
|
+
});
|
|
356
|
+
const branchOutcome = await deleteBranchIfMerged();
|
|
357
|
+
return {
|
|
358
|
+
success: true,
|
|
359
|
+
removedPath: result.removedPath,
|
|
360
|
+
repoRoot,
|
|
361
|
+
...branchOutcome,
|
|
362
|
+
...(result.fallback ? {
|
|
363
|
+
fallback: result.fallback,
|
|
364
|
+
forced: result.forced,
|
|
365
|
+
reason: result.reason,
|
|
366
|
+
convergence: forceFallbackConvergence,
|
|
367
|
+
} : {}),
|
|
368
|
+
};
|
|
369
|
+
} catch (e: any) {
|
|
370
|
+
const message = String(e?.message || e || 'worktree cleanup failed');
|
|
371
|
+
const dirty = message.includes('dirty worktree') || message.includes('local changes');
|
|
372
|
+
const isSubmoduleGuard = /working trees containing submodules cannot be moved or removed/i.test(message);
|
|
373
|
+
const submoduleForceBlocked = isSubmoduleGuard && !forceFallbackConvergence.allow;
|
|
374
|
+
|
|
375
|
+
// Fallback 1: submodule guard on --force path — deinit submodules first, then retry remove
|
|
376
|
+
if (isSubmoduleGuard && forceFallbackConvergence.allow) {
|
|
377
|
+
const { execFile } = await import('node:child_process');
|
|
378
|
+
const { promisify } = await import('node:util');
|
|
379
|
+
const execFileAsync = promisify(execFile);
|
|
380
|
+
const GIT_TIMEOUT_CLEANUP = 30_000;
|
|
381
|
+
const GIT_MAX_BUFFER_CLEANUP = 4 * 1024 * 1024;
|
|
382
|
+
try {
|
|
383
|
+
await execFileAsync('git', ['-C', workspace, 'submodule', 'deinit', '--all', '-f'], {
|
|
384
|
+
encoding: 'utf8', timeout: GIT_TIMEOUT_CLEANUP, maxBuffer: GIT_MAX_BUFFER_CLEANUP, windowsHide: true,
|
|
385
|
+
});
|
|
386
|
+
await execFileAsync('git', ['worktree', 'remove', '--force', workspace], {
|
|
387
|
+
cwd: repoRoot, encoding: 'utf8', timeout: GIT_TIMEOUT_CLEANUP, maxBuffer: GIT_MAX_BUFFER_CLEANUP, windowsHide: true,
|
|
388
|
+
});
|
|
389
|
+
const branchOutcome = await deleteBranchIfMerged();
|
|
390
|
+
return {
|
|
391
|
+
success: true,
|
|
392
|
+
removedPath: workspace,
|
|
393
|
+
repoRoot,
|
|
394
|
+
...branchOutcome,
|
|
395
|
+
fallback: 'git_worktree_remove_submodule_deinit' as const,
|
|
396
|
+
forced: true,
|
|
397
|
+
reason: 'working_trees_containing_submodules' as const,
|
|
398
|
+
convergence: forceFallbackConvergence,
|
|
399
|
+
};
|
|
400
|
+
} catch (deinitError: any) {
|
|
401
|
+
// Fallback 2: deinit+remove still failed — best-effort directory
|
|
402
|
+
// removal + prune. The path is already proven managed/converged
|
|
403
|
+
// here, and a leftover directory must NOT gate dropping the node
|
|
404
|
+
// from the mesh, so absorb Windows EINVAL/EPERM and report success
|
|
405
|
+
// with a residue warning instead of failing the whole removal.
|
|
406
|
+
const rm = await self.bestEffortRemoveWorktreeDir(workspace);
|
|
407
|
+
try {
|
|
408
|
+
await execFileAsync('git', ['worktree', 'prune'], {
|
|
409
|
+
cwd: repoRoot, encoding: 'utf8', timeout: GIT_TIMEOUT_CLEANUP, maxBuffer: GIT_MAX_BUFFER_CLEANUP, windowsHide: true,
|
|
410
|
+
});
|
|
411
|
+
} catch { /* prune is best-effort */ }
|
|
412
|
+
const branchOutcome = await deleteBranchIfMerged();
|
|
413
|
+
return {
|
|
414
|
+
success: true,
|
|
415
|
+
removedPath: workspace,
|
|
416
|
+
repoRoot,
|
|
417
|
+
...branchOutcome,
|
|
418
|
+
fallback: 'fs_rm_worktree_prune' as const,
|
|
419
|
+
forced: true,
|
|
420
|
+
reason: 'working_trees_containing_submodules' as const,
|
|
421
|
+
convergence: forceFallbackConvergence,
|
|
422
|
+
...(rm.residue ? {
|
|
423
|
+
residue: true,
|
|
424
|
+
residueWarning: `Worktree was de-registered from git but the directory could not be fully removed (leftover residue at '${workspace}'): ${rm.error || 'unknown error'}; deinit+remove first failed with: ${deinitError?.message || deinitError}. The node will be dropped from the mesh; remove the directory manually if needed.`,
|
|
425
|
+
residueError: rm.error,
|
|
426
|
+
} : {}),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
success: false,
|
|
433
|
+
code: dirty
|
|
434
|
+
? 'mesh_worktree_cleanup_dirty'
|
|
435
|
+
: submoduleForceBlocked
|
|
436
|
+
? 'mesh_worktree_cleanup_force_fallback_blocked'
|
|
437
|
+
: 'mesh_worktree_cleanup_failed',
|
|
438
|
+
error: submoduleForceBlocked
|
|
439
|
+
? `${message}; refusing --force fallback because convergence could not be verified: ${forceFallbackConvergence.error || 'unknown convergence state'}`
|
|
440
|
+
: message,
|
|
441
|
+
recoveryHint: dirty
|
|
442
|
+
? 'Commit, stash, or intentionally discard the worktree changes before retrying mesh_remove_node. The mesh registry entry is preserved until cleanup is safe.'
|
|
443
|
+
: submoduleForceBlocked
|
|
444
|
+
? 'Verify the worktree branch is merged/contained in the source default branch (for example origin/main) or mark the node with a safe branchConvergence final state, or pass force:true if content is confirmed already in main.'
|
|
445
|
+
: 'Inspect git worktree status/list from the source repo and retry after resolving the reported cleanup failure.',
|
|
446
|
+
...(submoduleForceBlocked ? { convergence: forceFallbackConvergence } : {}),
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export async function getWorktreeForceCleanupConvergence(self: DaemonCommandRouter, args: {
|
|
452
|
+
repoRoot: string;
|
|
453
|
+
workspace: string;
|
|
454
|
+
node: any;
|
|
455
|
+
}): Promise<{ allow: boolean; status?: string; source?: string; ref?: string; error?: string }> {
|
|
456
|
+
const metadataStatus = typeof args.node?.branchConvergence?.status === 'string'
|
|
457
|
+
? args.node.branchConvergence.status
|
|
458
|
+
: '';
|
|
459
|
+
if (metadataStatus === 'merged_to_main' || metadataStatus === 'cleanup_candidate' || metadataStatus === 'merged_pushed') {
|
|
460
|
+
return { allow: true, status: metadataStatus, source: 'node_branch_convergence' };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Also allow when the node's last recorded refine job reached final convergence merged_pushed
|
|
464
|
+
const refinedConvergence = typeof args.node?.refineState?.finalBranchConvergenceState?.status === 'string'
|
|
465
|
+
? args.node.refineState.finalBranchConvergenceState.status
|
|
466
|
+
: typeof args.node?.lastRefineResult?.finalBranchConvergenceState?.status === 'string'
|
|
467
|
+
? args.node.lastRefineResult.finalBranchConvergenceState.status
|
|
468
|
+
: '';
|
|
469
|
+
if (refinedConvergence === 'merged_pushed' || refinedConvergence === 'merged_to_main') {
|
|
470
|
+
return { allow: true, status: refinedConvergence, source: 'node_refine_state' };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const { execFile } = await import('node:child_process');
|
|
474
|
+
const { promisify } = await import('node:util');
|
|
475
|
+
const execFileAsync = promisify(execFile);
|
|
476
|
+
const runGit = async (gitArgs: string[], cwd: string): Promise<string> => {
|
|
477
|
+
const { stdout } = await execFileAsync('git', gitArgs, {
|
|
478
|
+
cwd,
|
|
479
|
+
encoding: 'utf8',
|
|
480
|
+
timeout: 30_000,
|
|
481
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
482
|
+
windowsHide: true,
|
|
483
|
+
});
|
|
484
|
+
return String(stdout || '').trim();
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
let head = '';
|
|
488
|
+
try {
|
|
489
|
+
head = await runGit(['rev-parse', 'HEAD'], args.workspace);
|
|
490
|
+
} catch (e: any) {
|
|
491
|
+
return { allow: false, error: `could not resolve worktree HEAD: ${e?.message || e}` };
|
|
492
|
+
}
|
|
493
|
+
if (!head) return { allow: false, error: 'worktree HEAD is empty' };
|
|
494
|
+
|
|
495
|
+
const candidateRefs: string[] = [];
|
|
496
|
+
try {
|
|
497
|
+
const defaultBranch = await runGit(['branch', '--show-current'], args.repoRoot);
|
|
498
|
+
if (defaultBranch) {
|
|
499
|
+
candidateRefs.push(defaultBranch, `origin/${defaultBranch}`);
|
|
500
|
+
}
|
|
501
|
+
} catch { /* fall through to common refs */ }
|
|
502
|
+
candidateRefs.push('origin/main', 'origin/master', 'main', 'master');
|
|
503
|
+
|
|
504
|
+
const seen = new Set<string>();
|
|
505
|
+
const checkedRefs: string[] = [];
|
|
506
|
+
const resolvedRefCommits: Array<{ ref: string; commit: string }> = [];
|
|
507
|
+
for (const ref of candidateRefs) {
|
|
508
|
+
if (!ref || seen.has(ref)) continue;
|
|
509
|
+
seen.add(ref);
|
|
510
|
+
let commit = '';
|
|
511
|
+
try {
|
|
512
|
+
commit = await runGit(['rev-parse', '--verify', `${ref}^{commit}`], args.repoRoot);
|
|
513
|
+
} catch {
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
checkedRefs.push(ref);
|
|
517
|
+
resolvedRefCommits.push({ ref, commit });
|
|
518
|
+
try {
|
|
519
|
+
await runGit(['merge-base', '--is-ancestor', head, commit], args.repoRoot);
|
|
520
|
+
return { allow: true, status: 'merged_to_default_ref', source: 'git_merge_base', ref };
|
|
521
|
+
} catch {
|
|
522
|
+
// Not contained in this candidate ref; keep checking other safe refs.
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// SHA-reachability fallback: the worktree HEAD is not an ancestor of any
|
|
527
|
+
// candidate ref, but its CONTENT may already be present via cherry-pick /
|
|
528
|
+
// squash / rebase (a different commit SHA carrying the same patch). The
|
|
529
|
+
// Refinery accepts such patch-equivalent landings; mirror that here so the
|
|
530
|
+
// cleanup guard does not falsely block a converged worktree. This is the
|
|
531
|
+
// heavier merge-tree/patch-id path, so it only runs after every ancestor
|
|
532
|
+
// check has already failed. Any failure stays conservative (NOT contained).
|
|
533
|
+
for (const { ref, commit } of resolvedRefCommits) {
|
|
534
|
+
let containment: MeshWorktreePatchContainmentSummary;
|
|
535
|
+
try {
|
|
536
|
+
containment = await checkWorktreeChangesPatchEquivalentInRef(args.repoRoot, commit, head);
|
|
537
|
+
} catch {
|
|
538
|
+
// Defensive: the helper is already exception-safe, but never let a
|
|
539
|
+
// thrown error escape into an allow.
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
if (containment.contained) {
|
|
543
|
+
return { allow: true, status: 'patch_equivalent_to_default_ref', source: 'git_patch_equivalence', ref };
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return {
|
|
548
|
+
allow: false,
|
|
549
|
+
status: metadataStatus || undefined,
|
|
550
|
+
error: checkedRefs.length
|
|
551
|
+
? `worktree HEAD is not contained in checked refs: ${checkedRefs.join(', ')}`
|
|
552
|
+
: 'no default/main refs were available for convergence verification',
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export function isCompletedHostedSession(self: DaemonCommandRouter, record: any): boolean {
|
|
557
|
+
return record?.lifecycle === 'stopped' || record?.lifecycle === 'failed' || record?.lifecycle === 'interrupted';
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export async function recordIntentionalMeshSessionStop(self: DaemonCommandRouter, args: {
|
|
561
|
+
meshId: string;
|
|
562
|
+
nodeId: string;
|
|
563
|
+
node: any;
|
|
564
|
+
sessionId: string;
|
|
565
|
+
mode: RepoMeshSessionCleanupMode;
|
|
566
|
+
source: 'mesh_cleanup_sessions' | 'mesh_remove_node' | 'magi_session_cleanup';
|
|
567
|
+
action: 'stop_session' | 'delete_session_force';
|
|
568
|
+
}): Promise<void> {
|
|
569
|
+
try {
|
|
570
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
571
|
+
appendLedgerEntry(args.meshId, {
|
|
572
|
+
kind: 'session_stopped',
|
|
573
|
+
nodeId: args.nodeId,
|
|
574
|
+
sessionId: args.sessionId,
|
|
575
|
+
payload: {
|
|
576
|
+
intentional: true,
|
|
577
|
+
reason: 'operator_cleanup',
|
|
578
|
+
intentionalStopReason: 'operator_cleanup',
|
|
579
|
+
source: args.source,
|
|
580
|
+
cleanupMode: args.mode,
|
|
581
|
+
action: args.action,
|
|
582
|
+
workspace: typeof args.node?.workspace === 'string' ? args.node.workspace : undefined,
|
|
583
|
+
},
|
|
584
|
+
});
|
|
585
|
+
} catch (e: any) {
|
|
586
|
+
LOG.warn('MeshCleanup', `Failed to record intentional cleanup stop for ${args.sessionId}: ${e?.message || e}`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
export async function cleanupMeshSessions(self: DaemonCommandRouter, args: {
|
|
591
|
+
meshId: string;
|
|
592
|
+
nodeId: string;
|
|
593
|
+
node: any;
|
|
594
|
+
mode: RepoMeshSessionCleanupMode;
|
|
595
|
+
sessionIds?: string[];
|
|
596
|
+
dryRun?: boolean;
|
|
597
|
+
source?: 'mesh_cleanup_sessions' | 'mesh_remove_node' | 'magi_session_cleanup';
|
|
598
|
+
/**
|
|
599
|
+
* MAGI auto-cleanup safety gate: a map of sessionId → the queue task id that
|
|
600
|
+
* session must have been AUTO-LAUNCHED for (record meta autoLaunchedForQueueTaskId).
|
|
601
|
+
* When set, a matched explicit session is only acted on if its record carries that
|
|
602
|
+
* exact marker. A reused idle session (no marker), the coordinator session, a
|
|
603
|
+
* re-assigned session, or any session whose marker points at a DIFFERENT task is
|
|
604
|
+
* skipped (reason 'auto_launch_marker_mismatch') — so MAGI never kills a session it
|
|
605
|
+
* didn't itself spawn for this fan-out. Only consulted alongside explicit sessionIds.
|
|
606
|
+
*/
|
|
607
|
+
requireAutoLaunchedForTaskIds?: Record<string, string>;
|
|
608
|
+
/**
|
|
609
|
+
* Opt-in orphan reclaim (default false). See SESSION-ACCUMULATION-LEAK.
|
|
610
|
+
* When true, a workspace-only live_runtime session (no node binding) OR a
|
|
611
|
+
* live_runtime session bound to a node absent from `liveMeshNodeIds` is
|
|
612
|
+
* stopped instead of skipped by the conservative shared-daemon guard. A
|
|
613
|
+
* session whose meshNodeId is STILL in `liveMeshNodeIds` (active sibling)
|
|
614
|
+
* is never reclaimed — that is the shared-daemon safety this preserves.
|
|
615
|
+
*/
|
|
616
|
+
reclaimOrphans?: boolean;
|
|
617
|
+
liveMeshNodeIds?: string[];
|
|
618
|
+
}): Promise<{ success: boolean; [key: string]: unknown }> {
|
|
619
|
+
if (args.mode === 'preserve') {
|
|
620
|
+
return { success: true, mode: 'preserve', matchedCount: 0, stoppedSessionIds: [], deletedSessionIds: [], skippedSessionIds: [] };
|
|
621
|
+
}
|
|
622
|
+
if (!self.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
623
|
+
|
|
624
|
+
const requestedSessionIds = Array.isArray(args.sessionIds)
|
|
625
|
+
? new Set(args.sessionIds.map(id => typeof id === 'string' ? id.trim() : '').filter(Boolean))
|
|
626
|
+
: undefined;
|
|
627
|
+
// Orphan-reclaim support (opt-in). The live-node id list lets us tell an
|
|
628
|
+
// orphan (owning node gone from the mesh) apart from a still-active sibling
|
|
629
|
+
// sharing this daemon. daemonIdsEquivalent (not raw ===) guards against the
|
|
630
|
+
// canon-identity defect class: daemonId forms (daemon_mach_ vs mach_) and
|
|
631
|
+
// normalized vs raw meshNodeId must compare equivalent, not literally equal.
|
|
632
|
+
const reclaimOrphans = args.reclaimOrphans === true;
|
|
633
|
+
const liveMeshNodeIds = Array.isArray(args.liveMeshNodeIds)
|
|
634
|
+
? args.liveMeshNodeIds.map(id => typeof id === 'string' ? id.trim() : '').filter(Boolean)
|
|
635
|
+
: [];
|
|
636
|
+
const isNodeStillLive = (candidateNodeId: string): boolean => {
|
|
637
|
+
if (!candidateNodeId) return false;
|
|
638
|
+
return liveMeshNodeIds.some(liveId =>
|
|
639
|
+
liveId === candidateNodeId
|
|
640
|
+
|| meshNodeIdMatches({ id: liveId }, candidateNodeId)
|
|
641
|
+
|| daemonIdsEquivalent(liveId, candidateNodeId));
|
|
642
|
+
};
|
|
643
|
+
const reclaimedOrphanSessionIds: string[] = [];
|
|
644
|
+
const sessions = await self.deps.sessionHostControl.listSessions();
|
|
645
|
+
const matched = sessions.filter(record => self.sessionMatchesMeshNode(record, args.node, args.nodeId, requestedSessionIds));
|
|
646
|
+
const hasExplicitSessionIds = !!requestedSessionIds?.size;
|
|
647
|
+
const stoppedSessionIds: string[] = [];
|
|
648
|
+
const deletedSessionIds: string[] = [];
|
|
649
|
+
const skippedSessionIds: string[] = [];
|
|
650
|
+
const skippedLiveSessionIds: string[] = [];
|
|
651
|
+
const skippedCoordinatorSessionIds: string[] = [];
|
|
652
|
+
const skippedLiveSessionReasons: Array<{ sessionId: string; reason: string }> = [];
|
|
653
|
+
const skippedMarkerMismatchSessionIds: string[] = [];
|
|
654
|
+
const actedLiveDelegateSessionIds: string[] = [];
|
|
655
|
+
const deleteUnsupportedSessionIds: string[] = [];
|
|
656
|
+
const recordsRemainSessionIds: string[] = [];
|
|
657
|
+
const errors: Array<{ sessionId: string; error: string }> = [];
|
|
658
|
+
const cleanupSource = args.source || 'mesh_cleanup_sessions';
|
|
659
|
+
const markedIntentionalStopSessionIds = new Set<string>();
|
|
660
|
+
const markIntentionalStop = async (sessionId: string, action: 'stop_session' | 'delete_session_force') => {
|
|
661
|
+
if (args.dryRun || markedIntentionalStopSessionIds.has(sessionId)) return;
|
|
662
|
+
markedIntentionalStopSessionIds.add(sessionId);
|
|
663
|
+
await self.recordIntentionalMeshSessionStop({
|
|
664
|
+
meshId: args.meshId,
|
|
665
|
+
nodeId: args.nodeId,
|
|
666
|
+
node: args.node,
|
|
667
|
+
sessionId,
|
|
668
|
+
mode: args.mode,
|
|
669
|
+
source: cleanupSource,
|
|
670
|
+
action,
|
|
671
|
+
});
|
|
672
|
+
};
|
|
673
|
+
const matchedBySurfaceKind = {
|
|
674
|
+
live_runtime: 0,
|
|
675
|
+
recovery_snapshot: 0,
|
|
676
|
+
inactive_record: 0,
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
for (const record of matched) {
|
|
680
|
+
const surfaceKind = getSessionHostSurfaceKind(record);
|
|
681
|
+
matchedBySurfaceKind[surfaceKind] += 1;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
for (const record of matched) {
|
|
685
|
+
const sessionId = String(record.sessionId);
|
|
686
|
+
const completed = self.isCompletedHostedSession(record);
|
|
687
|
+
const surfaceKind = getSessionHostSurfaceKind(record);
|
|
688
|
+
const liveRuntime = surfaceKind === 'live_runtime';
|
|
689
|
+
const coordinatorSession = readStringValue(record?.meta?.meshCoordinatorFor) === args.meshId;
|
|
690
|
+
// A delegate session was launched by the coordinator specifically FOR this node
|
|
691
|
+
// (meta.meshNodeId === this node). It is 1:1 bound to the node, even when the node
|
|
692
|
+
// shares its daemon runtime with the main/other nodes. Removing the node should be
|
|
693
|
+
// able to stop its own delegate session — the shared-daemon concern only applies to
|
|
694
|
+
// sessions we matched by workspace alone (which could belong to the coordinator or to
|
|
695
|
+
// a sibling node that is still active).
|
|
696
|
+
const recordNodeId = readStringValue(record?.meta?.meshNodeId);
|
|
697
|
+
const recordMeshNodeFor = readStringValue(record?.meta?.meshNodeFor);
|
|
698
|
+
const delegateBoundToThisNode = !!recordNodeId
|
|
699
|
+
&& recordNodeId === args.nodeId
|
|
700
|
+
&& (!recordMeshNodeFor || recordMeshNodeFor === args.meshId);
|
|
701
|
+
if (!hasExplicitSessionIds && coordinatorSession) {
|
|
702
|
+
skippedSessionIds.push(sessionId);
|
|
703
|
+
skippedCoordinatorSessionIds.push(sessionId);
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
// MAGI auto-cleanup marker gate. When the caller supplied a per-session
|
|
707
|
+
// expected autoLaunchedForQueueTaskId, the session must (a) carry the marker
|
|
708
|
+
// on its record meta AND (b) have it equal the expected replica task id.
|
|
709
|
+
// This is the safety core for MAGI: explicit session_ids bypass the
|
|
710
|
+
// self-coordinator / shared-daemon guards (hasExplicitSessionIds=true), so the
|
|
711
|
+
// ONLY thing protecting a reused-idle / coordinator / re-assigned session here
|
|
712
|
+
// is this marker check. Always-skip the coordinator session even when its id is
|
|
713
|
+
// passed explicitly (it never carries an autoLaunchedForQueueTaskId marker, so
|
|
714
|
+
// the mismatch branch already covers it, but be explicit for clarity).
|
|
715
|
+
if (args.requireAutoLaunchedForTaskIds) {
|
|
716
|
+
const decision = magiAutoLaunchedSessionCleanupDecision({
|
|
717
|
+
recordMarker: readStringValue(record?.meta?.autoLaunchedForQueueTaskId),
|
|
718
|
+
expectedTaskId: args.requireAutoLaunchedForTaskIds[sessionId],
|
|
719
|
+
isCoordinatorSession: coordinatorSession,
|
|
720
|
+
});
|
|
721
|
+
if (!decision.allow) {
|
|
722
|
+
skippedSessionIds.push(sessionId);
|
|
723
|
+
skippedMarkerMismatchSessionIds.push(sessionId);
|
|
724
|
+
skippedLiveSessionReasons.push({ sessionId, reason: decision.reason });
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
// Only the conservative shared-daemon guard for live sessions that are NOT a delegate
|
|
729
|
+
// explicitly bound to this node. Delegate-bound live sessions fall through and are
|
|
730
|
+
// stopped/deleted by the mode handlers below (which already record an intentional stop).
|
|
731
|
+
//
|
|
732
|
+
// Worktree-removal exception: when a WORKTREE node is being removed
|
|
733
|
+
// (source === 'mesh_remove_node' AND node.isLocalWorktree === true), its
|
|
734
|
+
// node-binding is already gone, so a still-live session in that workspace
|
|
735
|
+
// matches by workspace ALONE (recordNodeId is empty). The shared-daemon
|
|
736
|
+
// concern does not apply — a worktree has a private workspace path that is
|
|
737
|
+
// not shared with the base/other nodes — so leaving it skipped orphans the
|
|
738
|
+
// chat after the node + worktree dir + branch are gone. Clean it instead.
|
|
739
|
+
// This narrowly covers ONLY the pure workspace-only-no-binding case; a
|
|
740
|
+
// session bound to ANOTHER node (recordNodeId set and != this node) is still
|
|
741
|
+
// skipped (live_delegate_bound_to_other_node), and the coordinator session is
|
|
742
|
+
// already protected unconditionally above.
|
|
743
|
+
const matchedByWorkspaceOnly = !recordNodeId;
|
|
744
|
+
const isWorktreeNodeRemoval = cleanupSource === 'mesh_remove_node' && args.node?.isLocalWorktree === true;
|
|
745
|
+
const cleanWorkspaceOnlyForWorktree = isWorktreeNodeRemoval && matchedByWorkspaceOnly;
|
|
746
|
+
// Opt-in orphan reclaim (SESSION-ACCUMULATION-LEAK): a live_runtime that
|
|
747
|
+
// is either workspace-only (no node binding — the leaked spec-CLI case)
|
|
748
|
+
// or bound to a node no longer present in the mesh is an orphan and safe
|
|
749
|
+
// to stop. A session bound to a node STILL live in the mesh is an active
|
|
750
|
+
// sibling on this shared daemon and is left to the normal guard. Node
|
|
751
|
+
// equivalence uses isNodeStillLive (daemonIdsEquivalent under the hood),
|
|
752
|
+
// never a raw === on the id form.
|
|
753
|
+
const reclaimableOrphan = reclaimOrphans
|
|
754
|
+
&& liveRuntime
|
|
755
|
+
&& !delegateBoundToThisNode
|
|
756
|
+
&& (matchedByWorkspaceOnly || !isNodeStillLive(recordNodeId));
|
|
757
|
+
if (!hasExplicitSessionIds && liveRuntime && !delegateBoundToThisNode && !cleanWorkspaceOnlyForWorktree && !reclaimableOrphan) {
|
|
758
|
+
skippedSessionIds.push(sessionId);
|
|
759
|
+
skippedLiveSessionIds.push(sessionId);
|
|
760
|
+
const reason = recordNodeId && recordNodeId !== args.nodeId
|
|
761
|
+
? `live_delegate_bound_to_other_node:${recordNodeId}`
|
|
762
|
+
: matchedByWorkspaceOnly
|
|
763
|
+
? 'live_session_matched_by_workspace_only_no_node_binding'
|
|
764
|
+
: 'live_session_not_bound_to_this_node';
|
|
765
|
+
skippedLiveSessionReasons.push({ sessionId, reason });
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
if (cleanWorkspaceOnlyForWorktree && !delegateBoundToThisNode) {
|
|
769
|
+
// A workspace-only live session on a worktree being removed is treated
|
|
770
|
+
// like a bound delegate for accounting (so callers can see it was acted on).
|
|
771
|
+
actedLiveDelegateSessionIds.push(sessionId);
|
|
772
|
+
}
|
|
773
|
+
if (reclaimableOrphan && !cleanWorkspaceOnlyForWorktree && !delegateBoundToThisNode) {
|
|
774
|
+
// Reclaimed orphan — surface it distinctly so callers can audit what
|
|
775
|
+
// the opt-in reclaim path stopped that the normal guard would have kept.
|
|
776
|
+
reclaimedOrphanSessionIds.push(sessionId);
|
|
777
|
+
actedLiveDelegateSessionIds.push(sessionId);
|
|
778
|
+
}
|
|
779
|
+
if (!hasExplicitSessionIds && liveRuntime && delegateBoundToThisNode && args.mode === 'delete_stopped') {
|
|
780
|
+
// delete_stopped never stops live runtimes by contract — even bound delegates.
|
|
781
|
+
// Surface a clear reason instead of an unexplained skip so callers know to use
|
|
782
|
+
// stop / stop_and_delete to release a still-running bound delegate.
|
|
783
|
+
skippedSessionIds.push(sessionId);
|
|
784
|
+
skippedLiveSessionIds.push(sessionId);
|
|
785
|
+
skippedLiveSessionReasons.push({ sessionId, reason: 'live_delegate_preserved_by_delete_stopped_mode_use_stop_or_stop_and_delete' });
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
if (!hasExplicitSessionIds && liveRuntime && delegateBoundToThisNode) {
|
|
789
|
+
actedLiveDelegateSessionIds.push(sessionId);
|
|
790
|
+
}
|
|
791
|
+
try {
|
|
792
|
+
if (args.mode === 'stop') {
|
|
793
|
+
if (!completed) {
|
|
794
|
+
if (!args.dryRun) {
|
|
795
|
+
await markIntentionalStop(sessionId, 'stop_session');
|
|
796
|
+
await self.deps.sessionHostControl.stopSession(sessionId);
|
|
797
|
+
}
|
|
798
|
+
stoppedSessionIds.push(sessionId);
|
|
799
|
+
} else {
|
|
800
|
+
skippedSessionIds.push(sessionId);
|
|
801
|
+
}
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (args.mode === 'delete_stopped') {
|
|
806
|
+
if (completed) {
|
|
807
|
+
if (!args.dryRun) await self.deps.sessionHostControl.deleteSession(sessionId, { force: false });
|
|
808
|
+
deletedSessionIds.push(sessionId);
|
|
809
|
+
} else {
|
|
810
|
+
skippedSessionIds.push(sessionId);
|
|
811
|
+
}
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if (args.mode === 'stop_and_delete') {
|
|
816
|
+
if (!completed) await markIntentionalStop(sessionId, 'delete_session_force');
|
|
817
|
+
if (!args.dryRun) await self.deps.sessionHostControl.deleteSession(sessionId, { force: true });
|
|
818
|
+
deletedSessionIds.push(sessionId);
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
} catch (e: any) {
|
|
822
|
+
const message = e?.message || String(e);
|
|
823
|
+
if (message.includes('Unsupported session host request: delete_session')
|
|
824
|
+
&& (args.mode === 'delete_stopped' || args.mode === 'stop_and_delete')) {
|
|
825
|
+
deleteUnsupportedSessionIds.push(sessionId);
|
|
826
|
+
recordsRemainSessionIds.push(sessionId);
|
|
827
|
+
if (args.mode === 'stop_and_delete' && !completed) {
|
|
828
|
+
try {
|
|
829
|
+
await markIntentionalStop(sessionId, 'stop_session');
|
|
830
|
+
await self.deps.sessionHostControl.stopSession(sessionId);
|
|
831
|
+
stoppedSessionIds.push(sessionId);
|
|
832
|
+
} catch (stopError: any) {
|
|
833
|
+
errors.push({ sessionId, error: stopError?.message || String(stopError) });
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
skippedSessionIds.push(sessionId);
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
errors.push({ sessionId, error: message });
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const deleteUnsupported = deleteUnsupportedSessionIds.length > 0;
|
|
845
|
+
return {
|
|
846
|
+
success: errors.length === 0,
|
|
847
|
+
mode: args.mode,
|
|
848
|
+
dryRun: args.dryRun === true,
|
|
849
|
+
matchedCount: matched.length,
|
|
850
|
+
matchedBySurfaceKind,
|
|
851
|
+
stoppedSessionIds,
|
|
852
|
+
deletedSessionIds,
|
|
853
|
+
skippedSessionIds,
|
|
854
|
+
skippedLiveSessionIds,
|
|
855
|
+
skippedCoordinatorSessionIds,
|
|
856
|
+
...(skippedMarkerMismatchSessionIds.length ? { skippedMarkerMismatchSessionIds } : {}),
|
|
857
|
+
...(actedLiveDelegateSessionIds.length ? { actedLiveDelegateSessionIds } : {}),
|
|
858
|
+
...(reclaimedOrphanSessionIds.length ? { reclaimedOrphanSessionIds } : {}),
|
|
859
|
+
...(skippedLiveSessionReasons.length ? { skippedLiveSessionReasons } : {}),
|
|
860
|
+
...(deleteUnsupported ? {
|
|
861
|
+
deleteUnsupported: true,
|
|
862
|
+
effectiveCleanup: args.mode === 'stop_and_delete'
|
|
863
|
+
? 'stopped_only_records_remain'
|
|
864
|
+
: 'delete_unsupported_records_remain',
|
|
865
|
+
deleteUnsupportedSessionIds,
|
|
866
|
+
recordsRemainSessionIds,
|
|
867
|
+
} : {}),
|
|
868
|
+
...(errors.length ? { errors } : {}),
|
|
869
|
+
};
|
|
870
|
+
}
|