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
@@ -0,0 +1,179 @@
1
+ // Kept apart from run-state.ts: the resume lock reads process identity, and
2
+ // run-state.ts is also reached from read-only dashboard modules.
3
+ import { randomUUID } from "node:crypto";
4
+ import {
5
+ chmodSync,
6
+ closeSync,
7
+ linkSync,
8
+ lstatSync,
9
+ mkdirSync,
10
+ openSync,
11
+ readFileSync,
12
+ renameSync,
13
+ unlinkSync,
14
+ writeFileSync,
15
+ } from "node:fs";
16
+ import { hostname } from "node:os";
17
+ import { dirname, join } from "node:path";
18
+ import { processStartToken, verifyProcessGeneration } from "../agents/state/proc-start.ts";
19
+ import { stateFileMode } from "../storage/modes.ts";
20
+ import { assertWorkflowRunId } from "./approvals.ts";
21
+
22
+ export type WorkflowResumeOwnerState = "dead" | "live" | "unknown";
23
+
24
+ export interface WorkflowResumeLockOwner {
25
+ pid: number;
26
+ host: string;
27
+ created_at: string;
28
+ start_token?: string | null;
29
+ }
30
+
31
+ /** Acquire the exclusive lease shared by resume and reclaim. A lock whose
32
+ * same-host owner has exited, or whose PID now names a different process, is
33
+ * retired automatically. Every other existing lock refuses. */
34
+ export function acquireWorkflowResumeLease(coordRoot: string, runId: string): () => void {
35
+ const path = workflowResumeLockPath(coordRoot, runId);
36
+ const owner = {
37
+ owner_id: randomUUID(),
38
+ pid: process.pid,
39
+ host: hostname(),
40
+ created_at: new Date().toISOString(),
41
+ start_token: processStartToken(process.pid),
42
+ };
43
+ const ownerBytes = `${JSON.stringify(owner)}\n`;
44
+ mkdirSync(dirname(path), { recursive: true });
45
+
46
+ const acquire = (): { device: number; inode: number } | false => {
47
+ try {
48
+ const fd = openSync(path, "wx", stateFileMode());
49
+ try {
50
+ writeFileSync(fd, ownerBytes, "utf8");
51
+ } finally {
52
+ closeSync(fd);
53
+ }
54
+ chmodSync(path, stateFileMode());
55
+ const identity = lstatSync(path);
56
+ return { device: identity.dev, inode: identity.ino };
57
+ } catch (error) {
58
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
59
+ return false;
60
+ }
61
+ };
62
+
63
+ const acquired = acquire() || (retireDeadResumeLock(path) && acquire());
64
+ if (!acquired) {
65
+ const existing = readWorkflowResumeLock(coordRoot, runId);
66
+ const holder = existing
67
+ ? ` by pid ${existing.pid} on ${existing.host} (owner ${workflowResumeOwnerState(existing)})`
68
+ : "; its owner could not be read";
69
+ throw new Error(`workflow run ${runId} resume lease is already held${holder}`);
70
+ }
71
+
72
+ return () => {
73
+ try {
74
+ // Pin identity and bytes, so a stale closure never removes a replacement.
75
+ const identity = lstatSync(path);
76
+ if (
77
+ identity.isFile() &&
78
+ identity.dev === acquired.device &&
79
+ identity.ino === acquired.inode &&
80
+ readFileSync(path, "utf8") === ownerBytes
81
+ ) {
82
+ unlinkSync(path);
83
+ }
84
+ } catch {
85
+ // Leave an unreadable or replaced lock untouched.
86
+ }
87
+ };
88
+ }
89
+
90
+ export function workflowResumeLockPath(coordRoot: string, runId: string): string {
91
+ assertWorkflowRunId(runId);
92
+ return join(coordRoot, ".harnery", "workflows", runId, "resume.lock");
93
+ }
94
+
95
+ /** Only a same-host owner can be proven gone: by ESRCH, or by a recorded start
96
+ * token that no longer matches its PID. Foreign and unreadable owners stay live. */
97
+ export function workflowResumeOwnerState(owner: WorkflowResumeLockOwner): WorkflowResumeOwnerState {
98
+ if (owner.host !== hostname()) return "unknown";
99
+ try {
100
+ process.kill(owner.pid, 0);
101
+ } catch (error) {
102
+ const code = (error as NodeJS.ErrnoException).code;
103
+ if (code === "ESRCH") return "dead";
104
+ if (code !== "EPERM") return "unknown";
105
+ }
106
+ return verifyProcessGeneration(owner.pid, owner.start_token) === "different" ? "dead" : "live";
107
+ }
108
+
109
+ /** Move a dead owner's exact lock aside before deleting it. If a contender
110
+ * replaced it in between, the moved bytes (a fresh owner_id) differ, and that
111
+ * live lock is linked back. */
112
+ function retireDeadResumeLock(path: string): boolean {
113
+ let bytes: string;
114
+ try {
115
+ bytes = readFileSync(path, "utf8");
116
+ } catch (error) {
117
+ return (error as NodeJS.ErrnoException).code === "ENOENT";
118
+ }
119
+ const owner = parseResumeLock(bytes);
120
+ if (!owner || workflowResumeOwnerState(owner) !== "dead") return false;
121
+ const aside = `${path}.dead-${randomUUID()}`;
122
+ try {
123
+ renameSync(path, aside);
124
+ } catch (error) {
125
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return true;
126
+ throw error;
127
+ }
128
+ const exact = readFileSync(aside, "utf8") === bytes;
129
+ if (!exact) {
130
+ try {
131
+ linkSync(aside, path);
132
+ } catch {
133
+ // A third contender already holds the path; the moved owner's release
134
+ // finds a different inode and leaves that lock alone.
135
+ }
136
+ }
137
+ unlinkSync(aside);
138
+ return exact;
139
+ }
140
+
141
+ export function readWorkflowResumeLock(
142
+ coordRoot: string,
143
+ runId: string,
144
+ ): WorkflowResumeLockOwner | null {
145
+ return readResumeLease(workflowResumeLockPath(coordRoot, runId));
146
+ }
147
+
148
+ function readResumeLease(path: string): WorkflowResumeLockOwner | null {
149
+ try {
150
+ return parseResumeLock(readFileSync(path, "utf8"));
151
+ } catch {
152
+ return null;
153
+ }
154
+ }
155
+
156
+ function parseResumeLock(bytes: string): WorkflowResumeLockOwner | null {
157
+ try {
158
+ const value = JSON.parse(bytes) as Record<string, unknown>;
159
+ return typeof value.pid === "number" &&
160
+ Number.isSafeInteger(value.pid) &&
161
+ value.pid > 0 &&
162
+ typeof value.host === "string" &&
163
+ value.host.length > 0 &&
164
+ validTimestamp(value.created_at)
165
+ ? {
166
+ pid: value.pid,
167
+ host: value.host,
168
+ created_at: value.created_at,
169
+ start_token: typeof value.start_token === "string" ? value.start_token : null,
170
+ }
171
+ : null;
172
+ } catch {
173
+ return null;
174
+ }
175
+ }
176
+
177
+ function validTimestamp(value: unknown): value is string {
178
+ return typeof value === "string" && value.length <= 40 && Number.isFinite(Date.parse(value));
179
+ }
@@ -1,18 +1,5 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
- import {
3
- chmodSync,
4
- closeSync,
5
- existsSync,
6
- fsyncSync,
7
- lstatSync,
8
- mkdirSync,
9
- openSync,
10
- readFileSync,
11
- statSync,
12
- unlinkSync,
13
- writeFileSync,
14
- } from "node:fs";
15
- import { hostname } from "node:os";
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
16
3
  import { dirname, isAbsolute, join, resolve } from "node:path";
17
4
  import type { RepoSnapshot } from "../context/index.ts";
18
5
  import {
@@ -22,16 +9,9 @@ import {
22
9
  type PolicyNetworkAccess,
23
10
  policyDigest,
24
11
  } from "../policy/index.ts";
25
- import { stateFileMode } from "../storage/modes.ts";
26
12
  import { assertWorkflowRunId, readWorkflowApproval } from "./approvals.ts";
27
13
  import { isCanonicalWorkflowAttemptContext } from "./attempt-context.ts";
28
- import {
29
- fsyncParentDirectory,
30
- readJsonRecord,
31
- stableDigest,
32
- writeImmutableJson,
33
- } from "./durable-record.ts";
34
- import { beginWorkflowRecovery, type WorkflowRecoveryAuthorization } from "./recovery-audit.ts";
14
+ import { readJsonRecord, writeImmutableJson } from "./durable-record.ts";
35
15
  import { normalizeWorkflowSpecialists } from "./specialists.ts";
36
16
  import type {
37
17
  WorkflowAttemptContext,
@@ -46,7 +26,6 @@ import type {
46
26
  WorkspaceBinding,
47
27
  WorkspaceCompatibilityExecutionEvidence,
48
28
  } from "./workspaces/index.ts";
49
- import { acquireNoClobberLease } from "./workspaces/leases.ts";
50
29
  import {
51
30
  assertWorkspaceManifestAuthority,
52
31
  isWorkspaceBinding,
@@ -212,189 +191,6 @@ export function workflowRunManifestPath(coordRoot: string, runId: string): strin
212
191
  return join(coordRoot, ".harnery", "workflows", runId, "run.json");
213
192
  }
214
193
 
215
- /** Acquire an exclusive lease shared by resume and reclaim. Existing locks
216
- * always refuse; explicit audited recovery is a separate operation. */
217
- export function acquireWorkflowResumeLease(coordRoot: string, runId: string): () => void {
218
- assertWorkflowRunId(runId);
219
- const path = join(coordRoot, ".harnery", "workflows", runId, "resume.lock");
220
- const owner = {
221
- owner_id: randomUUID(),
222
- pid: process.pid,
223
- host: hostname(),
224
- created_at: new Date().toISOString(),
225
- };
226
- const ownerBytes = `${JSON.stringify(owner)}\n`;
227
- mkdirSync(dirname(path), { recursive: true });
228
-
229
- const acquire = (): { device: number; inode: number } | false => {
230
- try {
231
- const fd = openSync(path, "wx", stateFileMode());
232
- try {
233
- writeFileSync(fd, ownerBytes, "utf8");
234
- fsyncSync(fd);
235
- } finally {
236
- closeSync(fd);
237
- }
238
- chmodSync(path, stateFileMode());
239
- fsyncParentDirectory(path);
240
- const identity = lstatSync(path);
241
- return { device: identity.dev, inode: identity.ino };
242
- } catch (error) {
243
- if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
244
- return false;
245
- }
246
- };
247
-
248
- const acquired = withResumeLeaseMutation(coordRoot, runId, acquire);
249
- if (!acquired) {
250
- const existing = readResumeLease(path);
251
- const holder = existing
252
- ? ` by pid ${existing.pid} on ${existing.host}; its liveness has not been verified`
253
- : "; its owner could not be verified";
254
- throw new Error(`workflow run ${runId} resume lease is already held${holder}`);
255
- }
256
-
257
- return () => {
258
- try {
259
- // Only this live process can release this nonce. Acquisition never
260
- // replaces a present lock, and unlock requires ESRCH for its owner, so
261
- // neither supported writer can replace this lock during synchronous
262
- // release. Taking their mutation gate would strand a live owner's lock
263
- // on ordinary contention. Pin both identity and bytes for stale closures.
264
- const identity = lstatSync(path);
265
- if (
266
- identity.isFile() &&
267
- identity.dev === acquired.device &&
268
- identity.ino === acquired.inode &&
269
- readFileSync(path, "utf8") === ownerBytes
270
- ) {
271
- unlinkSync(path);
272
- fsyncParentDirectory(path);
273
- }
274
- } catch {
275
- // Leave an unreadable or replaced lock untouched.
276
- }
277
- };
278
- }
279
-
280
- export interface WorkflowResumeRecoveryOptions extends Partial<WorkflowRecoveryAuthorization> {
281
- yes?: boolean;
282
- lockSha256?: string;
283
- }
284
-
285
- export interface WorkflowResumeRecoveryResult {
286
- action: "preview" | "unlocked";
287
- run_id: string;
288
- lock_sha256: string | null;
289
- owner_state: "dead" | "live" | "unknown" | "absent";
290
- owner: { pid: number; host: string; created_at: string } | null;
291
- recovery_id?: string;
292
- }
293
-
294
- export function recoverWorkflowResumeLease(
295
- coordRoot: string,
296
- runId: string,
297
- options: WorkflowResumeRecoveryOptions,
298
- ): WorkflowResumeRecoveryResult {
299
- assertWorkflowRunId(runId);
300
- const path = join(coordRoot, ".harnery", "workflows", runId, "resume.lock");
301
- const inspect = (): WorkflowResumeRecoveryResult => {
302
- let lockSha256: string;
303
- try {
304
- const stat = lstatSync(path);
305
- if (!stat.isFile() || stat.size > 8 * 1024)
306
- throw new Error("resume lock is not a bounded regular file");
307
- lockSha256 = stableDigest({
308
- bytes: readFileSync(path, "utf8"),
309
- device: stat.dev,
310
- inode: stat.ino,
311
- });
312
- } catch (error) {
313
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
314
- return {
315
- action: "preview",
316
- run_id: runId,
317
- lock_sha256: null,
318
- owner_state: "absent",
319
- owner: null,
320
- };
321
- }
322
- const owner = readResumeLease(path);
323
- let ownerState: WorkflowResumeRecoveryResult["owner_state"] = "unknown";
324
- if (owner?.host === hostname()) {
325
- try {
326
- process.kill(owner.pid, 0);
327
- ownerState = "live";
328
- } catch (error) {
329
- if ((error as NodeJS.ErrnoException).code === "ESRCH") ownerState = "dead";
330
- }
331
- }
332
- return {
333
- action: "preview",
334
- run_id: runId,
335
- lock_sha256: lockSha256,
336
- owner_state: ownerState,
337
- owner,
338
- };
339
- };
340
- if (!options.yes) return inspect();
341
- return withResumeLeaseMutation(coordRoot, runId, () => {
342
- const observed = inspect();
343
- if (observed.owner_state !== "dead")
344
- throw new Error(`workflow resume unlock refused: owner is ${observed.owner_state}`);
345
- if (!options.lockSha256 || options.lockSha256 !== observed.lock_sha256)
346
- throw new Error("workflow resume unlock requires the exact current preview digest");
347
- const audit = beginWorkflowRecovery(coordRoot, {
348
- run_id: runId,
349
- action: "unlock_resume",
350
- target_sha256: options.lockSha256,
351
- target: { lock: "resume.lock", owner: observed.owner },
352
- actor: options.actor!,
353
- reason: options.reason!,
354
- });
355
- const current = inspect();
356
- if (current.lock_sha256 !== observed.lock_sha256 || current.owner_state !== "dead")
357
- throw new Error("workflow resume lock authority changed before unlock");
358
- audit.assertCurrent();
359
- unlinkSync(path);
360
- fsyncParentDirectory(path);
361
- audit.complete();
362
- return { ...observed, action: "unlocked", recovery_id: audit.intent.operation_id };
363
- });
364
- }
365
-
366
- function withResumeLeaseMutation<T>(coordRoot: string, runId: string, action: () => T): T {
367
- const authority = stableDigest({ run_id: runId, lock: "resume.lock" });
368
- const lease = acquireNoClobberLease({
369
- path: join(coordRoot, ".harnery", "workflows", runId, "resume-mutation.lease"),
370
- scope: "workflow-resume-mutation",
371
- authoritySha256: authority,
372
- staleAfterMs: 300_000,
373
- validateStaleOwner: (owner) => owner.authority_sha256 === authority,
374
- });
375
- try {
376
- return action();
377
- } finally {
378
- lease.release();
379
- }
380
- }
381
-
382
- function readResumeLease(path: string): { pid: number; host: string; created_at: string } | null {
383
- try {
384
- const value = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
385
- return typeof value.pid === "number" &&
386
- Number.isSafeInteger(value.pid) &&
387
- value.pid > 0 &&
388
- typeof value.host === "string" &&
389
- value.host.length > 0 &&
390
- validTimestamp(value.created_at)
391
- ? { pid: value.pid, host: value.host, created_at: value.created_at }
392
- : null;
393
- } catch {
394
- return null;
395
- }
396
- }
397
-
398
194
  function validExecution(value: WorkflowRunManifest["execution"]): boolean {
399
195
  if (!value || typeof value !== "object") return false;
400
196
  return (
@@ -23,7 +23,6 @@ import { builtinAdapterProfile, validateAdapterEffort } from "../adapters/profil
23
23
  import type { AdapterInvocation, AdapterRawResult } from "../adapters/types.ts";
24
24
  import { notFoundError } from "./adapters.ts";
25
25
  import { buildChildEnv } from "./child-env.ts";
26
- import { observeSpawnFailure } from "./failure-evidence.ts";
27
26
  import { resolveFullAccessArgv, resolveSandboxProjection } from "./sandbox-projection.ts";
28
27
  import { isUpstreamFailureText, vendorFailureText } from "./spawn-failure.ts";
29
28
  import type { Spawner, SpawnRequest, SpawnResult } from "./types.ts";
@@ -67,23 +66,6 @@ export function buildClaudeInvocation(req: SpawnRequest): AdapterInvocation {
67
66
  }
68
67
 
69
68
  export function normalizeClaudeResult(raw: AdapterRawResult): SpawnResult {
70
- const result = normalizeClaudeTransportResult(raw);
71
- if (result.ok) return result;
72
- let text = raw.stdout.trim();
73
- let source: "stdout" | "vendor_envelope" = "stdout";
74
- try {
75
- const envelope = JSON.parse(raw.stdout) as ClaudeEnvelope;
76
- if (typeof envelope.result === "string" && envelope.result.length > 0) {
77
- text = envelope.result;
78
- source = "vendor_envelope";
79
- }
80
- } catch {
81
- // Retain unstructured output without treating it as native authority.
82
- }
83
- return { ...result, text, failureEvidence: observeSpawnFailure(raw, text, source) };
84
- }
85
-
86
- function normalizeClaudeTransportResult(raw: AdapterRawResult): SpawnResult {
87
69
  if (raw.timedOut) {
88
70
  return {
89
71
  ok: false,
@@ -26,7 +26,6 @@ import { builtinAdapterProfile, validateAdapterEffort } from "../adapters/profil
26
26
  import type { AdapterInvocation, AdapterRawResult } from "../adapters/types.ts";
27
27
  import { notFoundError } from "./adapters.ts";
28
28
  import { buildChildEnv } from "./child-env.ts";
29
- import { observeSpawnFailure } from "./failure-evidence.ts";
30
29
  import { resolveFullAccessArgv, resolveSandboxProjection } from "./sandbox-projection.ts";
31
30
  import { isUpstreamFailureText, vendorFailureText } from "./spawn-failure.ts";
32
31
  import type { Spawner, SpawnRequest, SpawnResult } from "./types.ts";
@@ -79,18 +78,6 @@ export function buildCodexInvocation(req: SpawnRequest, resultFile?: string): Ad
79
78
  }
80
79
 
81
80
  export function normalizeCodexResult(raw: AdapterRawResult): SpawnResult {
82
- const result = normalizeCodexTransportResult(raw);
83
- if (result.ok) return result;
84
- const finalText = raw.resultFileText?.trim();
85
- const text = finalText || raw.stdout.trim();
86
- return {
87
- ...result,
88
- text,
89
- failureEvidence: observeSpawnFailure(raw, text, finalText ? "final_message" : "stdout"),
90
- };
91
- }
92
-
93
- function normalizeCodexTransportResult(raw: AdapterRawResult): SpawnResult {
94
81
  if (raw.timedOut) {
95
82
  return {
96
83
  ok: false,
@@ -24,7 +24,6 @@ import { builtinAdapterProfile, validateAdapterEffort } from "../adapters/profil
24
24
  import type { AdapterInvocation, AdapterRawResult } from "../adapters/types.ts";
25
25
  import { notFoundError } from "./adapters.ts";
26
26
  import { buildChildEnv } from "./child-env.ts";
27
- import { observeSpawnFailure } from "./failure-evidence.ts";
28
27
  import { resolveFullAccessArgv, resolveSandboxProjection } from "./sandbox-projection.ts";
29
28
  import { isUpstreamFailureText, vendorFailureText } from "./spawn-failure.ts";
30
29
  import type { Spawner, SpawnRequest, SpawnResult } from "./types.ts";
@@ -79,23 +78,6 @@ export function buildCursorInvocation(req: SpawnRequest): AdapterInvocation {
79
78
  }
80
79
 
81
80
  export function normalizeCursorResult(raw: AdapterRawResult): SpawnResult {
82
- const result = normalizeCursorTransportResult(raw);
83
- if (result.ok) return result;
84
- let text = raw.stdout;
85
- let source: "stdout" | "vendor_envelope" = "stdout";
86
- try {
87
- const envelope = JSON.parse(raw.stdout) as CursorEnvelope;
88
- if (typeof envelope.result === "string" && envelope.result.length > 0) {
89
- text = envelope.result;
90
- source = "vendor_envelope";
91
- }
92
- } catch {
93
- // Raw output remains evidence, not an inferred refusal or effect result.
94
- }
95
- return { ...result, text, failureEvidence: observeSpawnFailure(raw, text, source) };
96
- }
97
-
98
- function normalizeCursorTransportResult(raw: AdapterRawResult): SpawnResult {
99
81
  if (raw.timedOut) {
100
82
  return {
101
83
  ok: false,
@@ -22,7 +22,6 @@ import { builtinAdapterProfile, validateAdapterEffort } from "../adapters/profil
22
22
  import type { AdapterInvocation, AdapterRawResult } from "../adapters/types.ts";
23
23
  import { notFoundError } from "./adapters.ts";
24
24
  import { buildChildEnv } from "./child-env.ts";
25
- import { observeSpawnFailure } from "./failure-evidence.ts";
26
25
  import { resolveFullAccessArgv, resolveSandboxProjection } from "./sandbox-projection.ts";
27
26
  import { isUpstreamFailureText, vendorFailureText } from "./spawn-failure.ts";
28
27
  import type { Spawner, SpawnRequest, SpawnResult } from "./types.ts";
@@ -89,19 +88,6 @@ export function buildOpenCodeInvocation(req: SpawnRequest): AdapterInvocation {
89
88
  }
90
89
 
91
90
  export function normalizeOpenCodeResult(raw: AdapterRawResult): SpawnResult {
92
- const result = normalizeOpenCodeTransportResult(raw);
93
- if (result.ok) return result;
94
- const parsed = parseOpenCodeStream(raw.stdout);
95
- const text = parsed.text || raw.stdout.trim();
96
- return {
97
- ...result,
98
- text,
99
- sessionId: parsed.sessionId,
100
- failureEvidence: observeSpawnFailure(raw, text, parsed.text ? "vendor_envelope" : "stdout"),
101
- };
102
- }
103
-
104
- function normalizeOpenCodeTransportResult(raw: AdapterRawResult): SpawnResult {
105
91
  if (raw.timedOut) {
106
92
  return {
107
93
  ok: false,
@@ -137,12 +137,11 @@ export interface WorkflowAgentProof {
137
137
  session_id?: string;
138
138
  result?: ResultDigest;
139
139
  error?: string;
140
- /** Failed-spawn transport class. It is retained even when a script swallows
141
- * the rejection; charging additionally requires validated failure evidence. */
140
+ /** Set on a failed agent whose spawn was classified uninformative about the
141
+ * work (ADR 0046): environment (binary absent) or upstream (vendor refused).
142
+ * Absent ⇒ a work failure. Recorded even when a script's parallel() swallows
143
+ * the rejection, so the run-level class can still be derived from proof. */
142
144
  class?: SpawnFailureClass;
143
- /** Failure observations carried from the adapter. Missing observations are
144
- * unknown and cannot authorize an uncharged attempt. */
145
- failure_evidence?: SpawnFailureEvidence;
146
145
  /** Which vendor sandbox mode this agent launched with and why (ADR 0192).
147
146
  * Absent on a cached agent, which launched nothing. */
148
147
  access?: WorkerAccessDecision;
@@ -363,16 +362,15 @@ export interface WorkflowSpecialistProfile {
363
362
  }
364
363
 
365
364
  /**
366
- * The positively identified transport cause of a failed spawn (ADR 0046).
367
- * This class alone proves neither absence of effects nor retry eligibility.
365
+ * Why a failed run was uninformative about the work (ADR 0046). Absent means
366
+ * the attempt produced information about the work (or succeeded), which is the
367
+ * default and is charged against the attempt budget exactly as before.
368
368
  *
369
369
  * - `environment`: the run never started — a precondition was missing (the
370
- * vendor binary was absent). With positive no-effect/output evidence it is
371
- * uncharged and not retried: retrying an unchanged
370
+ * vendor binary was absent). Uncharged AND not retried: retrying an unchanged
372
371
  * environment cannot help, so the work item stops and names the precondition.
373
372
  * - `upstream`: the vendor was reached and refused (5xx, 429, circuit open).
374
- * Retry/free-attempt eligibility also requires positive no-effect evidence,
375
- * observed absent output, and no refusal. Unsupported evidence stays unknown.
373
+ * Uncharged, but retry stays available (bounded by max_uncharged_attempts).
376
374
  */
377
375
  export type SpawnFailureClass = "environment" | "upstream";
378
376
 
@@ -394,30 +392,6 @@ export type SpawnFailureClass = "environment" | "upstream";
394
392
  */
395
393
  export type RunFailureClass = SpawnFailureClass | "decision";
396
394
 
397
- /** Positive claims name the observation that supports them. A transport error
398
- * or an empty reply is never evidence that no effects occurred. */
399
- export type SpawnEffectEvidence =
400
- | { state: "unknown" }
401
- | { state: "none"; source: "spawn_errno"; reference: "ENOENT" }
402
- | { state: "none" | "observed"; source: "adapter_evidence"; reference: string };
403
-
404
- export type SpawnRefusalEvidence =
405
- | { state: "unknown" }
406
- | { state: "refused"; source: "vendor_envelope"; reference: string };
407
-
408
- export type SpawnOutputSource = "final_message" | "vendor_envelope" | "stdout";
409
-
410
- export type SpawnPartialOutputEvidence =
411
- | { state: "unknown" }
412
- | { state: "absent"; source: SpawnOutputSource }
413
- | { state: "present"; source: SpawnOutputSource; bytes: number; sha256: string };
414
-
415
- export interface SpawnFailureEvidence {
416
- effect: SpawnEffectEvidence;
417
- refusal: SpawnRefusalEvidence;
418
- partial_output: SpawnPartialOutputEvidence;
419
- }
420
-
421
395
  /** What a spawn adapter returns for one subagent run. */
422
396
  export interface SpawnResult {
423
397
  ok: boolean;
@@ -429,13 +403,9 @@ export interface SpawnResult {
429
403
  durationMs: number;
430
404
  /** Populated when ok=false. */
431
405
  error?: string;
432
- /** Transport classification when ok=false; never sufficient by itself to
433
- * grant free work or automatic redispatch. */
406
+ /** Set when ok=false and the failure was positively identified as
407
+ * uninformative about the work. Absent ⇒ a work failure (charged). */
434
408
  class?: SpawnFailureClass;
435
- /** Transport classification alone grants neither a retry nor free work.
436
- * Absent evidence is explicitly unknown. Positive adapter_evidence claims
437
- * require a documented producer observation, never an HTTP/text heuristic. */
438
- failureEvidence?: SpawnFailureEvidence;
439
409
  }
440
410
 
441
411
  export interface SpawnRequest {
@@ -417,7 +417,7 @@ function writeExclusive(path: string, bytes: string): void {
417
417
  }
418
418
  }
419
419
 
420
- function readOwner(path: string): { owner: NoClobberLeaseOwner; bytes: string } {
420
+ export function readOwner(path: string): { owner: NoClobberLeaseOwner; bytes: string } {
421
421
  let size: number;
422
422
  try {
423
423
  const stat = lstatSync(path);