pi-amq 0.1.1

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 (264) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/LICENSE +21 -0
  3. package/README.md +92 -0
  4. package/THIRD_PARTY_NOTICES.md +33 -0
  5. package/dist/extension/guidance.d.ts +25 -0
  6. package/dist/extension/guidance.d.ts.map +1 -0
  7. package/dist/extension/guidance.js +121 -0
  8. package/dist/extension/guidance.js.map +1 -0
  9. package/dist/extension/guide.d.ts +30 -0
  10. package/dist/extension/guide.d.ts.map +1 -0
  11. package/dist/extension/guide.js +128 -0
  12. package/dist/extension/guide.js.map +1 -0
  13. package/dist/extension/index.d.ts +4 -0
  14. package/dist/extension/index.d.ts.map +1 -0
  15. package/dist/extension/index.js +6 -0
  16. package/dist/extension/index.js.map +1 -0
  17. package/dist/extension/runtime.d.ts +48 -0
  18. package/dist/extension/runtime.d.ts.map +1 -0
  19. package/dist/extension/runtime.js +1095 -0
  20. package/dist/extension/runtime.js.map +1 -0
  21. package/dist/extension/tools.d.ts +12 -0
  22. package/dist/extension/tools.d.ts.map +1 -0
  23. package/dist/extension/tools.js +198 -0
  24. package/dist/extension/tools.js.map +1 -0
  25. package/dist/extension/worker-runtime.d.ts +34 -0
  26. package/dist/extension/worker-runtime.d.ts.map +1 -0
  27. package/dist/extension/worker-runtime.js +974 -0
  28. package/dist/extension/worker-runtime.js.map +1 -0
  29. package/dist/extension/worker-tools.d.ts +140 -0
  30. package/dist/extension/worker-tools.d.ts.map +1 -0
  31. package/dist/extension/worker-tools.js +217 -0
  32. package/dist/extension/worker-tools.js.map +1 -0
  33. package/dist/integration/confirmed-bind-onboarding.d.ts +44 -0
  34. package/dist/integration/confirmed-bind-onboarding.d.ts.map +1 -0
  35. package/dist/integration/confirmed-bind-onboarding.js +199 -0
  36. package/dist/integration/confirmed-bind-onboarding.js.map +1 -0
  37. package/dist/integration/fleet-view.d.ts +66 -0
  38. package/dist/integration/fleet-view.d.ts.map +1 -0
  39. package/dist/integration/fleet-view.js +238 -0
  40. package/dist/integration/fleet-view.js.map +1 -0
  41. package/dist/integration/fresh-session-dispatcher.d.ts +66 -0
  42. package/dist/integration/fresh-session-dispatcher.d.ts.map +1 -0
  43. package/dist/integration/fresh-session-dispatcher.js +103 -0
  44. package/dist/integration/fresh-session-dispatcher.js.map +1 -0
  45. package/dist/integration/launch-preview.d.ts +31 -0
  46. package/dist/integration/launch-preview.d.ts.map +1 -0
  47. package/dist/integration/launch-preview.js +325 -0
  48. package/dist/integration/launch-preview.js.map +1 -0
  49. package/dist/integration/transport-port.d.ts +39 -0
  50. package/dist/integration/transport-port.d.ts.map +1 -0
  51. package/dist/integration/transport-port.js +2 -0
  52. package/dist/integration/transport-port.js.map +1 -0
  53. package/dist/integration/worker-completion-notifier.d.ts +22 -0
  54. package/dist/integration/worker-completion-notifier.d.ts.map +1 -0
  55. package/dist/integration/worker-completion-notifier.js +81 -0
  56. package/dist/integration/worker-completion-notifier.js.map +1 -0
  57. package/dist/transport/amq-cli-adapter.d.ts +41 -0
  58. package/dist/transport/amq-cli-adapter.d.ts.map +1 -0
  59. package/dist/transport/amq-cli-adapter.js +291 -0
  60. package/dist/transport/amq-cli-adapter.js.map +1 -0
  61. package/dist/transport/amq-message.d.ts +47 -0
  62. package/dist/transport/amq-message.d.ts.map +1 -0
  63. package/dist/transport/amq-message.js +173 -0
  64. package/dist/transport/amq-message.js.map +1 -0
  65. package/dist/transport/amq-operations.d.ts +105 -0
  66. package/dist/transport/amq-operations.d.ts.map +1 -0
  67. package/dist/transport/amq-operations.js +320 -0
  68. package/dist/transport/amq-operations.js.map +1 -0
  69. package/dist/transport/atomic-json.d.ts +18 -0
  70. package/dist/transport/atomic-json.d.ts.map +1 -0
  71. package/dist/transport/atomic-json.js +90 -0
  72. package/dist/transport/atomic-json.js.map +1 -0
  73. package/dist/transport/binding-resolver.d.ts +46 -0
  74. package/dist/transport/binding-resolver.d.ts.map +1 -0
  75. package/dist/transport/binding-resolver.js +389 -0
  76. package/dist/transport/binding-resolver.js.map +1 -0
  77. package/dist/transport/bridge-journal.d.ts +100 -0
  78. package/dist/transport/bridge-journal.d.ts.map +1 -0
  79. package/dist/transport/bridge-journal.js +459 -0
  80. package/dist/transport/bridge-journal.js.map +1 -0
  81. package/dist/transport/dlq-retry-audit.d.ts +28 -0
  82. package/dist/transport/dlq-retry-audit.d.ts.map +1 -0
  83. package/dist/transport/dlq-retry-audit.js +66 -0
  84. package/dist/transport/dlq-retry-audit.js.map +1 -0
  85. package/dist/transport/inbox-pump.d.ts +81 -0
  86. package/dist/transport/inbox-pump.d.ts.map +1 -0
  87. package/dist/transport/inbox-pump.js +558 -0
  88. package/dist/transport/inbox-pump.js.map +1 -0
  89. package/dist/transport/index.d.ts +12 -0
  90. package/dist/transport/index.d.ts.map +1 -0
  91. package/dist/transport/index.js +12 -0
  92. package/dist/transport/index.js.map +1 -0
  93. package/dist/transport/presence-projector.d.ts +29 -0
  94. package/dist/transport/presence-projector.d.ts.map +1 -0
  95. package/dist/transport/presence-projector.js +73 -0
  96. package/dist/transport/presence-projector.js.map +1 -0
  97. package/dist/transport/priority-delivery-scheduler.d.ts +85 -0
  98. package/dist/transport/priority-delivery-scheduler.d.ts.map +1 -0
  99. package/dist/transport/priority-delivery-scheduler.js +210 -0
  100. package/dist/transport/priority-delivery-scheduler.js.map +1 -0
  101. package/dist/transport/receiver-lease.d.ts +102 -0
  102. package/dist/transport/receiver-lease.d.ts.map +1 -0
  103. package/dist/transport/receiver-lease.js +778 -0
  104. package/dist/transport/receiver-lease.js.map +1 -0
  105. package/dist/transport/runtime-status.d.ts +48 -0
  106. package/dist/transport/runtime-status.d.ts.map +1 -0
  107. package/dist/transport/runtime-status.js +71 -0
  108. package/dist/transport/runtime-status.js.map +1 -0
  109. package/dist/workflow/atomic-storage.d.ts +12 -0
  110. package/dist/workflow/atomic-storage.d.ts.map +1 -0
  111. package/dist/workflow/atomic-storage.js +104 -0
  112. package/dist/workflow/atomic-storage.js.map +1 -0
  113. package/dist/workflow/budget.d.ts +18 -0
  114. package/dist/workflow/budget.d.ts.map +1 -0
  115. package/dist/workflow/budget.js +74 -0
  116. package/dist/workflow/budget.js.map +1 -0
  117. package/dist/workflow/index.d.ts +17 -0
  118. package/dist/workflow/index.d.ts.map +1 -0
  119. package/dist/workflow/index.js +17 -0
  120. package/dist/workflow/index.js.map +1 -0
  121. package/dist/workflow/lifecycle-projection.d.ts +18 -0
  122. package/dist/workflow/lifecycle-projection.d.ts.map +1 -0
  123. package/dist/workflow/lifecycle-projection.js +22 -0
  124. package/dist/workflow/lifecycle-projection.js.map +1 -0
  125. package/dist/workflow/model-fallback.d.ts +2 -0
  126. package/dist/workflow/model-fallback.d.ts.map +1 -0
  127. package/dist/workflow/model-fallback.js +17 -0
  128. package/dist/workflow/model-fallback.js.map +1 -0
  129. package/dist/workflow/pi-session-fork.d.ts +18 -0
  130. package/dist/workflow/pi-session-fork.d.ts.map +1 -0
  131. package/dist/workflow/pi-session-fork.js +80 -0
  132. package/dist/workflow/pi-session-fork.js.map +1 -0
  133. package/dist/workflow/run-artifacts.d.ts +5 -0
  134. package/dist/workflow/run-artifacts.d.ts.map +1 -0
  135. package/dist/workflow/run-artifacts.js +25 -0
  136. package/dist/workflow/run-artifacts.js.map +1 -0
  137. package/dist/workflow/run-cleanup.d.ts +70 -0
  138. package/dist/workflow/run-cleanup.d.ts.map +1 -0
  139. package/dist/workflow/run-cleanup.js +222 -0
  140. package/dist/workflow/run-cleanup.js.map +1 -0
  141. package/dist/workflow/run-gate.d.ts +43 -0
  142. package/dist/workflow/run-gate.d.ts.map +1 -0
  143. package/dist/workflow/run-gate.js +70 -0
  144. package/dist/workflow/run-gate.js.map +1 -0
  145. package/dist/workflow/run-store.d.ts +208 -0
  146. package/dist/workflow/run-store.d.ts.map +1 -0
  147. package/dist/workflow/run-store.js +2965 -0
  148. package/dist/workflow/run-store.js.map +1 -0
  149. package/dist/workflow/spawn-confirmation.d.ts +44 -0
  150. package/dist/workflow/spawn-confirmation.d.ts.map +1 -0
  151. package/dist/workflow/spawn-confirmation.js +228 -0
  152. package/dist/workflow/spawn-confirmation.js.map +1 -0
  153. package/dist/workflow/system-process-inspector.d.ts +9 -0
  154. package/dist/workflow/system-process-inspector.d.ts.map +1 -0
  155. package/dist/workflow/system-process-inspector.js +129 -0
  156. package/dist/workflow/system-process-inspector.js.map +1 -0
  157. package/dist/workflow/worker-child-extension.d.ts +3 -0
  158. package/dist/workflow/worker-child-extension.d.ts.map +1 -0
  159. package/dist/workflow/worker-child-extension.js +182 -0
  160. package/dist/workflow/worker-child-extension.js.map +1 -0
  161. package/dist/workflow/worker-gate.d.ts +74 -0
  162. package/dist/workflow/worker-gate.d.ts.map +1 -0
  163. package/dist/workflow/worker-gate.js +165 -0
  164. package/dist/workflow/worker-gate.js.map +1 -0
  165. package/dist/workflow/worker-launch.d.ts +65 -0
  166. package/dist/workflow/worker-launch.d.ts.map +1 -0
  167. package/dist/workflow/worker-launch.js +338 -0
  168. package/dist/workflow/worker-launch.js.map +1 -0
  169. package/dist/workflow/worker-model-catalog.d.ts +36 -0
  170. package/dist/workflow/worker-model-catalog.d.ts.map +1 -0
  171. package/dist/workflow/worker-model-catalog.js +105 -0
  172. package/dist/workflow/worker-model-catalog.js.map +1 -0
  173. package/dist/workflow/worker-process-supervisor.d.ts +110 -0
  174. package/dist/workflow/worker-process-supervisor.d.ts.map +1 -0
  175. package/dist/workflow/worker-process-supervisor.js +1188 -0
  176. package/dist/workflow/worker-process-supervisor.js.map +1 -0
  177. package/dist/workflow/worker-prompt-capsule.d.ts +32 -0
  178. package/dist/workflow/worker-prompt-capsule.d.ts.map +1 -0
  179. package/dist/workflow/worker-prompt-capsule.js +52 -0
  180. package/dist/workflow/worker-prompt-capsule.js.map +1 -0
  181. package/dist/workflow/worker-role-presets.d.ts +38 -0
  182. package/dist/workflow/worker-role-presets.d.ts.map +1 -0
  183. package/dist/workflow/worker-role-presets.js +201 -0
  184. package/dist/workflow/worker-role-presets.js.map +1 -0
  185. package/dist/workflow/workflow-doctor.d.ts +57 -0
  186. package/dist/workflow/workflow-doctor.d.ts.map +1 -0
  187. package/dist/workflow/workflow-doctor.js +289 -0
  188. package/dist/workflow/workflow-doctor.js.map +1 -0
  189. package/dist/workflow/workflow-lease.d.ts +125 -0
  190. package/dist/workflow/workflow-lease.d.ts.map +1 -0
  191. package/dist/workflow/workflow-lease.js +689 -0
  192. package/dist/workflow/workflow-lease.js.map +1 -0
  193. package/dist/workflow/workflow-types.d.ts +335 -0
  194. package/dist/workflow/workflow-types.d.ts.map +1 -0
  195. package/dist/workflow/workflow-types.js +11 -0
  196. package/dist/workflow/workflow-types.js.map +1 -0
  197. package/dist/workflow/workspace-paths.d.ts +4 -0
  198. package/dist/workflow/workspace-paths.d.ts.map +1 -0
  199. package/dist/workflow/workspace-paths.js +22 -0
  200. package/dist/workflow/workspace-paths.js.map +1 -0
  201. package/docs/conformance.md +116 -0
  202. package/docs/demo-transcript.md +34 -0
  203. package/docs/design.md +1481 -0
  204. package/docs/operator-guide.md +100 -0
  205. package/extension/guidance.ts +162 -0
  206. package/extension/guide.ts +155 -0
  207. package/extension/index.ts +8 -0
  208. package/extension/runtime.ts +1224 -0
  209. package/extension/tools.ts +221 -0
  210. package/extension/worker-runtime.ts +1150 -0
  211. package/extension/worker-tools.ts +276 -0
  212. package/integration/confirmed-bind-onboarding.ts +296 -0
  213. package/integration/fleet-view.ts +286 -0
  214. package/integration/fresh-session-dispatcher.ts +166 -0
  215. package/integration/launch-preview.ts +396 -0
  216. package/integration/transport-port.ts +29 -0
  217. package/integration/worker-completion-notifier.ts +98 -0
  218. package/package.json +55 -0
  219. package/roles/oracle.md +9 -0
  220. package/roles/researcher.md +8 -0
  221. package/roles/reviewer.md +9 -0
  222. package/roles/scout.md +8 -0
  223. package/roles/worker.md +10 -0
  224. package/scripts/demo-two-terminal.sh +59 -0
  225. package/skills/coordination-patterns/SKILL.md +41 -0
  226. package/skills/coordination-patterns/references/supervised-worker-fan-out.md +12 -0
  227. package/skills/coordination-patterns/references/today-mail-team.md +36 -0
  228. package/transport/amq-cli-adapter.ts +381 -0
  229. package/transport/amq-message.ts +218 -0
  230. package/transport/amq-operations.ts +482 -0
  231. package/transport/atomic-json.ts +115 -0
  232. package/transport/binding-resolver.ts +565 -0
  233. package/transport/bridge-journal.ts +588 -0
  234. package/transport/dlq-retry-audit.ts +97 -0
  235. package/transport/inbox-pump.ts +729 -0
  236. package/transport/index.ts +11 -0
  237. package/transport/presence-projector.ts +104 -0
  238. package/transport/priority-delivery-scheduler.ts +328 -0
  239. package/transport/receiver-lease.ts +945 -0
  240. package/transport/runtime-status.ts +112 -0
  241. package/workflow/.gitkeep +1 -0
  242. package/workflow/atomic-storage.ts +120 -0
  243. package/workflow/budget.ts +90 -0
  244. package/workflow/index.ts +16 -0
  245. package/workflow/lifecycle-projection.ts +37 -0
  246. package/workflow/model-fallback.ts +17 -0
  247. package/workflow/pi-session-fork.ts +96 -0
  248. package/workflow/run-artifacts.ts +36 -0
  249. package/workflow/run-cleanup.ts +315 -0
  250. package/workflow/run-gate.ts +122 -0
  251. package/workflow/run-store.ts +3890 -0
  252. package/workflow/spawn-confirmation.ts +305 -0
  253. package/workflow/system-process-inspector.ts +142 -0
  254. package/workflow/worker-child-extension.ts +193 -0
  255. package/workflow/worker-gate.ts +238 -0
  256. package/workflow/worker-launch.ts +485 -0
  257. package/workflow/worker-model-catalog.ts +165 -0
  258. package/workflow/worker-process-supervisor.ts +1504 -0
  259. package/workflow/worker-prompt-capsule.ts +81 -0
  260. package/workflow/worker-role-presets.ts +241 -0
  261. package/workflow/workflow-doctor.ts +400 -0
  262. package/workflow/workflow-lease.ts +905 -0
  263. package/workflow/workflow-types.ts +367 -0
  264. package/workflow/workspace-paths.ts +32 -0
@@ -0,0 +1,689 @@
1
+ import { randomUUID as nodeRandomUUID } from "node:crypto";
2
+ import { chmod, mkdir, mkdtemp, readdir, rename, rm, stat, unlink, } from "node:fs/promises";
3
+ import { dirname, isAbsolute, join, normalize, resolve } from "node:path";
4
+ import { setTimeout as delay } from "node:timers/promises";
5
+ import { isNodeError, readWorkflowJson, sha256, syncWorkflowDirectory, writeWorkflowJson, } from "./atomic-storage.js";
6
+ const OWNER_KEYS = [
7
+ "schema",
8
+ "kind",
9
+ "stage",
10
+ "generation",
11
+ "workspaceId",
12
+ "resourceId",
13
+ "runId",
14
+ "workerId",
15
+ "attemptId",
16
+ "workerAttemptGeneration",
17
+ "supervisorGeneration",
18
+ "sessionPath",
19
+ "pid",
20
+ "processStartIdentity",
21
+ "bootIdentity",
22
+ "executable",
23
+ "hostname",
24
+ "createdAt",
25
+ ];
26
+ const GUARD_KEYS = [
27
+ "schema",
28
+ "generation",
29
+ "pid",
30
+ "processStartIdentity",
31
+ "bootIdentity",
32
+ "executable",
33
+ "hostname",
34
+ "createdAt",
35
+ ];
36
+ export class WorkflowLeaseFenceError extends Error {
37
+ constructor(message) {
38
+ super(message);
39
+ this.name = "WorkflowLeaseFenceError";
40
+ }
41
+ }
42
+ export class WorkflowLeaseManager {
43
+ #namespaceDirectory;
44
+ #workspaceId;
45
+ #inspector;
46
+ #randomUUID;
47
+ #now;
48
+ #guardWaitMs;
49
+ constructor(options) {
50
+ if (!isAbsolute(options.namespaceDirectory)) {
51
+ throw new TypeError("Workflow namespace must be absolute");
52
+ }
53
+ this.#namespaceDirectory = normalize(options.namespaceDirectory);
54
+ this.#workspaceId = requireNonEmpty(options.workspaceId, "workspace ID");
55
+ this.#inspector = options.inspector;
56
+ this.#randomUUID = options.randomUUID ?? nodeRandomUUID;
57
+ this.#now = options.now ?? (() => new Date());
58
+ this.#guardWaitMs = options.guardWaitMs ?? 1_000;
59
+ }
60
+ async acquireSupervisor() {
61
+ const current = await this.#inspector.current();
62
+ validateProcessIdentity(current, "current process identity");
63
+ return this.#acquire(this.#supervisorResource(), ownerFromIdentity({
64
+ kind: "supervisor",
65
+ stage: "active",
66
+ generation: this.#randomUUID(),
67
+ workspaceId: this.#workspaceId,
68
+ resourceId: this.#workspaceId,
69
+ identity: current,
70
+ now: this.#now(),
71
+ }));
72
+ }
73
+ async releaseSupervisor(generation) {
74
+ requireNonEmpty(generation, "supervisor generation");
75
+ const current = await this.#inspector.current();
76
+ return this.#release(this.#supervisorResource(), generation, (owner) => owner.kind === "supervisor" && ownerIsProcess(owner, current));
77
+ }
78
+ async withCurrentSupervisor(generation, operation) {
79
+ requireNonEmpty(generation, "supervisor generation");
80
+ const resource = this.#supervisorResource();
81
+ const guarded = await this.#withGuard(resource, async (current) => {
82
+ const projection = await this.#inspect(resource);
83
+ if (projection.kind !== "owned"
84
+ || projection.owner.kind !== "supervisor"
85
+ || projection.owner.generation !== generation
86
+ || projection.owner.workspaceId !== this.#workspaceId
87
+ || !ownerIsProcess(projection.owner, current)) {
88
+ throw new WorkflowLeaseFenceError("Workflow supervisor generation is not current");
89
+ }
90
+ return operation();
91
+ });
92
+ if (!guarded.ok)
93
+ throw new WorkflowLeaseFenceError(guarded.diagnostic);
94
+ return guarded.value;
95
+ }
96
+ async inspectSupervisor() {
97
+ await this.#initialize();
98
+ return this.#inspect(this.#supervisorResource());
99
+ }
100
+ async acquireWorkerSession(input) {
101
+ const sessionPath = canonicalSessionPath(input.sessionPath);
102
+ validateProcessIdentity(input.processIdentity, "worker attempt process identity");
103
+ requireNonEmpty(input.runId, "run ID");
104
+ requireNonEmpty(input.workerId, "worker ID");
105
+ requireNonEmpty(input.attemptId, "attempt ID");
106
+ requireNonEmpty(input.workerAttemptGeneration, "worker attempt generation");
107
+ return this.withCurrentSupervisor(input.supervisorGeneration, async () => {
108
+ const resource = this.#workerResource(sessionPath);
109
+ return this.#acquire(resource, ownerFromIdentity({
110
+ kind: "worker-session",
111
+ stage: "active",
112
+ generation: this.#randomUUID(),
113
+ workspaceId: this.#workspaceId,
114
+ resourceId: resource.resourceId,
115
+ runId: input.runId,
116
+ workerId: input.workerId,
117
+ attemptId: input.attemptId,
118
+ workerAttemptGeneration: input.workerAttemptGeneration,
119
+ supervisorGeneration: input.supervisorGeneration,
120
+ sessionPath,
121
+ identity: input.processIdentity,
122
+ now: this.#now(),
123
+ }));
124
+ });
125
+ }
126
+ async reserveWorkerSession(input) {
127
+ const sessionPath = canonicalSessionPath(input.sessionPath);
128
+ requireNonEmpty(input.runId, "run ID");
129
+ requireNonEmpty(input.workerId, "worker ID");
130
+ requireNonEmpty(input.attemptId, "attempt ID");
131
+ requireNonEmpty(input.workerAttemptGeneration, "worker attempt generation");
132
+ return this.withCurrentSupervisor(input.supervisorGeneration, async () => {
133
+ const current = await this.#inspector.current();
134
+ const resource = this.#workerResource(sessionPath);
135
+ return this.#acquire(resource, ownerFromIdentity({
136
+ kind: "worker-session",
137
+ stage: "reserved",
138
+ generation: this.#randomUUID(),
139
+ workspaceId: this.#workspaceId,
140
+ resourceId: resource.resourceId,
141
+ runId: input.runId,
142
+ workerId: input.workerId,
143
+ attemptId: input.attemptId,
144
+ workerAttemptGeneration: input.workerAttemptGeneration,
145
+ supervisorGeneration: input.supervisorGeneration,
146
+ sessionPath,
147
+ identity: current,
148
+ now: this.#now(),
149
+ }));
150
+ });
151
+ }
152
+ async activateWorkerSession(input) {
153
+ const sessionPath = canonicalSessionPath(input.sessionPath);
154
+ validateProcessIdentity(input.processIdentity, "worker attempt process identity");
155
+ return this.withCurrentSupervisor(input.supervisorGeneration, async () => {
156
+ const resource = this.#workerResource(sessionPath);
157
+ const guarded = await this.#withGuard(resource, async () => {
158
+ const projection = await this.#inspect(resource);
159
+ if (projection.kind !== "owned"
160
+ || projection.owner.kind !== "worker-session"
161
+ || projection.owner.stage !== "reserved"
162
+ || projection.owner.generation !== input.generation
163
+ || projection.owner.supervisorGeneration !== input.supervisorGeneration
164
+ || projection.owner.workerAttemptGeneration !== input.workerAttemptGeneration
165
+ || projection.owner.sessionPath !== sessionPath) {
166
+ throw new WorkflowLeaseFenceError("Worker-session reservation is not current");
167
+ }
168
+ const owner = {
169
+ ...projection.owner,
170
+ stage: "active",
171
+ pid: input.processIdentity.pid,
172
+ processStartIdentity: input.processIdentity.processStartIdentity,
173
+ bootIdentity: input.processIdentity.bootIdentity,
174
+ executable: input.processIdentity.executable,
175
+ hostname: input.processIdentity.hostname,
176
+ };
177
+ await writeWorkflowJson(join(resource.lockPath, "owner.json"), owner);
178
+ return owner;
179
+ });
180
+ if (!guarded.ok)
181
+ throw new WorkflowLeaseFenceError(guarded.diagnostic);
182
+ return guarded.value;
183
+ });
184
+ }
185
+ async releaseWorkerSession(input) {
186
+ const sessionPath = canonicalSessionPath(input.sessionPath);
187
+ return this.withCurrentSupervisor(input.supervisorGeneration, () => this.#release(this.#workerResource(sessionPath), input.generation, (owner) => owner.kind === "worker-session"
188
+ && owner.sessionPath === sessionPath
189
+ && owner.supervisorGeneration === input.supervisorGeneration));
190
+ }
191
+ async inspectWorkerSession(sessionPath) {
192
+ await this.#initialize();
193
+ return this.#inspect(this.#workerResource(canonicalSessionPath(sessionPath)));
194
+ }
195
+ async diagnoseWorkerSessionLeases() {
196
+ const parent = join(this.#namespaceDirectory, "worker-session-leases");
197
+ let entries;
198
+ try {
199
+ entries = await readdir(parent);
200
+ }
201
+ catch (error) {
202
+ if (isNodeError(error) && error.code === "ENOENT")
203
+ return [];
204
+ throw error;
205
+ }
206
+ const records = [];
207
+ for (const entry of entries.sort()) {
208
+ if (!entry.endsWith(".lock") || entry.startsWith("."))
209
+ continue;
210
+ const resourceId = entry.slice(0, -".lock".length);
211
+ const path = join(parent, entry);
212
+ records.push({
213
+ resourceId,
214
+ path,
215
+ projection: await this.#inspect({
216
+ kind: "worker-session",
217
+ resourceId,
218
+ lockPath: path,
219
+ guardPath: join(parent, `${resourceId}.lifecycle.guard.json`),
220
+ quarantineStem: `worker-session.${resourceId}`,
221
+ }),
222
+ });
223
+ }
224
+ return records;
225
+ }
226
+ async #acquire(resource, candidate) {
227
+ await this.#initialize();
228
+ const guarded = await this.#withGuard(resource, async (current) => {
229
+ const projection = await this.#inspect(resource);
230
+ if (projection.kind === "corrupt") {
231
+ return {
232
+ kind: "refused",
233
+ reason: "corrupt-owner",
234
+ diagnostic: projection.diagnostic,
235
+ };
236
+ }
237
+ let reclaimed;
238
+ if (projection.kind === "owned") {
239
+ const state = await classifyProcessRecord(projection.owner, current, this.#inspector);
240
+ if (state.kind === "live")
241
+ return { kind: "held", owner: projection.owner };
242
+ if (state.kind === "remote") {
243
+ return {
244
+ kind: "refused",
245
+ reason: "remote-owner",
246
+ diagnostic: `Workflow lease owner is on remote host ${projection.owner.hostname}`,
247
+ };
248
+ }
249
+ if (state.kind === "unknown") {
250
+ return {
251
+ kind: "refused",
252
+ reason: "unverifiable-owner",
253
+ diagnostic: state.diagnostic,
254
+ };
255
+ }
256
+ const quarantinePath = join(this.#namespaceDirectory, "quarantine", `${resource.quarantineStem}.${safePathPart(projection.owner.generation)}.${this.#now().getTime()}.${safePathPart(this.#randomUUID())}`);
257
+ await rename(resource.lockPath, quarantinePath);
258
+ await syncWorkflowDirectory(dirname(resource.lockPath));
259
+ await syncWorkflowDirectory(this.#namespaceDirectory);
260
+ reclaimed = { owner: projection.owner, quarantinePath };
261
+ }
262
+ await this.#createLease(resource, candidate);
263
+ return {
264
+ kind: "acquired",
265
+ owner: candidate,
266
+ ...(reclaimed ? { reclaimed } : {}),
267
+ };
268
+ });
269
+ return guarded.ok
270
+ ? guarded.value
271
+ : { kind: "refused", reason: guarded.reason, diagnostic: guarded.diagnostic };
272
+ }
273
+ async #release(resource, generation, ownerMatchesAuthority) {
274
+ requireNonEmpty(generation, "lease generation");
275
+ await this.#initialize();
276
+ const guarded = await this.#withGuard(resource, async () => {
277
+ const projection = await this.#inspect(resource);
278
+ if (projection.kind === "empty")
279
+ return { kind: "not-owner" };
280
+ if (projection.kind === "corrupt") {
281
+ return { kind: "refused", diagnostic: projection.diagnostic };
282
+ }
283
+ if (projection.owner.generation !== generation || !ownerMatchesAuthority(projection.owner)) {
284
+ return { kind: "not-owner", owner: projection.owner };
285
+ }
286
+ const releasedPath = join(this.#namespaceDirectory, `.${resource.quarantineStem}.released-${safePathPart(generation)}-${safePathPart(this.#randomUUID())}`);
287
+ await rename(resource.lockPath, releasedPath);
288
+ await syncWorkflowDirectory(dirname(resource.lockPath));
289
+ await syncWorkflowDirectory(this.#namespaceDirectory);
290
+ await rm(releasedPath, { recursive: true });
291
+ await syncWorkflowDirectory(this.#namespaceDirectory);
292
+ return { kind: "released" };
293
+ });
294
+ return guarded.ok
295
+ ? guarded.value
296
+ : { kind: "refused", diagnostic: guarded.diagnostic };
297
+ }
298
+ async #createLease(resource, owner) {
299
+ const parent = dirname(resource.lockPath);
300
+ await mkdir(parent, { recursive: true, mode: 0o700 });
301
+ const prepared = await mkdtemp(join(parent, ".lease.prepare-"));
302
+ await chmod(prepared, 0o700);
303
+ let installed = false;
304
+ try {
305
+ await writeWorkflowJson(join(prepared, "owner.json"), owner, { createOnly: true });
306
+ await syncWorkflowDirectory(prepared);
307
+ await rename(prepared, resource.lockPath);
308
+ installed = true;
309
+ await syncWorkflowDirectory(parent);
310
+ }
311
+ finally {
312
+ if (!installed)
313
+ await rm(prepared, { recursive: true, force: true });
314
+ }
315
+ }
316
+ async #inspect(resource) {
317
+ let raw;
318
+ try {
319
+ raw = await readWorkflowJson(join(resource.lockPath, "owner.json"));
320
+ }
321
+ catch (error) {
322
+ return { kind: "corrupt", diagnostic: `Workflow lease JSON is invalid: ${toError(error).message}` };
323
+ }
324
+ if (raw === undefined) {
325
+ try {
326
+ await stat(resource.lockPath);
327
+ }
328
+ catch (error) {
329
+ if (isNodeError(error) && error.code === "ENOENT")
330
+ return { kind: "empty" };
331
+ throw error;
332
+ }
333
+ return { kind: "corrupt", diagnostic: "Workflow lease exists without owner.json" };
334
+ }
335
+ try {
336
+ const owner = parseWorkflowLeaseOwnerV1(raw);
337
+ if (owner.workspaceId !== this.#workspaceId
338
+ || owner.kind !== resource.kind
339
+ || owner.resourceId !== resource.resourceId) {
340
+ throw new Error("Workflow lease resource identity does not match its namespace");
341
+ }
342
+ if (owner.kind === "worker-session"
343
+ && owner.sessionPath !== null
344
+ && this.#workerResource(owner.sessionPath).resourceId !== resource.resourceId) {
345
+ throw new Error("Worker-session lease path does not match its resource hash");
346
+ }
347
+ return { kind: "owned", owner };
348
+ }
349
+ catch (error) {
350
+ return { kind: "corrupt", diagnostic: toError(error).message };
351
+ }
352
+ }
353
+ async #withGuard(resource, operation) {
354
+ await this.#initialize();
355
+ await mkdir(join(resource.guardPath, ".."), { recursive: true, mode: 0o700 });
356
+ const current = await this.#inspector.current();
357
+ validateProcessIdentity(current, "current process identity");
358
+ const deadline = Date.now() + this.#guardWaitMs;
359
+ while (true) {
360
+ const guard = {
361
+ schema: 1,
362
+ generation: this.#randomUUID(),
363
+ pid: current.pid,
364
+ processStartIdentity: current.processStartIdentity,
365
+ bootIdentity: current.bootIdentity,
366
+ executable: current.executable,
367
+ hostname: current.hostname,
368
+ createdAt: this.#now().toISOString(),
369
+ };
370
+ try {
371
+ await writeWorkflowJson(resource.guardPath, guard, { createOnly: true });
372
+ }
373
+ catch (error) {
374
+ if (!isNodeError(error) || error.code !== "EEXIST")
375
+ throw error;
376
+ const existing = await this.#readGuard(resource.guardPath);
377
+ if (existing.kind === "corrupt") {
378
+ return { ok: false, reason: "lifecycle-unverifiable", diagnostic: existing.diagnostic };
379
+ }
380
+ if (existing.kind === "empty")
381
+ continue;
382
+ const state = await classifyProcessRecord(existing.guard, current, this.#inspector);
383
+ if (state.kind === "stale") {
384
+ const quarantinePath = join(this.#namespaceDirectory, "quarantine", `guard.${resource.quarantineStem}.${safePathPart(existing.guard.generation)}.${safePathPart(this.#randomUUID())}.json`);
385
+ try {
386
+ await rename(resource.guardPath, quarantinePath);
387
+ await syncWorkflowDirectory(dirname(resource.guardPath));
388
+ await syncWorkflowDirectory(this.#namespaceDirectory);
389
+ }
390
+ catch (renameError) {
391
+ if (!isNodeError(renameError) || renameError.code !== "ENOENT")
392
+ throw renameError;
393
+ }
394
+ continue;
395
+ }
396
+ if (state.kind === "remote" || state.kind === "unknown") {
397
+ return {
398
+ ok: false,
399
+ reason: "lifecycle-unverifiable",
400
+ diagnostic: state.kind === "remote"
401
+ ? `Workflow lifecycle guard belongs to remote host ${existing.guard.hostname}`
402
+ : state.diagnostic,
403
+ };
404
+ }
405
+ if (Date.now() >= deadline) {
406
+ return {
407
+ ok: false,
408
+ reason: "lifecycle-busy",
409
+ diagnostic: "Workflow lease lifecycle operation is busy; no owner was displaced",
410
+ };
411
+ }
412
+ await delay(5);
413
+ continue;
414
+ }
415
+ let value;
416
+ let operationError;
417
+ try {
418
+ value = await operation(current);
419
+ }
420
+ catch (error) {
421
+ operationError = error;
422
+ }
423
+ let releaseError;
424
+ try {
425
+ await this.#releaseGuard(resource.guardPath, guard.generation);
426
+ }
427
+ catch (error) {
428
+ releaseError = error;
429
+ }
430
+ if (operationError !== undefined && releaseError !== undefined) {
431
+ throw new AggregateError([operationError, releaseError], "Workflow lease operation and guard release both failed");
432
+ }
433
+ if (operationError !== undefined)
434
+ throw operationError;
435
+ if (releaseError !== undefined)
436
+ throw releaseError;
437
+ return { ok: true, value: value };
438
+ }
439
+ }
440
+ async #readGuard(path) {
441
+ let raw;
442
+ try {
443
+ raw = await readWorkflowJson(path);
444
+ }
445
+ catch (error) {
446
+ return { kind: "corrupt", diagnostic: `Workflow guard JSON is invalid: ${toError(error).message}` };
447
+ }
448
+ if (raw === undefined)
449
+ return { kind: "empty" };
450
+ try {
451
+ return { kind: "owned", guard: parseLifecycleGuardV1(raw) };
452
+ }
453
+ catch (error) {
454
+ return { kind: "corrupt", diagnostic: toError(error).message };
455
+ }
456
+ }
457
+ async #releaseGuard(path, generation) {
458
+ const existing = await this.#readGuard(path);
459
+ if (existing.kind !== "owned" || existing.guard.generation !== generation) {
460
+ throw new WorkflowLeaseFenceError("Workflow lifecycle guard generation changed before release");
461
+ }
462
+ await unlink(path);
463
+ await syncWorkflowDirectory(dirname(path));
464
+ }
465
+ async #initialize() {
466
+ await mkdir(this.#namespaceDirectory, { recursive: true, mode: 0o700 });
467
+ await mkdir(join(this.#namespaceDirectory, "worker-session-leases"), { recursive: true, mode: 0o700 });
468
+ await mkdir(join(this.#namespaceDirectory, "quarantine"), { recursive: true, mode: 0o700 });
469
+ }
470
+ #supervisorResource() {
471
+ return {
472
+ kind: "supervisor",
473
+ resourceId: this.#workspaceId,
474
+ lockPath: join(this.#namespaceDirectory, "supervisor.lock"),
475
+ guardPath: join(this.#namespaceDirectory, "supervisor.lifecycle.guard.json"),
476
+ quarantineStem: "supervisor",
477
+ };
478
+ }
479
+ #workerResource(sessionPath) {
480
+ const resourceId = sha256(sessionPath).slice("sha256:".length);
481
+ const parent = join(this.#namespaceDirectory, "worker-session-leases");
482
+ return {
483
+ kind: "worker-session",
484
+ resourceId,
485
+ lockPath: join(parent, `${resourceId}.lock`),
486
+ guardPath: join(parent, `${resourceId}.lifecycle.guard.json`),
487
+ quarantineStem: `worker-session.${resourceId}`,
488
+ };
489
+ }
490
+ }
491
+ export function parseWorkflowLeaseOwnerV1(value) {
492
+ const object = strictRecord(value, OWNER_KEYS, "workflow lease owner");
493
+ if (numeric(object, "schema") !== 1)
494
+ throw new Error("workflow lease owner schema must be 1");
495
+ const kind = enumString(object, "kind", ["supervisor", "worker-session"]);
496
+ const owner = {
497
+ schema: 1,
498
+ kind,
499
+ stage: enumString(object, "stage", ["active", "reserved"]),
500
+ generation: nonEmptyString(object, "generation"),
501
+ workspaceId: nonEmptyString(object, "workspaceId"),
502
+ resourceId: nonEmptyString(object, "resourceId"),
503
+ runId: nullableString(object, "runId"),
504
+ workerId: nullableString(object, "workerId"),
505
+ attemptId: nullableString(object, "attemptId"),
506
+ workerAttemptGeneration: nullableString(object, "workerAttemptGeneration"),
507
+ supervisorGeneration: nullableString(object, "supervisorGeneration"),
508
+ sessionPath: nullableAbsolutePath(object, "sessionPath"),
509
+ pid: positiveInteger(object, "pid"),
510
+ processStartIdentity: nonEmptyString(object, "processStartIdentity"),
511
+ bootIdentity: nullableString(object, "bootIdentity"),
512
+ executable: absolutePath(object, "executable"),
513
+ hostname: nonEmptyString(object, "hostname"),
514
+ createdAt: isoTimestamp(object, "createdAt"),
515
+ };
516
+ const workerFields = [
517
+ owner.runId,
518
+ owner.workerId,
519
+ owner.attemptId,
520
+ owner.workerAttemptGeneration,
521
+ owner.supervisorGeneration,
522
+ owner.sessionPath,
523
+ ];
524
+ if (kind === "supervisor" && workerFields.some((field) => field !== null)) {
525
+ throw new Error("Supervisor lease cannot contain worker-session fields");
526
+ }
527
+ if (kind === "supervisor" && owner.stage !== "active") {
528
+ throw new Error("Supervisor lease must be active");
529
+ }
530
+ if (kind === "worker-session" && workerFields.some((field) => field === null)) {
531
+ throw new Error("Worker-session lease requires run, worker, attempt, generation, and session fields");
532
+ }
533
+ return owner;
534
+ }
535
+ function parseLifecycleGuardV1(value) {
536
+ const object = strictRecord(value, GUARD_KEYS, "workflow lifecycle guard");
537
+ if (numeric(object, "schema") !== 1)
538
+ throw new Error("workflow guard schema must be 1");
539
+ return {
540
+ schema: 1,
541
+ generation: nonEmptyString(object, "generation"),
542
+ pid: positiveInteger(object, "pid"),
543
+ processStartIdentity: nonEmptyString(object, "processStartIdentity"),
544
+ bootIdentity: nullableString(object, "bootIdentity"),
545
+ executable: absolutePath(object, "executable"),
546
+ hostname: nonEmptyString(object, "hostname"),
547
+ createdAt: isoTimestamp(object, "createdAt"),
548
+ };
549
+ }
550
+ function ownerFromIdentity(input) {
551
+ return {
552
+ schema: 1,
553
+ kind: input.kind,
554
+ stage: input.stage,
555
+ generation: requireNonEmpty(input.generation, "lease generation"),
556
+ workspaceId: input.workspaceId,
557
+ resourceId: input.resourceId,
558
+ runId: input.runId ?? null,
559
+ workerId: input.workerId ?? null,
560
+ attemptId: input.attemptId ?? null,
561
+ workerAttemptGeneration: input.workerAttemptGeneration ?? null,
562
+ supervisorGeneration: input.supervisorGeneration ?? null,
563
+ sessionPath: input.sessionPath ?? null,
564
+ pid: input.identity.pid,
565
+ processStartIdentity: input.identity.processStartIdentity,
566
+ bootIdentity: input.identity.bootIdentity,
567
+ executable: input.identity.executable,
568
+ hostname: input.identity.hostname,
569
+ createdAt: input.now.toISOString(),
570
+ };
571
+ }
572
+ async function classifyProcessRecord(record, current, inspector) {
573
+ if (record.hostname !== current.hostname)
574
+ return { kind: "remote" };
575
+ const inspection = await inspector.inspect(record.pid);
576
+ if (inspection.kind === "absent")
577
+ return { kind: "stale", reason: "PID is absent" };
578
+ if (inspection.kind === "unknown")
579
+ return inspection;
580
+ const observed = inspection.identity;
581
+ if (record.processStartIdentity !== observed.processStartIdentity) {
582
+ return { kind: "stale", reason: "PID start identity changed" };
583
+ }
584
+ if (normalize(record.executable) !== normalize(observed.executable)) {
585
+ return { kind: "stale", reason: "PID executable changed" };
586
+ }
587
+ if (record.bootIdentity !== null
588
+ && observed.bootIdentity !== null
589
+ && record.bootIdentity !== observed.bootIdentity) {
590
+ return { kind: "stale", reason: "host boot identity changed" };
591
+ }
592
+ return { kind: "live" };
593
+ }
594
+ function ownerIsProcess(owner, identity) {
595
+ return owner.hostname === identity.hostname
596
+ && owner.pid === identity.pid
597
+ && owner.processStartIdentity === identity.processStartIdentity
598
+ && normalize(owner.executable) === normalize(identity.executable)
599
+ && (owner.bootIdentity === null
600
+ || identity.bootIdentity === null
601
+ || owner.bootIdentity === identity.bootIdentity);
602
+ }
603
+ function canonicalSessionPath(path) {
604
+ if (!isAbsolute(path))
605
+ throw new TypeError("Worker session path must be absolute");
606
+ return normalize(resolve(path));
607
+ }
608
+ function validateProcessIdentity(identity, name) {
609
+ if (!Number.isSafeInteger(identity.pid) || identity.pid <= 0)
610
+ throw new Error(`${name} PID is invalid`);
611
+ requireNonEmpty(identity.processStartIdentity, `${name} start identity`);
612
+ if (!isAbsolute(identity.executable))
613
+ throw new Error(`${name} executable must be absolute`);
614
+ requireNonEmpty(identity.hostname, `${name} hostname`);
615
+ }
616
+ function strictRecord(value, allowed, name) {
617
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
618
+ throw new Error(`${name} must be an object`);
619
+ }
620
+ const object = value;
621
+ const unknown = Object.keys(object).filter((key) => !allowed.includes(key));
622
+ if (unknown.length > 0)
623
+ throw new Error(`${name} contains unknown fields: ${unknown.join(", ")}`);
624
+ return object;
625
+ }
626
+ function enumString(object, key, values) {
627
+ const value = nonEmptyString(object, key);
628
+ if (!values.includes(value))
629
+ throw new Error(`${key} is invalid`);
630
+ return value;
631
+ }
632
+ function nonEmptyString(object, key) {
633
+ const value = object[key];
634
+ if (typeof value !== "string" || value.length === 0)
635
+ throw new Error(`${key} must be a non-empty string`);
636
+ return value;
637
+ }
638
+ function nullableString(object, key) {
639
+ const value = object[key];
640
+ if (value === null)
641
+ return null;
642
+ if (typeof value !== "string" || value.length === 0)
643
+ throw new Error(`${key} must be null or a non-empty string`);
644
+ return value;
645
+ }
646
+ function nullableAbsolutePath(object, key) {
647
+ const value = nullableString(object, key);
648
+ if (value === null)
649
+ return null;
650
+ if (!isAbsolute(value))
651
+ throw new Error(`${key} must be absolute`);
652
+ return normalize(value);
653
+ }
654
+ function absolutePath(object, key) {
655
+ const value = nonEmptyString(object, key);
656
+ if (!isAbsolute(value))
657
+ throw new Error(`${key} must be absolute`);
658
+ return normalize(value);
659
+ }
660
+ function positiveInteger(object, key) {
661
+ const value = numeric(object, key);
662
+ if (!Number.isSafeInteger(value) || value <= 0)
663
+ throw new Error(`${key} must be a positive integer`);
664
+ return value;
665
+ }
666
+ function numeric(object, key) {
667
+ const value = object[key];
668
+ if (typeof value !== "number" || !Number.isFinite(value))
669
+ throw new Error(`${key} must be a number`);
670
+ return value;
671
+ }
672
+ function isoTimestamp(object, key) {
673
+ const value = nonEmptyString(object, key);
674
+ if (!Number.isFinite(Date.parse(value)))
675
+ throw new Error(`${key} must be an ISO timestamp`);
676
+ return value;
677
+ }
678
+ function requireNonEmpty(value, name) {
679
+ if (value.length === 0)
680
+ throw new TypeError(`${name} is required`);
681
+ return value;
682
+ }
683
+ function safePathPart(value) {
684
+ return value.replace(/[^A-Za-z0-9._-]/g, "_");
685
+ }
686
+ function toError(error) {
687
+ return error instanceof Error ? error : new Error(String(error));
688
+ }
689
+ //# sourceMappingURL=workflow-lease.js.map