harnery 0.45.0 → 0.45.2

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.
Files changed (235) hide show
  1. package/dist/commander.js +0 -1
  2. package/dist/commands/files.d.ts +2 -2
  3. package/dist/commands/files.d.ts.map +1 -1
  4. package/dist/commands/files.js +4 -7
  5. package/dist/commands/servers.d.ts.map +1 -1
  6. package/dist/commands/servers.js +17 -11
  7. package/dist/commands/tunnel.d.ts +0 -15
  8. package/dist/commands/tunnel.d.ts.map +1 -1
  9. package/dist/commands/tunnel.js +153 -453
  10. package/dist/commands/workflow.d.ts.map +1 -1
  11. package/dist/commands/workflow.js +6 -45
  12. package/dist/core/agents/coord-client.d.ts.map +1 -1
  13. package/dist/core/agents/coord-client.js +0 -18
  14. package/dist/core/agents/live-authority-v3.d.ts +0 -1
  15. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  16. package/dist/core/agents/live-authority-v3.js +0 -11
  17. package/dist/core/agents/live-lifecycle-v3.d.ts +2 -4
  18. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
  19. package/dist/core/agents/live-lifecycle-v3.js +7 -28
  20. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  21. package/dist/core/agents/state/heartbeat-writer.js +14 -25
  22. package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
  23. package/dist/core/agents/state/live-coordination-writer.js +0 -5
  24. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  25. package/dist/core/agents/state/pidmap.js +2 -7
  26. package/dist/core/agents/state/proc-start.d.ts +20 -31
  27. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  28. package/dist/core/agents/state/proc-start.js +37 -70
  29. package/dist/core/agents/state/service-process.d.ts +61 -0
  30. package/dist/core/agents/state/service-process.d.ts.map +1 -0
  31. package/dist/core/agents/state/service-process.js +143 -0
  32. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  33. package/dist/core/agents/state/stale-sweep.js +70 -180
  34. package/dist/core/artifacts/activity.d.ts +4 -1
  35. package/dist/core/artifacts/activity.d.ts.map +1 -1
  36. package/dist/core/artifacts/activity.js +8 -2
  37. package/dist/core/artifacts/delivery-card.d.ts.map +1 -1
  38. package/dist/core/artifacts/delivery-card.js +1 -2
  39. package/dist/core/artifacts/index.d.ts.map +1 -1
  40. package/dist/core/artifacts/index.js +38 -12
  41. package/dist/core/context/index.d.ts +6 -4
  42. package/dist/core/context/index.d.ts.map +1 -1
  43. package/dist/core/context/index.js +194 -256
  44. package/dist/core/context/observation.d.ts +9 -9
  45. package/dist/core/context/observation.d.ts.map +1 -1
  46. package/dist/core/context/observation.js +37 -6
  47. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
  48. package/dist/core/events/v3/authority-outbox.js +0 -29
  49. package/dist/core/events/v3/producers/coordination-recorder.d.ts +0 -1
  50. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  51. package/dist/core/events/v3/producers/coordination-recorder.js +0 -7
  52. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  53. package/dist/core/events/v3/producers/recorder.js +64 -77
  54. package/dist/core/events/v3/runtime-build.d.ts +14 -0
  55. package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
  56. package/dist/core/events/v3/runtime-build.js +44 -0
  57. package/dist/core/governor/index.d.ts +2 -1
  58. package/dist/core/governor/index.d.ts.map +1 -1
  59. package/dist/core/governor/index.js +2 -1
  60. package/dist/core/governor/service-read.d.ts +3 -10
  61. package/dist/core/governor/service-read.d.ts.map +1 -1
  62. package/dist/core/governor/service-read.js +10 -94
  63. package/dist/core/governor/service.d.ts +3 -7
  64. package/dist/core/governor/service.d.ts.map +1 -1
  65. package/dist/core/governor/service.js +17 -182
  66. package/dist/core/hooks/cli.js +78 -48
  67. package/dist/core/hooks/resolve/owner.js +2 -2
  68. package/dist/core/resources/sampler.js +2 -2
  69. package/dist/core/resources/service-status.d.ts +2 -14
  70. package/dist/core/resources/service-status.d.ts.map +1 -1
  71. package/dist/core/resources/service-status.js +3 -23
  72. package/dist/core/resources/service.d.ts +3 -7
  73. package/dist/core/resources/service.d.ts.map +1 -1
  74. package/dist/core/resources/service.js +12 -101
  75. package/dist/core/resources/status.d.ts.map +1 -1
  76. package/dist/core/resources/status.js +3 -2
  77. package/dist/core/semantic/service.d.ts.map +1 -1
  78. package/dist/core/semantic/service.js +9 -2
  79. package/dist/core/servers/index.d.ts +11 -10
  80. package/dist/core/servers/index.d.ts.map +1 -1
  81. package/dist/core/servers/index.js +82 -192
  82. package/dist/core/servers/tunnels.d.ts +3 -5
  83. package/dist/core/servers/tunnels.d.ts.map +1 -1
  84. package/dist/core/servers/tunnels.js +2 -45
  85. package/dist/core/storage/durable-history.js +56 -22
  86. package/dist/core/storage/segments.d.ts.map +1 -1
  87. package/dist/core/storage/segments.js +73 -17
  88. package/dist/core/supervisor/service.d.ts +4 -7
  89. package/dist/core/supervisor/service.d.ts.map +1 -1
  90. package/dist/core/supervisor/service.js +23 -122
  91. package/dist/core/supervisor/services.js +2 -2
  92. package/dist/core/supervisor/status.d.ts +3 -17
  93. package/dist/core/supervisor/status.d.ts.map +1 -1
  94. package/dist/core/supervisor/status.js +7 -70
  95. package/dist/core/work/state.js +7 -7
  96. package/dist/core/workflow/engine.d.ts.map +1 -1
  97. package/dist/core/workflow/engine.js +23 -66
  98. package/dist/core/workflow/index.d.ts +3 -4
  99. package/dist/core/workflow/index.d.ts.map +1 -1
  100. package/dist/core/workflow/index.js +2 -2
  101. package/dist/core/workflow/live-session-v3.d.ts +0 -6
  102. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  103. package/dist/core/workflow/live-session-v3.js +5 -126
  104. package/dist/core/workflow/proof.d.ts +4 -4
  105. package/dist/core/workflow/proof.d.ts.map +1 -1
  106. package/dist/core/workflow/proof.js +34 -86
  107. package/dist/core/workflow/resume-lock.d.ts +17 -0
  108. package/dist/core/workflow/resume-lock.d.ts.map +1 -0
  109. package/dist/core/workflow/resume-lock.js +158 -0
  110. package/dist/core/workflow/run-state.d.ts +0 -21
  111. package/dist/core/workflow/run-state.d.ts.map +1 -1
  112. package/dist/core/workflow/run-state.js +3 -174
  113. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  114. package/dist/core/workflow/spawn-claude.js +0 -19
  115. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  116. package/dist/core/workflow/spawn-codex.js +0 -13
  117. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  118. package/dist/core/workflow/spawn-cursor.js +0 -19
  119. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  120. package/dist/core/workflow/spawn-opencode.js +0 -14
  121. package/dist/core/workflow/types.d.ts +11 -54
  122. package/dist/core/workflow/types.d.ts.map +1 -1
  123. package/dist/core/workflow/workspaces/leases.d.ts +4 -0
  124. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  125. package/dist/core/workflow/workspaces/leases.js +1 -1
  126. package/dist/core/workflow/workspaces/local-git.js +91 -210
  127. package/dist/core/workflow/workspaces/ownership.d.ts +0 -7
  128. package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -1
  129. package/dist/core/workflow/workspaces/ownership.js +3 -130
  130. package/dist/core/workflow/workspaces/reclaim.d.ts +4 -7
  131. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  132. package/dist/core/workflow/workspaces/reclaim.js +0 -4
  133. package/dist/core/workflow/workspaces/recovery.d.ts +1 -1
  134. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  135. package/dist/core/workflow/workspaces/recovery.js +1 -1
  136. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  137. package/dist/core/workflow/workspaces/state.js +1 -4
  138. package/dist/lib/identities/assume.d.ts.map +1 -1
  139. package/dist/lib/identities/assume.js +0 -4
  140. package/dist/lib/tunnel/state.d.ts +6 -13
  141. package/dist/lib/tunnel/state.d.ts.map +1 -1
  142. package/dist/lib/tunnel/state.js +12 -58
  143. package/package.json +1 -6
  144. package/src/commander.ts +0 -10
  145. package/src/commands/files.ts +5 -13
  146. package/src/commands/servers.ts +23 -12
  147. package/src/commands/tunnel.ts +166 -533
  148. package/src/commands/workflow.ts +8 -58
  149. package/src/core/agents/coord-client.ts +0 -20
  150. package/src/core/agents/live-authority-v3.ts +0 -17
  151. package/src/core/agents/live-lifecycle-v3.ts +14 -52
  152. package/src/core/agents/state/heartbeat-writer.ts +12 -29
  153. package/src/core/agents/state/live-coordination-writer.ts +0 -29
  154. package/src/core/agents/state/pidmap.ts +2 -15
  155. package/src/core/agents/state/proc-start.ts +41 -69
  156. package/src/core/agents/state/service-process.ts +217 -0
  157. package/src/core/agents/state/stale-sweep.ts +82 -247
  158. package/src/core/artifacts/activity.ts +9 -2
  159. package/src/core/artifacts/delivery-card.ts +1 -7
  160. package/src/core/artifacts/index.ts +45 -12
  161. package/src/core/context/index.ts +207 -272
  162. package/src/core/context/observation.ts +40 -9
  163. package/src/core/events/v3/authority-outbox.ts +0 -56
  164. package/src/core/events/v3/producers/coordination-recorder.ts +0 -15
  165. package/src/core/events/v3/producers/recorder.ts +68 -94
  166. package/src/core/events/v3/runtime-build.ts +62 -0
  167. package/src/core/governor/index.ts +1 -2
  168. package/src/core/governor/service-read.ts +15 -114
  169. package/src/core/governor/service.ts +36 -220
  170. package/src/core/hooks/cli.ts +85 -47
  171. package/src/core/hooks/resolve/owner.ts +2 -2
  172. package/src/core/resources/sampler.ts +3 -2
  173. package/src/core/resources/service-status.ts +6 -35
  174. package/src/core/resources/service.ts +23 -133
  175. package/src/core/resources/status.ts +3 -2
  176. package/src/core/semantic/service.ts +8 -2
  177. package/src/core/servers/index.ts +86 -228
  178. package/src/core/servers/tunnels.ts +3 -55
  179. package/src/core/storage/durable-history.ts +52 -20
  180. package/src/core/storage/segments.ts +79 -17
  181. package/src/core/supervisor/service.ts +33 -155
  182. package/src/core/supervisor/services.ts +2 -2
  183. package/src/core/supervisor/status.ts +10 -85
  184. package/src/core/work/state.ts +7 -7
  185. package/src/core/workflow/engine.ts +22 -74
  186. package/src/core/workflow/index.ts +2 -13
  187. package/src/core/workflow/live-session-v3.ts +5 -146
  188. package/src/core/workflow/proof.ts +40 -104
  189. package/src/core/workflow/resume-lock.ts +179 -0
  190. package/src/core/workflow/run-state.ts +3 -207
  191. package/src/core/workflow/spawn-claude.ts +0 -18
  192. package/src/core/workflow/spawn-codex.ts +0 -13
  193. package/src/core/workflow/spawn-cursor.ts +0 -18
  194. package/src/core/workflow/spawn-opencode.ts +0 -14
  195. package/src/core/workflow/types.ts +11 -41
  196. package/src/core/workflow/workspaces/leases.ts +1 -1
  197. package/src/core/workflow/workspaces/local-git.ts +120 -239
  198. package/src/core/workflow/workspaces/ownership.ts +3 -158
  199. package/src/core/workflow/workspaces/reclaim.ts +3 -9
  200. package/src/core/workflow/workspaces/recovery.ts +1 -1
  201. package/src/core/workflow/workspaces/state.ts +1 -5
  202. package/src/lib/identities/assume.ts +0 -4
  203. package/src/lib/tunnel/state.ts +15 -70
  204. package/dist/commands/turn-rollback.d.ts +0 -5
  205. package/dist/commands/turn-rollback.d.ts.map +0 -1
  206. package/dist/commands/turn-rollback.js +0 -76
  207. package/dist/core/agents/state/process-handle.d.ts +0 -26
  208. package/dist/core/agents/state/process-handle.d.ts.map +0 -1
  209. package/dist/core/agents/state/process-handle.js +0 -121
  210. package/dist/core/agents/state/unchanged-file.d.ts +0 -44
  211. package/dist/core/agents/state/unchanged-file.d.ts.map +0 -1
  212. package/dist/core/agents/state/unchanged-file.js +0 -390
  213. package/dist/core/events/v3/generation-custody.d.ts +0 -5
  214. package/dist/core/events/v3/generation-custody.d.ts.map +0 -1
  215. package/dist/core/events/v3/generation-custody.js +0 -50
  216. package/dist/core/turn-rollback/content.d.ts +0 -53
  217. package/dist/core/turn-rollback/content.d.ts.map +0 -1
  218. package/dist/core/turn-rollback/content.js +0 -396
  219. package/dist/core/turn-rollback/index.d.ts +0 -72
  220. package/dist/core/turn-rollback/index.d.ts.map +0 -1
  221. package/dist/core/turn-rollback/index.js +0 -443
  222. package/dist/core/workflow/failure-evidence.d.ts +0 -16
  223. package/dist/core/workflow/failure-evidence.d.ts.map +0 -1
  224. package/dist/core/workflow/failure-evidence.js +0 -114
  225. package/dist/core/workflow/recovery-audit.d.ts +0 -30
  226. package/dist/core/workflow/recovery-audit.d.ts.map +0 -1
  227. package/dist/core/workflow/recovery-audit.js +0 -116
  228. package/src/commands/turn-rollback.ts +0 -111
  229. package/src/core/agents/state/process-handle.ts +0 -165
  230. package/src/core/agents/state/unchanged-file.ts +0 -456
  231. package/src/core/events/v3/generation-custody.ts +0 -59
  232. package/src/core/turn-rollback/content.ts +0 -499
  233. package/src/core/turn-rollback/index.ts +0 -580
  234. package/src/core/workflow/failure-evidence.ts +0 -141
  235. package/src/core/workflow/recovery-audit.ts +0 -181
@@ -1,141 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import type { AdapterRawResult } from "../adapters/types.ts";
3
- import type { SpawnFailureEvidence, SpawnOutputSource, SpawnResult } from "./types.ts";
4
-
5
- export function unknownFailureEvidence(): SpawnFailureEvidence {
6
- return {
7
- effect: { state: "unknown" },
8
- refusal: { state: "unknown" },
9
- partial_output: { state: "unknown" },
10
- };
11
- }
12
-
13
- /** Built-in subprocess adapters observe output and process-start failures.
14
- * Their current vendor envelopes do not prove effects or native refusal.
15
- * In particular, blank output and 429/5xx statuses leave both unknown. */
16
- export function observeSpawnFailure(
17
- raw: Pick<AdapterRawResult, "spawnErrno" | "timedOut"> &
18
- Partial<Pick<AdapterRawResult, "stdout" | "resultFileText">>,
19
- text: string,
20
- source: SpawnOutputSource,
21
- ): SpawnFailureEvidence {
22
- const bytes = Buffer.byteLength(text);
23
- return {
24
- effect:
25
- raw.spawnErrno === "ENOENT" &&
26
- !raw.timedOut &&
27
- bytes === 0 &&
28
- !raw.stdout?.trim() &&
29
- !raw.resultFileText?.trim()
30
- ? { state: "none", source: "spawn_errno", reference: "ENOENT" }
31
- : { state: "unknown" },
32
- refusal: { state: "unknown" },
33
- partial_output:
34
- bytes > 0
35
- ? {
36
- state: "present",
37
- source,
38
- bytes,
39
- sha256: createHash("sha256").update(text).digest("hex"),
40
- }
41
- : { state: "absent", source },
42
- };
43
- }
44
-
45
- function record(value: unknown): value is Record<string, unknown> {
46
- return typeof value === "object" && value !== null && !Array.isArray(value);
47
- }
48
-
49
- function keys(value: Record<string, unknown>, expected: readonly string[]): boolean {
50
- return (
51
- Object.keys(value).length === expected.length &&
52
- expected.every((key) => Object.hasOwn(value, key))
53
- );
54
- }
55
-
56
- function reference(value: unknown): value is string {
57
- return typeof value === "string" && value.trim().length > 0 && value.length <= 512;
58
- }
59
-
60
- function validEffect(value: unknown): boolean {
61
- if (!record(value)) return false;
62
- if (value.state === "unknown") return keys(value, ["state"]);
63
- if (!keys(value, ["state", "source", "reference"])) return false;
64
- if (value.state === "none" && value.source === "spawn_errno") {
65
- return value.reference === "ENOENT";
66
- }
67
- return (
68
- (value.state === "none" || value.state === "observed") &&
69
- value.source === "adapter_evidence" &&
70
- reference(value.reference)
71
- );
72
- }
73
-
74
- function validRefusal(value: unknown): boolean {
75
- if (!record(value)) return false;
76
- if (value.state === "unknown") return keys(value, ["state"]);
77
- return (
78
- keys(value, ["state", "source", "reference"]) &&
79
- value.state === "refused" &&
80
- value.source === "vendor_envelope" &&
81
- reference(value.reference)
82
- );
83
- }
84
-
85
- function validOutput(value: unknown): boolean {
86
- if (!record(value)) return false;
87
- if (value.state === "unknown") return keys(value, ["state"]);
88
- if (
89
- value.source !== "final_message" &&
90
- value.source !== "vendor_envelope" &&
91
- value.source !== "stdout"
92
- ) {
93
- return false;
94
- }
95
- if (value.state === "absent") return keys(value, ["state", "source"]);
96
- return (
97
- value.state === "present" &&
98
- keys(value, ["state", "source", "bytes", "sha256"]) &&
99
- Number.isSafeInteger(value.bytes) &&
100
- (value.bytes as number) > 0 &&
101
- typeof value.sha256 === "string" &&
102
- /^[a-f0-9]{64}$/.test(value.sha256)
103
- );
104
- }
105
-
106
- export function isSpawnFailureEvidence(value: unknown): value is SpawnFailureEvidence {
107
- return (
108
- record(value) &&
109
- keys(value, ["effect", "refusal", "partial_output"]) &&
110
- validEffect(value.effect) &&
111
- validRefusal(value.refusal) &&
112
- validOutput(value.partial_output)
113
- );
114
- }
115
-
116
- /** Missing observations are unknown; malformed positive claims are rejected.
117
- * This is also used when reading persisted workflow proof. */
118
- export function resolveFailureEvidence(value: unknown): SpawnFailureEvidence {
119
- if (value === undefined) return unknownFailureEvidence();
120
- if (!isSpawnFailureEvidence(value)) throw new Error("invalid spawn failure evidence");
121
- return value;
122
- }
123
-
124
- export function failureEvidenceAllowsUncharged(value: unknown): boolean {
125
- const evidence = resolveFailureEvidence(value);
126
- return (
127
- evidence.effect.state === "none" &&
128
- evidence.refusal.state !== "refused" &&
129
- evidence.partial_output.state === "absent"
130
- );
131
- }
132
-
133
- /** A provider retry requires positive no-effect evidence. A missing binary is
134
- * known not to have started, but retrying that unchanged environment cannot help. */
135
- export function canRetryFailedSpawn(result: SpawnResult): boolean {
136
- return (
137
- !result.ok &&
138
- result.class !== "environment" &&
139
- failureEvidenceAllowsUncharged(result.failureEvidence)
140
- );
141
- }
@@ -1,181 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { existsSync, lstatSync, mkdirSync, readdirSync } from "node:fs";
3
- import { hostname } from "node:os";
4
- import { dirname, join } from "node:path";
5
- import { stateDirMode } from "../storage/modes.ts";
6
- import { assertWorkflowRunId } from "./approvals.ts";
7
- import {
8
- fsyncParentDirectory,
9
- readJsonRecord,
10
- stableDigest,
11
- writeImmutableJson,
12
- } from "./durable-record.ts";
13
-
14
- export interface WorkflowRecoveryAuthorization {
15
- actor: string;
16
- reason: string;
17
- }
18
-
19
- export interface WorkflowRecoveryIntent extends WorkflowRecoveryAuthorization {
20
- schema_version: 1;
21
- operation_id: string;
22
- run_id: string;
23
- action: "reclaim_unknown_liveness" | "unlock_resume";
24
- target_sha256: string;
25
- target: Record<string, unknown>;
26
- recorded_at: string;
27
- pid: number;
28
- host: string;
29
- outcome: "unknown";
30
- }
31
-
32
- export interface WorkflowRecoveryRecord {
33
- intent: WorkflowRecoveryIntent;
34
- outcome: "unknown" | "completed";
35
- }
36
-
37
- export function validateWorkflowRecoveryAuthorization(
38
- authorization: WorkflowRecoveryAuthorization,
39
- ): void {
40
- for (const [field, limit] of [
41
- ["actor", 200],
42
- ["reason", 2_000],
43
- ] as const) {
44
- const value = authorization[field];
45
- if (
46
- typeof value !== "string" ||
47
- !value.trim() ||
48
- value.length > limit ||
49
- Array.from(value).some(
50
- (character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127,
51
- )
52
- ) {
53
- throw new Error(
54
- `workflow recovery requires a nonempty ${field} of at most ${limit} characters without control characters`,
55
- );
56
- }
57
- }
58
- }
59
-
60
- /** Canonical recovery ledger: immutable intent is durable before effects.
61
- * Completion cites that exact intent; absence always means unknown. */
62
- export function beginWorkflowRecovery(
63
- coordRoot: string,
64
- input: Omit<
65
- WorkflowRecoveryIntent,
66
- "schema_version" | "operation_id" | "recorded_at" | "pid" | "host" | "outcome"
67
- >,
68
- ): { intent: WorkflowRecoveryIntent; assertCurrent(): void; complete(): void } {
69
- assertWorkflowRunId(input.run_id);
70
- validateWorkflowRecoveryAuthorization(input);
71
- if (!/^[a-f0-9]{64}$/.test(input.target_sha256))
72
- throw new Error("workflow recovery requires an exact preview digest");
73
- const intent: WorkflowRecoveryIntent = {
74
- ...input,
75
- schema_version: 1,
76
- operation_id: `recovery-${randomUUID()}`,
77
- recorded_at: new Date().toISOString(),
78
- pid: process.pid,
79
- host: hostname(),
80
- outcome: "unknown",
81
- };
82
- const directory = join(
83
- coordRoot,
84
- ".harnery",
85
- "workflows",
86
- input.run_id,
87
- "recovery",
88
- intent.operation_id,
89
- );
90
- // Sync each newly published directory name too. Syncing only the intent's
91
- // immediate parent cannot make a newly created ancestor durable.
92
- const runDirectory = join(coordRoot, ".harnery", "workflows", input.run_id);
93
- ensureDurableDirectory(runDirectory);
94
- const ledgerDirectory = join(runDirectory, "recovery");
95
- ensureDurableDirectory(ledgerDirectory);
96
- fsyncParentDirectory(ledgerDirectory);
97
- mkdirSync(directory, { mode: stateDirMode() });
98
- fsyncParentDirectory(directory);
99
- const intentPath = join(directory, "intent.json");
100
- writeImmutableJson(intentPath, intent);
101
- const assertCurrent = () => {
102
- if (stableDigest(readJsonRecord(intentPath, "recovery intent")) !== stableDigest(intent))
103
- throw new Error("workflow recovery audit authority changed");
104
- };
105
- return {
106
- intent,
107
- assertCurrent,
108
- complete: () => {
109
- assertCurrent();
110
- writeImmutableJson(join(directory, "completion.json"), {
111
- schema_version: 1,
112
- operation_id: intent.operation_id,
113
- intent_sha256: stableDigest(intent),
114
- outcome: "completed",
115
- recorded_at: new Date().toISOString(),
116
- });
117
- },
118
- };
119
- }
120
-
121
- function ensureDurableDirectory(path: string): void {
122
- if (existsSync(path)) {
123
- if (!lstatSync(path).isDirectory())
124
- throw new Error("workflow recovery ledger is not a directory");
125
- return;
126
- }
127
- ensureDurableDirectory(dirname(path));
128
- try {
129
- mkdirSync(path, { mode: stateDirMode() });
130
- } catch (error) {
131
- if ((error as NodeJS.ErrnoException).code !== "EEXIST" || !lstatSync(path).isDirectory())
132
- throw error;
133
- }
134
- fsyncParentDirectory(path);
135
- }
136
-
137
- export function readWorkflowRecoveryLedger(
138
- coordRoot: string,
139
- runId: string,
140
- ): WorkflowRecoveryRecord[] {
141
- assertWorkflowRunId(runId);
142
- const directory = join(coordRoot, ".harnery", "workflows", runId, "recovery");
143
- if (!existsSync(directory)) return [];
144
- return readdirSync(directory)
145
- .sort()
146
- .map((entry) => {
147
- if (!/^recovery-[a-f0-9-]{36}$/.test(entry))
148
- throw new Error("invalid workflow recovery ledger entry");
149
- const intent = readJsonRecord<WorkflowRecoveryIntent>(
150
- join(directory, entry, "intent.json"),
151
- "recovery intent",
152
- );
153
- if (
154
- intent.schema_version !== 1 ||
155
- intent.operation_id !== entry ||
156
- intent.run_id !== runId ||
157
- intent.outcome !== "unknown" ||
158
- !["reclaim_unknown_liveness", "unlock_resume"].includes(intent.action) ||
159
- !/^[a-f0-9]{64}$/.test(intent.target_sha256)
160
- ) {
161
- throw new Error("invalid workflow recovery intent authority");
162
- }
163
- validateWorkflowRecoveryAuthorization(intent);
164
- const completionPath = join(directory, entry, "completion.json");
165
- if (!existsSync(completionPath)) return { intent, outcome: "unknown" };
166
- const completion = readJsonRecord<{
167
- schema_version: number;
168
- operation_id: string;
169
- intent_sha256: string;
170
- outcome: string;
171
- }>(completionPath, "recovery completion");
172
- if (
173
- completion.schema_version !== 1 ||
174
- completion.operation_id !== entry ||
175
- completion.intent_sha256 !== stableDigest(intent) ||
176
- completion.outcome !== "completed"
177
- )
178
- throw new Error("invalid workflow recovery completion authority");
179
- return { intent, outcome: "completed" };
180
- });
181
- }