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
|
@@ -0,0 +1,232 @@
|
|
|
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.ts";
|
|
4
|
+
import { acquireNoClobberLease, type NoClobberLease } from "./leases.ts";
|
|
5
|
+
import { containsPath } from "./paths.ts";
|
|
6
|
+
import type { WorkspaceClaim } from "./state.ts";
|
|
7
|
+
import { isWorkspaceAllocationRequest } from "./validate.ts";
|
|
8
|
+
|
|
9
|
+
const STALE_MS = 5 * 60 * 1_000;
|
|
10
|
+
const RESERVATION_SCOPE = "workspace-reclaim";
|
|
11
|
+
|
|
12
|
+
function leaseRoot(coordRoot: string, providerId: string): string {
|
|
13
|
+
return join(resolve(coordRoot), ".harnery", "workspaces", providerId, ".leases");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Serialize only publication and reservation registration, never Git work. */
|
|
17
|
+
function withPublicationGate<T>(coordRoot: string, providerId: string, operation: () => T): T {
|
|
18
|
+
const authority = stableDigest({ provider_id: providerId, scope: "workspace-publication" });
|
|
19
|
+
const gate = acquireNoClobberLease({
|
|
20
|
+
path: join(leaseRoot(coordRoot, providerId), "publication.lock"),
|
|
21
|
+
scope: "workspace-publication",
|
|
22
|
+
authoritySha256: authority,
|
|
23
|
+
staleAfterMs: STALE_MS,
|
|
24
|
+
validateStaleOwner: (owner) => owner.authority_sha256 === authority,
|
|
25
|
+
});
|
|
26
|
+
try {
|
|
27
|
+
return operation();
|
|
28
|
+
} finally {
|
|
29
|
+
gate.release();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function reservationPath(coordRoot: string, claim: WorkspaceClaim): string {
|
|
34
|
+
return join(leaseRoot(coordRoot, claim.provider_id), `reclaim-${claim.binding_id}.lock`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function acquireReservation(coordRoot: string, claim: WorkspaceClaim): NoClobberLease {
|
|
38
|
+
const authority = stableDigest(claim);
|
|
39
|
+
return acquireNoClobberLease({
|
|
40
|
+
path: reservationPath(coordRoot, claim),
|
|
41
|
+
scope: RESERVATION_SCOPE,
|
|
42
|
+
authoritySha256: authority,
|
|
43
|
+
staleAfterMs: STALE_MS,
|
|
44
|
+
metadata: { binding_id: claim.binding_id, request_sha256: claim.request_sha256 },
|
|
45
|
+
validateStaleOwner: (owner) =>
|
|
46
|
+
owner.authority_sha256 === authority &&
|
|
47
|
+
owner.metadata?.binding_id === claim.binding_id &&
|
|
48
|
+
owner.metadata?.request_sha256 === claim.request_sha256,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface WorkspaceReclaimReservation {
|
|
53
|
+
assertCurrent(): void;
|
|
54
|
+
release(): void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The immutable claim is the authority; the lease reserves its destructive path. */
|
|
58
|
+
export function reserveWorkspaceReclaim(
|
|
59
|
+
coordRoot: string,
|
|
60
|
+
claim: WorkspaceClaim,
|
|
61
|
+
): WorkspaceReclaimReservation {
|
|
62
|
+
assertClaimAuthority(claim, claim.provider_id, claim.binding_id);
|
|
63
|
+
const lease = withPublicationGate(coordRoot, claim.provider_id, () =>
|
|
64
|
+
acquireReservation(coordRoot, claim),
|
|
65
|
+
);
|
|
66
|
+
return {
|
|
67
|
+
assertCurrent() {
|
|
68
|
+
const path = reservationPath(coordRoot, claim);
|
|
69
|
+
const current = join(path, "current");
|
|
70
|
+
const pin = join(path, lease.owner.owner_file);
|
|
71
|
+
const a = lstatSync(current, { bigint: true });
|
|
72
|
+
const b = lstatSync(pin, { bigint: true });
|
|
73
|
+
if (
|
|
74
|
+
!a.isFile() ||
|
|
75
|
+
!b.isFile() ||
|
|
76
|
+
a.dev !== b.dev ||
|
|
77
|
+
a.ino !== b.ino ||
|
|
78
|
+
readFileSync(current, "utf8") !== readFileSync(pin, "utf8") ||
|
|
79
|
+
stableDigest(readJsonRecord(current, "reclaim reservation")) !== stableDigest(lease.owner)
|
|
80
|
+
) {
|
|
81
|
+
throw new Error("workspace reclaim reservation owner changed");
|
|
82
|
+
}
|
|
83
|
+
const persisted = readJsonRecord<WorkspaceClaim>(
|
|
84
|
+
join(dirname(dirname(path)), claim.binding_id, "claim.json"),
|
|
85
|
+
"reserved workspace claim",
|
|
86
|
+
);
|
|
87
|
+
if (stableDigest(persisted) !== stableDigest(claim)) {
|
|
88
|
+
throw new Error("workspace reclaim reservation claim changed");
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
release: () => lease.release(),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function publishWorkspaceClaim<T>(
|
|
96
|
+
coordRoot: string,
|
|
97
|
+
claim: WorkspaceClaim,
|
|
98
|
+
publish: () => T,
|
|
99
|
+
): T {
|
|
100
|
+
assertClaimAuthority(claim, claim.provider_id, claim.binding_id);
|
|
101
|
+
return withPublicationGate(coordRoot, claim.provider_id, () => {
|
|
102
|
+
for (const entry of readdirSync(leaseRoot(coordRoot, claim.provider_id), {
|
|
103
|
+
withFileTypes: true,
|
|
104
|
+
})) {
|
|
105
|
+
if (!entry.name.startsWith("reclaim-") || !entry.name.endsWith(".lock")) continue;
|
|
106
|
+
if (!entry.isDirectory()) throw new Error("workspace reclaim reservation is not a directory");
|
|
107
|
+
const path = join(leaseRoot(coordRoot, claim.provider_id), entry.name);
|
|
108
|
+
const current = join(path, "current");
|
|
109
|
+
try {
|
|
110
|
+
lstatSync(current);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
const id = entry.name.slice("reclaim-".length, -".lock".length);
|
|
116
|
+
const reserved = readJsonRecord<WorkspaceClaim>(
|
|
117
|
+
join(dirname(leaseRoot(coordRoot, claim.provider_id)), id, "claim.json"),
|
|
118
|
+
"reserved workspace claim",
|
|
119
|
+
);
|
|
120
|
+
assertClaimAuthority(reserved, claim.provider_id, id);
|
|
121
|
+
// Unknown reservation authority is never interpreted as an unrelated owner.
|
|
122
|
+
const owner = readJsonRecord<NoClobberLease["owner"]>(current, "reclaim reservation");
|
|
123
|
+
if (
|
|
124
|
+
owner.schema_version !== 1 ||
|
|
125
|
+
typeof owner.owner_id !== "string" ||
|
|
126
|
+
!/^[a-f0-9-]{36}$/.test(owner.owner_id) ||
|
|
127
|
+
owner.owner_file !== `owner-${owner.owner_id}.json` ||
|
|
128
|
+
!Number.isSafeInteger(owner.pid) ||
|
|
129
|
+
owner.pid <= 0 ||
|
|
130
|
+
typeof owner.host !== "string" ||
|
|
131
|
+
owner.host.length === 0 ||
|
|
132
|
+
!Number.isFinite(Date.parse(owner.created_at)) ||
|
|
133
|
+
!Number.isFinite(Date.parse(owner.expires_at)) ||
|
|
134
|
+
Date.parse(owner.expires_at) < Date.parse(owner.created_at) ||
|
|
135
|
+
owner.scope !== RESERVATION_SCOPE ||
|
|
136
|
+
owner.authority_sha256 !== stableDigest(reserved) ||
|
|
137
|
+
owner.metadata?.binding_id !== id ||
|
|
138
|
+
owner.metadata?.request_sha256 !== reserved.request_sha256
|
|
139
|
+
) {
|
|
140
|
+
throw new Error("workspace reclaim reservation authority is unproven");
|
|
141
|
+
}
|
|
142
|
+
const a = lstatSync(current, { bigint: true });
|
|
143
|
+
const pin = join(path, owner.owner_file);
|
|
144
|
+
const b = lstatSync(pin, { bigint: true });
|
|
145
|
+
if (
|
|
146
|
+
!a.isFile() ||
|
|
147
|
+
!b.isFile() ||
|
|
148
|
+
a.dev !== b.dev ||
|
|
149
|
+
a.ino !== b.ino ||
|
|
150
|
+
readFileSync(current, "utf8") !== readFileSync(pin, "utf8")
|
|
151
|
+
)
|
|
152
|
+
throw new Error("workspace reclaim reservation owner is unproven");
|
|
153
|
+
if (!claimOverlapsWorkspace(claim, reserved.workspace_root)) continue;
|
|
154
|
+
// This succeeds only for an expired, provably dead, exact prior owner.
|
|
155
|
+
// It retires the reservation, never the immutable claim or resource.
|
|
156
|
+
const recovered = acquireReservation(coordRoot, reserved);
|
|
157
|
+
recovered.release();
|
|
158
|
+
}
|
|
159
|
+
return publish();
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function assertClaimAuthority(claim: WorkspaceClaim, providerId: string, id: string): void {
|
|
164
|
+
if (
|
|
165
|
+
claim?.schema_version !== 1 ||
|
|
166
|
+
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(providerId) ||
|
|
167
|
+
claim.provider_id !== providerId ||
|
|
168
|
+
claim.provider_version !== "1" ||
|
|
169
|
+
claim.binding_id !== id ||
|
|
170
|
+
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(id) ||
|
|
171
|
+
!isWorkspaceAllocationRequest(claim.request) ||
|
|
172
|
+
claim.request.provider_id !== providerId ||
|
|
173
|
+
stableDigest(claim.request) !== claim.request_sha256 ||
|
|
174
|
+
!/^[a-f0-9]{64}$/.test(claim.recovery_token) ||
|
|
175
|
+
stableDigest(claim.writable_root) !== stableDigest(claim.request.selected_writable_root)
|
|
176
|
+
) {
|
|
177
|
+
throw new Error("retained workspace claim authority is unproven");
|
|
178
|
+
}
|
|
179
|
+
for (const path of claimPaths(claim)) {
|
|
180
|
+
if (typeof path !== "string" || !isAbsolute(path) || resolve(path) !== path) {
|
|
181
|
+
throw new Error("retained workspace claim path is unproven");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (!containsPath(claim.workspace_root, claim.active_root)) {
|
|
185
|
+
throw new Error("retained workspace active root escapes its claim");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function claimPaths(claim: WorkspaceClaim): string[] {
|
|
190
|
+
return [
|
|
191
|
+
claim.workspace_root,
|
|
192
|
+
claim.active_root,
|
|
193
|
+
claim.repository?.source_root?.realpath,
|
|
194
|
+
claim.repository?.common_dir?.realpath,
|
|
195
|
+
];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Include absent descendants without losing aliases in an existing ancestor. */
|
|
199
|
+
function physicalPath(path: string): string {
|
|
200
|
+
const tail: string[] = [];
|
|
201
|
+
let cursor = path;
|
|
202
|
+
for (;;) {
|
|
203
|
+
try {
|
|
204
|
+
lstatSync(cursor);
|
|
205
|
+
return resolve(realpathSync(cursor), ...tail);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT" || dirname(cursor) === cursor) {
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
// A dangling symlink must fail, rather than be treated as an absent directory.
|
|
211
|
+
try {
|
|
212
|
+
if (lstatSync(cursor).isSymbolicLink()) throw new Error("unresolved workspace alias");
|
|
213
|
+
} catch (missing) {
|
|
214
|
+
if ((missing as NodeJS.ErrnoException).code !== "ENOENT") throw missing;
|
|
215
|
+
}
|
|
216
|
+
tail.unshift(basename(cursor));
|
|
217
|
+
cursor = dirname(cursor);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function claimOverlapsWorkspace(claim: WorkspaceClaim, workspaceRoot: string): boolean {
|
|
223
|
+
const targets = [workspaceRoot, physicalPath(workspaceRoot)];
|
|
224
|
+
return claimPaths(claim).some((path, index) =>
|
|
225
|
+
[path, physicalPath(path)].some((candidate) =>
|
|
226
|
+
targets.some(
|
|
227
|
+
(target) =>
|
|
228
|
+
containsPath(target, candidate) || (index < 2 && containsPath(candidate, target)),
|
|
229
|
+
),
|
|
230
|
+
),
|
|
231
|
+
);
|
|
232
|
+
}
|
|
@@ -17,9 +17,59 @@
|
|
|
17
17
|
|
|
18
18
|
import { existsSync } from "node:fs";
|
|
19
19
|
import { git, gitMaybe } from "./git.ts";
|
|
20
|
+
import type { WorkflowLiveness } from "./recovery.ts";
|
|
21
|
+
import type { WorkspaceCleanupAttempt, WorkspaceCleanupReceipt } from "./types.ts";
|
|
20
22
|
|
|
21
23
|
export type ReclaimMode = "salvage" | "discard";
|
|
22
24
|
|
|
25
|
+
export type WorkflowReclaimRefusalCode =
|
|
26
|
+
| "workflow_reclaim_run_live"
|
|
27
|
+
| "workflow_reclaim_liveness_unknown"
|
|
28
|
+
| "workflow_reclaim_ownership_unproven"
|
|
29
|
+
| "workflow_reclaim_lease_held";
|
|
30
|
+
|
|
31
|
+
export class WorkflowReclaimError extends Error {
|
|
32
|
+
constructor(
|
|
33
|
+
readonly code: WorkflowReclaimRefusalCode,
|
|
34
|
+
message: string,
|
|
35
|
+
) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = "WorkflowReclaimError";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface WorkspaceReclaimOptions {
|
|
42
|
+
mode: ReclaimMode;
|
|
43
|
+
yes?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ReclaimLeaseHolder {
|
|
47
|
+
scope: "binding" | "repository" | "resume";
|
|
48
|
+
owner_id?: string;
|
|
49
|
+
pid?: number;
|
|
50
|
+
host?: string;
|
|
51
|
+
detail?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface WorkspaceReclaimPreview {
|
|
55
|
+
action: "preview";
|
|
56
|
+
run_id: string;
|
|
57
|
+
mode: ReclaimMode;
|
|
58
|
+
liveness: WorkflowLiveness;
|
|
59
|
+
ownership: "ok" | "unproven";
|
|
60
|
+
ownership_detail?: string;
|
|
61
|
+
dirty_paths: string[] | null;
|
|
62
|
+
lease_holder: ReclaimLeaseHolder[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type WorkspaceReclaimResult =
|
|
66
|
+
| WorkspaceReclaimPreview
|
|
67
|
+
| (Omit<WorkspaceReclaimPreview, "action"> & {
|
|
68
|
+
action: "reclaimed";
|
|
69
|
+
preparation: ReclaimPreparation;
|
|
70
|
+
cleanup: WorkspaceCleanupAttempt | WorkspaceCleanupReceipt;
|
|
71
|
+
});
|
|
72
|
+
|
|
23
73
|
export type ReclaimPreparation =
|
|
24
74
|
| { action: "salvaged"; branch: string; commit: string; detail: string }
|
|
25
75
|
| { action: "discarded"; detail: string }
|
|
@@ -30,6 +80,8 @@ export interface PrepareReclaimInput {
|
|
|
30
80
|
worktreePath: string;
|
|
31
81
|
mode: ReclaimMode;
|
|
32
82
|
runId: string;
|
|
83
|
+
/** Revalidate the held reservation and peer authority immediately before effects. */
|
|
84
|
+
beforeEffect?: () => void;
|
|
33
85
|
}
|
|
34
86
|
|
|
35
87
|
/**
|
|
@@ -59,7 +111,9 @@ export function prepareReclaim(input: PrepareReclaimInput): ReclaimPreparation {
|
|
|
59
111
|
if (input.mode === "discard") {
|
|
60
112
|
// Ordered: reset drops tracked modifications, clean removes what reset
|
|
61
113
|
// cannot see. Running clean first would leave staged deletions behind.
|
|
114
|
+
input.beforeEffect?.();
|
|
62
115
|
git(input.worktreePath, ["reset", "--hard"]);
|
|
116
|
+
input.beforeEffect?.();
|
|
63
117
|
git(input.worktreePath, ["clean", "-fd"]);
|
|
64
118
|
return { action: "discarded", detail: "uncommitted changes were discarded on request" };
|
|
65
119
|
}
|
|
@@ -78,11 +132,15 @@ export function prepareReclaim(input: PrepareReclaimInput): ReclaimPreparation {
|
|
|
78
132
|
// ref ends untouched, and the work lives on a ref cleanup has no reason to
|
|
79
133
|
// touch.
|
|
80
134
|
const before = git(input.worktreePath, ["rev-parse", "HEAD"]).trim();
|
|
135
|
+
input.beforeEffect?.();
|
|
81
136
|
git(input.worktreePath, ["add", "-A"]);
|
|
137
|
+
input.beforeEffect?.();
|
|
82
138
|
git(input.worktreePath, ["commit", "--no-verify", "-m", salvageMessage(input.runId)]);
|
|
83
139
|
const commit = git(input.worktreePath, ["rev-parse", "HEAD"]).trim();
|
|
84
140
|
const branch = salvageBranch(input.runId);
|
|
141
|
+
input.beforeEffect?.();
|
|
85
142
|
git(input.worktreePath, ["branch", "--force", branch, commit]);
|
|
143
|
+
input.beforeEffect?.();
|
|
86
144
|
git(input.worktreePath, ["reset", "--hard", before]);
|
|
87
145
|
return {
|
|
88
146
|
action: "salvaged",
|
|
@@ -6,16 +6,6 @@ export type WorkflowLiveness = "active" | "inactive" | "unknown";
|
|
|
6
6
|
|
|
7
7
|
export function readWorkflowLiveness(coordRoot: string, runId: string): WorkflowLiveness {
|
|
8
8
|
const root = resolve(coordRoot);
|
|
9
|
-
const proofPath = join(root, ".harnery", "workflows", runId, "proof.json");
|
|
10
|
-
if (existsSync(proofPath)) {
|
|
11
|
-
try {
|
|
12
|
-
const proof = readWorkflowProof(root, runId);
|
|
13
|
-
return ["succeeded", "failed"].includes(proof.run.status) ? "inactive" : "unknown";
|
|
14
|
-
} catch {
|
|
15
|
-
return "unknown";
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
9
|
const transcriptPath = join(root, ".harnery", "workflows", runId, "transcript.jsonl");
|
|
20
10
|
if (!existsSync(transcriptPath)) return "unknown";
|
|
21
11
|
let events: Array<{ event?: string; run_id?: string; ok?: unknown; status?: unknown }>;
|
|
@@ -35,13 +25,23 @@ export function readWorkflowLiveness(coordRoot: string, runId: string): Workflow
|
|
|
35
25
|
} catch {
|
|
36
26
|
return "unknown";
|
|
37
27
|
}
|
|
38
|
-
if (
|
|
28
|
+
if (
|
|
29
|
+
events.some(
|
|
30
|
+
(item) =>
|
|
31
|
+
!item ||
|
|
32
|
+
typeof item !== "object" ||
|
|
33
|
+
Array.isArray(item) ||
|
|
34
|
+
item.run_id !== runId ||
|
|
35
|
+
typeof item.event !== "string",
|
|
36
|
+
)
|
|
37
|
+
) {
|
|
39
38
|
return "unknown";
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
let liveness: WorkflowLiveness | undefined;
|
|
43
42
|
for (const item of events) {
|
|
44
|
-
if (item.event === "run.start" || item.event === "run.resume"
|
|
43
|
+
if (item.event === "run.start" || item.event === "run.resume" || item.event === "agent.start")
|
|
44
|
+
liveness = "active";
|
|
45
45
|
if (item.event === "run.parked") liveness = "inactive";
|
|
46
46
|
if (item.event === "run.end") {
|
|
47
47
|
if (typeof item.ok !== "boolean") return "unknown";
|
|
@@ -52,5 +52,17 @@ export function readWorkflowLiveness(coordRoot: string, runId: string): Workflow
|
|
|
52
52
|
liveness = "inactive";
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
-
|
|
55
|
+
// A terminal proof can predate a resumed run. Only the current transcript
|
|
56
|
+
// can establish that no later activity invalidated that terminal observation.
|
|
57
|
+
if (liveness !== "inactive") return liveness ?? "unknown";
|
|
58
|
+
const proofPath = join(root, ".harnery", "workflows", runId, "proof.json");
|
|
59
|
+
if (existsSync(proofPath)) {
|
|
60
|
+
try {
|
|
61
|
+
const proof = readWorkflowProof(root, runId);
|
|
62
|
+
return ["succeeded", "failed"].includes(proof.run.status) ? "inactive" : "unknown";
|
|
63
|
+
} catch {
|
|
64
|
+
return "unknown";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return "inactive";
|
|
56
68
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
fsyncSync,
|
|
6
6
|
mkdirSync,
|
|
7
7
|
openSync,
|
|
8
|
+
readdirSync,
|
|
8
9
|
readFileSync,
|
|
9
10
|
statSync,
|
|
10
11
|
writeFileSync,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
stableDigest,
|
|
19
20
|
writeImmutableJson,
|
|
20
21
|
} from "../durable-record.ts";
|
|
22
|
+
import { assertClaimAuthority, publishWorkspaceClaim } from "./ownership.ts";
|
|
21
23
|
|
|
22
24
|
export { appendWorkflowTranscriptEvent } from "../transcript.ts";
|
|
23
25
|
|
|
@@ -141,6 +143,10 @@ export function readWorkspaceRequest(
|
|
|
141
143
|
}
|
|
142
144
|
|
|
143
145
|
export function writeWorkspaceClaim(coordRoot: string, claim: WorkspaceClaim): boolean {
|
|
146
|
+
return publishWorkspaceClaim(coordRoot, claim, () => writeClaim(coordRoot, claim));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function writeClaim(coordRoot: string, claim: WorkspaceClaim): boolean {
|
|
144
150
|
const path = workspaceClaimPath(coordRoot, claim.provider_id, claim.binding_id);
|
|
145
151
|
try {
|
|
146
152
|
return writeImmutableJson(path, claim);
|
|
@@ -154,6 +160,20 @@ export function writeWorkspaceClaim(coordRoot: string, claim: WorkspaceClaim): b
|
|
|
154
160
|
}
|
|
155
161
|
}
|
|
156
162
|
|
|
163
|
+
export function readWorkspaceClaims(coordRoot: string, providerId: string): WorkspaceClaim[] {
|
|
164
|
+
const root = join(resolve(coordRoot), ".harnery", "workspaces", providerId);
|
|
165
|
+
if (!existsSync(root)) return [];
|
|
166
|
+
return readdirSync(root, { withFileTypes: true })
|
|
167
|
+
.filter((entry) => entry.name !== ".leases")
|
|
168
|
+
.map((entry) => {
|
|
169
|
+
if (!entry.isDirectory()) throw new Error("retained workspace inventory is unproven");
|
|
170
|
+
const claim = readWorkspaceClaim(coordRoot, providerId, entry.name);
|
|
171
|
+
if (!claim) throw new Error("retained workspace ownership claim is missing");
|
|
172
|
+
assertClaimAuthority(claim, providerId, entry.name);
|
|
173
|
+
return claim;
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
157
177
|
export function readWorkspaceClaim(
|
|
158
178
|
coordRoot: string,
|
|
159
179
|
providerId: string,
|