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
|
@@ -39,7 +39,8 @@ export type LeaseRecoveryStep =
|
|
|
39
39
|
| "claim_created"
|
|
40
40
|
| "stale_pinned"
|
|
41
41
|
| "current_removed"
|
|
42
|
-
| "new_current_linked"
|
|
42
|
+
| "new_current_linked"
|
|
43
|
+
| "recovery_retired";
|
|
43
44
|
|
|
44
45
|
export interface AcquireNoClobberLeaseInput {
|
|
45
46
|
path: string;
|
|
@@ -51,6 +52,8 @@ export interface AcquireNoClobberLeaseInput {
|
|
|
51
52
|
now?: () => number;
|
|
52
53
|
pid?: number;
|
|
53
54
|
host?: string;
|
|
55
|
+
/** Probe dependency for deterministic liveness-failure tests. */
|
|
56
|
+
probePid?: (pid: number) => void;
|
|
54
57
|
/** Fault-injection hook immediately before the lease directory is created. */
|
|
55
58
|
onLeaseDirectoryCreate?: () => void;
|
|
56
59
|
/** Fault-injection hook after contention is observed and before owner inspection. */
|
|
@@ -137,8 +140,10 @@ export function acquireNoClobberLease(input: AcquireNoClobberLeaseInput): NoClob
|
|
|
137
140
|
if (observed === undefined) {
|
|
138
141
|
throw new Error(`lease ${input.scope} changed repeatedly during contention`);
|
|
139
142
|
}
|
|
140
|
-
if (leaseIsLive(observed.owner, now(), input.staleAfterMs)) {
|
|
141
|
-
throw new Error(
|
|
143
|
+
if (leaseIsLive(observed.owner, now(), input.staleAfterMs, input.probePid)) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`lease ${input.scope} is held by a live or unexpired owner, or its liveness is unknown`,
|
|
146
|
+
);
|
|
142
147
|
}
|
|
143
148
|
if (
|
|
144
149
|
observed.owner.scope !== input.scope ||
|
|
@@ -156,7 +161,7 @@ export function acquireNoClobberLease(input: AcquireNoClobberLeaseInput): NoClob
|
|
|
156
161
|
linkSync(currentPath, pinPath);
|
|
157
162
|
assertSameInodeAndBytes(currentPath, pinPath, observed.bytes);
|
|
158
163
|
} catch {
|
|
159
|
-
abortUnchangedRecoveryClaim(leaseDir, recoveryPath,
|
|
164
|
+
abortUnchangedRecoveryClaim(leaseDir, recoveryPath, owner);
|
|
160
165
|
recoveryClaimed = false;
|
|
161
166
|
throw new Error(`lease ${input.scope} recovery raced with another contender`);
|
|
162
167
|
}
|
|
@@ -174,10 +179,9 @@ export function acquireNoClobberLease(input: AcquireNoClobberLeaseInput): NoClob
|
|
|
174
179
|
input.onRecoveryStep?.("new_current_linked");
|
|
175
180
|
|
|
176
181
|
removePinnedOwner(leaseDir, pinPath, observed.owner);
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
fsyncDirectory(leaseDir);
|
|
182
|
+
retireRecoveryClaim(leaseDir, recoveryPath, owner, () =>
|
|
183
|
+
input.onRecoveryStep?.("recovery_retired"),
|
|
184
|
+
);
|
|
181
185
|
return {
|
|
182
186
|
...leaseHandle(leaseDir, ownerPath, ownerBytes, owner),
|
|
183
187
|
recovered_owner: observed.owner,
|
|
@@ -266,7 +270,7 @@ function reclaimAbandonedRecovery(
|
|
|
266
270
|
now: number,
|
|
267
271
|
): void {
|
|
268
272
|
const claim = readRecoveryClaim(recoveryPath);
|
|
269
|
-
if (leaseIsLive(claim.claimant, now, input.staleAfterMs)) {
|
|
273
|
+
if (leaseIsLive(claim.claimant, now, input.staleAfterMs, input.probePid)) {
|
|
270
274
|
throw new Error(`lease ${input.scope} recovery is already in progress`);
|
|
271
275
|
}
|
|
272
276
|
if (
|
|
@@ -326,16 +330,34 @@ function reclaimAbandonedRecovery(
|
|
|
326
330
|
function abortUnchangedRecoveryClaim(
|
|
327
331
|
leaseDir: string,
|
|
328
332
|
recoveryPath: string,
|
|
329
|
-
pinPath: string,
|
|
330
333
|
claimant: NoClobberLeaseOwner,
|
|
331
334
|
): void {
|
|
332
335
|
const claim = readRecoveryClaim(recoveryPath);
|
|
333
336
|
if (claim.claimant.owner_id !== claimant.owner_id) {
|
|
334
337
|
throw new Error(`lease ${claimant.scope} recovery ownership changed`);
|
|
335
338
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
+
retireRecoveryClaim(leaseDir, recoveryPath, claimant);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Move this claimant's recovery directory to a private name before emptying
|
|
344
|
+
* it. Renaming a directory onto an existing empty directory replaces it, so
|
|
345
|
+
* emptying `recovery` in place would let a contender's claim land there and
|
|
346
|
+
* fail this claimant's final rmdir after it already holds the lease.
|
|
347
|
+
*/
|
|
348
|
+
function retireRecoveryClaim(
|
|
349
|
+
leaseDir: string,
|
|
350
|
+
recoveryPath: string,
|
|
351
|
+
claimant: NoClobberLeaseOwner,
|
|
352
|
+
onRetired?: () => void,
|
|
353
|
+
): void {
|
|
354
|
+
const retired = join(leaseDir, `recovery-retired-${claimant.owner_id}`);
|
|
355
|
+
renameSync(recoveryPath, retired);
|
|
356
|
+
fsyncDirectory(leaseDir);
|
|
357
|
+
onRetired?.();
|
|
358
|
+
safeUnlink(join(retired, "stale-pin"));
|
|
359
|
+
unlinkSync(join(retired, "claim.json"));
|
|
360
|
+
rmdirSync(retired);
|
|
339
361
|
fsyncDirectory(leaseDir);
|
|
340
362
|
}
|
|
341
363
|
|
|
@@ -466,7 +488,14 @@ function isStringRecord(value: unknown): value is Record<string, string> {
|
|
|
466
488
|
);
|
|
467
489
|
}
|
|
468
490
|
|
|
469
|
-
function leaseIsLive(
|
|
491
|
+
function leaseIsLive(
|
|
492
|
+
owner: NoClobberLeaseOwner,
|
|
493
|
+
now: number,
|
|
494
|
+
staleAfterMs: number,
|
|
495
|
+
probePid: (pid: number) => void = (pid) => {
|
|
496
|
+
process.kill(pid, 0);
|
|
497
|
+
},
|
|
498
|
+
): boolean {
|
|
470
499
|
const created = Date.parse(owner.created_at);
|
|
471
500
|
const expires = Date.parse(owner.expires_at);
|
|
472
501
|
if (
|
|
@@ -478,12 +507,14 @@ function leaseIsLive(owner: NoClobberLeaseOwner, now: number, staleAfterMs: numb
|
|
|
478
507
|
) {
|
|
479
508
|
return true;
|
|
480
509
|
}
|
|
481
|
-
|
|
510
|
+
// Expiry only makes a same-host owner eligible for a process check. Neither
|
|
511
|
+
// a remote owner nor an unreadable probe proves that its work has stopped.
|
|
512
|
+
if (owner.host !== hostname()) return true;
|
|
482
513
|
try {
|
|
483
|
-
|
|
514
|
+
probePid(owner.pid);
|
|
484
515
|
return true;
|
|
485
516
|
} catch (error) {
|
|
486
|
-
return (error as NodeJS.ErrnoException).code
|
|
517
|
+
return (error as NodeJS.ErrnoException).code !== "ESRCH";
|
|
487
518
|
}
|
|
488
519
|
}
|
|
489
520
|
|
|
@@ -9,7 +9,9 @@ import {
|
|
|
9
9
|
} from "node:fs";
|
|
10
10
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
11
11
|
import { stateDirMode, stateFileMode } from "../../storage/modes.ts";
|
|
12
|
+
import { acquireWorkflowResumeLease } from "../run-state.ts";
|
|
12
13
|
import { WorkspaceAttestationError } from "./attestation-error.ts";
|
|
14
|
+
import { cleanupWorkspace } from "./cleanup.ts";
|
|
13
15
|
import {
|
|
14
16
|
git,
|
|
15
17
|
gitMaybe,
|
|
@@ -22,6 +24,11 @@ import {
|
|
|
22
24
|
worktreeInventory,
|
|
23
25
|
} from "./git.ts";
|
|
24
26
|
import { acquireNoClobberLease } from "./leases.ts";
|
|
27
|
+
import {
|
|
28
|
+
claimOverlapsWorkspace,
|
|
29
|
+
reserveWorkspaceReclaim,
|
|
30
|
+
type WorkspaceReclaimReservation,
|
|
31
|
+
} from "./ownership.ts";
|
|
25
32
|
import {
|
|
26
33
|
assertContainedExisting,
|
|
27
34
|
assertPathIdentity,
|
|
@@ -36,6 +43,16 @@ import {
|
|
|
36
43
|
type ValidatedRoot,
|
|
37
44
|
validateConfiguredRoot,
|
|
38
45
|
} from "./paths.ts";
|
|
46
|
+
import {
|
|
47
|
+
prepareReclaim,
|
|
48
|
+
type ReclaimLeaseHolder,
|
|
49
|
+
type ReclaimMode,
|
|
50
|
+
type ReclaimPreparation,
|
|
51
|
+
WorkflowReclaimError,
|
|
52
|
+
type WorkspaceReclaimOptions,
|
|
53
|
+
type WorkspaceReclaimPreview,
|
|
54
|
+
type WorkspaceReclaimResult,
|
|
55
|
+
} from "./reclaim.ts";
|
|
39
56
|
import { readWorkflowLiveness } from "./recovery.ts";
|
|
40
57
|
import {
|
|
41
58
|
appendWorkspaceEvent,
|
|
@@ -43,6 +60,7 @@ import {
|
|
|
43
60
|
readWorkflowSupplement,
|
|
44
61
|
readWorkspaceBinding,
|
|
45
62
|
readWorkspaceClaim,
|
|
63
|
+
readWorkspaceClaims,
|
|
46
64
|
readWorkspaceEvents,
|
|
47
65
|
stableDigest,
|
|
48
66
|
type WorkspaceClaim,
|
|
@@ -86,11 +104,18 @@ export interface LocalGitWorktreeProviderOptions {
|
|
|
86
104
|
descriptorPathsSupported?: () => boolean;
|
|
87
105
|
}
|
|
88
106
|
|
|
107
|
+
export interface LocalGitWorktreeProvider extends WorkspaceProvider {
|
|
108
|
+
reclaim(
|
|
109
|
+
binding: WorkspaceBinding,
|
|
110
|
+
options: WorkspaceReclaimOptions,
|
|
111
|
+
): Promise<WorkspaceReclaimResult>;
|
|
112
|
+
}
|
|
113
|
+
|
|
89
114
|
export function createLocalGitWorktreeProvider(
|
|
90
115
|
options: LocalGitWorktreeProviderOptions,
|
|
91
|
-
):
|
|
116
|
+
): LocalGitWorktreeProvider {
|
|
92
117
|
const coordRoot = resolve(options.coordRoot);
|
|
93
|
-
|
|
118
|
+
const provider: LocalGitWorktreeProvider = {
|
|
94
119
|
probe: (input) => probe(input, options.descriptorPathsSupported),
|
|
95
120
|
allocate: (request) => allocate(coordRoot, request),
|
|
96
121
|
readBinding: async (binding) => {
|
|
@@ -103,7 +128,9 @@ export function createLocalGitWorktreeProvider(
|
|
|
103
128
|
previewIntegration: (input) => previewIntegration(coordRoot, input),
|
|
104
129
|
applyAuthorizedIntegration: (input) => applyAuthorizedIntegration(coordRoot, input),
|
|
105
130
|
cleanup: (binding, intent) => cleanup(coordRoot, binding, intent),
|
|
131
|
+
reclaim: (binding, reclaimOptions) => reclaim(coordRoot, provider, binding, reclaimOptions),
|
|
106
132
|
};
|
|
133
|
+
return provider;
|
|
107
134
|
}
|
|
108
135
|
|
|
109
136
|
export async function probe(
|
|
@@ -783,6 +810,258 @@ function cancelUnderLease(
|
|
|
783
810
|
return { ...base, status: "cancelled" };
|
|
784
811
|
}
|
|
785
812
|
|
|
813
|
+
function reclaimPreview(
|
|
814
|
+
coordRoot: string,
|
|
815
|
+
binding: WorkspaceBinding,
|
|
816
|
+
mode: ReclaimMode,
|
|
817
|
+
): WorkspaceReclaimPreview {
|
|
818
|
+
const preview: WorkspaceReclaimPreview = {
|
|
819
|
+
action: "preview",
|
|
820
|
+
run_id: binding.run_id,
|
|
821
|
+
mode,
|
|
822
|
+
liveness: readWorkflowLiveness(coordRoot, binding.run_id),
|
|
823
|
+
ownership: "unproven",
|
|
824
|
+
dirty_paths: null,
|
|
825
|
+
lease_holder: reclaimLeaseHolders(coordRoot, binding),
|
|
826
|
+
};
|
|
827
|
+
try {
|
|
828
|
+
const current = attestSync(coordRoot, binding);
|
|
829
|
+
if (current.status === "ok") {
|
|
830
|
+
preview.ownership = "ok";
|
|
831
|
+
preview.dirty_paths = [...(current.repository?.dirty_paths ?? [])];
|
|
832
|
+
} else {
|
|
833
|
+
preview.ownership_detail =
|
|
834
|
+
current.provider_drift.join("; ") || "ownership attestation failed";
|
|
835
|
+
}
|
|
836
|
+
} catch (error) {
|
|
837
|
+
preview.ownership_detail = (error as Error).message;
|
|
838
|
+
}
|
|
839
|
+
return preview;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function assertReclaimLiveness(coordRoot: string, binding: WorkspaceBinding): void {
|
|
843
|
+
const liveness = readWorkflowLiveness(coordRoot, binding.run_id);
|
|
844
|
+
if (liveness === "inactive") return;
|
|
845
|
+
throw new WorkflowReclaimError(
|
|
846
|
+
liveness === "active" ? "workflow_reclaim_run_live" : "workflow_reclaim_liveness_unknown",
|
|
847
|
+
liveness === "active"
|
|
848
|
+
? `run ${binding.run_id} is active; its workspace was left untouched`
|
|
849
|
+
: `run ${binding.run_id} has missing, corrupt, or ambiguous liveness evidence; its workspace was left untouched`,
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function assertReclaimOwnership(coordRoot: string, binding: WorkspaceBinding): WorkspaceClaim {
|
|
854
|
+
try {
|
|
855
|
+
const claim = requiredClaim(coordRoot, binding);
|
|
856
|
+
const current = attestSync(coordRoot, binding);
|
|
857
|
+
if (current.status !== "ok") {
|
|
858
|
+
throw new Error(current.provider_drift.join("; ") || "ownership attestation failed");
|
|
859
|
+
}
|
|
860
|
+
return claim;
|
|
861
|
+
} catch (error) {
|
|
862
|
+
throw new WorkflowReclaimError("workflow_reclaim_ownership_unproven", (error as Error).message);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function assertPeerWorkspaceAuthority(coordRoot: string, claim: WorkspaceClaim): void {
|
|
867
|
+
try {
|
|
868
|
+
for (const peer of readWorkspaceClaims(coordRoot, PROVIDER_ID)) {
|
|
869
|
+
if (peer.binding_id === claim.binding_id) continue;
|
|
870
|
+
const binding = readWorkspaceBinding(coordRoot, PROVIDER_ID, peer.binding_id);
|
|
871
|
+
const events = readWorkspaceEvents(coordRoot, PROVIDER_ID, peer.binding_id);
|
|
872
|
+
if (binding) requiredClaim(coordRoot, binding);
|
|
873
|
+
else if (events.some((event) => event.event === "bound")) {
|
|
874
|
+
throw new Error("retained peer binding authority is missing");
|
|
875
|
+
}
|
|
876
|
+
if (!claimOverlapsWorkspace(peer, claim.workspace_root)) continue;
|
|
877
|
+
// Process termination, archival and an expired reservation do not release
|
|
878
|
+
// a resource. Require the provider's release record AND current absence.
|
|
879
|
+
if (binding && events.some((event) => event.event === "released")) {
|
|
880
|
+
const current = attestSync(coordRoot, binding);
|
|
881
|
+
if (current.status === "ok" && current.resource_state === "released") continue;
|
|
882
|
+
}
|
|
883
|
+
throw new Error(`retained peer workspace ${peer.binding_id} overlaps the reclaim path`);
|
|
884
|
+
}
|
|
885
|
+
} catch (error) {
|
|
886
|
+
throw new WorkflowReclaimError("workflow_reclaim_ownership_unproven", (error as Error).message);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function reclaimAuthorityGuard(
|
|
891
|
+
coordRoot: string,
|
|
892
|
+
binding: WorkspaceBinding,
|
|
893
|
+
claim: WorkspaceClaim,
|
|
894
|
+
reservation: WorkspaceReclaimReservation,
|
|
895
|
+
): void {
|
|
896
|
+
try {
|
|
897
|
+
reservation.assertCurrent();
|
|
898
|
+
assertReclaimLiveness(coordRoot, binding);
|
|
899
|
+
assertReclaimOwnership(coordRoot, binding);
|
|
900
|
+
assertPeerWorkspaceAuthority(coordRoot, claim);
|
|
901
|
+
} catch (error) {
|
|
902
|
+
if (error instanceof WorkflowReclaimError) throw error;
|
|
903
|
+
throw new WorkflowReclaimError("workflow_reclaim_ownership_unproven", (error as Error).message);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
async function reclaim(
|
|
908
|
+
coordRoot: string,
|
|
909
|
+
provider: WorkspaceProvider,
|
|
910
|
+
binding: WorkspaceBinding,
|
|
911
|
+
options: WorkspaceReclaimOptions,
|
|
912
|
+
): Promise<WorkspaceReclaimResult> {
|
|
913
|
+
if (options.mode !== "salvage" && options.mode !== "discard") {
|
|
914
|
+
throw new Error("unsupported workspace reclaim mode");
|
|
915
|
+
}
|
|
916
|
+
if (!options.yes) return reclaimPreview(coordRoot, binding, options.mode);
|
|
917
|
+
|
|
918
|
+
// Liveness comes before terminal-proof cleanup helpers. A live run may not
|
|
919
|
+
// have a proof yet, and a missing proof must never be discovered after reset.
|
|
920
|
+
assertReclaimLiveness(coordRoot, binding);
|
|
921
|
+
const claim = assertReclaimOwnership(coordRoot, binding);
|
|
922
|
+
let reservation: WorkspaceReclaimReservation;
|
|
923
|
+
try {
|
|
924
|
+
reservation = reserveWorkspaceReclaim(coordRoot, claim);
|
|
925
|
+
} catch (error) {
|
|
926
|
+
throw new WorkflowReclaimError("workflow_reclaim_lease_held", (error as Error).message);
|
|
927
|
+
}
|
|
928
|
+
try {
|
|
929
|
+
reclaimAuthorityGuard(coordRoot, binding, claim, reservation);
|
|
930
|
+
let releaseResume: () => void;
|
|
931
|
+
try {
|
|
932
|
+
releaseResume = acquireWorkflowResumeLease(coordRoot, binding.run_id);
|
|
933
|
+
} catch (error) {
|
|
934
|
+
throw new WorkflowReclaimError("workflow_reclaim_lease_held", (error as Error).message);
|
|
935
|
+
}
|
|
936
|
+
try {
|
|
937
|
+
let releaseBinding: () => void;
|
|
938
|
+
try {
|
|
939
|
+
releaseBinding = acquireLease(
|
|
940
|
+
coordRoot,
|
|
941
|
+
workspaceLockPath(coordRoot, PROVIDER_ID, binding.binding_id),
|
|
942
|
+
claim,
|
|
943
|
+
"binding",
|
|
944
|
+
);
|
|
945
|
+
} catch (error) {
|
|
946
|
+
throw new WorkflowReclaimError("workflow_reclaim_lease_held", (error as Error).message);
|
|
947
|
+
}
|
|
948
|
+
try {
|
|
949
|
+
let releaseRepository: () => void;
|
|
950
|
+
try {
|
|
951
|
+
releaseRepository = acquireRepositoryLease(coordRoot, claim);
|
|
952
|
+
} catch (error) {
|
|
953
|
+
throw new WorkflowReclaimError("workflow_reclaim_lease_held", (error as Error).message);
|
|
954
|
+
}
|
|
955
|
+
try {
|
|
956
|
+
assertReclaimLiveness(coordRoot, binding);
|
|
957
|
+
assertReclaimOwnership(coordRoot, binding);
|
|
958
|
+
const preview = reclaimPreview(coordRoot, binding, options.mode);
|
|
959
|
+
let preparation: ReclaimPreparation | undefined;
|
|
960
|
+
// Keep ordinary cleanup's terminal-evidence checks, intent and receipts.
|
|
961
|
+
// Its provider callback uses the leases already held here instead of
|
|
962
|
+
// trying to acquire them again. Preparation occurs only after all of
|
|
963
|
+
// cleanup's path, ownership, ref and registration checks have succeeded.
|
|
964
|
+
const cleanupResult = await cleanupWorkspace({
|
|
965
|
+
coordRoot,
|
|
966
|
+
runId: binding.run_id,
|
|
967
|
+
provider: {
|
|
968
|
+
...provider,
|
|
969
|
+
cleanup: async (candidate, intent) => {
|
|
970
|
+
assertReclaimLiveness(coordRoot, candidate);
|
|
971
|
+
assertReclaimOwnership(coordRoot, candidate);
|
|
972
|
+
return cleanupUnderLease(coordRoot, candidate, claim, intent, {
|
|
973
|
+
mode: options.mode,
|
|
974
|
+
beforeEffect: () =>
|
|
975
|
+
reclaimAuthorityGuard(coordRoot, candidate, claim, reservation),
|
|
976
|
+
prepared: (value) => {
|
|
977
|
+
preparation = value;
|
|
978
|
+
},
|
|
979
|
+
});
|
|
980
|
+
},
|
|
981
|
+
},
|
|
982
|
+
});
|
|
983
|
+
if (
|
|
984
|
+
!preparation &&
|
|
985
|
+
cleanupResult.status !== "released" &&
|
|
986
|
+
cleanupResult.status !== "already_released"
|
|
987
|
+
) {
|
|
988
|
+
throw new WorkflowReclaimError(
|
|
989
|
+
"workflow_reclaim_ownership_unproven",
|
|
990
|
+
("reason" in cleanupResult ? cleanupResult.reason : undefined) ??
|
|
991
|
+
"workspace cleanup preconditions could not be proved",
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
return {
|
|
995
|
+
...preview,
|
|
996
|
+
action: "reclaimed",
|
|
997
|
+
lease_holder: [],
|
|
998
|
+
preparation: preparation ?? {
|
|
999
|
+
action: "already_gone",
|
|
1000
|
+
detail: "the workspace is already released",
|
|
1001
|
+
},
|
|
1002
|
+
cleanup: cleanupResult,
|
|
1003
|
+
};
|
|
1004
|
+
} finally {
|
|
1005
|
+
releaseRepository();
|
|
1006
|
+
}
|
|
1007
|
+
} finally {
|
|
1008
|
+
releaseBinding();
|
|
1009
|
+
}
|
|
1010
|
+
} finally {
|
|
1011
|
+
releaseResume();
|
|
1012
|
+
}
|
|
1013
|
+
} finally {
|
|
1014
|
+
reservation.release();
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function reclaimLeaseHolders(coordRoot: string, binding: WorkspaceBinding): ReclaimLeaseHolder[] {
|
|
1019
|
+
const paths: Array<[ReclaimLeaseHolder["scope"], string]> = [
|
|
1020
|
+
["binding", workspaceLockPath(coordRoot, PROVIDER_ID, binding.binding_id)],
|
|
1021
|
+
];
|
|
1022
|
+
if (binding.repository) {
|
|
1023
|
+
const key = stableDigest({ common: binding.repository.common_dir.identity });
|
|
1024
|
+
paths.push([
|
|
1025
|
+
"repository",
|
|
1026
|
+
join(coordRoot, ".harnery", "workspaces", PROVIDER_ID, ".leases", `repository-${key}.lock`),
|
|
1027
|
+
]);
|
|
1028
|
+
}
|
|
1029
|
+
const holders: ReclaimLeaseHolder[] = [];
|
|
1030
|
+
try {
|
|
1031
|
+
const owner = JSON.parse(
|
|
1032
|
+
readFileSync(join(coordRoot, ".harnery", "workflows", binding.run_id, "resume.lock"), "utf8"),
|
|
1033
|
+
) as Record<string, unknown>;
|
|
1034
|
+
holders.push({
|
|
1035
|
+
scope: "resume",
|
|
1036
|
+
pid: typeof owner.pid === "number" ? owner.pid : undefined,
|
|
1037
|
+
host: typeof owner.host === "string" ? owner.host : undefined,
|
|
1038
|
+
});
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
1041
|
+
holders.push({ scope: "resume", detail: "lease holder could not be read" });
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
for (const [scope, path] of paths) {
|
|
1045
|
+
try {
|
|
1046
|
+
const owner = JSON.parse(readFileSync(join(path, "current"), "utf8")) as Record<
|
|
1047
|
+
string,
|
|
1048
|
+
unknown
|
|
1049
|
+
>;
|
|
1050
|
+
holders.push({
|
|
1051
|
+
scope,
|
|
1052
|
+
owner_id: typeof owner.owner_id === "string" ? owner.owner_id : undefined,
|
|
1053
|
+
pid: typeof owner.pid === "number" ? owner.pid : undefined,
|
|
1054
|
+
host: typeof owner.host === "string" ? owner.host : undefined,
|
|
1055
|
+
});
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
1058
|
+
holders.push({ scope, detail: "lease holder could not be read" });
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
return holders;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
786
1065
|
async function cleanup(
|
|
787
1066
|
coordRoot: string,
|
|
788
1067
|
binding: WorkspaceBinding,
|
|
@@ -799,7 +1078,17 @@ async function cleanup(
|
|
|
799
1078
|
try {
|
|
800
1079
|
const releaseRepository = acquireRepositoryLease(coordRoot, claim);
|
|
801
1080
|
try {
|
|
802
|
-
|
|
1081
|
+
const reservation = reserveWorkspaceReclaim(coordRoot, claim);
|
|
1082
|
+
try {
|
|
1083
|
+
const authorityGuard = () => {
|
|
1084
|
+
reservation.assertCurrent();
|
|
1085
|
+
assertPeerWorkspaceAuthority(coordRoot, claim);
|
|
1086
|
+
};
|
|
1087
|
+
authorityGuard();
|
|
1088
|
+
return cleanupUnderLease(coordRoot, binding, claim, intent, undefined, authorityGuard);
|
|
1089
|
+
} finally {
|
|
1090
|
+
reservation.release();
|
|
1091
|
+
}
|
|
803
1092
|
} finally {
|
|
804
1093
|
releaseRepository();
|
|
805
1094
|
}
|
|
@@ -813,6 +1102,12 @@ function cleanupUnderLease(
|
|
|
813
1102
|
binding: WorkspaceBinding,
|
|
814
1103
|
claim: WorkspaceClaim,
|
|
815
1104
|
intent: WorkspaceCleanupIntent,
|
|
1105
|
+
reclaimRequest?: {
|
|
1106
|
+
mode: ReclaimMode;
|
|
1107
|
+
beforeEffect: () => void;
|
|
1108
|
+
prepared: (value: ReclaimPreparation) => void;
|
|
1109
|
+
},
|
|
1110
|
+
beforeRemoval?: () => void,
|
|
816
1111
|
): WorkspaceCleanupResult {
|
|
817
1112
|
const repository = binding.repository;
|
|
818
1113
|
if (
|
|
@@ -871,11 +1166,11 @@ function cleanupUnderLease(
|
|
|
871
1166
|
return { ...base, status: "blocked", reason: "ownership attestation failed" };
|
|
872
1167
|
}
|
|
873
1168
|
if (
|
|
874
|
-
(before.repository?.dirty_paths.length ?? 0) > 0 ||
|
|
1169
|
+
(!reclaimRequest && (before.repository?.dirty_paths.length ?? 0) > 0) ||
|
|
875
1170
|
(before.repository?.conflicts.length ?? 0) > 0 ||
|
|
876
1171
|
(before.repository?.operation_in_progress.length ?? 0) > 0
|
|
877
1172
|
) {
|
|
878
|
-
appendWorkspaceEvent(coordRoot, claim, "preserved_dirty");
|
|
1173
|
+
if (!reclaimRequest) appendWorkspaceEvent(coordRoot, claim, "preserved_dirty");
|
|
879
1174
|
return {
|
|
880
1175
|
...base,
|
|
881
1176
|
status: "preserved_dirty",
|
|
@@ -949,6 +1244,19 @@ function cleanupUnderLease(
|
|
|
949
1244
|
};
|
|
950
1245
|
}
|
|
951
1246
|
|
|
1247
|
+
if (reclaimRequest) {
|
|
1248
|
+
assertReclaimLiveness(coordRoot, binding);
|
|
1249
|
+
assertReclaimOwnership(coordRoot, binding);
|
|
1250
|
+
revalidateClaimResources(claim, root);
|
|
1251
|
+
reclaimRequest.prepared(
|
|
1252
|
+
prepareReclaim({
|
|
1253
|
+
worktreePath: binding.workspace_root,
|
|
1254
|
+
mode: reclaimRequest.mode,
|
|
1255
|
+
runId: binding.run_id,
|
|
1256
|
+
beforeEffect: reclaimRequest.beforeEffect,
|
|
1257
|
+
}),
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
952
1260
|
appendWorkspaceEvent(coordRoot, claim, "cleanup_pending", {
|
|
953
1261
|
worktree_path: binding.workspace_root,
|
|
954
1262
|
gitdir: repository.gitdir,
|
|
@@ -958,6 +1266,8 @@ function cleanupUnderLease(
|
|
|
958
1266
|
target_ref_oid: targetRef.out,
|
|
959
1267
|
});
|
|
960
1268
|
if (before.workspace_exists) {
|
|
1269
|
+
reclaimRequest?.beforeEffect();
|
|
1270
|
+
beforeRemoval?.();
|
|
961
1271
|
revalidateClaimResources(claim, root);
|
|
962
1272
|
assertContainedExisting(root, binding.workspace_root, "workspace cleanup path");
|
|
963
1273
|
git(repository.source_root.realpath, ["worktree", "remove", binding.workspace_root]);
|