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,315 @@
1
+ import { randomUUID as nodeRandomUUID } from "node:crypto";
2
+ import { lstat, mkdir, readdir, rm } from "node:fs/promises";
3
+ import { isAbsolute, join, normalize, relative, sep } from "node:path";
4
+
5
+ import { writeWorkflowJson } from "./atomic-storage.js";
6
+ import { runDirectoryForRequest } from "./run-store.js";
7
+ import { WorkflowLeaseManager } from "./workflow-lease.js";
8
+ import type { RunProjectionV2, WorkerAttemptProjection } from "./workflow-types.js";
9
+
10
+ export interface CleanupItem {
11
+ readonly kind: "run" | "worker-session" | "artifact";
12
+ readonly path: string;
13
+ readonly bytes: number;
14
+ }
15
+
16
+ export interface CleanupCandidate {
17
+ readonly requestKey: string;
18
+ readonly runId: string;
19
+ readonly updatedAt: string;
20
+ readonly ageMs: number;
21
+ readonly items: readonly CleanupItem[];
22
+ readonly reclaimableBytes: number;
23
+ }
24
+
25
+ export interface CleanupSkip {
26
+ readonly requestKey: string;
27
+ readonly runId: string;
28
+ readonly reason:
29
+ | "not-old-enough"
30
+ | "active-run"
31
+ | "ambiguous-run"
32
+ | "active-worker"
33
+ | "held-session-lease"
34
+ | "corrupt-session-lease";
35
+ readonly diagnostic: string;
36
+ }
37
+
38
+ export interface RunCleanupPlan {
39
+ readonly dryRun: true;
40
+ readonly olderThanMs: number;
41
+ readonly candidates: readonly CleanupCandidate[];
42
+ readonly skipped: readonly CleanupSkip[];
43
+ readonly reclaimableBytes: number;
44
+ }
45
+
46
+ export interface RunCleanupResult extends Omit<RunCleanupPlan, "dryRun"> {
47
+ readonly dryRun: false;
48
+ readonly auditEventPath: string | null;
49
+ readonly deletedRunIds: readonly string[];
50
+ }
51
+
52
+ interface CleanupRunCatalog {
53
+ listRuns(): Promise<readonly RunProjectionV2[]>;
54
+ }
55
+
56
+ interface CleanupLeaseAuthority {
57
+ withCurrentSupervisor<T>(generation: string, operation: () => Promise<T>): Promise<T>;
58
+ inspectWorkerSession(sessionPath: string): ReturnType<WorkflowLeaseManager["inspectWorkerSession"]>;
59
+ }
60
+
61
+ export interface WorkflowRunCleanupOptions {
62
+ readonly namespaceDirectory: string;
63
+ readonly store: CleanupRunCatalog;
64
+ readonly leaseManager: CleanupLeaseAuthority;
65
+ readonly supervisorGeneration: string;
66
+ readonly now?: () => Date;
67
+ readonly randomUUID?: () => string;
68
+ readonly removePath?: (path: string) => Promise<void>;
69
+ }
70
+
71
+ export class CleanupConfirmationError extends Error {
72
+ constructor(message: string) {
73
+ super(message);
74
+ this.name = "CleanupConfirmationError";
75
+ }
76
+ }
77
+
78
+ export class WorkflowRunCleanup {
79
+ readonly #namespaceDirectory: string;
80
+ readonly #store: CleanupRunCatalog;
81
+ readonly #leaseManager: CleanupLeaseAuthority;
82
+ readonly #supervisorGeneration: string;
83
+ readonly #now: () => Date;
84
+ readonly #randomUUID: () => string;
85
+ readonly #removePath: (path: string) => Promise<void>;
86
+
87
+ constructor(options: WorkflowRunCleanupOptions) {
88
+ if (!isAbsolute(options.namespaceDirectory)) {
89
+ throw new TypeError("Workflow cleanup namespace must be absolute");
90
+ }
91
+ this.#namespaceDirectory = normalize(options.namespaceDirectory);
92
+ this.#store = options.store;
93
+ this.#leaseManager = options.leaseManager;
94
+ this.#supervisorGeneration = requireNonEmpty(
95
+ options.supervisorGeneration,
96
+ "workflow supervisor generation",
97
+ );
98
+ this.#now = options.now ?? (() => new Date());
99
+ this.#randomUUID = options.randomUUID ?? nodeRandomUUID;
100
+ this.#removePath = options.removePath ?? (async (path) => {
101
+ assertOwnedPath(this.#namespaceDirectory, path);
102
+ await rm(path, { recursive: true, force: true });
103
+ });
104
+ }
105
+
106
+ async plan(olderThanMs: number): Promise<RunCleanupPlan> {
107
+ const age = positiveInteger(olderThanMs, "cleanup age");
108
+ const nowMs = this.#now().getTime();
109
+ const candidates: CleanupCandidate[] = [];
110
+ const skipped: CleanupSkip[] = [];
111
+ for (const run of await this.#store.listRuns()) {
112
+ const evaluated = await this.#evaluate(run, age, nowMs);
113
+ if ("reason" in evaluated) skipped.push(evaluated);
114
+ else candidates.push(evaluated);
115
+ }
116
+ candidates.sort((left, right) => left.runId.localeCompare(right.runId));
117
+ skipped.sort((left, right) => left.runId.localeCompare(right.runId));
118
+ return {
119
+ dryRun: true,
120
+ olderThanMs: age,
121
+ candidates,
122
+ skipped,
123
+ reclaimableBytes: candidates.reduce((sum, candidate) => sum + candidate.reclaimableBytes, 0),
124
+ };
125
+ }
126
+
127
+ async execute(input: {
128
+ readonly olderThanMs: number;
129
+ readonly confirmed: boolean;
130
+ }): Promise<RunCleanupResult> {
131
+ if (!input.confirmed) {
132
+ throw new CleanupConfirmationError("Workflow cleanup deletion requires explicit confirmation");
133
+ }
134
+ return this.#leaseManager.withCurrentSupervisor(this.#supervisorGeneration, async () => {
135
+ const plan = await this.plan(input.olderThanMs);
136
+ if (plan.candidates.length === 0) {
137
+ return { ...plan, dryRun: false, auditEventPath: null, deletedRunIds: [] };
138
+ }
139
+ const auditEventPath = await this.#recordAudit(plan);
140
+ const deletedRunIds: string[] = [];
141
+ for (const candidate of plan.candidates) {
142
+ for (const session of candidate.items.filter((item) => item.kind === "worker-session")) {
143
+ await this.#removePath(session.path);
144
+ }
145
+ const run = candidate.items.find((item) => item.kind === "run");
146
+ if (!run) throw new Error(`Cleanup candidate ${candidate.runId} has no run directory`);
147
+ await this.#removePath(run.path);
148
+ deletedRunIds.push(candidate.runId);
149
+ }
150
+ return { ...plan, dryRun: false, auditEventPath, deletedRunIds };
151
+ });
152
+ }
153
+
154
+ async #evaluate(
155
+ run: RunProjectionV2,
156
+ olderThanMs: number,
157
+ nowMs: number,
158
+ ): Promise<CleanupCandidate | CleanupSkip> {
159
+ const ageMs = Math.max(0, nowMs - Date.parse(run.updatedAt));
160
+ if (ageMs < olderThanMs) {
161
+ return skip(run, "not-old-enough", `run age ${ageMs}ms is below ${olderThanMs}ms`);
162
+ }
163
+ if (run.state === "ambiguous" || run.state === "needs-attention"
164
+ || run.unresolvedAmbiguities.length > 0) {
165
+ return skip(run, "ambiguous-run", `run state is ${run.state}`);
166
+ }
167
+ if (run.state !== "done" && run.state !== "cancelled") {
168
+ return skip(run, "active-run", `run state is ${run.state}`);
169
+ }
170
+ const attempts = Object.values(run.workers).flatMap((worker) => worker.attempts);
171
+ if (attempts.some(isActiveAttempt)) {
172
+ return skip(run, "active-worker", "at least one worker attempt is active or lacks exit evidence");
173
+ }
174
+ const sessionPaths = [...new Set(attempts.map((attempt) => attempt.session.path))].sort();
175
+ for (const sessionPath of sessionPaths) {
176
+ const lease = await this.#leaseManager.inspectWorkerSession(sessionPath);
177
+ if (lease.kind === "owned") {
178
+ return skip(
179
+ run,
180
+ "held-session-lease",
181
+ `session ${sessionPath} is held by generation ${lease.owner.generation}`,
182
+ );
183
+ }
184
+ if (lease.kind === "corrupt") {
185
+ return skip(run, "corrupt-session-lease", lease.diagnostic);
186
+ }
187
+ }
188
+ const runPath = runDirectoryForRequest(this.#namespaceDirectory, run.requestKey);
189
+ const artifactPath = join(runPath, "artifacts");
190
+ const runBytes = await pathBytes(runPath);
191
+ const items: CleanupItem[] = [{ kind: "run", path: runPath, bytes: runBytes }];
192
+ for (const sessionPath of sessionPaths) {
193
+ assertOwnedPath(this.#namespaceDirectory, sessionPath);
194
+ items.push({ kind: "worker-session", path: sessionPath, bytes: await pathBytes(sessionPath) });
195
+ }
196
+ const artifactBytes = await pathBytes(artifactPath);
197
+ if (artifactBytes > 0) items.push({ kind: "artifact", path: artifactPath, bytes: artifactBytes });
198
+ return {
199
+ requestKey: run.requestKey,
200
+ runId: run.runId,
201
+ updatedAt: run.updatedAt,
202
+ ageMs,
203
+ items,
204
+ reclaimableBytes: runBytes + items
205
+ .filter((item) => item.kind === "worker-session")
206
+ .reduce((sum, item) => sum + item.bytes, 0),
207
+ };
208
+ }
209
+
210
+ async #recordAudit(plan: RunCleanupPlan): Promise<string> {
211
+ const directory = join(this.#namespaceDirectory, "cleanup-ledger", "events");
212
+ await mkdir(directory, { recursive: true, mode: 0o700 });
213
+ const eventId = `cleanup_${requireNonEmpty(this.#randomUUID(), "cleanup event UUID")}`;
214
+ const path = join(directory, `${this.#now().getTime()}.${safePathPart(eventId)}.json`);
215
+ await writeWorkflowJson(path, {
216
+ schema: 1,
217
+ eventId,
218
+ type: "cleanup-started",
219
+ occurredAt: this.#now().toISOString(),
220
+ supervisorGeneration: this.#supervisorGeneration,
221
+ olderThanMs: plan.olderThanMs,
222
+ candidates: plan.candidates,
223
+ reclaimableBytes: plan.reclaimableBytes,
224
+ }, { createOnly: true });
225
+ return path;
226
+ }
227
+ }
228
+
229
+ export async function confirmCleanupDeletion(input: {
230
+ readonly dryRun: boolean;
231
+ readonly yes: boolean;
232
+ readonly tui: boolean;
233
+ readonly confirm?: () => Promise<boolean>;
234
+ }): Promise<boolean> {
235
+ if (input.dryRun) return false;
236
+ if (input.tui) {
237
+ if (!input.confirm || !await input.confirm()) {
238
+ throw new CleanupConfirmationError("Workflow cleanup was not confirmed; no state changed");
239
+ }
240
+ return true;
241
+ }
242
+ if (!input.yes) {
243
+ throw new CleanupConfirmationError("Headless workflow cleanup requires explicit yes=true");
244
+ }
245
+ return true;
246
+ }
247
+
248
+ export function parseCleanupAge(value: string): number {
249
+ const match = /^(\d+)(ms|s|m|h|d|w)$/.exec(value.trim());
250
+ if (!match) throw new TypeError("Cleanup age must use <integer><ms|s|m|h|d|w>");
251
+ const amount = Number(match[1]);
252
+ const multiplier = { ms: 1, s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000 }[
253
+ match[2] as "ms" | "s" | "m" | "h" | "d" | "w"
254
+ ];
255
+ const result = amount * multiplier;
256
+ return positiveInteger(result, "cleanup age");
257
+ }
258
+
259
+ function isActiveAttempt(attempt: WorkerAttemptProjection): boolean {
260
+ return attempt.pidStartEvidence.observedExitAt === null
261
+ || attempt.pidStartEvidence.exitStatus === null
262
+ || attempt.state === "pending"
263
+ || attempt.state === "running"
264
+ || attempt.state === "blocked"
265
+ || attempt.state === "ambiguous";
266
+ }
267
+
268
+ function skip(
269
+ run: RunProjectionV2,
270
+ reason: CleanupSkip["reason"],
271
+ diagnostic: string,
272
+ ): CleanupSkip {
273
+ return { requestKey: run.requestKey, runId: run.runId, reason, diagnostic };
274
+ }
275
+
276
+ async function pathBytes(path: string): Promise<number> {
277
+ let info;
278
+ try {
279
+ info = await lstat(path);
280
+ } catch (error) {
281
+ if (isNodeError(error) && error.code === "ENOENT") return 0;
282
+ throw error;
283
+ }
284
+ if (!info.isDirectory()) return info.size;
285
+ let total = info.size;
286
+ for (const entry of await readdir(path, { withFileTypes: true })) {
287
+ total += await pathBytes(join(path, entry.name));
288
+ }
289
+ return total;
290
+ }
291
+
292
+ function assertOwnedPath(namespaceDirectory: string, path: string): void {
293
+ const child = relative(namespaceDirectory, normalize(path));
294
+ if (child.length === 0 || child === ".." || child.startsWith(`..${sep}`) || isAbsolute(child)) {
295
+ throw new Error(`Workflow cleanup path escapes its namespace: ${path}`);
296
+ }
297
+ }
298
+
299
+ function positiveInteger(value: number, name: string): number {
300
+ if (!Number.isSafeInteger(value) || value <= 0) throw new TypeError(`${name} must be positive`);
301
+ return value;
302
+ }
303
+
304
+ function requireNonEmpty(value: string, name: string): string {
305
+ if (value.length === 0) throw new TypeError(`${name} is required`);
306
+ return value;
307
+ }
308
+
309
+ function safePathPart(value: string): string {
310
+ return value.replace(/[^A-Za-z0-9._-]/g, "_");
311
+ }
312
+
313
+ function isNodeError(error: unknown): error is NodeJS.ErrnoException {
314
+ return error instanceof Error && "code" in error;
315
+ }
@@ -0,0 +1,122 @@
1
+ import { RUN_STATES, type RunState } from "./workflow-types.js";
2
+
3
+ export { RUN_STATES, type RunState };
4
+
5
+ export interface RunGateFacts {
6
+ readonly objective: {
7
+ readonly evidenceComplete: boolean;
8
+ readonly verifierPassed: boolean;
9
+ readonly prerequisitesComplete: boolean;
10
+ };
11
+ readonly workers: {
12
+ readonly active: boolean;
13
+ readonly uncertain: boolean;
14
+ readonly detached: boolean;
15
+ };
16
+ readonly cancellation: {
17
+ readonly requested: boolean;
18
+ readonly settled: boolean;
19
+ };
20
+ readonly input: { readonly delivered: boolean };
21
+ readonly reconciliation: { readonly recorded: boolean; readonly uncertaintyCleared: boolean };
22
+ readonly attention: { readonly cleared: boolean };
23
+ }
24
+
25
+ export type RunGateGuard =
26
+ | "objective.evidence.complete"
27
+ | "objective.verifier.passed"
28
+ | "objective.prerequisites.complete"
29
+ | "workers.none_active"
30
+ | "workers.none_uncertain"
31
+ | "workers.none_detached"
32
+ | "cancellation.requested"
33
+ | "cancellation.settled"
34
+ | "input.delivered"
35
+ | "reconciliation.recorded"
36
+ | "reconciliation.uncertainty_cleared"
37
+ | "attention.cleared";
38
+
39
+ export interface RunTransitionRule {
40
+ readonly from: RunState;
41
+ readonly to: RunState;
42
+ readonly guards: readonly RunGateGuard[];
43
+ }
44
+
45
+ const DONE_GUARDS: readonly RunGateGuard[] = [
46
+ "objective.evidence.complete",
47
+ "objective.verifier.passed",
48
+ "objective.prerequisites.complete",
49
+ "workers.none_active",
50
+ "workers.none_uncertain",
51
+ "workers.none_detached",
52
+ ];
53
+
54
+ const RECONCILED: readonly RunGateGuard[] = [
55
+ "reconciliation.recorded",
56
+ "reconciliation.uncertainty_cleared",
57
+ ];
58
+
59
+ export const RUN_LEGALITY_TABLE: readonly RunTransitionRule[] = [
60
+ { from: "open", to: "verifying", guards: [] },
61
+ { from: "open", to: "blocked", guards: [] },
62
+ { from: "open", to: "cancel-requested", guards: ["cancellation.requested"] },
63
+ { from: "open", to: "ambiguous", guards: [] },
64
+ { from: "open", to: "needs-attention", guards: [] },
65
+ { from: "verifying", to: "open", guards: [] },
66
+ { from: "verifying", to: "blocked", guards: [] },
67
+ { from: "verifying", to: "done", guards: DONE_GUARDS },
68
+ { from: "verifying", to: "cancel-requested", guards: ["cancellation.requested"] },
69
+ { from: "verifying", to: "ambiguous", guards: [] },
70
+ { from: "verifying", to: "needs-attention", guards: [] },
71
+ { from: "blocked", to: "open", guards: ["input.delivered"] },
72
+ { from: "blocked", to: "cancel-requested", guards: ["cancellation.requested"] },
73
+ { from: "blocked", to: "ambiguous", guards: [] },
74
+ { from: "blocked", to: "needs-attention", guards: [] },
75
+ { from: "cancel-requested", to: "cancelled", guards: ["cancellation.settled", "workers.none_uncertain"] },
76
+ { from: "cancel-requested", to: "ambiguous", guards: [] },
77
+ { from: "cancel-requested", to: "needs-attention", guards: [] },
78
+ { from: "ambiguous", to: "open", guards: RECONCILED },
79
+ { from: "ambiguous", to: "verifying", guards: RECONCILED },
80
+ { from: "ambiguous", to: "blocked", guards: RECONCILED },
81
+ { from: "ambiguous", to: "cancel-requested", guards: [...RECONCILED, "cancellation.requested"] },
82
+ { from: "ambiguous", to: "cancelled", guards: [...RECONCILED, "cancellation.settled"] },
83
+ { from: "ambiguous", to: "done", guards: [...RECONCILED, ...DONE_GUARDS] },
84
+ { from: "ambiguous", to: "needs-attention", guards: RECONCILED },
85
+ { from: "needs-attention", to: "open", guards: ["attention.cleared", "workers.none_detached"] },
86
+ { from: "needs-attention", to: "verifying", guards: ["attention.cleared", "workers.none_detached"] },
87
+ { from: "needs-attention", to: "blocked", guards: ["attention.cleared"] },
88
+ { from: "needs-attention", to: "cancel-requested", guards: ["cancellation.requested"] },
89
+ { from: "needs-attention", to: "ambiguous", guards: [] },
90
+ ] as const;
91
+
92
+ export function evaluateRunTransition(
93
+ from: RunState,
94
+ to: RunState,
95
+ facts: RunGateFacts,
96
+ ): { readonly allowed: true } | {
97
+ readonly allowed: false;
98
+ readonly reason: "undeclared-transition" | "guard-failed";
99
+ readonly blockers: readonly RunGateGuard[];
100
+ } {
101
+ const rule = RUN_LEGALITY_TABLE.find((candidate) => candidate.from === from && candidate.to === to);
102
+ if (!rule) return { allowed: false, reason: "undeclared-transition", blockers: [] };
103
+ const blockers = rule.guards.filter((guard) => !guardSatisfied(guard, facts));
104
+ return blockers.length === 0 ? { allowed: true } : { allowed: false, reason: "guard-failed", blockers };
105
+ }
106
+
107
+ function guardSatisfied(guard: RunGateGuard, facts: RunGateFacts): boolean {
108
+ switch (guard) {
109
+ case "objective.evidence.complete": return facts.objective.evidenceComplete;
110
+ case "objective.verifier.passed": return facts.objective.verifierPassed;
111
+ case "objective.prerequisites.complete": return facts.objective.prerequisitesComplete;
112
+ case "workers.none_active": return !facts.workers.active;
113
+ case "workers.none_uncertain": return !facts.workers.uncertain;
114
+ case "workers.none_detached": return !facts.workers.detached;
115
+ case "cancellation.requested": return facts.cancellation.requested;
116
+ case "cancellation.settled": return facts.cancellation.settled;
117
+ case "input.delivered": return facts.input.delivered;
118
+ case "reconciliation.recorded": return facts.reconciliation.recorded;
119
+ case "reconciliation.uncertainty_cleared": return facts.reconciliation.uncertaintyCleared;
120
+ case "attention.cleared": return facts.attention.cleared;
121
+ }
122
+ }