harnery 0.43.0 → 0.45.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.js +1 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +41 -48
- 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/files.d.ts +2 -2
- package/dist/commands/files.d.ts.map +1 -1
- package/dist/commands/files.js +7 -4
- 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 +26 -17
- 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 +15 -0
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +453 -153
- package/dist/commands/turn-rollback.d.ts +5 -0
- package/dist/commands/turn-rollback.d.ts.map +1 -0
- package/dist/commands/turn-rollback.js +76 -0
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +68 -30
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +18 -0
- 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 +8 -12
- 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 +7 -30
- 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/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 -1
- package/dist/core/agents/state/proc-start.d.ts.map +1 -1
- package/dist/core/agents/state/proc-start.js +46 -1
- package/dist/core/agents/state/process-handle.d.ts +26 -0
- package/dist/core/agents/state/process-handle.d.ts.map +1 -0
- package/dist/core/agents/state/process-handle.js +121 -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/artifacts/delivery-card.d.ts.map +1 -1
- package/dist/core/artifacts/delivery-card.js +2 -1
- 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/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 +33 -5
- 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 +7 -13
- package/dist/core/resources/service.d.ts.map +1 -1
- package/dist/core/resources/service.js +34 -91
- 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/service.d.ts.map +1 -1
- package/dist/core/semantic/service.js +2 -9
- package/dist/core/servers/index.d.ts +34 -22
- package/dist/core/servers/index.d.ts.map +1 -1
- package/dist/core/servers/index.js +297 -144
- package/dist/core/servers/tunnels.d.ts +7 -7
- package/dist/core/servers/tunnels.d.ts.map +1 -1
- package/dist/core/servers/tunnels.js +47 -10
- 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 +7 -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/turn-rollback/content.d.ts +53 -0
- package/dist/core/turn-rollback/content.d.ts.map +1 -0
- package/dist/core/turn-rollback/content.js +396 -0
- package/dist/core/turn-rollback/index.d.ts +72 -0
- package/dist/core/turn-rollback/index.d.ts.map +1 -0
- package/dist/core/turn-rollback/index.js +443 -0
- 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 +5 -3
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +3 -2
- 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/recovery-audit.d.ts +30 -0
- package/dist/core/workflow/recovery-audit.d.ts.map +1 -0
- package/dist/core/workflow/recovery-audit.js +116 -0
- package/dist/core/workflow/run-state.d.ts +20 -1
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +141 -32
- 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 +328 -8
- 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 +40 -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 +2 -0
- package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/recovery.js +42 -10
- 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/state.d.ts +13 -6
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +58 -12
- 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 +12 -1
- package/src/commander.ts +10 -0
- package/src/commands/agents.ts +54 -63
- package/src/commands/browse.ts +50 -14
- package/src/commands/diagnostics.ts +10 -0
- package/src/commands/doctor.ts +26 -7
- package/src/commands/files.ts +13 -5
- 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 +27 -26
- package/src/commands/supervisor.ts +7 -2
- package/src/commands/toc.ts +14 -6
- package/src/commands/tunnel.ts +533 -166
- package/src/commands/turn-rollback.ts +111 -0
- package/src/commands/workflow.ts +89 -30
- package/src/core/agents/coord-client.ts +20 -0
- 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 +10 -12
- package/src/core/agents/render/session-context.ts +9 -32
- 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/pidmap.ts +13 -0
- package/src/core/agents/state/proc-start.ts +62 -2
- package/src/core/agents/state/process-handle.ts +165 -0
- package/src/core/agents/state/stale-sweep.ts +247 -82
- package/src/core/agents/state/unchanged-file.ts +456 -0
- package/src/core/artifacts/delivery-card.ts +7 -1
- 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/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 +33 -5
- 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 +57 -95
- package/src/core/resources/status.ts +57 -8
- package/src/core/semantic/service.ts +2 -8
- package/src/core/servers/index.ts +376 -141
- package/src/core/servers/tunnels.ts +57 -13
- 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 +87 -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/turn-rollback/content.ts +499 -0
- package/src/core/turn-rollback/index.ts +580 -0
- 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 +12 -2
- package/src/core/workflow/live-session-v3.ts +146 -5
- package/src/core/workflow/proof.ts +134 -26
- package/src/core/workflow/recovery-audit.ts +181 -0
- package/src/core/workflow/run-state.ts +165 -34
- 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 +388 -7
- package/src/core/workflow/workspaces/ownership.ts +232 -0
- package/src/core/workflow/workspaces/reclaim.ts +62 -0
- package/src/core/workflow/workspaces/recovery.ts +45 -10
- 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/state.ts +70 -15
- package/src/lib/zip/index.ts +483 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"leases.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/leases.ts"],"names":[],"mappings":"AAwBA,MAAM,WAAW,mBAAmB;IAClC,cAAc,EAAE,CAAC,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,iBAAiB,GACzB,eAAe,GACf,cAAc,GACd,iBAAiB,GACjB,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"leases.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/leases.ts"],"names":[],"mappings":"AAwBA,MAAM,WAAW,mBAAmB;IAClC,cAAc,EAAE,CAAC,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,iBAAiB,GACzB,eAAe,GACf,cAAc,GACd,iBAAiB,GACjB,oBAAoB,GACpB,kBAAkB,CAAC;AAEvB,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC5C,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,OAAO,CAAC;IAC7D,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,8EAA8E;IAC9E,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;IACpC,qFAAqF;IACrF,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,IAAI,CAAC;CACpD;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,mBAAmB,CAAC;IAC3B,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,OAAO,IAAI,IAAI,CAAC;CACjB;AAUD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,0BAA0B,GAAG,cAAc,CAgHvF"}
|
|
@@ -71,8 +71,8 @@ export function acquireNoClobberLease(input) {
|
|
|
71
71
|
if (observed === undefined) {
|
|
72
72
|
throw new Error(`lease ${input.scope} changed repeatedly during contention`);
|
|
73
73
|
}
|
|
74
|
-
if (leaseIsLive(observed.owner, now(), input.staleAfterMs)) {
|
|
75
|
-
throw new Error(`lease ${input.scope} is held by a live or unexpired owner`);
|
|
74
|
+
if (leaseIsLive(observed.owner, now(), input.staleAfterMs, input.probePid)) {
|
|
75
|
+
throw new Error(`lease ${input.scope} is held by a live or unexpired owner, or its liveness is unknown`);
|
|
76
76
|
}
|
|
77
77
|
if (observed.owner.scope !== input.scope ||
|
|
78
78
|
(input.validateStaleOwner !== undefined && !input.validateStaleOwner(observed.owner))) {
|
|
@@ -87,7 +87,7 @@ export function acquireNoClobberLease(input) {
|
|
|
87
87
|
assertSameInodeAndBytes(currentPath, pinPath, observed.bytes);
|
|
88
88
|
}
|
|
89
89
|
catch {
|
|
90
|
-
abortUnchangedRecoveryClaim(leaseDir, recoveryPath,
|
|
90
|
+
abortUnchangedRecoveryClaim(leaseDir, recoveryPath, owner);
|
|
91
91
|
recoveryClaimed = false;
|
|
92
92
|
throw new Error(`lease ${input.scope} recovery raced with another contender`);
|
|
93
93
|
}
|
|
@@ -102,10 +102,7 @@ export function acquireNoClobberLease(input) {
|
|
|
102
102
|
fsyncDirectory(leaseDir);
|
|
103
103
|
input.onRecoveryStep?.("new_current_linked");
|
|
104
104
|
removePinnedOwner(leaseDir, pinPath, observed.owner);
|
|
105
|
-
|
|
106
|
-
unlinkSync(join(recoveryPath, "claim.json"));
|
|
107
|
-
rmdirSync(recoveryPath);
|
|
108
|
-
fsyncDirectory(leaseDir);
|
|
105
|
+
retireRecoveryClaim(leaseDir, recoveryPath, owner, () => input.onRecoveryStep?.("recovery_retired"));
|
|
109
106
|
return {
|
|
110
107
|
...leaseHandle(leaseDir, ownerPath, ownerBytes, owner),
|
|
111
108
|
recovered_owner: observed.owner,
|
|
@@ -184,7 +181,7 @@ function createRecoveryClaim(leaseDir, recoveryPath, claimant, observed) {
|
|
|
184
181
|
}
|
|
185
182
|
function reclaimAbandonedRecovery(leaseDir, recoveryPath, input, now) {
|
|
186
183
|
const claim = readRecoveryClaim(recoveryPath);
|
|
187
|
-
if (leaseIsLive(claim.claimant, now, input.staleAfterMs)) {
|
|
184
|
+
if (leaseIsLive(claim.claimant, now, input.staleAfterMs, input.probePid)) {
|
|
188
185
|
throw new Error(`lease ${input.scope} recovery is already in progress`);
|
|
189
186
|
}
|
|
190
187
|
if (claim.claimant.scope !== input.scope ||
|
|
@@ -231,14 +228,27 @@ function reclaimAbandonedRecovery(leaseDir, recoveryPath, input, now) {
|
|
|
231
228
|
rmdirSync(quarantine);
|
|
232
229
|
fsyncDirectory(leaseDir);
|
|
233
230
|
}
|
|
234
|
-
function abortUnchangedRecoveryClaim(leaseDir, recoveryPath,
|
|
231
|
+
function abortUnchangedRecoveryClaim(leaseDir, recoveryPath, claimant) {
|
|
235
232
|
const claim = readRecoveryClaim(recoveryPath);
|
|
236
233
|
if (claim.claimant.owner_id !== claimant.owner_id) {
|
|
237
234
|
throw new Error(`lease ${claimant.scope} recovery ownership changed`);
|
|
238
235
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
236
|
+
retireRecoveryClaim(leaseDir, recoveryPath, claimant);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Move this claimant's recovery directory to a private name before emptying
|
|
240
|
+
* it. Renaming a directory onto an existing empty directory replaces it, so
|
|
241
|
+
* emptying `recovery` in place would let a contender's claim land there and
|
|
242
|
+
* fail this claimant's final rmdir after it already holds the lease.
|
|
243
|
+
*/
|
|
244
|
+
function retireRecoveryClaim(leaseDir, recoveryPath, claimant, onRetired) {
|
|
245
|
+
const retired = join(leaseDir, `recovery-retired-${claimant.owner_id}`);
|
|
246
|
+
renameSync(recoveryPath, retired);
|
|
247
|
+
fsyncDirectory(leaseDir);
|
|
248
|
+
onRetired?.();
|
|
249
|
+
safeUnlink(join(retired, "stale-pin"));
|
|
250
|
+
unlinkSync(join(retired, "claim.json"));
|
|
251
|
+
rmdirSync(retired);
|
|
242
252
|
fsyncDirectory(leaseDir);
|
|
243
253
|
}
|
|
244
254
|
function readRecoveryClaim(recoveryPath) {
|
|
@@ -355,7 +365,9 @@ function isStringRecord(value) {
|
|
|
355
365
|
!Array.isArray(value) &&
|
|
356
366
|
Object.entries(value).every(([key, item]) => SAFE_COMPONENT.test(key) && typeof item === "string" && item.length <= 1_000));
|
|
357
367
|
}
|
|
358
|
-
function leaseIsLive(owner, now, staleAfterMs) {
|
|
368
|
+
function leaseIsLive(owner, now, staleAfterMs, probePid = (pid) => {
|
|
369
|
+
process.kill(pid, 0);
|
|
370
|
+
}) {
|
|
359
371
|
const created = Date.parse(owner.created_at);
|
|
360
372
|
const expires = Date.parse(owner.expires_at);
|
|
361
373
|
if (!Number.isFinite(created) ||
|
|
@@ -365,14 +377,16 @@ function leaseIsLive(owner, now, staleAfterMs) {
|
|
|
365
377
|
now < expires) {
|
|
366
378
|
return true;
|
|
367
379
|
}
|
|
380
|
+
// Expiry only makes a same-host owner eligible for a process check. Neither
|
|
381
|
+
// a remote owner nor an unreadable probe proves that its work has stopped.
|
|
368
382
|
if (owner.host !== hostname())
|
|
369
|
-
return
|
|
383
|
+
return true;
|
|
370
384
|
try {
|
|
371
|
-
|
|
385
|
+
probePid(owner.pid);
|
|
372
386
|
return true;
|
|
373
387
|
}
|
|
374
388
|
catch (error) {
|
|
375
|
-
return error.code
|
|
389
|
+
return error.code !== "ESRCH";
|
|
376
390
|
}
|
|
377
391
|
}
|
|
378
392
|
function assertExactOwner(currentPath, ownerPath, expectedBytes, owner) {
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type WorkspaceReclaimOptions, type WorkspaceReclaimResult } from "./reclaim.js";
|
|
2
|
+
import type { WorkspaceBinding, WorkspaceProbeInput, WorkspaceProbeResult, WorkspaceProvider } from "./types.js";
|
|
2
3
|
export interface LocalGitWorktreeProviderOptions {
|
|
3
4
|
coordRoot: string;
|
|
4
5
|
descriptorPathsSupported?: () => boolean;
|
|
5
6
|
}
|
|
6
|
-
export
|
|
7
|
+
export interface LocalGitWorktreeProvider extends WorkspaceProvider {
|
|
8
|
+
reclaim(binding: WorkspaceBinding, options: WorkspaceReclaimOptions): Promise<WorkspaceReclaimResult>;
|
|
9
|
+
}
|
|
10
|
+
export declare function createLocalGitWorktreeProvider(options: LocalGitWorktreeProviderOptions): LocalGitWorktreeProvider;
|
|
7
11
|
export declare function probe(input: WorkspaceProbeInput, descriptorPathsSupported?: () => boolean): Promise<WorkspaceProbeResult>;
|
|
8
12
|
//# sourceMappingURL=local-git.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local-git.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/local-git.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"local-git.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/local-git.ts"],"names":[],"mappings":"AA8CA,OAAO,EAML,KAAK,uBAAuB,EAE5B,KAAK,sBAAsB,EAC5B,MAAM,cAAc,CAAC;AAiBtB,OAAO,KAAK,EAYV,gBAAgB,EAIhB,mBAAmB,EACnB,oBAAoB,EACpB,iBAAiB,EAElB,MAAM,YAAY,CAAC;AAUpB,MAAM,WAAW,+BAA+B;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,wBAAwB,CAAC,EAAE,MAAM,OAAO,CAAC;CAC1C;AAED,MAAM,WAAW,wBAAyB,SAAQ,iBAAiB;IACjE,OAAO,CACL,OAAO,EAAE,gBAAgB,EACzB,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,sBAAsB,CAAC,CAAC;CACpC;AAED,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,+BAA+B,GACvC,wBAAwB,CAkB1B;AAED,wBAAsB,KAAK,CACzB,KAAK,EAAE,mBAAmB,EAC1B,wBAAwB,GAAE,MAAM,OAAwC,GACvE,OAAO,CAAC,oBAAoB,CAAC,CA4E/B"}
|
|
@@ -2,12 +2,17 @@ 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 { beginWorkflowRecovery, validateWorkflowRecoveryAuthorization } from "../recovery-audit.js";
|
|
6
|
+
import { acquireWorkflowResumeLease } from "../run-state.js";
|
|
5
7
|
import { WorkspaceAttestationError } from "./attestation-error.js";
|
|
8
|
+
import { cleanupWorkspace } from "./cleanup.js";
|
|
6
9
|
import { git, gitMaybe, gitMaybeWithDirectoryDescriptor, gitOperations, isAncestor, nulEntries, overwriteRiskPaths, parseWorktreeInventory, worktreeInventory, } from "./git.js";
|
|
7
10
|
import { acquireNoClobberLease } from "./leases.js";
|
|
11
|
+
import { claimOverlapsWorkspace, reserveWorkspaceReclaim, } from "./ownership.js";
|
|
8
12
|
import { assertContainedExisting, assertPathIdentity, candidateUnderRoot, containsPath, createContainedDirectories, descriptorBackedPathsSupported, descriptorPath, filesystemIdentity, openContainedDirectory, sameFilesystemIdentity, validateConfiguredRoot, } from "./paths.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
13
|
+
import { prepareReclaim, WorkflowReclaimError, } from "./reclaim.js";
|
|
14
|
+
import { hasActiveWorkflowEvidence, readWorkflowLiveness } from "./recovery.js";
|
|
15
|
+
import { appendWorkspaceEvent, fileSha256, readWorkflowSupplement, readWorkspaceBinding, readWorkspaceClaim, readWorkspaceClaims, readWorkspaceEvents, stableDigest, workspaceClaimPath, workspaceLockPath, writeWorkspaceBinding, writeWorkspaceClaim, } from "./state.js";
|
|
11
16
|
import { isWorkspaceAttestation } from "./validate.js";
|
|
12
17
|
const PROVIDER_ID = "local-git-worktree";
|
|
13
18
|
const PROVIDER_VERSION = "1";
|
|
@@ -17,7 +22,7 @@ const OBJECT_FORMAT = /^[a-f0-9]{40,64}$/;
|
|
|
17
22
|
const REF_FORMAT = /^refs\/heads\/[A-Za-z0-9][A-Za-z0-9._/-]{0,180}$/;
|
|
18
23
|
export function createLocalGitWorktreeProvider(options) {
|
|
19
24
|
const coordRoot = resolve(options.coordRoot);
|
|
20
|
-
|
|
25
|
+
const provider = {
|
|
21
26
|
probe: (input) => probe(input, options.descriptorPathsSupported),
|
|
22
27
|
allocate: (request) => allocate(coordRoot, request),
|
|
23
28
|
readBinding: async (binding) => {
|
|
@@ -30,7 +35,9 @@ export function createLocalGitWorktreeProvider(options) {
|
|
|
30
35
|
previewIntegration: (input) => previewIntegration(coordRoot, input),
|
|
31
36
|
applyAuthorizedIntegration: (input) => applyAuthorizedIntegration(coordRoot, input),
|
|
32
37
|
cleanup: (binding, intent) => cleanup(coordRoot, binding, intent),
|
|
38
|
+
reclaim: (binding, reclaimOptions) => reclaim(coordRoot, provider, binding, reclaimOptions),
|
|
33
39
|
};
|
|
40
|
+
return provider;
|
|
34
41
|
}
|
|
35
42
|
export async function probe(input, descriptorPathsSupported = descriptorBackedPathsSupported) {
|
|
36
43
|
const unsupported = [];
|
|
@@ -585,6 +592,293 @@ function cancelUnderLease(coordRoot, binding, claim) {
|
|
|
585
592
|
appendWorkspaceEvent(coordRoot, claim, "cancellation_outcome", { status: "cancelled" });
|
|
586
593
|
return { ...base, status: "cancelled" };
|
|
587
594
|
}
|
|
595
|
+
function reclaimPreview(coordRoot, binding, mode) {
|
|
596
|
+
const preview = {
|
|
597
|
+
action: "preview",
|
|
598
|
+
run_id: binding.run_id,
|
|
599
|
+
mode,
|
|
600
|
+
liveness: readWorkflowLiveness(coordRoot, binding.run_id),
|
|
601
|
+
evidence_sha256: reclaimEvidenceDigest(coordRoot, binding),
|
|
602
|
+
ownership: "unproven",
|
|
603
|
+
dirty_paths: null,
|
|
604
|
+
lease_holder: reclaimLeaseHolders(coordRoot, binding),
|
|
605
|
+
};
|
|
606
|
+
try {
|
|
607
|
+
const current = attestSync(coordRoot, binding);
|
|
608
|
+
if (current.status === "ok") {
|
|
609
|
+
preview.ownership = "ok";
|
|
610
|
+
preview.dirty_paths = [...(current.repository?.dirty_paths ?? [])];
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
preview.ownership_detail =
|
|
614
|
+
current.provider_drift.join("; ") || "ownership attestation failed";
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
catch (error) {
|
|
618
|
+
preview.ownership_detail = error.message;
|
|
619
|
+
}
|
|
620
|
+
return preview;
|
|
621
|
+
}
|
|
622
|
+
function reclaimEvidenceDigest(coordRoot, binding) {
|
|
623
|
+
const evidence = (name) => {
|
|
624
|
+
try {
|
|
625
|
+
return readFileSync(join(coordRoot, ".harnery", "workflows", binding.run_id, name), "utf8");
|
|
626
|
+
}
|
|
627
|
+
catch (error) {
|
|
628
|
+
if (error.code === "ENOENT")
|
|
629
|
+
return null;
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
return stableDigest({
|
|
634
|
+
binding,
|
|
635
|
+
proof: evidence("proof.json"),
|
|
636
|
+
transcript: evidence("transcript.jsonl"),
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
function assertReclaimLiveness(coordRoot, binding, recovery) {
|
|
640
|
+
const liveness = readWorkflowLiveness(coordRoot, binding.run_id);
|
|
641
|
+
if (liveness === "active" || (recovery && hasActiveWorkflowEvidence(coordRoot, binding.run_id))) {
|
|
642
|
+
throw new WorkflowReclaimError("workflow_reclaim_run_live", `run ${binding.run_id} has active evidence; its workspace was left untouched`);
|
|
643
|
+
}
|
|
644
|
+
if (recovery && reclaimEvidenceDigest(coordRoot, binding) !== recovery.evidenceSha256) {
|
|
645
|
+
throw new WorkflowReclaimError("workflow_reclaim_recovery_mismatch", "workflow reclaim requires the exact current preview evidence digest");
|
|
646
|
+
}
|
|
647
|
+
if (liveness === "inactive")
|
|
648
|
+
return;
|
|
649
|
+
if (liveness === "unknown" && recovery)
|
|
650
|
+
return;
|
|
651
|
+
throw new WorkflowReclaimError("workflow_reclaim_liveness_unknown", `run ${binding.run_id} has missing, corrupt, or ambiguous liveness evidence; its workspace was left untouched`);
|
|
652
|
+
}
|
|
653
|
+
function assertReclaimOwnership(coordRoot, binding) {
|
|
654
|
+
try {
|
|
655
|
+
const claim = requiredClaim(coordRoot, binding);
|
|
656
|
+
const current = attestSync(coordRoot, binding);
|
|
657
|
+
if (current.status !== "ok") {
|
|
658
|
+
throw new Error(current.provider_drift.join("; ") || "ownership attestation failed");
|
|
659
|
+
}
|
|
660
|
+
return claim;
|
|
661
|
+
}
|
|
662
|
+
catch (error) {
|
|
663
|
+
throw new WorkflowReclaimError("workflow_reclaim_ownership_unproven", error.message);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
function assertPeerWorkspaceAuthority(coordRoot, claim) {
|
|
667
|
+
try {
|
|
668
|
+
for (const peer of readWorkspaceClaims(coordRoot, PROVIDER_ID)) {
|
|
669
|
+
if (peer.binding_id === claim.binding_id)
|
|
670
|
+
continue;
|
|
671
|
+
const binding = readWorkspaceBinding(coordRoot, PROVIDER_ID, peer.binding_id);
|
|
672
|
+
const events = readWorkspaceEvents(coordRoot, PROVIDER_ID, peer.binding_id);
|
|
673
|
+
if (binding)
|
|
674
|
+
requiredClaim(coordRoot, binding);
|
|
675
|
+
else if (events.some((event) => event.event === "bound")) {
|
|
676
|
+
throw new Error("retained peer binding authority is missing");
|
|
677
|
+
}
|
|
678
|
+
if (!claimOverlapsWorkspace(peer, claim.workspace_root))
|
|
679
|
+
continue;
|
|
680
|
+
// Process termination, archival and an expired reservation do not release
|
|
681
|
+
// a resource. Require the provider's release record AND current absence.
|
|
682
|
+
if (binding && events.some((event) => event.event === "released")) {
|
|
683
|
+
const current = attestSync(coordRoot, binding);
|
|
684
|
+
if (current.status === "ok" && current.resource_state === "released")
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
throw new Error(`retained peer workspace ${peer.binding_id} overlaps the reclaim path`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
catch (error) {
|
|
691
|
+
throw new WorkflowReclaimError("workflow_reclaim_ownership_unproven", error.message);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
function reclaimAuthorityGuard(coordRoot, binding, claim, reservation, recovery) {
|
|
695
|
+
try {
|
|
696
|
+
reservation.assertCurrent();
|
|
697
|
+
assertReclaimLiveness(coordRoot, binding, recovery);
|
|
698
|
+
assertReclaimOwnership(coordRoot, binding);
|
|
699
|
+
assertPeerWorkspaceAuthority(coordRoot, claim);
|
|
700
|
+
}
|
|
701
|
+
catch (error) {
|
|
702
|
+
if (error instanceof WorkflowReclaimError)
|
|
703
|
+
throw error;
|
|
704
|
+
throw new WorkflowReclaimError("workflow_reclaim_ownership_unproven", error.message);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
async function reclaim(coordRoot, provider, binding, options) {
|
|
708
|
+
if (options.mode !== "salvage" && options.mode !== "discard") {
|
|
709
|
+
throw new Error("unsupported workspace reclaim mode");
|
|
710
|
+
}
|
|
711
|
+
if (!options.yes)
|
|
712
|
+
return reclaimPreview(coordRoot, binding, options.mode);
|
|
713
|
+
if (options.unknownLiveness)
|
|
714
|
+
validateWorkflowRecoveryAuthorization(options.unknownLiveness);
|
|
715
|
+
// Liveness comes before terminal-proof cleanup helpers. A live run may not
|
|
716
|
+
// have a proof yet, and a missing proof must never be discovered after reset.
|
|
717
|
+
assertReclaimLiveness(coordRoot, binding, options.unknownLiveness);
|
|
718
|
+
const claim = assertReclaimOwnership(coordRoot, binding);
|
|
719
|
+
let reservation;
|
|
720
|
+
try {
|
|
721
|
+
reservation = reserveWorkspaceReclaim(coordRoot, claim);
|
|
722
|
+
}
|
|
723
|
+
catch (error) {
|
|
724
|
+
throw new WorkflowReclaimError("workflow_reclaim_lease_held", error.message);
|
|
725
|
+
}
|
|
726
|
+
try {
|
|
727
|
+
reclaimAuthorityGuard(coordRoot, binding, claim, reservation, options.unknownLiveness);
|
|
728
|
+
let releaseResume;
|
|
729
|
+
try {
|
|
730
|
+
releaseResume = acquireWorkflowResumeLease(coordRoot, binding.run_id);
|
|
731
|
+
}
|
|
732
|
+
catch (error) {
|
|
733
|
+
throw new WorkflowReclaimError("workflow_reclaim_lease_held", error.message);
|
|
734
|
+
}
|
|
735
|
+
try {
|
|
736
|
+
let releaseBinding;
|
|
737
|
+
try {
|
|
738
|
+
releaseBinding = acquireLease(coordRoot, workspaceLockPath(coordRoot, PROVIDER_ID, binding.binding_id), claim, "binding");
|
|
739
|
+
}
|
|
740
|
+
catch (error) {
|
|
741
|
+
throw new WorkflowReclaimError("workflow_reclaim_lease_held", error.message);
|
|
742
|
+
}
|
|
743
|
+
try {
|
|
744
|
+
let releaseRepository;
|
|
745
|
+
try {
|
|
746
|
+
releaseRepository = acquireRepositoryLease(coordRoot, claim);
|
|
747
|
+
}
|
|
748
|
+
catch (error) {
|
|
749
|
+
throw new WorkflowReclaimError("workflow_reclaim_lease_held", error.message);
|
|
750
|
+
}
|
|
751
|
+
try {
|
|
752
|
+
assertReclaimLiveness(coordRoot, binding, options.unknownLiveness);
|
|
753
|
+
assertReclaimOwnership(coordRoot, binding);
|
|
754
|
+
const preview = reclaimPreview(coordRoot, binding, options.mode);
|
|
755
|
+
const audit = options.unknownLiveness
|
|
756
|
+
? beginWorkflowRecovery(coordRoot, {
|
|
757
|
+
run_id: binding.run_id,
|
|
758
|
+
action: "reclaim_unknown_liveness",
|
|
759
|
+
actor: options.unknownLiveness.actor,
|
|
760
|
+
reason: options.unknownLiveness.reason,
|
|
761
|
+
target_sha256: options.unknownLiveness.evidenceSha256,
|
|
762
|
+
target: {
|
|
763
|
+
binding_id: binding.binding_id,
|
|
764
|
+
workspace_root: binding.workspace_root,
|
|
765
|
+
mode: options.mode,
|
|
766
|
+
liveness: preview.liveness,
|
|
767
|
+
},
|
|
768
|
+
})
|
|
769
|
+
: undefined;
|
|
770
|
+
let preparation;
|
|
771
|
+
// Keep ordinary cleanup's terminal-evidence checks, intent and receipts.
|
|
772
|
+
// Its provider callback uses the leases already held here instead of
|
|
773
|
+
// trying to acquire them again. Preparation occurs only after all of
|
|
774
|
+
// cleanup's path, ownership, ref and registration checks have succeeded.
|
|
775
|
+
const cleanupResult = await cleanupWorkspace({
|
|
776
|
+
coordRoot,
|
|
777
|
+
runId: binding.run_id,
|
|
778
|
+
provider: {
|
|
779
|
+
...provider,
|
|
780
|
+
cleanup: async (candidate, intent) => {
|
|
781
|
+
assertReclaimLiveness(coordRoot, candidate, options.unknownLiveness);
|
|
782
|
+
assertReclaimOwnership(coordRoot, candidate);
|
|
783
|
+
return cleanupUnderLease(coordRoot, candidate, claim, intent, {
|
|
784
|
+
mode: options.mode,
|
|
785
|
+
recovery: options.unknownLiveness,
|
|
786
|
+
beforeEffect: () => {
|
|
787
|
+
audit?.assertCurrent();
|
|
788
|
+
reclaimAuthorityGuard(coordRoot, candidate, claim, reservation, options.unknownLiveness);
|
|
789
|
+
},
|
|
790
|
+
beforeRefEffect: () => {
|
|
791
|
+
audit?.assertCurrent();
|
|
792
|
+
reservation.assertCurrent();
|
|
793
|
+
assertReclaimLiveness(coordRoot, candidate, options.unknownLiveness);
|
|
794
|
+
requiredClaim(coordRoot, candidate);
|
|
795
|
+
assertPeerWorkspaceAuthority(coordRoot, claim);
|
|
796
|
+
},
|
|
797
|
+
prepared: (value) => {
|
|
798
|
+
preparation = value;
|
|
799
|
+
},
|
|
800
|
+
});
|
|
801
|
+
},
|
|
802
|
+
},
|
|
803
|
+
});
|
|
804
|
+
if (!preparation &&
|
|
805
|
+
cleanupResult.status !== "released" &&
|
|
806
|
+
cleanupResult.status !== "already_released") {
|
|
807
|
+
throw new WorkflowReclaimError("workflow_reclaim_ownership_unproven", ("reason" in cleanupResult ? cleanupResult.reason : undefined) ??
|
|
808
|
+
"workspace cleanup preconditions could not be proved");
|
|
809
|
+
}
|
|
810
|
+
if (cleanupResult.status === "released" || cleanupResult.status === "already_released")
|
|
811
|
+
audit?.complete();
|
|
812
|
+
return {
|
|
813
|
+
...preview,
|
|
814
|
+
action: "reclaimed",
|
|
815
|
+
lease_holder: [],
|
|
816
|
+
preparation: preparation ?? {
|
|
817
|
+
action: "already_gone",
|
|
818
|
+
detail: "the workspace is already released",
|
|
819
|
+
},
|
|
820
|
+
cleanup: cleanupResult,
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
finally {
|
|
824
|
+
releaseRepository();
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
finally {
|
|
828
|
+
releaseBinding();
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
finally {
|
|
832
|
+
releaseResume();
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
finally {
|
|
836
|
+
reservation.release();
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
function reclaimLeaseHolders(coordRoot, binding) {
|
|
840
|
+
const paths = [
|
|
841
|
+
["binding", workspaceLockPath(coordRoot, PROVIDER_ID, binding.binding_id)],
|
|
842
|
+
];
|
|
843
|
+
if (binding.repository) {
|
|
844
|
+
const key = stableDigest({ common: binding.repository.common_dir.identity });
|
|
845
|
+
paths.push([
|
|
846
|
+
"repository",
|
|
847
|
+
join(coordRoot, ".harnery", "workspaces", PROVIDER_ID, ".leases", `repository-${key}.lock`),
|
|
848
|
+
]);
|
|
849
|
+
}
|
|
850
|
+
const holders = [];
|
|
851
|
+
try {
|
|
852
|
+
const owner = JSON.parse(readFileSync(join(coordRoot, ".harnery", "workflows", binding.run_id, "resume.lock"), "utf8"));
|
|
853
|
+
holders.push({
|
|
854
|
+
scope: "resume",
|
|
855
|
+
pid: typeof owner.pid === "number" ? owner.pid : undefined,
|
|
856
|
+
host: typeof owner.host === "string" ? owner.host : undefined,
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
catch (error) {
|
|
860
|
+
if (error.code !== "ENOENT") {
|
|
861
|
+
holders.push({ scope: "resume", detail: "lease holder could not be read" });
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
for (const [scope, path] of paths) {
|
|
865
|
+
try {
|
|
866
|
+
const owner = JSON.parse(readFileSync(join(path, "current"), "utf8"));
|
|
867
|
+
holders.push({
|
|
868
|
+
scope,
|
|
869
|
+
owner_id: typeof owner.owner_id === "string" ? owner.owner_id : undefined,
|
|
870
|
+
pid: typeof owner.pid === "number" ? owner.pid : undefined,
|
|
871
|
+
host: typeof owner.host === "string" ? owner.host : undefined,
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
catch (error) {
|
|
875
|
+
if (error.code !== "ENOENT") {
|
|
876
|
+
holders.push({ scope, detail: "lease holder could not be read" });
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return holders;
|
|
881
|
+
}
|
|
588
882
|
async function cleanup(coordRoot, binding, intent) {
|
|
589
883
|
if (intent.mode !== "normal")
|
|
590
884
|
throw new Error("unsupported workspace cleanup mode");
|
|
@@ -593,7 +887,18 @@ async function cleanup(coordRoot, binding, intent) {
|
|
|
593
887
|
try {
|
|
594
888
|
const releaseRepository = acquireRepositoryLease(coordRoot, claim);
|
|
595
889
|
try {
|
|
596
|
-
|
|
890
|
+
const reservation = reserveWorkspaceReclaim(coordRoot, claim);
|
|
891
|
+
try {
|
|
892
|
+
const authorityGuard = () => {
|
|
893
|
+
reservation.assertCurrent();
|
|
894
|
+
assertPeerWorkspaceAuthority(coordRoot, claim);
|
|
895
|
+
};
|
|
896
|
+
authorityGuard();
|
|
897
|
+
return cleanupUnderLease(coordRoot, binding, claim, intent, undefined, authorityGuard);
|
|
898
|
+
}
|
|
899
|
+
finally {
|
|
900
|
+
reservation.release();
|
|
901
|
+
}
|
|
597
902
|
}
|
|
598
903
|
finally {
|
|
599
904
|
releaseRepository();
|
|
@@ -603,7 +908,7 @@ async function cleanup(coordRoot, binding, intent) {
|
|
|
603
908
|
releaseBinding();
|
|
604
909
|
}
|
|
605
910
|
}
|
|
606
|
-
function cleanupUnderLease(coordRoot, binding, claim, intent) {
|
|
911
|
+
function cleanupUnderLease(coordRoot, binding, claim, intent, reclaimRequest, beforeRemoval) {
|
|
607
912
|
const repository = binding.repository;
|
|
608
913
|
if (!repository ||
|
|
609
914
|
intent.schema_version !== 1 ||
|
|
@@ -641,7 +946,7 @@ function cleanupUnderLease(coordRoot, binding, claim, intent) {
|
|
|
641
946
|
};
|
|
642
947
|
}
|
|
643
948
|
const liveness = readWorkflowLiveness(coordRoot, binding.run_id);
|
|
644
|
-
if (liveness !== "inactive") {
|
|
949
|
+
if (liveness !== "inactive" && !(liveness === "unknown" && reclaimRequest?.recovery)) {
|
|
645
950
|
return {
|
|
646
951
|
...base,
|
|
647
952
|
status: "blocked",
|
|
@@ -653,10 +958,11 @@ function cleanupUnderLease(coordRoot, binding, claim, intent) {
|
|
|
653
958
|
if (before.workspace_exists && before.status !== "ok") {
|
|
654
959
|
return { ...base, status: "blocked", reason: "ownership attestation failed" };
|
|
655
960
|
}
|
|
656
|
-
if ((before.repository?.dirty_paths.length ?? 0) > 0 ||
|
|
961
|
+
if ((!reclaimRequest && (before.repository?.dirty_paths.length ?? 0) > 0) ||
|
|
657
962
|
(before.repository?.conflicts.length ?? 0) > 0 ||
|
|
658
963
|
(before.repository?.operation_in_progress.length ?? 0) > 0) {
|
|
659
|
-
|
|
964
|
+
if (!reclaimRequest)
|
|
965
|
+
appendWorkspaceEvent(coordRoot, claim, "preserved_dirty");
|
|
660
966
|
return {
|
|
661
967
|
...base,
|
|
662
968
|
status: "preserved_dirty",
|
|
@@ -710,6 +1016,17 @@ function cleanupUnderLease(coordRoot, binding, claim, intent) {
|
|
|
710
1016
|
reason: "missing worktree still has ambiguous Git registration",
|
|
711
1017
|
};
|
|
712
1018
|
}
|
|
1019
|
+
if (reclaimRequest) {
|
|
1020
|
+
assertReclaimLiveness(coordRoot, binding, reclaimRequest.recovery);
|
|
1021
|
+
assertReclaimOwnership(coordRoot, binding);
|
|
1022
|
+
revalidateClaimResources(claim, root);
|
|
1023
|
+
reclaimRequest.prepared(prepareReclaim({
|
|
1024
|
+
worktreePath: binding.workspace_root,
|
|
1025
|
+
mode: reclaimRequest.mode,
|
|
1026
|
+
runId: binding.run_id,
|
|
1027
|
+
beforeEffect: reclaimRequest.beforeEffect,
|
|
1028
|
+
}));
|
|
1029
|
+
}
|
|
713
1030
|
appendWorkspaceEvent(coordRoot, claim, "cleanup_pending", {
|
|
714
1031
|
worktree_path: binding.workspace_root,
|
|
715
1032
|
gitdir: repository.gitdir,
|
|
@@ -719,6 +1036,8 @@ function cleanupUnderLease(coordRoot, binding, claim, intent) {
|
|
|
719
1036
|
target_ref_oid: targetRef.out,
|
|
720
1037
|
});
|
|
721
1038
|
if (before.workspace_exists) {
|
|
1039
|
+
reclaimRequest?.beforeEffect();
|
|
1040
|
+
beforeRemoval?.();
|
|
722
1041
|
revalidateClaimResources(claim, root);
|
|
723
1042
|
assertContainedExisting(root, binding.workspace_root, "workspace cleanup path");
|
|
724
1043
|
git(repository.source_root.realpath, ["worktree", "remove", binding.workspace_root]);
|
|
@@ -738,6 +1057,7 @@ function cleanupUnderLease(coordRoot, binding, claim, intent) {
|
|
|
738
1057
|
return { ...base, status: "blocked", reason: "workspace ref changed during cleanup" };
|
|
739
1058
|
}
|
|
740
1059
|
revalidateClaimResources(claim, root);
|
|
1060
|
+
reclaimRequest?.beforeRefEffect();
|
|
741
1061
|
const deleted = gitMaybe(repository.source_root.realpath, [
|
|
742
1062
|
"update-ref",
|
|
743
1063
|
"-d",
|
|
@@ -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"}
|