harnery 0.42.0 → 0.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/commander.d.ts +16 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +18 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +95 -52
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +38 -11
- package/dist/commands/diagnostics.d.ts.map +1 -1
- package/dist/commands/diagnostics.js +10 -0
- package/dist/commands/doctor.d.ts +12 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +22 -7
- package/dist/commands/governor.js +22 -10
- package/dist/commands/inbox.d.ts +1 -1
- package/dist/commands/inbox.d.ts.map +1 -1
- package/dist/commands/inbox.js +55 -2
- package/dist/commands/rm.d.ts.map +1 -1
- package/dist/commands/rm.js +28 -4
- package/dist/commands/servers.d.ts.map +1 -1
- package/dist/commands/servers.js +16 -1
- package/dist/commands/supervisor.d.ts.map +1 -1
- package/dist/commands/supervisor.js +7 -2
- package/dist/commands/toc.d.ts +14 -0
- package/dist/commands/toc.d.ts.map +1 -1
- package/dist/commands/toc.js +7 -4
- package/dist/commands/tunnel.d.ts +65 -0
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +290 -101
- package/dist/commands/web-fetch.d.ts +3 -2
- package/dist/commands/web-fetch.d.ts.map +1 -1
- package/dist/commands/web-fetch.js +25 -19
- package/dist/commands/web.d.ts +7 -1
- package/dist/commands/web.d.ts.map +1 -1
- package/dist/commands/web.js +9 -3
- package/dist/commands/workflow.js +24 -30
- package/dist/core/agents/live-authority-v3.d.ts +1 -0
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
- package/dist/core/agents/live-authority-v3.js +11 -0
- package/dist/core/agents/live-lifecycle-v3.d.ts +4 -2
- package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
- package/dist/core/agents/live-lifecycle-v3.js +28 -7
- package/dist/core/agents/mailbox.d.ts +9 -8
- package/dist/core/agents/mailbox.d.ts.map +1 -1
- package/dist/core/agents/mailbox.js +24 -17
- package/dist/core/agents/render/prompt-context.d.ts +2 -0
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +19 -17
- package/dist/core/agents/render/session-context.d.ts +2 -8
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +64 -43
- package/dist/core/agents/rules/stop-hook.js +3 -3
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +25 -14
- package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-writer.js +5 -0
- package/dist/core/agents/state/names.d.ts +43 -3
- package/dist/core/agents/state/names.d.ts.map +1 -1
- package/dist/core/agents/state/names.js +87 -3
- package/dist/core/agents/state/pidmap.d.ts.map +1 -1
- package/dist/core/agents/state/pidmap.js +5 -0
- package/dist/core/agents/state/proc-start.d.ts +19 -0
- package/dist/core/agents/state/proc-start.d.ts.map +1 -1
- package/dist/core/agents/state/proc-start.js +41 -0
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +180 -70
- package/dist/core/agents/state/unchanged-file.d.ts +44 -0
- package/dist/core/agents/state/unchanged-file.d.ts.map +1 -0
- package/dist/core/agents/state/unchanged-file.js +390 -0
- package/dist/core/context/index.d.ts +22 -10
- package/dist/core/context/index.d.ts.map +1 -1
- package/dist/core/context/index.js +381 -119
- package/dist/core/context/observation.d.ts +26 -0
- package/dist/core/context/observation.d.ts.map +1 -0
- package/dist/core/context/observation.js +11 -0
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +25 -14
- package/dist/core/diagnostics/path-identity.d.ts +3 -0
- package/dist/core/diagnostics/path-identity.d.ts.map +1 -0
- package/dist/core/diagnostics/path-identity.js +12 -0
- package/dist/core/diagnostics/sanitize.d.ts.map +1 -1
- package/dist/core/diagnostics/sanitize.js +4 -3
- package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
- package/dist/core/events/v3/authority-outbox.js +29 -0
- package/dist/core/events/v3/generation-custody.d.ts +5 -0
- package/dist/core/events/v3/generation-custody.d.ts.map +1 -0
- package/dist/core/events/v3/generation-custody.js +50 -0
- package/dist/core/events/v3/producers/coordination-recorder.d.ts +1 -0
- package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/coordination-recorder.js +7 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +77 -64
- package/dist/core/events/v3/support-storage/pack-reader.d.ts.map +1 -1
- package/dist/core/events/v3/support-storage/pack-reader.js +42 -16
- package/dist/core/governor/planning.d.ts.map +1 -1
- package/dist/core/governor/planning.js +14 -1
- package/dist/core/governor/service-read.d.ts +10 -2
- package/dist/core/governor/service-read.d.ts.map +1 -1
- package/dist/core/governor/service-read.js +105 -22
- package/dist/core/governor/service.d.ts +20 -2
- package/dist/core/governor/service.d.ts.map +1 -1
- package/dist/core/governor/service.js +72 -47
- package/dist/core/hooks/cli.js +120 -23
- package/dist/core/hooks/fork-lineage.d.ts +40 -0
- package/dist/core/hooks/fork-lineage.d.ts.map +1 -0
- package/dist/core/hooks/fork-lineage.js +62 -0
- package/dist/core/hooks/resolve/transcript.d.ts +16 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +84 -0
- package/dist/core/hooks/shell-rm.d.ts +2 -0
- package/dist/core/hooks/shell-rm.d.ts.map +1 -0
- package/dist/core/hooks/shell-rm.js +105 -0
- package/dist/core/hooks/shell-tokens.d.ts +12 -0
- package/dist/core/hooks/shell-tokens.d.ts.map +1 -0
- package/dist/core/hooks/shell-tokens.js +97 -0
- package/dist/core/hooks/shell-waiter.d.ts.map +1 -1
- package/dist/core/hooks/shell-waiter.js +3 -93
- package/dist/core/inbox/contract.d.ts +2 -1
- package/dist/core/inbox/contract.d.ts.map +1 -1
- package/dist/core/inbox/index.d.ts +2 -0
- package/dist/core/inbox/index.d.ts.map +1 -1
- package/dist/core/inbox/index.js +2 -0
- package/dist/core/inbox/peer-messages.d.ts +37 -0
- package/dist/core/inbox/peer-messages.d.ts.map +1 -0
- package/dist/core/inbox/peer-messages.js +72 -0
- package/dist/core/inbox/service.d.ts +8 -1
- package/dist/core/inbox/service.d.ts.map +1 -1
- package/dist/core/inbox/service.js +67 -21
- package/dist/core/inbox/targets.d.ts +22 -0
- package/dist/core/inbox/targets.d.ts.map +1 -0
- package/dist/core/inbox/targets.js +108 -0
- package/dist/core/inbox/watcher.js +7 -3
- package/dist/core/journal/index.d.ts.map +1 -1
- package/dist/core/journal/index.js +17 -2
- package/dist/core/resources/contract.d.ts +3 -0
- package/dist/core/resources/contract.d.ts.map +1 -1
- package/dist/core/resources/service-status.d.ts +14 -1
- package/dist/core/resources/service-status.d.ts.map +1 -1
- package/dist/core/resources/service-status.js +23 -13
- package/dist/core/resources/service.d.ts +9 -1
- package/dist/core/resources/service.d.ts.map +1 -1
- package/dist/core/resources/service.js +47 -27
- package/dist/core/resources/status.d.ts +2 -5
- package/dist/core/resources/status.d.ts.map +1 -1
- package/dist/core/resources/status.js +36 -7
- package/dist/core/semantic/contract.d.ts +5 -5
- package/dist/core/servers/index.d.ts +24 -13
- package/dist/core/servers/index.d.ts.map +1 -1
- package/dist/core/servers/index.js +132 -92
- package/dist/core/servers/tunnels.d.ts +3 -5
- package/dist/core/servers/tunnels.d.ts.map +1 -1
- package/dist/core/servers/tunnels.js +3 -9
- package/dist/core/storage/process-log.d.ts +12 -0
- package/dist/core/storage/process-log.d.ts.map +1 -1
- package/dist/core/storage/process-log.js +10 -2
- package/dist/core/supervisor/contract.d.ts +12 -0
- package/dist/core/supervisor/contract.d.ts.map +1 -1
- package/dist/core/supervisor/explanations.d.ts.map +1 -1
- package/dist/core/supervisor/explanations.js +2 -1
- package/dist/core/supervisor/findings.d.ts.map +1 -1
- package/dist/core/supervisor/findings.js +54 -9
- package/dist/core/supervisor/service.d.ts +9 -1
- package/dist/core/supervisor/service.d.ts.map +1 -1
- package/dist/core/supervisor/service.js +59 -29
- package/dist/core/supervisor/services.d.ts.map +1 -1
- package/dist/core/supervisor/services.js +22 -1
- package/dist/core/supervisor/status.d.ts +17 -2
- package/dist/core/supervisor/status.d.ts.map +1 -1
- package/dist/core/supervisor/status.js +70 -12
- package/dist/core/supervisor/timeline.d.ts.map +1 -1
- package/dist/core/supervisor/timeline.js +2 -1
- package/dist/core/work/state.js +7 -7
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +105 -32
- package/dist/core/workflow/failure-evidence.d.ts +16 -0
- package/dist/core/workflow/failure-evidence.d.ts.map +1 -0
- package/dist/core/workflow/failure-evidence.js +114 -0
- package/dist/core/workflow/index.d.ts +1 -1
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +1 -1
- package/dist/core/workflow/live-session-v3.d.ts +6 -0
- package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
- package/dist/core/workflow/live-session-v3.js +126 -5
- package/dist/core/workflow/proof.d.ts +5 -5
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +115 -20
- package/dist/core/workflow/run-state.d.ts +3 -1
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +31 -27
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +19 -0
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +13 -0
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +19 -0
- package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
- package/dist/core/workflow/spawn-opencode.js +14 -0
- package/dist/core/workflow/types.d.ts +64 -17
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/index.d.ts +0 -1
- package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/index.js +0 -1
- package/dist/core/workflow/workspaces/leases.d.ts +3 -1
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.js +30 -16
- package/dist/core/workflow/workspaces/local-git.d.ts +6 -2
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.js +270 -6
- package/dist/core/workflow/workspaces/ownership.d.ts +11 -0
- package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/ownership.js +194 -0
- package/dist/core/workflow/workspaces/reclaim.d.ts +35 -0
- package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/reclaim.js +14 -0
- package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/recovery.js +21 -13
- package/dist/core/workflow/workspaces/state.d.ts +1 -0
- package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/state.js +21 -1
- package/dist/lib/browser/windows-chrome.d.ts +100 -12
- package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
- package/dist/lib/browser/windows-chrome.js +262 -73
- package/dist/lib/docs-links.d.ts.map +1 -1
- package/dist/lib/docs-links.js +45 -2
- package/dist/lib/guarded-remove.d.ts +7 -0
- package/dist/lib/guarded-remove.d.ts.map +1 -1
- package/dist/lib/guarded-remove.js +106 -12
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +4 -0
- package/dist/lib/tunnel/allowlist.d.ts +44 -0
- package/dist/lib/tunnel/allowlist.d.ts.map +1 -0
- package/dist/lib/tunnel/allowlist.js +204 -0
- package/dist/lib/tunnel/current-address.d.ts +41 -0
- package/dist/lib/tunnel/current-address.d.ts.map +1 -0
- package/dist/lib/tunnel/current-address.js +123 -0
- package/dist/lib/tunnel/gate.js +13 -6
- package/dist/lib/tunnel/processes.d.ts +39 -0
- package/dist/lib/tunnel/processes.d.ts.map +1 -0
- package/dist/lib/tunnel/processes.js +121 -0
- package/dist/lib/tunnel/state.d.ts +28 -3
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +98 -27
- package/dist/lib/zip/index.d.ts +41 -0
- package/dist/lib/zip/index.d.ts.map +1 -0
- package/dist/lib/zip/index.js +398 -0
- package/package.json +8 -1
- package/src/commander.ts +32 -0
- package/src/commands/agents.ts +151 -74
- package/src/commands/browse.ts +50 -14
- package/src/commands/diagnostics.ts +10 -0
- package/src/commands/doctor.ts +26 -7
- package/src/commands/governor.ts +23 -10
- package/src/commands/inbox.ts +52 -2
- package/src/commands/rm.ts +43 -4
- package/src/commands/servers.ts +16 -4
- package/src/commands/supervisor.ts +7 -2
- package/src/commands/toc.ts +14 -6
- package/src/commands/tunnel.ts +394 -116
- package/src/commands/web-fetch.ts +30 -19
- package/src/commands/web.ts +9 -3
- package/src/commands/workflow.ts +30 -30
- package/src/core/agents/live-authority-v3.ts +17 -0
- package/src/core/agents/live-lifecycle-v3.ts +52 -14
- package/src/core/agents/mailbox.ts +26 -17
- package/src/core/agents/render/prompt-context.ts +35 -20
- package/src/core/agents/render/session-context.ts +92 -48
- package/src/core/agents/rules/stop-hook.ts +3 -3
- package/src/core/agents/state/heartbeat-writer.ts +29 -12
- package/src/core/agents/state/live-coordination-writer.ts +29 -0
- package/src/core/agents/state/names.ts +117 -6
- package/src/core/agents/state/pidmap.ts +13 -0
- package/src/core/agents/state/proc-start.ts +57 -0
- package/src/core/agents/state/stale-sweep.ts +247 -82
- package/src/core/agents/state/unchanged-file.ts +456 -0
- package/src/core/context/index.ts +446 -136
- package/src/core/context/observation.ts +29 -0
- package/src/core/diagnostics/bundle.ts +23 -18
- package/src/core/diagnostics/path-identity.ts +15 -0
- package/src/core/diagnostics/sanitize.ts +4 -3
- package/src/core/events/v3/authority-outbox.ts +56 -0
- package/src/core/events/v3/generation-custody.ts +59 -0
- package/src/core/events/v3/producers/coordination-recorder.ts +15 -0
- package/src/core/events/v3/producers/recorder.ts +94 -68
- package/src/core/events/v3/support-storage/pack-reader.ts +45 -19
- package/src/core/governor/planning.ts +20 -8
- package/src/core/governor/service-read.ts +128 -20
- package/src/core/governor/service.ts +119 -42
- package/src/core/hooks/cli.ts +139 -22
- package/src/core/hooks/fork-lineage.ts +82 -0
- package/src/core/hooks/resolve/transcript.ts +88 -0
- package/src/core/hooks/shell-rm.ts +103 -0
- package/src/core/hooks/shell-tokens.ts +96 -0
- package/src/core/hooks/shell-waiter.ts +4 -91
- package/src/core/inbox/contract.ts +2 -1
- package/src/core/inbox/index.ts +2 -0
- package/src/core/inbox/peer-messages.ts +94 -0
- package/src/core/inbox/service.ts +88 -22
- package/src/core/inbox/targets.ts +148 -0
- package/src/core/inbox/watcher.ts +8 -9
- package/src/core/journal/index.ts +19 -2
- package/src/core/resources/contract.ts +4 -0
- package/src/core/resources/service-status.ts +35 -12
- package/src/core/resources/service.ts +71 -24
- package/src/core/resources/status.ts +57 -8
- package/src/core/servers/index.ts +177 -91
- package/src/core/servers/tunnels.ts +3 -11
- package/src/core/storage/process-log.ts +17 -2
- package/src/core/supervisor/contract.ts +13 -0
- package/src/core/supervisor/explanations.ts +4 -1
- package/src/core/supervisor/findings.ts +68 -10
- package/src/core/supervisor/service.ts +88 -26
- package/src/core/supervisor/services.ts +26 -8
- package/src/core/supervisor/status.ts +89 -12
- package/src/core/supervisor/timeline.ts +2 -1
- package/src/core/work/state.ts +7 -7
- package/src/core/workflow/engine.ts +131 -49
- package/src/core/workflow/failure-evidence.ts +141 -0
- package/src/core/workflow/index.ts +0 -1
- package/src/core/workflow/live-session-v3.ts +146 -5
- package/src/core/workflow/proof.ts +134 -26
- package/src/core/workflow/run-state.ts +31 -29
- package/src/core/workflow/spawn-claude.ts +18 -0
- package/src/core/workflow/spawn-codex.ts +13 -0
- package/src/core/workflow/spawn-cursor.ts +18 -0
- package/src/core/workflow/spawn-opencode.ts +14 -0
- package/src/core/workflow/types.ts +51 -17
- package/src/core/workflow/workspaces/index.ts +0 -1
- package/src/core/workflow/workspaces/leases.ts +48 -17
- package/src/core/workflow/workspaces/local-git.ts +315 -5
- package/src/core/workflow/workspaces/ownership.ts +232 -0
- package/src/core/workflow/workspaces/reclaim.ts +58 -0
- package/src/core/workflow/workspaces/recovery.ts +25 -13
- package/src/core/workflow/workspaces/state.ts +20 -0
- package/src/lib/browser/windows-chrome.ts +346 -86
- package/src/lib/docs-links.ts +49 -2
- package/src/lib/guarded-remove.ts +132 -14
- package/src/lib/identities/assume.ts +4 -0
- package/src/lib/tunnel/allowlist.ts +208 -0
- package/src/lib/tunnel/current-address.ts +158 -0
- package/src/lib/tunnel/gate.ts +16 -8
- package/src/lib/tunnel/processes.ts +149 -0
- package/src/lib/tunnel/state.ts +123 -27
- package/src/lib/zip/index.ts +483 -0
|
@@ -2,12 +2,16 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import { stateDirMode, stateFileMode } from "../../storage/modes.js";
|
|
5
|
+
import { acquireWorkflowResumeLease } from "../run-state.js";
|
|
5
6
|
import { WorkspaceAttestationError } from "./attestation-error.js";
|
|
7
|
+
import { cleanupWorkspace } from "./cleanup.js";
|
|
6
8
|
import { git, gitMaybe, gitMaybeWithDirectoryDescriptor, gitOperations, isAncestor, nulEntries, overwriteRiskPaths, parseWorktreeInventory, worktreeInventory, } from "./git.js";
|
|
7
9
|
import { acquireNoClobberLease } from "./leases.js";
|
|
10
|
+
import { claimOverlapsWorkspace, reserveWorkspaceReclaim, } from "./ownership.js";
|
|
8
11
|
import { assertContainedExisting, assertPathIdentity, candidateUnderRoot, containsPath, createContainedDirectories, descriptorBackedPathsSupported, descriptorPath, filesystemIdentity, openContainedDirectory, sameFilesystemIdentity, validateConfiguredRoot, } from "./paths.js";
|
|
12
|
+
import { prepareReclaim, WorkflowReclaimError, } from "./reclaim.js";
|
|
9
13
|
import { readWorkflowLiveness } from "./recovery.js";
|
|
10
|
-
import { appendWorkspaceEvent, fileSha256, readWorkflowSupplement, readWorkspaceBinding, readWorkspaceClaim, readWorkspaceEvents, stableDigest, workspaceClaimPath, workspaceLockPath, writeWorkspaceBinding, writeWorkspaceClaim, } from "./state.js";
|
|
14
|
+
import { appendWorkspaceEvent, fileSha256, readWorkflowSupplement, readWorkspaceBinding, readWorkspaceClaim, readWorkspaceClaims, readWorkspaceEvents, stableDigest, workspaceClaimPath, workspaceLockPath, writeWorkspaceBinding, writeWorkspaceClaim, } from "./state.js";
|
|
11
15
|
import { isWorkspaceAttestation } from "./validate.js";
|
|
12
16
|
const PROVIDER_ID = "local-git-worktree";
|
|
13
17
|
const PROVIDER_VERSION = "1";
|
|
@@ -17,7 +21,7 @@ const OBJECT_FORMAT = /^[a-f0-9]{40,64}$/;
|
|
|
17
21
|
const REF_FORMAT = /^refs\/heads\/[A-Za-z0-9][A-Za-z0-9._/-]{0,180}$/;
|
|
18
22
|
export function createLocalGitWorktreeProvider(options) {
|
|
19
23
|
const coordRoot = resolve(options.coordRoot);
|
|
20
|
-
|
|
24
|
+
const provider = {
|
|
21
25
|
probe: (input) => probe(input, options.descriptorPathsSupported),
|
|
22
26
|
allocate: (request) => allocate(coordRoot, request),
|
|
23
27
|
readBinding: async (binding) => {
|
|
@@ -30,7 +34,9 @@ export function createLocalGitWorktreeProvider(options) {
|
|
|
30
34
|
previewIntegration: (input) => previewIntegration(coordRoot, input),
|
|
31
35
|
applyAuthorizedIntegration: (input) => applyAuthorizedIntegration(coordRoot, input),
|
|
32
36
|
cleanup: (binding, intent) => cleanup(coordRoot, binding, intent),
|
|
37
|
+
reclaim: (binding, reclaimOptions) => reclaim(coordRoot, provider, binding, reclaimOptions),
|
|
33
38
|
};
|
|
39
|
+
return provider;
|
|
34
40
|
}
|
|
35
41
|
export async function probe(input, descriptorPathsSupported = descriptorBackedPathsSupported) {
|
|
36
42
|
const unsupported = [];
|
|
@@ -585,6 +591,239 @@ function cancelUnderLease(coordRoot, binding, claim) {
|
|
|
585
591
|
appendWorkspaceEvent(coordRoot, claim, "cancellation_outcome", { status: "cancelled" });
|
|
586
592
|
return { ...base, status: "cancelled" };
|
|
587
593
|
}
|
|
594
|
+
function reclaimPreview(coordRoot, binding, mode) {
|
|
595
|
+
const preview = {
|
|
596
|
+
action: "preview",
|
|
597
|
+
run_id: binding.run_id,
|
|
598
|
+
mode,
|
|
599
|
+
liveness: readWorkflowLiveness(coordRoot, binding.run_id),
|
|
600
|
+
ownership: "unproven",
|
|
601
|
+
dirty_paths: null,
|
|
602
|
+
lease_holder: reclaimLeaseHolders(coordRoot, binding),
|
|
603
|
+
};
|
|
604
|
+
try {
|
|
605
|
+
const current = attestSync(coordRoot, binding);
|
|
606
|
+
if (current.status === "ok") {
|
|
607
|
+
preview.ownership = "ok";
|
|
608
|
+
preview.dirty_paths = [...(current.repository?.dirty_paths ?? [])];
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
preview.ownership_detail =
|
|
612
|
+
current.provider_drift.join("; ") || "ownership attestation failed";
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
catch (error) {
|
|
616
|
+
preview.ownership_detail = error.message;
|
|
617
|
+
}
|
|
618
|
+
return preview;
|
|
619
|
+
}
|
|
620
|
+
function assertReclaimLiveness(coordRoot, binding) {
|
|
621
|
+
const liveness = readWorkflowLiveness(coordRoot, binding.run_id);
|
|
622
|
+
if (liveness === "inactive")
|
|
623
|
+
return;
|
|
624
|
+
throw new WorkflowReclaimError(liveness === "active" ? "workflow_reclaim_run_live" : "workflow_reclaim_liveness_unknown", liveness === "active"
|
|
625
|
+
? `run ${binding.run_id} is active; its workspace was left untouched`
|
|
626
|
+
: `run ${binding.run_id} has missing, corrupt, or ambiguous liveness evidence; its workspace was left untouched`);
|
|
627
|
+
}
|
|
628
|
+
function assertReclaimOwnership(coordRoot, binding) {
|
|
629
|
+
try {
|
|
630
|
+
const claim = requiredClaim(coordRoot, binding);
|
|
631
|
+
const current = attestSync(coordRoot, binding);
|
|
632
|
+
if (current.status !== "ok") {
|
|
633
|
+
throw new Error(current.provider_drift.join("; ") || "ownership attestation failed");
|
|
634
|
+
}
|
|
635
|
+
return claim;
|
|
636
|
+
}
|
|
637
|
+
catch (error) {
|
|
638
|
+
throw new WorkflowReclaimError("workflow_reclaim_ownership_unproven", error.message);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
function assertPeerWorkspaceAuthority(coordRoot, claim) {
|
|
642
|
+
try {
|
|
643
|
+
for (const peer of readWorkspaceClaims(coordRoot, PROVIDER_ID)) {
|
|
644
|
+
if (peer.binding_id === claim.binding_id)
|
|
645
|
+
continue;
|
|
646
|
+
const binding = readWorkspaceBinding(coordRoot, PROVIDER_ID, peer.binding_id);
|
|
647
|
+
const events = readWorkspaceEvents(coordRoot, PROVIDER_ID, peer.binding_id);
|
|
648
|
+
if (binding)
|
|
649
|
+
requiredClaim(coordRoot, binding);
|
|
650
|
+
else if (events.some((event) => event.event === "bound")) {
|
|
651
|
+
throw new Error("retained peer binding authority is missing");
|
|
652
|
+
}
|
|
653
|
+
if (!claimOverlapsWorkspace(peer, claim.workspace_root))
|
|
654
|
+
continue;
|
|
655
|
+
// Process termination, archival and an expired reservation do not release
|
|
656
|
+
// a resource. Require the provider's release record AND current absence.
|
|
657
|
+
if (binding && events.some((event) => event.event === "released")) {
|
|
658
|
+
const current = attestSync(coordRoot, binding);
|
|
659
|
+
if (current.status === "ok" && current.resource_state === "released")
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
throw new Error(`retained peer workspace ${peer.binding_id} overlaps the reclaim path`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
catch (error) {
|
|
666
|
+
throw new WorkflowReclaimError("workflow_reclaim_ownership_unproven", error.message);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
function reclaimAuthorityGuard(coordRoot, binding, claim, reservation) {
|
|
670
|
+
try {
|
|
671
|
+
reservation.assertCurrent();
|
|
672
|
+
assertReclaimLiveness(coordRoot, binding);
|
|
673
|
+
assertReclaimOwnership(coordRoot, binding);
|
|
674
|
+
assertPeerWorkspaceAuthority(coordRoot, claim);
|
|
675
|
+
}
|
|
676
|
+
catch (error) {
|
|
677
|
+
if (error instanceof WorkflowReclaimError)
|
|
678
|
+
throw error;
|
|
679
|
+
throw new WorkflowReclaimError("workflow_reclaim_ownership_unproven", error.message);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
async function reclaim(coordRoot, provider, binding, options) {
|
|
683
|
+
if (options.mode !== "salvage" && options.mode !== "discard") {
|
|
684
|
+
throw new Error("unsupported workspace reclaim mode");
|
|
685
|
+
}
|
|
686
|
+
if (!options.yes)
|
|
687
|
+
return reclaimPreview(coordRoot, binding, options.mode);
|
|
688
|
+
// Liveness comes before terminal-proof cleanup helpers. A live run may not
|
|
689
|
+
// have a proof yet, and a missing proof must never be discovered after reset.
|
|
690
|
+
assertReclaimLiveness(coordRoot, binding);
|
|
691
|
+
const claim = assertReclaimOwnership(coordRoot, binding);
|
|
692
|
+
let reservation;
|
|
693
|
+
try {
|
|
694
|
+
reservation = reserveWorkspaceReclaim(coordRoot, claim);
|
|
695
|
+
}
|
|
696
|
+
catch (error) {
|
|
697
|
+
throw new WorkflowReclaimError("workflow_reclaim_lease_held", error.message);
|
|
698
|
+
}
|
|
699
|
+
try {
|
|
700
|
+
reclaimAuthorityGuard(coordRoot, binding, claim, reservation);
|
|
701
|
+
let releaseResume;
|
|
702
|
+
try {
|
|
703
|
+
releaseResume = acquireWorkflowResumeLease(coordRoot, binding.run_id);
|
|
704
|
+
}
|
|
705
|
+
catch (error) {
|
|
706
|
+
throw new WorkflowReclaimError("workflow_reclaim_lease_held", error.message);
|
|
707
|
+
}
|
|
708
|
+
try {
|
|
709
|
+
let releaseBinding;
|
|
710
|
+
try {
|
|
711
|
+
releaseBinding = acquireLease(coordRoot, workspaceLockPath(coordRoot, PROVIDER_ID, binding.binding_id), claim, "binding");
|
|
712
|
+
}
|
|
713
|
+
catch (error) {
|
|
714
|
+
throw new WorkflowReclaimError("workflow_reclaim_lease_held", error.message);
|
|
715
|
+
}
|
|
716
|
+
try {
|
|
717
|
+
let releaseRepository;
|
|
718
|
+
try {
|
|
719
|
+
releaseRepository = acquireRepositoryLease(coordRoot, claim);
|
|
720
|
+
}
|
|
721
|
+
catch (error) {
|
|
722
|
+
throw new WorkflowReclaimError("workflow_reclaim_lease_held", error.message);
|
|
723
|
+
}
|
|
724
|
+
try {
|
|
725
|
+
assertReclaimLiveness(coordRoot, binding);
|
|
726
|
+
assertReclaimOwnership(coordRoot, binding);
|
|
727
|
+
const preview = reclaimPreview(coordRoot, binding, options.mode);
|
|
728
|
+
let preparation;
|
|
729
|
+
// Keep ordinary cleanup's terminal-evidence checks, intent and receipts.
|
|
730
|
+
// Its provider callback uses the leases already held here instead of
|
|
731
|
+
// trying to acquire them again. Preparation occurs only after all of
|
|
732
|
+
// cleanup's path, ownership, ref and registration checks have succeeded.
|
|
733
|
+
const cleanupResult = await cleanupWorkspace({
|
|
734
|
+
coordRoot,
|
|
735
|
+
runId: binding.run_id,
|
|
736
|
+
provider: {
|
|
737
|
+
...provider,
|
|
738
|
+
cleanup: async (candidate, intent) => {
|
|
739
|
+
assertReclaimLiveness(coordRoot, candidate);
|
|
740
|
+
assertReclaimOwnership(coordRoot, candidate);
|
|
741
|
+
return cleanupUnderLease(coordRoot, candidate, claim, intent, {
|
|
742
|
+
mode: options.mode,
|
|
743
|
+
beforeEffect: () => reclaimAuthorityGuard(coordRoot, candidate, claim, reservation),
|
|
744
|
+
prepared: (value) => {
|
|
745
|
+
preparation = value;
|
|
746
|
+
},
|
|
747
|
+
});
|
|
748
|
+
},
|
|
749
|
+
},
|
|
750
|
+
});
|
|
751
|
+
if (!preparation &&
|
|
752
|
+
cleanupResult.status !== "released" &&
|
|
753
|
+
cleanupResult.status !== "already_released") {
|
|
754
|
+
throw new WorkflowReclaimError("workflow_reclaim_ownership_unproven", ("reason" in cleanupResult ? cleanupResult.reason : undefined) ??
|
|
755
|
+
"workspace cleanup preconditions could not be proved");
|
|
756
|
+
}
|
|
757
|
+
return {
|
|
758
|
+
...preview,
|
|
759
|
+
action: "reclaimed",
|
|
760
|
+
lease_holder: [],
|
|
761
|
+
preparation: preparation ?? {
|
|
762
|
+
action: "already_gone",
|
|
763
|
+
detail: "the workspace is already released",
|
|
764
|
+
},
|
|
765
|
+
cleanup: cleanupResult,
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
finally {
|
|
769
|
+
releaseRepository();
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
finally {
|
|
773
|
+
releaseBinding();
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
finally {
|
|
777
|
+
releaseResume();
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
finally {
|
|
781
|
+
reservation.release();
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
function reclaimLeaseHolders(coordRoot, binding) {
|
|
785
|
+
const paths = [
|
|
786
|
+
["binding", workspaceLockPath(coordRoot, PROVIDER_ID, binding.binding_id)],
|
|
787
|
+
];
|
|
788
|
+
if (binding.repository) {
|
|
789
|
+
const key = stableDigest({ common: binding.repository.common_dir.identity });
|
|
790
|
+
paths.push([
|
|
791
|
+
"repository",
|
|
792
|
+
join(coordRoot, ".harnery", "workspaces", PROVIDER_ID, ".leases", `repository-${key}.lock`),
|
|
793
|
+
]);
|
|
794
|
+
}
|
|
795
|
+
const holders = [];
|
|
796
|
+
try {
|
|
797
|
+
const owner = JSON.parse(readFileSync(join(coordRoot, ".harnery", "workflows", binding.run_id, "resume.lock"), "utf8"));
|
|
798
|
+
holders.push({
|
|
799
|
+
scope: "resume",
|
|
800
|
+
pid: typeof owner.pid === "number" ? owner.pid : undefined,
|
|
801
|
+
host: typeof owner.host === "string" ? owner.host : undefined,
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
catch (error) {
|
|
805
|
+
if (error.code !== "ENOENT") {
|
|
806
|
+
holders.push({ scope: "resume", detail: "lease holder could not be read" });
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
for (const [scope, path] of paths) {
|
|
810
|
+
try {
|
|
811
|
+
const owner = JSON.parse(readFileSync(join(path, "current"), "utf8"));
|
|
812
|
+
holders.push({
|
|
813
|
+
scope,
|
|
814
|
+
owner_id: typeof owner.owner_id === "string" ? owner.owner_id : undefined,
|
|
815
|
+
pid: typeof owner.pid === "number" ? owner.pid : undefined,
|
|
816
|
+
host: typeof owner.host === "string" ? owner.host : undefined,
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
catch (error) {
|
|
820
|
+
if (error.code !== "ENOENT") {
|
|
821
|
+
holders.push({ scope, detail: "lease holder could not be read" });
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return holders;
|
|
826
|
+
}
|
|
588
827
|
async function cleanup(coordRoot, binding, intent) {
|
|
589
828
|
if (intent.mode !== "normal")
|
|
590
829
|
throw new Error("unsupported workspace cleanup mode");
|
|
@@ -593,7 +832,18 @@ async function cleanup(coordRoot, binding, intent) {
|
|
|
593
832
|
try {
|
|
594
833
|
const releaseRepository = acquireRepositoryLease(coordRoot, claim);
|
|
595
834
|
try {
|
|
596
|
-
|
|
835
|
+
const reservation = reserveWorkspaceReclaim(coordRoot, claim);
|
|
836
|
+
try {
|
|
837
|
+
const authorityGuard = () => {
|
|
838
|
+
reservation.assertCurrent();
|
|
839
|
+
assertPeerWorkspaceAuthority(coordRoot, claim);
|
|
840
|
+
};
|
|
841
|
+
authorityGuard();
|
|
842
|
+
return cleanupUnderLease(coordRoot, binding, claim, intent, undefined, authorityGuard);
|
|
843
|
+
}
|
|
844
|
+
finally {
|
|
845
|
+
reservation.release();
|
|
846
|
+
}
|
|
597
847
|
}
|
|
598
848
|
finally {
|
|
599
849
|
releaseRepository();
|
|
@@ -603,7 +853,7 @@ async function cleanup(coordRoot, binding, intent) {
|
|
|
603
853
|
releaseBinding();
|
|
604
854
|
}
|
|
605
855
|
}
|
|
606
|
-
function cleanupUnderLease(coordRoot, binding, claim, intent) {
|
|
856
|
+
function cleanupUnderLease(coordRoot, binding, claim, intent, reclaimRequest, beforeRemoval) {
|
|
607
857
|
const repository = binding.repository;
|
|
608
858
|
if (!repository ||
|
|
609
859
|
intent.schema_version !== 1 ||
|
|
@@ -653,10 +903,11 @@ function cleanupUnderLease(coordRoot, binding, claim, intent) {
|
|
|
653
903
|
if (before.workspace_exists && before.status !== "ok") {
|
|
654
904
|
return { ...base, status: "blocked", reason: "ownership attestation failed" };
|
|
655
905
|
}
|
|
656
|
-
if ((before.repository?.dirty_paths.length ?? 0) > 0 ||
|
|
906
|
+
if ((!reclaimRequest && (before.repository?.dirty_paths.length ?? 0) > 0) ||
|
|
657
907
|
(before.repository?.conflicts.length ?? 0) > 0 ||
|
|
658
908
|
(before.repository?.operation_in_progress.length ?? 0) > 0) {
|
|
659
|
-
|
|
909
|
+
if (!reclaimRequest)
|
|
910
|
+
appendWorkspaceEvent(coordRoot, claim, "preserved_dirty");
|
|
660
911
|
return {
|
|
661
912
|
...base,
|
|
662
913
|
status: "preserved_dirty",
|
|
@@ -710,6 +961,17 @@ function cleanupUnderLease(coordRoot, binding, claim, intent) {
|
|
|
710
961
|
reason: "missing worktree still has ambiguous Git registration",
|
|
711
962
|
};
|
|
712
963
|
}
|
|
964
|
+
if (reclaimRequest) {
|
|
965
|
+
assertReclaimLiveness(coordRoot, binding);
|
|
966
|
+
assertReclaimOwnership(coordRoot, binding);
|
|
967
|
+
revalidateClaimResources(claim, root);
|
|
968
|
+
reclaimRequest.prepared(prepareReclaim({
|
|
969
|
+
worktreePath: binding.workspace_root,
|
|
970
|
+
mode: reclaimRequest.mode,
|
|
971
|
+
runId: binding.run_id,
|
|
972
|
+
beforeEffect: reclaimRequest.beforeEffect,
|
|
973
|
+
}));
|
|
974
|
+
}
|
|
713
975
|
appendWorkspaceEvent(coordRoot, claim, "cleanup_pending", {
|
|
714
976
|
worktree_path: binding.workspace_root,
|
|
715
977
|
gitdir: repository.gitdir,
|
|
@@ -719,6 +981,8 @@ function cleanupUnderLease(coordRoot, binding, claim, intent) {
|
|
|
719
981
|
target_ref_oid: targetRef.out,
|
|
720
982
|
});
|
|
721
983
|
if (before.workspace_exists) {
|
|
984
|
+
reclaimRequest?.beforeEffect();
|
|
985
|
+
beforeRemoval?.();
|
|
722
986
|
revalidateClaimResources(claim, root);
|
|
723
987
|
assertContainedExisting(root, binding.workspace_root, "workspace cleanup path");
|
|
724
988
|
git(repository.source_root.realpath, ["worktree", "remove", binding.workspace_root]);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { WorkspaceClaim } from "./state.js";
|
|
2
|
+
export interface WorkspaceReclaimReservation {
|
|
3
|
+
assertCurrent(): void;
|
|
4
|
+
release(): void;
|
|
5
|
+
}
|
|
6
|
+
/** The immutable claim is the authority; the lease reserves its destructive path. */
|
|
7
|
+
export declare function reserveWorkspaceReclaim(coordRoot: string, claim: WorkspaceClaim): WorkspaceReclaimReservation;
|
|
8
|
+
export declare function publishWorkspaceClaim<T>(coordRoot: string, claim: WorkspaceClaim, publish: () => T): T;
|
|
9
|
+
export declare function assertClaimAuthority(claim: WorkspaceClaim, providerId: string, id: string): void;
|
|
10
|
+
export declare function claimOverlapsWorkspace(claim: WorkspaceClaim, workspaceRoot: string): boolean;
|
|
11
|
+
//# sourceMappingURL=ownership.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ownership.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/ownership.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AA8CjD,MAAM,WAAW,2BAA2B;IAC1C,aAAa,IAAI,IAAI,CAAC;IACtB,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,qFAAqF;AACrF,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,cAAc,GACpB,2BAA2B,CAgC7B;AAED,wBAAgB,qBAAqB,CAAC,CAAC,EACrC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,cAAc,EACrB,OAAO,EAAE,MAAM,CAAC,GACf,CAAC,CA8DH;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAwBhG;AAmCD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAU5F"}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import { readJsonRecord, stableDigest } from "../durable-record.js";
|
|
4
|
+
import { acquireNoClobberLease } from "./leases.js";
|
|
5
|
+
import { containsPath } from "./paths.js";
|
|
6
|
+
import { isWorkspaceAllocationRequest } from "./validate.js";
|
|
7
|
+
const STALE_MS = 5 * 60 * 1_000;
|
|
8
|
+
const RESERVATION_SCOPE = "workspace-reclaim";
|
|
9
|
+
function leaseRoot(coordRoot, providerId) {
|
|
10
|
+
return join(resolve(coordRoot), ".harnery", "workspaces", providerId, ".leases");
|
|
11
|
+
}
|
|
12
|
+
/** Serialize only publication and reservation registration, never Git work. */
|
|
13
|
+
function withPublicationGate(coordRoot, providerId, operation) {
|
|
14
|
+
const authority = stableDigest({ provider_id: providerId, scope: "workspace-publication" });
|
|
15
|
+
const gate = acquireNoClobberLease({
|
|
16
|
+
path: join(leaseRoot(coordRoot, providerId), "publication.lock"),
|
|
17
|
+
scope: "workspace-publication",
|
|
18
|
+
authoritySha256: authority,
|
|
19
|
+
staleAfterMs: STALE_MS,
|
|
20
|
+
validateStaleOwner: (owner) => owner.authority_sha256 === authority,
|
|
21
|
+
});
|
|
22
|
+
try {
|
|
23
|
+
return operation();
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
gate.release();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function reservationPath(coordRoot, claim) {
|
|
30
|
+
return join(leaseRoot(coordRoot, claim.provider_id), `reclaim-${claim.binding_id}.lock`);
|
|
31
|
+
}
|
|
32
|
+
function acquireReservation(coordRoot, claim) {
|
|
33
|
+
const authority = stableDigest(claim);
|
|
34
|
+
return acquireNoClobberLease({
|
|
35
|
+
path: reservationPath(coordRoot, claim),
|
|
36
|
+
scope: RESERVATION_SCOPE,
|
|
37
|
+
authoritySha256: authority,
|
|
38
|
+
staleAfterMs: STALE_MS,
|
|
39
|
+
metadata: { binding_id: claim.binding_id, request_sha256: claim.request_sha256 },
|
|
40
|
+
validateStaleOwner: (owner) => owner.authority_sha256 === authority &&
|
|
41
|
+
owner.metadata?.binding_id === claim.binding_id &&
|
|
42
|
+
owner.metadata?.request_sha256 === claim.request_sha256,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/** The immutable claim is the authority; the lease reserves its destructive path. */
|
|
46
|
+
export function reserveWorkspaceReclaim(coordRoot, claim) {
|
|
47
|
+
assertClaimAuthority(claim, claim.provider_id, claim.binding_id);
|
|
48
|
+
const lease = withPublicationGate(coordRoot, claim.provider_id, () => acquireReservation(coordRoot, claim));
|
|
49
|
+
return {
|
|
50
|
+
assertCurrent() {
|
|
51
|
+
const path = reservationPath(coordRoot, claim);
|
|
52
|
+
const current = join(path, "current");
|
|
53
|
+
const pin = join(path, lease.owner.owner_file);
|
|
54
|
+
const a = lstatSync(current, { bigint: true });
|
|
55
|
+
const b = lstatSync(pin, { bigint: true });
|
|
56
|
+
if (!a.isFile() ||
|
|
57
|
+
!b.isFile() ||
|
|
58
|
+
a.dev !== b.dev ||
|
|
59
|
+
a.ino !== b.ino ||
|
|
60
|
+
readFileSync(current, "utf8") !== readFileSync(pin, "utf8") ||
|
|
61
|
+
stableDigest(readJsonRecord(current, "reclaim reservation")) !== stableDigest(lease.owner)) {
|
|
62
|
+
throw new Error("workspace reclaim reservation owner changed");
|
|
63
|
+
}
|
|
64
|
+
const persisted = readJsonRecord(join(dirname(dirname(path)), claim.binding_id, "claim.json"), "reserved workspace claim");
|
|
65
|
+
if (stableDigest(persisted) !== stableDigest(claim)) {
|
|
66
|
+
throw new Error("workspace reclaim reservation claim changed");
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
release: () => lease.release(),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export function publishWorkspaceClaim(coordRoot, claim, publish) {
|
|
73
|
+
assertClaimAuthority(claim, claim.provider_id, claim.binding_id);
|
|
74
|
+
return withPublicationGate(coordRoot, claim.provider_id, () => {
|
|
75
|
+
for (const entry of readdirSync(leaseRoot(coordRoot, claim.provider_id), {
|
|
76
|
+
withFileTypes: true,
|
|
77
|
+
})) {
|
|
78
|
+
if (!entry.name.startsWith("reclaim-") || !entry.name.endsWith(".lock"))
|
|
79
|
+
continue;
|
|
80
|
+
if (!entry.isDirectory())
|
|
81
|
+
throw new Error("workspace reclaim reservation is not a directory");
|
|
82
|
+
const path = join(leaseRoot(coordRoot, claim.provider_id), entry.name);
|
|
83
|
+
const current = join(path, "current");
|
|
84
|
+
try {
|
|
85
|
+
lstatSync(current);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (error.code === "ENOENT")
|
|
89
|
+
continue;
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
const id = entry.name.slice("reclaim-".length, -".lock".length);
|
|
93
|
+
const reserved = readJsonRecord(join(dirname(leaseRoot(coordRoot, claim.provider_id)), id, "claim.json"), "reserved workspace claim");
|
|
94
|
+
assertClaimAuthority(reserved, claim.provider_id, id);
|
|
95
|
+
// Unknown reservation authority is never interpreted as an unrelated owner.
|
|
96
|
+
const owner = readJsonRecord(current, "reclaim reservation");
|
|
97
|
+
if (owner.schema_version !== 1 ||
|
|
98
|
+
typeof owner.owner_id !== "string" ||
|
|
99
|
+
!/^[a-f0-9-]{36}$/.test(owner.owner_id) ||
|
|
100
|
+
owner.owner_file !== `owner-${owner.owner_id}.json` ||
|
|
101
|
+
!Number.isSafeInteger(owner.pid) ||
|
|
102
|
+
owner.pid <= 0 ||
|
|
103
|
+
typeof owner.host !== "string" ||
|
|
104
|
+
owner.host.length === 0 ||
|
|
105
|
+
!Number.isFinite(Date.parse(owner.created_at)) ||
|
|
106
|
+
!Number.isFinite(Date.parse(owner.expires_at)) ||
|
|
107
|
+
Date.parse(owner.expires_at) < Date.parse(owner.created_at) ||
|
|
108
|
+
owner.scope !== RESERVATION_SCOPE ||
|
|
109
|
+
owner.authority_sha256 !== stableDigest(reserved) ||
|
|
110
|
+
owner.metadata?.binding_id !== id ||
|
|
111
|
+
owner.metadata?.request_sha256 !== reserved.request_sha256) {
|
|
112
|
+
throw new Error("workspace reclaim reservation authority is unproven");
|
|
113
|
+
}
|
|
114
|
+
const a = lstatSync(current, { bigint: true });
|
|
115
|
+
const pin = join(path, owner.owner_file);
|
|
116
|
+
const b = lstatSync(pin, { bigint: true });
|
|
117
|
+
if (!a.isFile() ||
|
|
118
|
+
!b.isFile() ||
|
|
119
|
+
a.dev !== b.dev ||
|
|
120
|
+
a.ino !== b.ino ||
|
|
121
|
+
readFileSync(current, "utf8") !== readFileSync(pin, "utf8"))
|
|
122
|
+
throw new Error("workspace reclaim reservation owner is unproven");
|
|
123
|
+
if (!claimOverlapsWorkspace(claim, reserved.workspace_root))
|
|
124
|
+
continue;
|
|
125
|
+
// This succeeds only for an expired, provably dead, exact prior owner.
|
|
126
|
+
// It retires the reservation, never the immutable claim or resource.
|
|
127
|
+
const recovered = acquireReservation(coordRoot, reserved);
|
|
128
|
+
recovered.release();
|
|
129
|
+
}
|
|
130
|
+
return publish();
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
export function assertClaimAuthority(claim, providerId, id) {
|
|
134
|
+
if (claim?.schema_version !== 1 ||
|
|
135
|
+
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(providerId) ||
|
|
136
|
+
claim.provider_id !== providerId ||
|
|
137
|
+
claim.provider_version !== "1" ||
|
|
138
|
+
claim.binding_id !== id ||
|
|
139
|
+
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(id) ||
|
|
140
|
+
!isWorkspaceAllocationRequest(claim.request) ||
|
|
141
|
+
claim.request.provider_id !== providerId ||
|
|
142
|
+
stableDigest(claim.request) !== claim.request_sha256 ||
|
|
143
|
+
!/^[a-f0-9]{64}$/.test(claim.recovery_token) ||
|
|
144
|
+
stableDigest(claim.writable_root) !== stableDigest(claim.request.selected_writable_root)) {
|
|
145
|
+
throw new Error("retained workspace claim authority is unproven");
|
|
146
|
+
}
|
|
147
|
+
for (const path of claimPaths(claim)) {
|
|
148
|
+
if (typeof path !== "string" || !isAbsolute(path) || resolve(path) !== path) {
|
|
149
|
+
throw new Error("retained workspace claim path is unproven");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (!containsPath(claim.workspace_root, claim.active_root)) {
|
|
153
|
+
throw new Error("retained workspace active root escapes its claim");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function claimPaths(claim) {
|
|
157
|
+
return [
|
|
158
|
+
claim.workspace_root,
|
|
159
|
+
claim.active_root,
|
|
160
|
+
claim.repository?.source_root?.realpath,
|
|
161
|
+
claim.repository?.common_dir?.realpath,
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
/** Include absent descendants without losing aliases in an existing ancestor. */
|
|
165
|
+
function physicalPath(path) {
|
|
166
|
+
const tail = [];
|
|
167
|
+
let cursor = path;
|
|
168
|
+
for (;;) {
|
|
169
|
+
try {
|
|
170
|
+
lstatSync(cursor);
|
|
171
|
+
return resolve(realpathSync(cursor), ...tail);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
if (error.code !== "ENOENT" || dirname(cursor) === cursor) {
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
// A dangling symlink must fail, rather than be treated as an absent directory.
|
|
178
|
+
try {
|
|
179
|
+
if (lstatSync(cursor).isSymbolicLink())
|
|
180
|
+
throw new Error("unresolved workspace alias");
|
|
181
|
+
}
|
|
182
|
+
catch (missing) {
|
|
183
|
+
if (missing.code !== "ENOENT")
|
|
184
|
+
throw missing;
|
|
185
|
+
}
|
|
186
|
+
tail.unshift(basename(cursor));
|
|
187
|
+
cursor = dirname(cursor);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
export function claimOverlapsWorkspace(claim, workspaceRoot) {
|
|
192
|
+
const targets = [workspaceRoot, physicalPath(workspaceRoot)];
|
|
193
|
+
return claimPaths(claim).some((path, index) => [path, physicalPath(path)].some((candidate) => targets.some((target) => containsPath(target, candidate) || (index < 2 && containsPath(candidate, target)))));
|
|
194
|
+
}
|
|
@@ -14,7 +14,40 @@
|
|
|
14
14
|
* always has. So reclaim adds no second removal path that could diverge from the
|
|
15
15
|
* audited one, and a force-delete of live work exists nowhere in the codebase.
|
|
16
16
|
*/
|
|
17
|
+
import type { WorkflowLiveness } from "./recovery.js";
|
|
18
|
+
import type { WorkspaceCleanupAttempt, WorkspaceCleanupReceipt } from "./types.js";
|
|
17
19
|
export type ReclaimMode = "salvage" | "discard";
|
|
20
|
+
export type WorkflowReclaimRefusalCode = "workflow_reclaim_run_live" | "workflow_reclaim_liveness_unknown" | "workflow_reclaim_ownership_unproven" | "workflow_reclaim_lease_held";
|
|
21
|
+
export declare class WorkflowReclaimError extends Error {
|
|
22
|
+
readonly code: WorkflowReclaimRefusalCode;
|
|
23
|
+
constructor(code: WorkflowReclaimRefusalCode, message: string);
|
|
24
|
+
}
|
|
25
|
+
export interface WorkspaceReclaimOptions {
|
|
26
|
+
mode: ReclaimMode;
|
|
27
|
+
yes?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface ReclaimLeaseHolder {
|
|
30
|
+
scope: "binding" | "repository" | "resume";
|
|
31
|
+
owner_id?: string;
|
|
32
|
+
pid?: number;
|
|
33
|
+
host?: string;
|
|
34
|
+
detail?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface WorkspaceReclaimPreview {
|
|
37
|
+
action: "preview";
|
|
38
|
+
run_id: string;
|
|
39
|
+
mode: ReclaimMode;
|
|
40
|
+
liveness: WorkflowLiveness;
|
|
41
|
+
ownership: "ok" | "unproven";
|
|
42
|
+
ownership_detail?: string;
|
|
43
|
+
dirty_paths: string[] | null;
|
|
44
|
+
lease_holder: ReclaimLeaseHolder[];
|
|
45
|
+
}
|
|
46
|
+
export type WorkspaceReclaimResult = WorkspaceReclaimPreview | (Omit<WorkspaceReclaimPreview, "action"> & {
|
|
47
|
+
action: "reclaimed";
|
|
48
|
+
preparation: ReclaimPreparation;
|
|
49
|
+
cleanup: WorkspaceCleanupAttempt | WorkspaceCleanupReceipt;
|
|
50
|
+
});
|
|
18
51
|
export type ReclaimPreparation = {
|
|
19
52
|
action: "salvaged";
|
|
20
53
|
branch: string;
|
|
@@ -34,6 +67,8 @@ export interface PrepareReclaimInput {
|
|
|
34
67
|
worktreePath: string;
|
|
35
68
|
mode: ReclaimMode;
|
|
36
69
|
runId: string;
|
|
70
|
+
/** Revalidate the held reservation and peer authority immediately before effects. */
|
|
71
|
+
beforeEffect?: () => void;
|
|
37
72
|
}
|
|
38
73
|
/**
|
|
39
74
|
* Bring the worktree to a clean state so ordinary cleanup can release it.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reclaim.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/reclaim.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;
|
|
1
|
+
{"version":3,"file":"reclaim.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/reclaim.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,KAAK,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAEnF,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,SAAS,CAAC;AAEhD,MAAM,MAAM,0BAA0B,GAClC,2BAA2B,GAC3B,mCAAmC,GACnC,qCAAqC,GACrC,6BAA6B,CAAC;AAElC,qBAAa,oBAAqB,SAAQ,KAAK;IAE3C,QAAQ,CAAC,IAAI,EAAE,0BAA0B;gBAAhC,IAAI,EAAE,0BAA0B,EACzC,OAAO,EAAE,MAAM;CAKlB;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,WAAW,CAAC;IAClB,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,SAAS,GAAG,YAAY,GAAG,QAAQ,CAAC;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,SAAS,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,SAAS,EAAE,IAAI,GAAG,UAAU,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAC7B,YAAY,EAAE,kBAAkB,EAAE,CAAC;CACpC;AAED,MAAM,MAAM,sBAAsB,GAC9B,uBAAuB,GACvB,CAAC,IAAI,CAAC,uBAAuB,EAAE,QAAQ,CAAC,GAAG;IACzC,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,EAAE,kBAAkB,CAAC;IAChC,OAAO,EAAE,uBAAuB,GAAG,uBAAuB,CAAC;CAC5D,CAAC,CAAC;AAEP,MAAM,MAAM,kBAAkB,GAC1B;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACtE;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,MAAM,EAAE,eAAe,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC3C;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/C,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,WAAW,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,qFAAqF;IACrF,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,kBAAkB,CAwD7E;AAYD;qEACqE;AACrE,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnD"}
|
|
@@ -16,6 +16,14 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { existsSync } from "node:fs";
|
|
18
18
|
import { git, gitMaybe } from "./git.js";
|
|
19
|
+
export class WorkflowReclaimError extends Error {
|
|
20
|
+
code;
|
|
21
|
+
constructor(code, message) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.code = code;
|
|
24
|
+
this.name = "WorkflowReclaimError";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
19
27
|
/**
|
|
20
28
|
* Bring the worktree to a clean state so ordinary cleanup can release it.
|
|
21
29
|
*
|
|
@@ -41,7 +49,9 @@ export function prepareReclaim(input) {
|
|
|
41
49
|
if (input.mode === "discard") {
|
|
42
50
|
// Ordered: reset drops tracked modifications, clean removes what reset
|
|
43
51
|
// cannot see. Running clean first would leave staged deletions behind.
|
|
52
|
+
input.beforeEffect?.();
|
|
44
53
|
git(input.worktreePath, ["reset", "--hard"]);
|
|
54
|
+
input.beforeEffect?.();
|
|
45
55
|
git(input.worktreePath, ["clean", "-fd"]);
|
|
46
56
|
return { action: "discarded", detail: "uncommitted changes were discarded on request" };
|
|
47
57
|
}
|
|
@@ -59,11 +69,15 @@ export function prepareReclaim(input) {
|
|
|
59
69
|
// ref ends untouched, and the work lives on a ref cleanup has no reason to
|
|
60
70
|
// touch.
|
|
61
71
|
const before = git(input.worktreePath, ["rev-parse", "HEAD"]).trim();
|
|
72
|
+
input.beforeEffect?.();
|
|
62
73
|
git(input.worktreePath, ["add", "-A"]);
|
|
74
|
+
input.beforeEffect?.();
|
|
63
75
|
git(input.worktreePath, ["commit", "--no-verify", "-m", salvageMessage(input.runId)]);
|
|
64
76
|
const commit = git(input.worktreePath, ["rev-parse", "HEAD"]).trim();
|
|
65
77
|
const branch = salvageBranch(input.runId);
|
|
78
|
+
input.beforeEffect?.();
|
|
66
79
|
git(input.worktreePath, ["branch", "--force", branch, commit]);
|
|
80
|
+
input.beforeEffect?.();
|
|
67
81
|
git(input.worktreePath, ["reset", "--hard", before]);
|
|
68
82
|
return {
|
|
69
83
|
action: "salvaged",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recovery.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/recovery.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC;AAEjE,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,gBAAgB,
|
|
1
|
+
{"version":3,"file":"recovery.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/recovery.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC;AAEjE,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,gBAAgB,CA6DvF"}
|