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,3890 @@
1
+ import { randomUUID as nodeRandomUUID } from "node:crypto";
2
+ import {
3
+ chmod,
4
+ mkdir,
5
+ mkdtemp,
6
+ readdir,
7
+ rename,
8
+ rm,
9
+ stat,
10
+ } from "node:fs/promises";
11
+ import { isAbsolute, join, normalize, relative, sep } from "node:path";
12
+
13
+ import {
14
+ canonicalJson,
15
+ isNodeError,
16
+ readWorkflowJson,
17
+ sha256,
18
+ syncWorkflowDirectory,
19
+ writeWorkflowJson,
20
+ type WorkflowAtomicFaultInjector,
21
+ } from "./atomic-storage.js";
22
+ import type {
23
+ AmbiguityId,
24
+ AmbiguityProjection,
25
+ AttemptId,
26
+ AcceptanceCheck,
27
+ ContentRef,
28
+ ContractRevisionId,
29
+ EvidenceNode,
30
+ ModelSpec,
31
+ ObjectiveContract,
32
+ PeerMessageId,
33
+ PeerMessageProjection,
34
+ RunBudget,
35
+ RunBudgetProjection,
36
+ RunId,
37
+ RunProjectionV2,
38
+ RunSource,
39
+ ReportSchemaV1,
40
+ StepId,
41
+ WorkerAttemptProjection,
42
+ WorkerBudget,
43
+ WorkerContract,
44
+ WorkerContinuationProvenance,
45
+ WorkerId,
46
+ WorkerProjection,
47
+ WorkerProposalProvenance,
48
+ WorkerRequest,
49
+ WorkerRoleContract,
50
+ WorkerLaunchContractV1,
51
+ WorkspaceGrant,
52
+ } from "./workflow-types.js";
53
+ import { parseWorkerLaunchContractV1 } from "./worker-launch.js";
54
+ import {
55
+ UNLIMITED,
56
+ budgetExceeded,
57
+ budgetExhausted,
58
+ budgetValue,
59
+ parseBudgetLimit,
60
+ remainingBudget,
61
+ reserveBudget,
62
+ } from "./budget.js";
63
+ import {
64
+ classifyObservedTermination,
65
+ evaluateWorkerAttemptTransition,
66
+ normalizeWorkerAttemptState,
67
+ type WorkerAttemptState,
68
+ type WorkerGateFacts,
69
+ } from "./worker-gate.js";
70
+ import { evaluateRunTransition, type RunGateFacts } from "./run-gate.js";
71
+ import { WorkflowLeaseManager, type ProcessIdentity } from "./workflow-lease.js";
72
+
73
+ const EVENT_KEYS = [
74
+ "schema",
75
+ "eventId",
76
+ "seq",
77
+ "runId",
78
+ "type",
79
+ "supervisorGeneration",
80
+ "occurredAt",
81
+ "previousHash",
82
+ "payload",
83
+ "checksum",
84
+ ] as const;
85
+
86
+ type WorkflowEventType =
87
+ | "run-opened"
88
+ | "worker-spawn-requested"
89
+ | "attempt-spawn-intent"
90
+ | "attempt-start-observed"
91
+ | "worker-ready-observed"
92
+ | "attempt-usage-observed"
93
+ | "cancel-requested"
94
+ | "cancel-signal-sent"
95
+ | "attempt-exit-observed"
96
+ | "worker-needs-attention"
97
+ | "contract-revision-requested"
98
+ | "contract-revision-delivered"
99
+ | "collect-observed"
100
+ | "silent-stall-recorded"
101
+ | "gate-evaluated"
102
+ | "worker-wait-declared"
103
+ | "steer-delivered"
104
+ | "peer-message-requested"
105
+ | "peer-message-delivery-observed"
106
+ | "ambiguity-recorded"
107
+ | "reconciliation-recorded";
108
+
109
+ interface WorkflowLedgerEventV1 {
110
+ readonly schema: 1;
111
+ readonly eventId: string;
112
+ readonly seq: number;
113
+ readonly runId: RunId;
114
+ readonly type: WorkflowEventType;
115
+ readonly supervisorGeneration: string;
116
+ readonly occurredAt: string;
117
+ readonly previousHash: string | null;
118
+ readonly payload: Record<string, unknown>;
119
+ readonly checksum: string;
120
+ }
121
+
122
+ export interface StartRunInput {
123
+ readonly requestKey: string;
124
+ readonly source: RunSource;
125
+ readonly objective: ObjectiveContract;
126
+ }
127
+
128
+ export type StartRunResult =
129
+ | { readonly kind: "created"; readonly projection: RunProjectionV2 }
130
+ | { readonly kind: "existing"; readonly projection: RunProjectionV2 };
131
+
132
+ export interface WorkflowRunStoreOptions {
133
+ readonly namespaceDirectory: string;
134
+ readonly leaseManager: WorkflowLeaseManager;
135
+ readonly supervisorGeneration: string;
136
+ readonly randomUUID?: () => string;
137
+ readonly now?: () => Date;
138
+ readonly atomicFault?: WorkflowAtomicFaultInjector;
139
+ }
140
+
141
+ export type RunRecordDiagnostic =
142
+ | {
143
+ readonly kind: "healthy";
144
+ readonly directory: string;
145
+ readonly projection: RunProjectionV2;
146
+ }
147
+ | {
148
+ readonly kind: "corrupt";
149
+ readonly directory: string;
150
+ readonly diagnostic: string;
151
+ };
152
+
153
+ export class RunConflictError extends Error {
154
+ constructor(message: string) {
155
+ super(message);
156
+ this.name = "RunConflictError";
157
+ }
158
+ }
159
+
160
+ export class LedgerIntegrityError extends Error {
161
+ constructor(message: string) {
162
+ super(message);
163
+ this.name = "LedgerIntegrityError";
164
+ }
165
+ }
166
+
167
+ export class BudgetExceededError extends Error {
168
+ constructor(message: string) {
169
+ super(message);
170
+ this.name = "BudgetExceededError";
171
+ }
172
+ }
173
+
174
+ export interface PrepareWorkerIntentResult {
175
+ readonly kind: "created" | "existing";
176
+ readonly projection: RunProjectionV2;
177
+ readonly worker: WorkerProjection;
178
+ }
179
+
180
+ export interface PrepareAttemptResult {
181
+ readonly projection: RunProjectionV2;
182
+ readonly worker: WorkerProjection;
183
+ readonly attempt: WorkerAttemptProjection;
184
+ }
185
+
186
+ export class WorkflowRunStore {
187
+ readonly #namespaceDirectory: string;
188
+ readonly #leaseManager: WorkflowLeaseManager;
189
+ readonly #supervisorGeneration: string;
190
+ readonly #randomUUID: () => string;
191
+ readonly #now: () => Date;
192
+ readonly #atomicFault: WorkflowAtomicFaultInjector | undefined;
193
+ readonly #listeners = new Set<(projection: RunProjectionV2) => void>();
194
+
195
+ constructor(options: WorkflowRunStoreOptions) {
196
+ if (!isAbsolute(options.namespaceDirectory)) {
197
+ throw new TypeError("Workflow run-store namespace must be absolute");
198
+ }
199
+ this.#namespaceDirectory = normalize(options.namespaceDirectory);
200
+ this.#leaseManager = options.leaseManager;
201
+ this.#supervisorGeneration = requireNonEmpty(
202
+ options.supervisorGeneration,
203
+ "supervisor generation",
204
+ );
205
+ this.#randomUUID = options.randomUUID ?? nodeRandomUUID;
206
+ this.#now = options.now ?? (() => new Date());
207
+ this.#atomicFault = options.atomicFault;
208
+ }
209
+
210
+ async startRun(input: StartRunInput): Promise<StartRunResult> {
211
+ const requestKey = validateRequestKey(input.requestKey);
212
+ const source = parseRunSource(input.source);
213
+ const objective = parseObjectiveContract(input.objective);
214
+ const requestFingerprint = fingerprint({ source, objective });
215
+ return this.#leaseManager.withCurrentSupervisor(this.#supervisorGeneration, async () => {
216
+ await this.#initialize();
217
+ const existing = await this.readRun(requestKey);
218
+ if (existing) return existingRun(existing, requestFingerprint);
219
+
220
+ const runId = this.#id("run") as RunId;
221
+ const occurredAt = this.#now().toISOString();
222
+ const event = createEvent({
223
+ eventId: this.#id("event"),
224
+ seq: 0,
225
+ runId,
226
+ type: "run-opened",
227
+ supervisorGeneration: this.#supervisorGeneration,
228
+ occurredAt,
229
+ previousHash: null,
230
+ payload: {
231
+ requestKey,
232
+ requestFingerprint,
233
+ source,
234
+ objective,
235
+ },
236
+ });
237
+ const projection = foldEvents([event]);
238
+ const installed = await this.#installInitialRun(requestKey, event, projection);
239
+ if (installed) {
240
+ this.#notify(projection);
241
+ return { kind: "created", projection };
242
+ }
243
+ const raced = await this.readRun(requestKey);
244
+ if (!raced) throw new LedgerIntegrityError("Run directory appeared without a readable ledger");
245
+ return existingRun(raced, requestFingerprint);
246
+ });
247
+ }
248
+
249
+ subscribe(listener: (projection: RunProjectionV2) => void): () => void {
250
+ this.#listeners.add(listener);
251
+ return () => this.#listeners.delete(listener);
252
+ }
253
+
254
+ async readRun(requestKey: string): Promise<RunProjectionV2 | undefined> {
255
+ validateRequestKey(requestKey);
256
+ const directory = runDirectoryForRequest(this.#namespaceDirectory, requestKey);
257
+ try {
258
+ await stat(directory);
259
+ } catch (error) {
260
+ if (isNodeError(error) && error.code === "ENOENT") return undefined;
261
+ throw error;
262
+ }
263
+ // snapshot.json is deliberately not read here: the immutable event chain is authority.
264
+ const projection = foldEvents(await readEventChain(directory));
265
+ if (projection.requestKey !== requestKey) {
266
+ throw new LedgerIntegrityError("Run request-key hash collision or directory mismatch");
267
+ }
268
+ return projection;
269
+ }
270
+
271
+ async listRuns(): Promise<readonly RunProjectionV2[]> {
272
+ const byRequest = this.#byRequestDirectory();
273
+ let entries: string[];
274
+ try {
275
+ entries = await readdir(byRequest);
276
+ } catch (error) {
277
+ if (isNodeError(error) && error.code === "ENOENT") return [];
278
+ throw error;
279
+ }
280
+ const runs: RunProjectionV2[] = [];
281
+ for (const entry of entries.sort()) {
282
+ if (entry.startsWith(".")) continue;
283
+ const directory = join(byRequest, entry);
284
+ const chain = await readEventChain(directory);
285
+ runs.push(foldEvents(chain));
286
+ }
287
+ return runs.sort((left, right) => left.runId.localeCompare(right.runId));
288
+ }
289
+
290
+ async diagnoseRunRecords(): Promise<readonly RunRecordDiagnostic[]> {
291
+ const byRequest = this.#byRequestDirectory();
292
+ let entries: string[];
293
+ try {
294
+ entries = await readdir(byRequest);
295
+ } catch (error) {
296
+ if (isNodeError(error) && error.code === "ENOENT") return [];
297
+ throw error;
298
+ }
299
+ const records: RunRecordDiagnostic[] = [];
300
+ for (const entry of entries.sort()) {
301
+ if (entry.startsWith(".")) continue;
302
+ const directory = join(byRequest, entry);
303
+ try {
304
+ const projection = foldEvents(await readEventChain(directory));
305
+ if (runDirectoryForRequest(this.#namespaceDirectory, projection.requestKey) !== directory) {
306
+ throw new LedgerIntegrityError("Run request-key hash does not match its directory");
307
+ }
308
+ records.push({ kind: "healthy", directory, projection });
309
+ } catch (error) {
310
+ records.push({
311
+ kind: "corrupt",
312
+ directory,
313
+ diagnostic: error instanceof Error ? error.message : String(error),
314
+ });
315
+ }
316
+ }
317
+ return records;
318
+ }
319
+
320
+ async recordWorkerIntent(inputRequestKey: string, input: {
321
+ readonly createdByDriverSessionId: string;
322
+ readonly sessionPath: string;
323
+ readonly request: WorkerRequest;
324
+ }): Promise<RunProjectionV2> {
325
+ return (await this.prepareWorkerIntent(inputRequestKey, {
326
+ ...input,
327
+ parentWorkerId: null,
328
+ depth: 1,
329
+ })).projection;
330
+ }
331
+
332
+ async prepareWorkerIntent(inputRequestKey: string, input: {
333
+ readonly createdByDriverSessionId: string;
334
+ readonly sessionPath: string;
335
+ readonly request: WorkerRequest;
336
+ readonly parentWorkerId: WorkerId | null;
337
+ readonly depth: number;
338
+ readonly continuation?: WorkerContinuationProvenance;
339
+ }): Promise<PrepareWorkerIntentResult> {
340
+ // This state-only seam records an atomic call that already occurred. It does not create a
341
+ // process or schedule future work.
342
+ const requestKey = validateRequestKey(inputRequestKey);
343
+ const createdByDriverSessionId = requireNonEmpty(
344
+ input.createdByDriverSessionId,
345
+ "driver session ID",
346
+ );
347
+ if (!isAbsolute(input.sessionPath)) throw new TypeError("Worker session path must be absolute");
348
+ const sessionPath = normalize(input.sessionPath);
349
+ const request = parseWorkerRequest(input.request);
350
+ const depth = positiveIntegerValue(input.depth, "worker depth");
351
+ const parentWorkerId = input.parentWorkerId;
352
+ const continuation = input.continuation === undefined
353
+ ? null
354
+ : parseWorkerContinuationProvenance(input.continuation);
355
+ const contractInputFingerprint = fingerprint({
356
+ request,
357
+ createdByDriverSessionId,
358
+ sessionPath,
359
+ parentWorkerId,
360
+ depth,
361
+ ...(continuation ? { continuation } : {}),
362
+ });
363
+ let disposition: "created" | "existing" = "existing";
364
+ const projection = await this.#withRunMutation(requestKey, async (current, events) => {
365
+ const existing = Object.values(current.workers).find(
366
+ (worker) => worker.spawnRequestKey === request.requestKey,
367
+ );
368
+ if (existing) {
369
+ if (existing.contractFingerprint !== contractInputFingerprint) {
370
+ throw new RunConflictError(
371
+ `Worker request key ${request.requestKey} already has a different contract`,
372
+ );
373
+ }
374
+ return { kind: "existing" as const, projection: current };
375
+ }
376
+ assertWorkerCapabilitySubset(current, request);
377
+ assertSpawnBudget(current, request.budget, depth, this.#now());
378
+ if (parentWorkerId !== null && !current.workers[parentWorkerId]) {
379
+ throw new TypeError(`Parent worker ${parentWorkerId} does not exist in this run`);
380
+ }
381
+ if (continuation !== null) {
382
+ assertContinuationSource(current, continuation, sessionPath);
383
+ }
384
+ const workerId = this.#id("worker") as WorkerId;
385
+ const stepId = this.#id("step") as StepId;
386
+ const attemptId = this.#id("attempt") as AttemptId;
387
+ const contract: WorkerContract = {
388
+ ...request,
389
+ context: request.context ?? "fresh",
390
+ runId: current.runId,
391
+ workerId,
392
+ createdByDriverSessionId,
393
+ };
394
+ disposition = "created";
395
+ const event = this.#nextEvent(current, "worker-spawn-requested", {
396
+ stepId,
397
+ workerId,
398
+ contractFingerprint: contractInputFingerprint,
399
+ contract,
400
+ continuation,
401
+ parentWorkerId,
402
+ depth,
403
+ attempt: initialAttempt(
404
+ attemptId,
405
+ 1,
406
+ this.#randomUUID(),
407
+ this.#supervisorGeneration,
408
+ sessionPath,
409
+ request.sideEffectPolicy,
410
+ request.model,
411
+ null,
412
+ ),
413
+ });
414
+ return { kind: "append" as const, event, events };
415
+ });
416
+ const worker = Object.values(projection.workers).find(
417
+ (candidate) => candidate.spawnRequestKey === request.requestKey,
418
+ );
419
+ if (!worker) throw new LedgerIntegrityError("Worker intent committed without a projection");
420
+ return { kind: disposition, projection, worker };
421
+ }
422
+
423
+ async prepareReplacementAttempt(inputRequestKey: string, input: {
424
+ readonly workerId: WorkerId;
425
+ readonly model?: ModelSpec;
426
+ readonly fallbackReason?: string;
427
+ }): Promise<PrepareAttemptResult> {
428
+ const requestKey = validateRequestKey(inputRequestKey);
429
+ const workerId = requireNonEmpty(input.workerId, "worker ID") as WorkerId;
430
+ const projection = await this.#withRunMutation(requestKey, async (current, events) => {
431
+ const worker = requiredWorker(current, workerId);
432
+ const previous = requiredCurrentAttempt(worker);
433
+ if (isAttemptProcessActive(previous)) {
434
+ throw new Error(`Worker ${workerId} still has an active attempt`);
435
+ }
436
+ if (previous.state !== "failed" && previous.state !== "cancelled") {
437
+ throw new Error(`Worker ${workerId} cannot retry from ${previous.state}`);
438
+ }
439
+ if (budgetExhausted(worker.attempts.length, worker.contract.budget.maxAttempts)) {
440
+ throw new BudgetExceededError(`Worker ${workerId} exhausted its attempt budget`);
441
+ }
442
+ assertReplacementBudget(current, worker.contract.budget, worker.depth, this.#now());
443
+ const model = input.model ?? previous.modelSelection?.model ?? worker.contract.model;
444
+ if (![worker.contract.model, ...(worker.contract.modelFallbacks ?? [])].some(
445
+ (candidate) => candidate.provider === model.provider && candidate.id === model.id,
446
+ )) {
447
+ throw new TypeError(`Replacement model is outside the immutable fallback chain: ${model.provider}/${model.id}`);
448
+ }
449
+ const attempt = initialAttempt(
450
+ this.#id("attempt") as AttemptId,
451
+ worker.attempts.length + 1,
452
+ this.#randomUUID(),
453
+ this.#supervisorGeneration,
454
+ previous.session.path,
455
+ worker.contract.sideEffectPolicy,
456
+ model,
457
+ input.fallbackReason ?? null,
458
+ );
459
+ return {
460
+ kind: "append" as const,
461
+ events,
462
+ event: this.#nextEvent(current, "attempt-spawn-intent", { workerId, attempt }),
463
+ };
464
+ });
465
+ const worker = requiredWorker(projection, workerId);
466
+ return { projection, worker, attempt: requiredCurrentAttempt(worker) };
467
+ }
468
+
469
+ async recordAttemptStarted(inputRequestKey: string, input: {
470
+ readonly workerId: WorkerId;
471
+ readonly attemptId: AttemptId;
472
+ readonly workerAttemptGeneration: string;
473
+ readonly leaseGeneration: string;
474
+ readonly processIdentity: ProcessIdentity;
475
+ readonly launchContract: WorkerLaunchContractV1;
476
+ }): Promise<RunProjectionV2> {
477
+ const requestKey = validateRequestKey(inputRequestKey);
478
+ return this.#withRunMutation(requestKey, async (current, events) => {
479
+ const { attempt } = requiredWorkerAttempt(current, input.workerId, input.attemptId);
480
+ assertAttemptGeneration(attempt, input.workerAttemptGeneration, this.#supervisorGeneration);
481
+ if (attempt.state === "running") {
482
+ if (attempt.pidStartEvidence.pid !== input.processIdentity.pid) {
483
+ throw new RunConflictError(`Attempt ${input.attemptId} already has different PID evidence`);
484
+ }
485
+ return { kind: "existing" as const, projection: current };
486
+ }
487
+ if (attempt.state !== "pending") {
488
+ throw new Error(`Attempt ${input.attemptId} cannot start from ${attempt.state}`);
489
+ }
490
+ return {
491
+ kind: "append" as const,
492
+ events,
493
+ event: this.#nextEvent(current, "attempt-start-observed", {
494
+ workerId: input.workerId,
495
+ attemptId: input.attemptId,
496
+ workerAttemptGeneration: input.workerAttemptGeneration,
497
+ leaseGeneration: requireNonEmpty(input.leaseGeneration, "lease generation"),
498
+ processIdentity: parseProcessIdentity(input.processIdentity),
499
+ launchContract: parseWorkerLaunchContractV1(input.launchContract),
500
+ }),
501
+ };
502
+ });
503
+ }
504
+
505
+ async recordWorkerReady(inputRequestKey: string, input: {
506
+ readonly workerId: WorkerId;
507
+ readonly attemptId: AttemptId;
508
+ readonly workerAttemptGeneration: string;
509
+ readonly sessionFile: string;
510
+ }): Promise<RunProjectionV2> {
511
+ const requestKey = validateRequestKey(inputRequestKey);
512
+ if (!isAbsolute(input.sessionFile)) {
513
+ throw new TypeError("Worker session file must be absolute");
514
+ }
515
+ return this.#withRunMutation(requestKey, async (current, events) => {
516
+ const { worker, attempt } = requiredWorkerAttempt(current, input.workerId, input.attemptId);
517
+ assertAttemptGeneration(attempt, input.workerAttemptGeneration, this.#supervisorGeneration);
518
+ const sessionFile = normalize(input.sessionFile);
519
+ const sessionRelative = relative(attempt.session.path, sessionFile);
520
+ if (sessionRelative.length === 0
521
+ || sessionRelative === ".."
522
+ || sessionRelative.startsWith(`..${sep}`)
523
+ || isAbsolute(sessionRelative)) {
524
+ throw new TypeError("Worker session file must be inside its leased session directory");
525
+ }
526
+ if (worker.continuation !== null && worker.continuation.sessionFile !== sessionFile) {
527
+ throw new RunConflictError("Continued worker did not open the proven prior session file");
528
+ }
529
+ if (attempt.session.file === sessionFile) return { kind: "existing" as const, projection: current };
530
+ if (attempt.session.file !== null) {
531
+ throw new RunConflictError(`Attempt ${input.attemptId} already has a different session file`);
532
+ }
533
+ return {
534
+ kind: "append" as const,
535
+ events,
536
+ event: this.#nextEvent(current, "worker-ready-observed", {
537
+ workerId: input.workerId,
538
+ attemptId: input.attemptId,
539
+ workerAttemptGeneration: input.workerAttemptGeneration,
540
+ sessionFile,
541
+ }),
542
+ };
543
+ });
544
+ }
545
+
546
+ async recordAttemptUsage(inputRequestKey: string, input: {
547
+ readonly workerId: WorkerId;
548
+ readonly attemptId: AttemptId;
549
+ readonly workerAttemptGeneration: string;
550
+ readonly tokens: number;
551
+ readonly costUsd: number;
552
+ readonly outputBytes: number;
553
+ }): Promise<RunProjectionV2> {
554
+ const requestKey = validateRequestKey(inputRequestKey);
555
+ const usage = parseUsage({
556
+ tokens: input.tokens,
557
+ costUsd: input.costUsd,
558
+ outputBytes: input.outputBytes,
559
+ });
560
+ return this.#withRunMutation(requestKey, async (current, events) => {
561
+ const { attempt } = requiredWorkerAttempt(current, input.workerId, input.attemptId);
562
+ assertAttemptGeneration(attempt, input.workerAttemptGeneration, this.#supervisorGeneration);
563
+ if (usage.tokens < attempt.usage.tokens
564
+ || usage.costUsd < attempt.usage.costUsd
565
+ || usage.outputBytes < attempt.usage.outputBytes) {
566
+ throw new TypeError("Attempt usage must be monotonic");
567
+ }
568
+ if (canonicalJson(usage) === canonicalJson(attempt.usage)) {
569
+ return { kind: "existing" as const, projection: current };
570
+ }
571
+ return {
572
+ kind: "append" as const,
573
+ events,
574
+ event: this.#nextEvent(current, "attempt-usage-observed", {
575
+ workerId: input.workerId,
576
+ attemptId: input.attemptId,
577
+ workerAttemptGeneration: input.workerAttemptGeneration,
578
+ usage,
579
+ }),
580
+ };
581
+ });
582
+ }
583
+
584
+ async requestWorkerCancellation(inputRequestKey: string, input: {
585
+ readonly workerId: WorkerId;
586
+ readonly reason: string;
587
+ }): Promise<RunProjectionV2> {
588
+ const requestKey = validateRequestKey(inputRequestKey);
589
+ const reason = requireNonEmpty(input.reason, "cancellation reason");
590
+ return this.#withRunMutation(requestKey, async (current, events) => {
591
+ const worker = requiredWorker(current, input.workerId);
592
+ const attempt = requiredCurrentAttempt(worker);
593
+ if (attempt.cancellation.requestedAt !== null) {
594
+ if (attempt.cancellation.reason !== reason) {
595
+ throw new RunConflictError(`Worker ${input.workerId} already has a different cancellation intent`);
596
+ }
597
+ return { kind: "existing" as const, projection: current };
598
+ }
599
+ if (!isAttemptProcessActive(attempt)) {
600
+ throw new Error(`Worker ${input.workerId} has no active attempt to cancel`);
601
+ }
602
+ return {
603
+ kind: "append" as const,
604
+ events,
605
+ event: this.#nextEvent(current, "cancel-requested", {
606
+ workerId: input.workerId,
607
+ attemptId: attempt.attemptId,
608
+ workerAttemptGeneration: attempt.workerAttemptGeneration,
609
+ reason,
610
+ }),
611
+ };
612
+ });
613
+ }
614
+
615
+ async recordCancellationSignalSent(inputRequestKey: string, input: {
616
+ readonly workerId: WorkerId;
617
+ readonly attemptId: AttemptId;
618
+ readonly workerAttemptGeneration: string;
619
+ readonly signal: string;
620
+ }): Promise<RunProjectionV2> {
621
+ const requestKey = validateRequestKey(inputRequestKey);
622
+ return this.#withRunMutation(requestKey, async (current, events) => {
623
+ const { attempt } = requiredWorkerAttempt(current, input.workerId, input.attemptId);
624
+ assertAttemptGeneration(attempt, input.workerAttemptGeneration, this.#supervisorGeneration);
625
+ if (attempt.cancellation.requestedAt === null) {
626
+ throw new Error("Cancellation signal cannot precede durable cancellation intent");
627
+ }
628
+ if (attempt.cancellation.sentAt !== null) return { kind: "existing" as const, projection: current };
629
+ return {
630
+ kind: "append" as const,
631
+ events,
632
+ event: this.#nextEvent(current, "cancel-signal-sent", {
633
+ workerId: input.workerId,
634
+ attemptId: input.attemptId,
635
+ workerAttemptGeneration: input.workerAttemptGeneration,
636
+ signal: requireNonEmpty(input.signal, "cancellation signal"),
637
+ }),
638
+ };
639
+ });
640
+ }
641
+
642
+ async recordAttemptExit(inputRequestKey: string, input: {
643
+ readonly workerId: WorkerId;
644
+ readonly attemptId: AttemptId;
645
+ readonly workerAttemptGeneration: string;
646
+ readonly exitStatus: { readonly code: number | null; readonly signal: string | null };
647
+ readonly candidateOutcome: { readonly valueOrRef: string | null; readonly hash: string | null };
648
+ readonly uncertainSideEffects: boolean;
649
+ readonly reason: string;
650
+ }): Promise<RunProjectionV2> {
651
+ const requestKey = validateRequestKey(inputRequestKey);
652
+ const exitStatus = parseExitStatus(input.exitStatus);
653
+ const candidateOutcome = parseCandidateOutcome(input.candidateOutcome);
654
+ const reason = requireNonEmpty(input.reason, "attempt exit reason");
655
+ return this.#withRunMutation(requestKey, async (current, events) => {
656
+ const { attempt } = requiredWorkerAttempt(current, input.workerId, input.attemptId);
657
+ assertAttemptGeneration(attempt, input.workerAttemptGeneration, this.#supervisorGeneration);
658
+ if (attempt.pidStartEvidence.observedExitAt !== null) {
659
+ return { kind: "existing" as const, projection: current };
660
+ }
661
+ const ambiguityId = input.uncertainSideEffects
662
+ ? (this.#id("ambiguity") as AmbiguityId)
663
+ : null;
664
+ return {
665
+ kind: "append" as const,
666
+ events,
667
+ event: this.#nextEvent(current, "attempt-exit-observed", {
668
+ workerId: input.workerId,
669
+ attemptId: input.attemptId,
670
+ workerAttemptGeneration: input.workerAttemptGeneration,
671
+ exitStatus,
672
+ candidateOutcome,
673
+ uncertainSideEffects: input.uncertainSideEffects,
674
+ reason,
675
+ ambiguityId,
676
+ }),
677
+ };
678
+ });
679
+ }
680
+
681
+ async recordWorkerNeedsAttention(inputRequestKey: string, input: {
682
+ readonly workerId: WorkerId;
683
+ readonly attemptId: AttemptId;
684
+ readonly workerAttemptGeneration: string;
685
+ readonly reason: string;
686
+ readonly ambiguous: boolean;
687
+ }): Promise<RunProjectionV2> {
688
+ const requestKey = validateRequestKey(inputRequestKey);
689
+ const reason = requireNonEmpty(input.reason, "worker attention reason");
690
+ return this.#withRunMutation(requestKey, async (current, events) => {
691
+ const { worker, attempt } = requiredWorkerAttempt(current, input.workerId, input.attemptId);
692
+ assertAttemptGeneration(attempt, input.workerAttemptGeneration, this.#supervisorGeneration);
693
+ if (worker.state === "ambiguous"
694
+ || (!input.ambiguous && attempt.control.status === "detached")) {
695
+ return { kind: "existing" as const, projection: current };
696
+ }
697
+ return {
698
+ kind: "append" as const,
699
+ events,
700
+ event: this.#nextEvent(current, "worker-needs-attention", {
701
+ workerId: input.workerId,
702
+ attemptId: input.attemptId,
703
+ workerAttemptGeneration: input.workerAttemptGeneration,
704
+ reason,
705
+ ambiguous: input.ambiguous,
706
+ ambiguityId: input.ambiguous ? (this.#id("ambiguity") as AmbiguityId) : null,
707
+ }),
708
+ };
709
+ });
710
+ }
711
+
712
+ async requestContractRevision(inputRequestKey: string, input: {
713
+ readonly workerId: WorkerId;
714
+ readonly correlation?: string;
715
+ readonly goal?: string;
716
+ readonly acceptance?: readonly string[];
717
+ readonly constraints?: readonly string[];
718
+ readonly report_schema?: ReportSchemaV1 | null;
719
+ readonly requires_human_approval?: boolean;
720
+ readonly acceptance_check?: AcceptanceCheck | null;
721
+ }): Promise<RunProjectionV2> {
722
+ const requestKey = validateRequestKey(inputRequestKey);
723
+ const correlation = input.correlation === undefined
724
+ ? null
725
+ : requireBoundedText(input.correlation, "contract revision correlation", 256);
726
+ return this.#withRunMutation(requestKey, async (current, events) => {
727
+ const worker = requiredWorker(current, input.workerId);
728
+ if (worker.state === "done" || worker.state === "failed" || worker.state === "cancelled") {
729
+ throw new Error(`Worker ${input.workerId} contract is terminal`);
730
+ }
731
+ if (worker.state === "blocked") {
732
+ requiredMatchingWait(worker, correlation, false);
733
+ }
734
+ const previous = latestWorkerContract(worker);
735
+ const revisedRequest = parseWorkerRequest({
736
+ ...previous,
737
+ goal: input.goal ?? previous.goal,
738
+ acceptance: input.acceptance ?? previous.acceptance,
739
+ constraints: input.constraints ?? previous.constraints,
740
+ ...optionalRevisionField("report_schema", input, previous),
741
+ ...optionalRevisionField("acceptance_check", input, previous),
742
+ ...(input.requires_human_approval === undefined
743
+ ? (previous.requires_human_approval === undefined
744
+ ? {}
745
+ : { requires_human_approval: previous.requires_human_approval })
746
+ : { requires_human_approval: input.requires_human_approval }),
747
+ }, true);
748
+ const contract: WorkerContract = {
749
+ ...revisedRequest,
750
+ context: revisedRequest.context ?? "fresh",
751
+ runId: worker.contract.runId,
752
+ workerId: worker.workerId,
753
+ createdByDriverSessionId: worker.contract.createdByDriverSessionId,
754
+ };
755
+ if (canonicalJson(contract) === canonicalJson(previous)) {
756
+ throw new TypeError("Contract revision must change at least one field");
757
+ }
758
+ const revisionId = this.#id("revision") as ContractRevisionId;
759
+ return {
760
+ kind: "append" as const,
761
+ events,
762
+ event: this.#nextEvent(current, "contract-revision-requested", {
763
+ workerId: worker.workerId,
764
+ revisionId,
765
+ revision: worker.contractRevisions.length + 1,
766
+ contract,
767
+ correlation,
768
+ }),
769
+ };
770
+ });
771
+ }
772
+
773
+ async recordContractRevisionDelivered(inputRequestKey: string, input: {
774
+ readonly workerId: WorkerId;
775
+ readonly revisionId: ContractRevisionId;
776
+ readonly boundary: "next-inference";
777
+ }): Promise<RunProjectionV2> {
778
+ const requestKey = validateRequestKey(inputRequestKey);
779
+ return this.#withRunMutation(requestKey, async (current, events) => {
780
+ const worker = requiredWorker(current, input.workerId);
781
+ const revision = worker.contractRevisions.find(
782
+ (candidate) => candidate.revisionId === input.revisionId,
783
+ );
784
+ if (!revision) throw new Error(`Unknown contract revision ${input.revisionId}`);
785
+ if (revision.status === "delivered") return { kind: "existing" as const, projection: current };
786
+ if (worker.contractRevisions.some(
787
+ (candidate) => candidate.revision < revision.revision && candidate.status !== "delivered",
788
+ )) {
789
+ throw new Error("Contract revisions must be delivered in order");
790
+ }
791
+ if (input.boundary !== "next-inference") {
792
+ throw new TypeError("Contract revisions may be delivered only at the next inference boundary");
793
+ }
794
+ if (worker.state === "blocked") {
795
+ requiredMatchingWait(worker, revision.correlation, false);
796
+ }
797
+ return {
798
+ kind: "append" as const,
799
+ events,
800
+ event: this.#nextEvent(current, "contract-revision-delivered", {
801
+ workerId: worker.workerId,
802
+ revisionId: revision.revisionId,
803
+ boundary: input.boundary,
804
+ correlation: revision.correlation,
805
+ }),
806
+ };
807
+ });
808
+ }
809
+
810
+ async recordCollectObservation(inputRequestKey: string, input: {
811
+ readonly workerId: WorkerId;
812
+ readonly structuredPayload: unknown;
813
+ readonly consecutiveRounds: number;
814
+ }): Promise<RunProjectionV2> {
815
+ const requestKey = validateRequestKey(inputRequestKey);
816
+ const consecutiveRounds = positiveIntegerValue(
817
+ input.consecutiveRounds,
818
+ "no-progress consecutive rounds",
819
+ );
820
+ if (consecutiveRounds < 2) throw new TypeError("No-progress threshold must be at least 2");
821
+ const payloadFingerprint = fingerprint(input.structuredPayload);
822
+ return this.#withRunMutation(requestKey, async (current, events) => {
823
+ const worker = requiredWorker(current, input.workerId);
824
+ if (worker.state !== "running") {
825
+ throw new Error(`No-progress observation requires running worker; ${input.workerId} is ${worker.state}`);
826
+ }
827
+ return {
828
+ kind: "append" as const,
829
+ events,
830
+ event: this.#nextEvent(current, "collect-observed", {
831
+ workerId: input.workerId,
832
+ payloadFingerprint,
833
+ consecutiveRounds,
834
+ }),
835
+ };
836
+ });
837
+ }
838
+
839
+ async recordSilentStall(inputRequestKey: string, input: {
840
+ readonly workerId: WorkerId;
841
+ }): Promise<RunProjectionV2> {
842
+ const requestKey = validateRequestKey(inputRequestKey);
843
+ return this.#withRunMutation(requestKey, async (current, events) => {
844
+ const worker = requiredWorker(current, input.workerId);
845
+ if (worker.state !== "running") {
846
+ throw new Error(`Silent-stall observation requires running worker; ${input.workerId} is ${worker.state}`);
847
+ }
848
+ if (worker.stall.silent.detected) {
849
+ return { kind: "existing" as const, projection: current };
850
+ }
851
+ const expectedCadenceMs = expectedWorkerCadenceMs(effectiveWorkerContract(worker).budget);
852
+ const silentForMs = this.#now().getTime() - new Date(worker.lastLedgerAt).getTime();
853
+ if (silentForMs < expectedCadenceMs) {
854
+ throw new Error(
855
+ `Worker ${input.workerId} has been silent for ${silentForMs}ms; expected cadence is ${expectedCadenceMs}ms`,
856
+ );
857
+ }
858
+ return {
859
+ kind: "append" as const,
860
+ events,
861
+ event: this.#nextEvent(current, "silent-stall-recorded", {
862
+ workerId: input.workerId,
863
+ expectedCadenceMs,
864
+ }),
865
+ };
866
+ });
867
+ }
868
+
869
+ async recordGateEvaluation(inputRequestKey: string, input: {
870
+ readonly workerId: WorkerId;
871
+ readonly reportMatches: boolean | null;
872
+ readonly acceptanceCheckExitCode: number | null;
873
+ readonly humanApproved: boolean | null;
874
+ readonly evidenceExists?: boolean;
875
+ }): Promise<RunProjectionV2> {
876
+ const requestKey = validateRequestKey(inputRequestKey);
877
+ return this.#withRunMutation(requestKey, async (current, events) => {
878
+ const worker = requiredWorker(current, input.workerId);
879
+ const attempt = requiredCurrentAttempt(worker);
880
+ const contract = effectiveWorkerContract(worker);
881
+ const facts: WorkerGateFacts = {
882
+ evidence: {
883
+ exists: input.evidenceExists ?? (
884
+ attempt.candidateOutcome.valueOrRef !== null
885
+ || attempt.candidateOutcome.hash !== null
886
+ ),
887
+ },
888
+ report: {
889
+ schemaDeclared: contract.report_schema !== undefined,
890
+ matches: input.reportMatches ?? false,
891
+ },
892
+ acceptanceCheck: {
893
+ declared: contract.acceptance_check !== undefined,
894
+ exitCode: input.acceptanceCheckExitCode,
895
+ },
896
+ human: {
897
+ approvalRequired: contract.requires_human_approval === true,
898
+ approved: input.humanApproved === true,
899
+ },
900
+ budget: { exceeded: workerBudgetExceeded(attempt, contract.budget) },
901
+ runtime: {
902
+ supervisorLost: false,
903
+ effectsUncertain: attempt.sideEffects.status === "uncertain",
904
+ },
905
+ reconcile: { decisionExists: false },
906
+ input: { kind: null, correlationMatches: false },
907
+ cancel: { verbReceived: false },
908
+ };
909
+ const result = evaluateWorkerAttemptTransition(attempt.state, "done", facts);
910
+ const blockers = result.allowed ? [] : result.blockers;
911
+ return {
912
+ kind: "append" as const,
913
+ events,
914
+ event: this.#nextEvent(current, "gate-evaluated", {
915
+ workerId: worker.workerId,
916
+ attemptId: attempt.attemptId,
917
+ fromState: attempt.state,
918
+ targetState: "done",
919
+ evidenceExists: facts.evidence.exists,
920
+ reportMatches: input.reportMatches,
921
+ acceptanceCheckExitCode: input.acceptanceCheckExitCode,
922
+ humanApproved: input.humanApproved,
923
+ budgetExceeded: facts.budget.exceeded,
924
+ allowed: result.allowed,
925
+ blockers,
926
+ }),
927
+ };
928
+ });
929
+ }
930
+
931
+ async recordSteerDelivered(inputRequestKey: string, input: {
932
+ readonly workerId: WorkerId;
933
+ readonly messageHash: string;
934
+ readonly correlation?: string;
935
+ }): Promise<RunProjectionV2> {
936
+ const requestKey = validateRequestKey(inputRequestKey);
937
+ const messageHash = requireNonEmpty(input.messageHash, "steer message hash");
938
+ const correlation = input.correlation === undefined
939
+ ? null
940
+ : requireBoundedText(input.correlation, "steer correlation", 256);
941
+ return this.#withRunMutation(requestKey, async (current, events) => {
942
+ const worker = requiredWorker(current, input.workerId);
943
+ if (worker.state === "blocked") requiredMatchingWait(worker, correlation, false);
944
+ return {
945
+ kind: "append" as const,
946
+ events,
947
+ event: this.#nextEvent(current, "steer-delivered", {
948
+ workerId: input.workerId,
949
+ messageHash,
950
+ correlation,
951
+ }),
952
+ };
953
+ });
954
+ }
955
+
956
+ async recordWorkerWaitDeclared(inputRequestKey: string, input: {
957
+ readonly workerId: WorkerId;
958
+ readonly attemptId: AttemptId;
959
+ readonly workerAttemptGeneration: string;
960
+ readonly reason: string;
961
+ readonly correlation: string;
962
+ }): Promise<RunProjectionV2> {
963
+ const requestKey = validateRequestKey(inputRequestKey);
964
+ const reason = requireBoundedText(input.reason, "worker wait reason", 2_048);
965
+ const correlation = requireBoundedText(input.correlation, "worker wait correlation", 256);
966
+ return this.#withRunMutation(requestKey, async (current, events) => {
967
+ const { worker, attempt } = requiredWorkerAttempt(current, input.workerId, input.attemptId);
968
+ assertAttemptGeneration(attempt, input.workerAttemptGeneration, this.#supervisorGeneration);
969
+ if (effectiveWorkerContract(worker).channel !== "rpc") {
970
+ throw new TypeError("Only an RPC worker can declare a typed wait");
971
+ }
972
+ if (worker.state !== "running" || attempt.state !== "running") {
973
+ throw new RunConflictError(`Typed wait requires running worker; ${worker.workerId} is ${worker.state}`);
974
+ }
975
+ return {
976
+ kind: "append" as const,
977
+ events,
978
+ event: this.#nextEvent(current, "worker-wait-declared", {
979
+ workerId: worker.workerId,
980
+ attemptId: attempt.attemptId,
981
+ workerAttemptGeneration: attempt.workerAttemptGeneration,
982
+ supervisorGeneration: attempt.supervisorGeneration,
983
+ reason,
984
+ correlation,
985
+ }),
986
+ };
987
+ });
988
+ }
989
+
990
+ async recordPeerMessageRequested(inputRequestKey: string, input: {
991
+ readonly senderWorkerId: WorkerId;
992
+ readonly recipientWorkerId: WorkerId;
993
+ readonly grant: string;
994
+ readonly correlation: string;
995
+ readonly messageHash: string;
996
+ }): Promise<{ readonly projection: RunProjectionV2; readonly message: PeerMessageProjection }> {
997
+ const requestKey = validateRequestKey(inputRequestKey);
998
+ const grant = requireNonEmpty(input.grant, "peer-message grant");
999
+ const correlation = requireNonEmpty(input.correlation, "peer-message correlation");
1000
+ const messageHash = requireSha256(input.messageHash, "peer-message hash");
1001
+ const messageId = this.#id("peer") as PeerMessageId;
1002
+ const projection = await this.#withRunMutation(requestKey, async (current, events) => {
1003
+ const sender = requiredWorker(current, input.senderWorkerId);
1004
+ const recipient = requiredWorker(current, input.recipientWorkerId);
1005
+ if (sender.workerId === recipient.workerId) throw new TypeError("Peer message cannot target its sender");
1006
+ if (!effectiveWorkerContract(sender).capabilities.includes(grant)) {
1007
+ throw new TypeError(`Sender contract does not grant ${grant}`);
1008
+ }
1009
+ const grantedRecipient = resolvePeerGrantRecipient(current, grant, false);
1010
+ if (grantedRecipient.workerId !== recipient.workerId) {
1011
+ throw new TypeError(`Peer grant ${grant} does not resolve to ${recipient.workerId}`);
1012
+ }
1013
+ if (effectiveWorkerContract(sender).channel !== "rpc"
1014
+ || effectiveWorkerContract(recipient).channel !== "rpc") {
1015
+ throw new TypeError("Peer messaging requires RPC sender and recipient contracts");
1016
+ }
1017
+ if (recipient.state !== "running" && recipient.state !== "blocked") {
1018
+ throw new RunConflictError(
1019
+ `Peer-message recipient must be running or blocked; ${recipient.workerId} is ${recipient.state}`,
1020
+ );
1021
+ }
1022
+ if (recipient.state === "blocked") requiredMatchingWait(recipient, correlation, false);
1023
+ return {
1024
+ kind: "append" as const,
1025
+ events,
1026
+ event: this.#nextEvent(current, "peer-message-requested", {
1027
+ workerId: sender.workerId,
1028
+ messageId,
1029
+ senderWorkerId: sender.workerId,
1030
+ recipientWorkerId: recipient.workerId,
1031
+ grant,
1032
+ correlation,
1033
+ messageHash,
1034
+ }),
1035
+ };
1036
+ });
1037
+ const message = projection.workers[input.senderWorkerId]?.peerMessages.find(
1038
+ (candidate) => candidate.messageId === messageId,
1039
+ );
1040
+ if (!message) throw new LedgerIntegrityError("Peer message committed without sender projection");
1041
+ return { projection, message };
1042
+ }
1043
+
1044
+ async recordPeerMessageDelivery(inputRequestKey: string, input: {
1045
+ readonly messageId: PeerMessageId;
1046
+ readonly recipientWorkerId: WorkerId;
1047
+ readonly recipientAttemptId: AttemptId;
1048
+ readonly recipientWorkerAttemptGeneration: string;
1049
+ readonly status: "delivered" | "indeterminate";
1050
+ readonly diagnostic?: string;
1051
+ }): Promise<RunProjectionV2> {
1052
+ const requestKey = validateRequestKey(inputRequestKey);
1053
+ const diagnostic = input.diagnostic === undefined
1054
+ ? null
1055
+ : requireNonEmpty(input.diagnostic, "peer-message delivery diagnostic");
1056
+ return this.#withRunMutation(requestKey, async (current, events) => {
1057
+ const recipient = requiredWorker(current, input.recipientWorkerId);
1058
+ const attempt = requiredCurrentAttempt(recipient);
1059
+ if (attempt.attemptId !== input.recipientAttemptId
1060
+ || attempt.workerAttemptGeneration !== input.recipientWorkerAttemptGeneration) {
1061
+ throw new RunConflictError("Peer-message recipient generation is stale");
1062
+ }
1063
+ const message = recipient.peerMessages.find((candidate) => candidate.messageId === input.messageId);
1064
+ if (!message) throw new TypeError(`Unknown peer message ${input.messageId}`);
1065
+ if (message.status !== "pending") {
1066
+ if (message.status === input.status) return { kind: "existing" as const, projection: current };
1067
+ throw new RunConflictError(`Peer message ${input.messageId} is already ${message.status}`);
1068
+ }
1069
+ if (input.status === "delivered" && recipient.state === "blocked") {
1070
+ requiredMatchingWait(recipient, message.correlation, false);
1071
+ }
1072
+ return {
1073
+ kind: "append" as const,
1074
+ events,
1075
+ event: this.#nextEvent(current, "peer-message-delivery-observed", {
1076
+ workerId: message.senderWorkerId,
1077
+ messageId: message.messageId,
1078
+ senderWorkerId: message.senderWorkerId,
1079
+ recipientWorkerId: message.recipientWorkerId,
1080
+ recipientAttemptId: input.recipientAttemptId,
1081
+ recipientWorkerAttemptGeneration: input.recipientWorkerAttemptGeneration,
1082
+ status: input.status,
1083
+ diagnostic,
1084
+ }),
1085
+ };
1086
+ });
1087
+ }
1088
+
1089
+ async markAmbiguous(inputRequestKey: string, input: {
1090
+ readonly ambiguityId?: AmbiguityId;
1091
+ readonly reason: string;
1092
+ readonly relatedIds: readonly string[];
1093
+ }): Promise<RunProjectionV2> {
1094
+ const requestKey = validateRequestKey(inputRequestKey);
1095
+ const reason = requireNonEmpty(input.reason, "ambiguity reason");
1096
+ const relatedIds = input.relatedIds.map((id) => requireNonEmpty(id, "related ID"));
1097
+ const ambiguityId = input.ambiguityId ?? (this.#id("ambiguity") as AmbiguityId);
1098
+ return this.#withRunMutation(requestKey, async (current, events) => {
1099
+ const existing = current.unresolvedAmbiguities.find(
1100
+ (ambiguity) => ambiguity.ambiguityId === ambiguityId,
1101
+ );
1102
+ if (existing) {
1103
+ if (existing.reason !== reason || canonicalJson(existing.relatedIds) !== canonicalJson(relatedIds)) {
1104
+ throw new RunConflictError(`Ambiguity ${ambiguityId} already has different evidence`);
1105
+ }
1106
+ return { kind: "existing" as const, projection: current };
1107
+ }
1108
+ return {
1109
+ kind: "append" as const,
1110
+ events,
1111
+ event: this.#nextEvent(current, "ambiguity-recorded", {
1112
+ ambiguityId,
1113
+ reason,
1114
+ relatedIds,
1115
+ status: "needs-reconcile",
1116
+ }),
1117
+ };
1118
+ });
1119
+ }
1120
+
1121
+ async recordReconciliation(inputRequestKey: string, input: {
1122
+ readonly ambiguityId: AmbiguityId;
1123
+ readonly decision: string;
1124
+ readonly evidenceRefs: readonly string[];
1125
+ readonly workerState?: Exclude<WorkerAttemptProjection["state"], "ambiguous">;
1126
+ }): Promise<RunProjectionV2> {
1127
+ const requestKey = validateRequestKey(inputRequestKey);
1128
+ const ambiguityId = requireNonEmpty(input.ambiguityId, "ambiguity ID") as AmbiguityId;
1129
+ const decision = requireNonEmpty(input.decision, "reconciliation decision");
1130
+ const evidenceRefs = input.evidenceRefs.map((ref) => requireNonEmpty(ref, "evidence reference"));
1131
+ return this.#withRunMutation(requestKey, async (current, events) => {
1132
+ const ambiguity = current.unresolvedAmbiguities.find(
1133
+ (ambiguity) => ambiguity.ambiguityId === ambiguityId,
1134
+ );
1135
+ if (!ambiguity) {
1136
+ const existing = events.find((event) => event.type === "reconciliation-recorded"
1137
+ && event.payload["ambiguityId"] === ambiguityId);
1138
+ if (existing
1139
+ && existing.payload["decision"] === decision
1140
+ && canonicalJson(existing.payload["evidenceRefs"]) === canonicalJson(evidenceRefs)
1141
+ && (existing.payload["workerState"] ?? null) === (input.workerState ?? null)) {
1142
+ return { kind: "existing" as const, projection: current };
1143
+ }
1144
+ throw new Error(`Ambiguity ${ambiguityId} is not awaiting reconciliation`);
1145
+ }
1146
+ const relatedWorker = Object.values(current.workers).find(
1147
+ (worker) => ambiguity.relatedIds.includes(worker.workerId),
1148
+ );
1149
+ const relatedAttempt = relatedWorker?.attempts.find(
1150
+ (attempt) => ambiguity.relatedIds.includes(attempt.attemptId),
1151
+ );
1152
+ if (relatedWorker && relatedAttempt?.state === "ambiguous") {
1153
+ if (input.workerState === undefined) {
1154
+ throw new TypeError("Worker ambiguity reconciliation requires an explicit workerState");
1155
+ }
1156
+ if (input.workerState === "blocked") {
1157
+ const activeWaits = relatedWorker.waits.filter((wait) =>
1158
+ wait.status === "waiting" && wait.attemptId === relatedAttempt.attemptId);
1159
+ if (activeWaits.length !== 1) {
1160
+ throw new TypeError("Reconciliation to blocked requires the attempt's existing typed wait");
1161
+ }
1162
+ }
1163
+ const contract = effectiveWorkerContract(relatedWorker);
1164
+ const result = evaluateWorkerAttemptTransition(
1165
+ "ambiguous",
1166
+ input.workerState,
1167
+ {
1168
+ evidence: { exists: relatedWorker.gate.evidenceExists },
1169
+ report: {
1170
+ schemaDeclared: contract.report_schema !== undefined,
1171
+ matches: relatedWorker.gate.reportMatches === true,
1172
+ },
1173
+ acceptanceCheck: {
1174
+ declared: contract.acceptance_check !== undefined,
1175
+ exitCode: relatedWorker.gate.acceptanceCheckExitCode,
1176
+ },
1177
+ human: {
1178
+ approvalRequired: contract.requires_human_approval === true,
1179
+ approved: relatedWorker.gate.humanApproved === true,
1180
+ },
1181
+ budget: { exceeded: workerBudgetExceeded(relatedAttempt, contract.budget) },
1182
+ runtime: { supervisorLost: false, effectsUncertain: false, certain: true },
1183
+ failure: { observed: input.workerState === "failed" },
1184
+ termination: { observed: input.workerState === "cancelled" },
1185
+ wait: { declared: input.workerState === "blocked" },
1186
+ reconcile: { decisionExists: true, uncertaintyCleared: true },
1187
+ input: { kind: null, correlationMatches: false },
1188
+ cancel: { verbReceived: relatedAttempt.cancellation.requestedAt !== null },
1189
+ },
1190
+ );
1191
+ if (!result.allowed) {
1192
+ throw new Error(`Reconciled worker transition is blocked: ${result.blockers.join(", ")}`);
1193
+ }
1194
+ } else if (input.workerState !== undefined) {
1195
+ throw new TypeError("workerState is valid only for a related ambiguous worker attempt");
1196
+ }
1197
+ return {
1198
+ kind: "append" as const,
1199
+ events,
1200
+ event: this.#nextEvent(current, "reconciliation-recorded", {
1201
+ ambiguityId,
1202
+ decision,
1203
+ evidenceRefs,
1204
+ workerId: relatedWorker?.workerId ?? null,
1205
+ attemptId: relatedAttempt?.attemptId ?? null,
1206
+ workerState: input.workerState ?? null,
1207
+ }),
1208
+ };
1209
+ });
1210
+ }
1211
+
1212
+ async #withRunMutation(
1213
+ requestKey: string,
1214
+ decide: (
1215
+ current: RunProjectionV2,
1216
+ events: readonly WorkflowLedgerEventV1[],
1217
+ ) => Promise<
1218
+ | { readonly kind: "existing"; readonly projection: RunProjectionV2 }
1219
+ | {
1220
+ readonly kind: "append";
1221
+ readonly event: WorkflowLedgerEventV1;
1222
+ readonly events: readonly WorkflowLedgerEventV1[];
1223
+ }
1224
+ >,
1225
+ ): Promise<RunProjectionV2> {
1226
+ return this.#leaseManager.withCurrentSupervisor(this.#supervisorGeneration, async () => {
1227
+ const directory = runDirectoryForRequest(this.#namespaceDirectory, requestKey);
1228
+ const events = await readEventChain(directory);
1229
+ const current = foldEvents(events);
1230
+ const decision = await decide(current, events);
1231
+ if (decision.kind === "existing") return decision.projection;
1232
+ await writeWorkflowJson(
1233
+ eventPath(directory, decision.event),
1234
+ decision.event,
1235
+ {
1236
+ createOnly: true,
1237
+ ...(this.#atomicFault ? { fault: this.#atomicFault } : {}),
1238
+ },
1239
+ );
1240
+ const projection = foldEvents([...decision.events, decision.event]);
1241
+ await writeWorkflowJson(join(directory, "snapshot.json"), projection, {
1242
+ ...(this.#atomicFault ? { fault: this.#atomicFault } : {}),
1243
+ });
1244
+ this.#notify(projection);
1245
+ return projection;
1246
+ });
1247
+ }
1248
+
1249
+ #notify(projection: RunProjectionV2): void {
1250
+ for (const listener of this.#listeners) {
1251
+ try {
1252
+ listener(projection);
1253
+ } catch {
1254
+ // Projection consumers are advisory and cannot roll back a committed ledger event.
1255
+ }
1256
+ }
1257
+ }
1258
+
1259
+ #nextEvent(
1260
+ current: RunProjectionV2,
1261
+ type: Exclude<WorkflowEventType, "run-opened">,
1262
+ payload: Record<string, unknown>,
1263
+ ): WorkflowLedgerEventV1 {
1264
+ return createEvent({
1265
+ eventId: this.#id("event"),
1266
+ seq: current.ledger.throughSeq + 1,
1267
+ runId: current.runId,
1268
+ type,
1269
+ supervisorGeneration: this.#supervisorGeneration,
1270
+ occurredAt: this.#now().toISOString(),
1271
+ previousHash: current.ledger.chainHash,
1272
+ payload,
1273
+ });
1274
+ }
1275
+
1276
+ async #installInitialRun(
1277
+ requestKey: string,
1278
+ event: WorkflowLedgerEventV1,
1279
+ projection: RunProjectionV2,
1280
+ ): Promise<boolean> {
1281
+ const parent = this.#byRequestDirectory();
1282
+ const destination = runDirectoryForRequest(this.#namespaceDirectory, requestKey);
1283
+ const prepared = await mkdtemp(join(parent, ".run.prepare-"));
1284
+ await chmod(prepared, 0o700);
1285
+ let installed = false;
1286
+ try {
1287
+ const events = join(prepared, "events");
1288
+ await mkdir(events, { mode: 0o700 });
1289
+ await writeWorkflowJson(eventPath(prepared, event), event, {
1290
+ createOnly: true,
1291
+ ...(this.#atomicFault ? { fault: this.#atomicFault } : {}),
1292
+ });
1293
+ await syncWorkflowDirectory(events);
1294
+ await writeWorkflowJson(join(prepared, "snapshot.json"), projection, {
1295
+ createOnly: true,
1296
+ ...(this.#atomicFault ? { fault: this.#atomicFault } : {}),
1297
+ });
1298
+ await syncWorkflowDirectory(prepared);
1299
+ try {
1300
+ await rename(prepared, destination);
1301
+ } catch (error) {
1302
+ if (isNodeError(error) && (error.code === "EEXIST" || error.code === "ENOTEMPTY")) {
1303
+ return false;
1304
+ }
1305
+ throw error;
1306
+ }
1307
+ installed = true;
1308
+ await syncWorkflowDirectory(parent);
1309
+ return true;
1310
+ } finally {
1311
+ if (!installed) await rm(prepared, { recursive: true, force: true });
1312
+ }
1313
+ }
1314
+
1315
+ async #initialize(): Promise<void> {
1316
+ await mkdir(this.#namespaceDirectory, { recursive: true, mode: 0o700 });
1317
+ await mkdir(join(this.#namespaceDirectory, "runs"), { recursive: true, mode: 0o700 });
1318
+ await mkdir(this.#byRequestDirectory(), { recursive: true, mode: 0o700 });
1319
+ }
1320
+
1321
+ #byRequestDirectory(): string {
1322
+ return join(this.#namespaceDirectory, "runs", "by-request");
1323
+ }
1324
+
1325
+ #id(prefix: "run" | "event" | "worker" | "step" | "attempt" | "ambiguity" | "revision" | "peer"): string {
1326
+ return `${prefix}_${requireNonEmpty(this.#randomUUID(), `${prefix} UUID`)}`;
1327
+ }
1328
+ }
1329
+
1330
+ export function runDirectoryForRequest(namespaceDirectory: string, requestKey: string): string {
1331
+ if (!isAbsolute(namespaceDirectory)) throw new TypeError("Workflow namespace must be absolute");
1332
+ const key = validateRequestKey(requestKey);
1333
+ return join(
1334
+ normalize(namespaceDirectory),
1335
+ "runs",
1336
+ "by-request",
1337
+ sha256(key).slice("sha256:".length),
1338
+ );
1339
+ }
1340
+
1341
+ async function readEventChain(runDirectory: string): Promise<readonly WorkflowLedgerEventV1[]> {
1342
+ let files: string[];
1343
+ try {
1344
+ files = (await readdir(join(runDirectory, "events")))
1345
+ .filter((entry) => entry.endsWith(".json") && !entry.startsWith("."))
1346
+ .sort();
1347
+ } catch (error) {
1348
+ if (isNodeError(error) && error.code === "ENOENT") {
1349
+ throw new LedgerIntegrityError("Run ledger events directory is missing");
1350
+ }
1351
+ throw error;
1352
+ }
1353
+ if (files.length === 0) throw new LedgerIntegrityError("Run ledger is empty");
1354
+ const events: WorkflowLedgerEventV1[] = [];
1355
+ let expectedPrevious: string | null = null;
1356
+ for (let index = 0; index < files.length; index += 1) {
1357
+ const file = files[index]!;
1358
+ const raw = await readWorkflowJson(join(runDirectory, "events", file));
1359
+ if (raw === undefined) throw new LedgerIntegrityError(`Run event ${file} disappeared during recovery`);
1360
+ const event = parseEvent(raw);
1361
+ if (event.seq !== index) {
1362
+ throw new LedgerIntegrityError(`Run ledger gap: expected seq ${index}, got ${event.seq}`);
1363
+ }
1364
+ if (!file.startsWith(`${formatSequence(event.seq)}.${safePathPart(event.eventId)}.`)) {
1365
+ throw new LedgerIntegrityError(`Run event filename does not match seq/event ID: ${file}`);
1366
+ }
1367
+ if (event.previousHash !== expectedPrevious) {
1368
+ throw new LedgerIntegrityError(`Run ledger chain diverged at seq ${event.seq}`);
1369
+ }
1370
+ expectedPrevious = event.checksum;
1371
+ events.push(event);
1372
+ }
1373
+ return events;
1374
+ }
1375
+
1376
+ function foldEvents(events: readonly WorkflowLedgerEventV1[]): RunProjectionV2 {
1377
+ let projection: RunProjectionV2 | undefined;
1378
+ for (const event of events) {
1379
+ if (!projection) {
1380
+ if (event.seq !== 0 || event.type !== "run-opened" || event.previousHash !== null) {
1381
+ throw new LedgerIntegrityError("Run ledger must begin with run-opened at seq 0");
1382
+ }
1383
+ const payload = parseRunOpenedPayload(event.payload);
1384
+ projection = {
1385
+ schema: 2,
1386
+ runId: event.runId,
1387
+ requestKey: payload.requestKey,
1388
+ requestFingerprint: payload.requestFingerprint,
1389
+ source: payload.source,
1390
+ objective: payload.objective,
1391
+ budget: initialBudgetProjection(payload.objective.budget),
1392
+ state: "open",
1393
+ workers: {},
1394
+ unresolvedAmbiguities: [],
1395
+ ledger: { throughSeq: 0, chainHash: event.checksum },
1396
+ createdAt: event.occurredAt,
1397
+ updatedAt: event.occurredAt,
1398
+ };
1399
+ continue;
1400
+ }
1401
+ if (event.runId !== projection.runId) {
1402
+ throw new LedgerIntegrityError(`Run ID changed at seq ${event.seq}`);
1403
+ }
1404
+ if (event.type === "run-opened") throw new LedgerIntegrityError("run-opened may occur only once");
1405
+ switch (event.type) {
1406
+ case "worker-spawn-requested": {
1407
+ const payload = parseWorkerSpawnPayload(event.payload);
1408
+ if (projection.workers[payload.workerId]) {
1409
+ throw new LedgerIntegrityError(`Duplicate worker ID ${payload.workerId}`);
1410
+ }
1411
+ const worker: WorkerProjection = {
1412
+ workerId: payload.workerId,
1413
+ spawnRequestKey: payload.contract.requestKey,
1414
+ atomicCallStepId: payload.stepId,
1415
+ contractFingerprint: payload.contractFingerprint,
1416
+ contract: payload.contract,
1417
+ continuation: payload.continuation,
1418
+ parentWorkerId: payload.parentWorkerId,
1419
+ depth: payload.depth,
1420
+ state: "pending",
1421
+ lastLedgerAt: event.occurredAt,
1422
+ attempts: [payload.attempt],
1423
+ contractRevisions: [],
1424
+ peerMessages: [],
1425
+ waits: [],
1426
+ stall: {
1427
+ silent: { detected: false, expectedCadenceMs: null, recordedAt: null },
1428
+ noProgress: {
1429
+ detected: false,
1430
+ fingerprint: null,
1431
+ identicalRounds: 0,
1432
+ recordedAt: null,
1433
+ },
1434
+ },
1435
+ gate: {
1436
+ evidenceExists: false,
1437
+ reportMatches: null,
1438
+ acceptanceCheckExitCode: null,
1439
+ humanApproved: null,
1440
+ blockers: [],
1441
+ },
1442
+ acceptedOutcome: null,
1443
+ };
1444
+ projection = reserveProjectionBudget(
1445
+ { ...projection, workers: { ...projection.workers, [worker.workerId]: worker } },
1446
+ worker.contract.budget,
1447
+ true,
1448
+ );
1449
+ break;
1450
+ }
1451
+ case "attempt-spawn-intent": {
1452
+ const payload = parseAttemptSpawnIntentPayload(event.payload);
1453
+ const worker = requiredWorker(projection, payload.workerId);
1454
+ if (worker.attempts.some((attempt) => attempt.attemptId === payload.attempt.attemptId)) {
1455
+ throw new LedgerIntegrityError(`Duplicate attempt ID ${payload.attempt.attemptId}`);
1456
+ }
1457
+ projection = reserveProjectionBudget(updateWorker(projection, payload.workerId, (current) => ({
1458
+ ...current,
1459
+ state: "pending",
1460
+ attempts: [...current.attempts, payload.attempt],
1461
+ })), worker.contract.budget, false);
1462
+ break;
1463
+ }
1464
+ case "attempt-start-observed": {
1465
+ const payload = parseAttemptStartPayload(event.payload);
1466
+ projection = updateAttempt(projection, payload.workerId, payload.attemptId, (attempt) => {
1467
+ assertFoldGeneration(attempt, payload.workerAttemptGeneration, event.supervisorGeneration);
1468
+ return {
1469
+ ...attempt,
1470
+ state: gatedWorkerState(attempt.state, "running", workerGateFacts()),
1471
+ launchContract: payload.launchContract,
1472
+ session: { ...attempt.session, leaseGeneration: payload.leaseGeneration },
1473
+ pidStartEvidence: {
1474
+ pid: payload.processIdentity.pid,
1475
+ processStartIdentity: payload.processIdentity.processStartIdentity,
1476
+ bootIdentity: payload.processIdentity.bootIdentity,
1477
+ executable: payload.processIdentity.executable,
1478
+ hostname: payload.processIdentity.hostname,
1479
+ spawnedAt: event.occurredAt,
1480
+ observedExitAt: null,
1481
+ exitStatus: null,
1482
+ },
1483
+ };
1484
+ }, "running");
1485
+ break;
1486
+ }
1487
+ case "worker-ready-observed": {
1488
+ const payload = parseWorkerReadyPayload(event.payload);
1489
+ projection = updateAttempt(projection, payload.workerId, payload.attemptId, (attempt) => {
1490
+ assertFoldGeneration(attempt, payload.workerAttemptGeneration, event.supervisorGeneration);
1491
+ return { ...attempt, session: { ...attempt.session, file: payload.sessionFile } };
1492
+ });
1493
+ break;
1494
+ }
1495
+ case "attempt-usage-observed": {
1496
+ const payload = parseAttemptUsagePayload(event.payload);
1497
+ const before = requiredWorkerAttempt(projection, payload.workerId, payload.attemptId).attempt.usage;
1498
+ projection = updateAttempt(projection, payload.workerId, payload.attemptId, (attempt) => {
1499
+ assertFoldGeneration(attempt, payload.workerAttemptGeneration, event.supervisorGeneration);
1500
+ if (payload.usage.tokens < attempt.usage.tokens
1501
+ || payload.usage.costUsd < attempt.usage.costUsd
1502
+ || payload.usage.outputBytes < attempt.usage.outputBytes) {
1503
+ throw new LedgerIntegrityError("Attempt usage regressed in the ledger");
1504
+ }
1505
+ return { ...attempt, usage: payload.usage };
1506
+ });
1507
+ projection = spendProjectionBudget(projection, {
1508
+ tokens: payload.usage.tokens - before.tokens,
1509
+ costUsd: payload.usage.costUsd - before.costUsd,
1510
+ outputBytes: payload.usage.outputBytes - before.outputBytes,
1511
+ });
1512
+ break;
1513
+ }
1514
+ case "cancel-requested": {
1515
+ const payload = parseCancelRequestedPayload(event.payload);
1516
+ projection = updateAttempt(projection, payload.workerId, payload.attemptId, (attempt) => {
1517
+ assertFoldGeneration(attempt, payload.workerAttemptGeneration, event.supervisorGeneration);
1518
+ return {
1519
+ ...attempt,
1520
+ cancellation: {
1521
+ ...attempt.cancellation,
1522
+ reason: payload.reason,
1523
+ requestedAt: event.occurredAt,
1524
+ },
1525
+ };
1526
+ });
1527
+ break;
1528
+ }
1529
+ case "cancel-signal-sent": {
1530
+ const payload = parseCancelSignalPayload(event.payload);
1531
+ projection = updateAttempt(projection, payload.workerId, payload.attemptId, (attempt) => {
1532
+ assertFoldGeneration(attempt, payload.workerAttemptGeneration, event.supervisorGeneration);
1533
+ return {
1534
+ ...attempt,
1535
+ cancellation: { ...attempt.cancellation, sentAt: event.occurredAt },
1536
+ };
1537
+ });
1538
+ break;
1539
+ }
1540
+ case "attempt-exit-observed": {
1541
+ const payload = parseAttemptExitPayload(event.payload);
1542
+ const currentAttempt = requiredWorkerAttempt(
1543
+ projection,
1544
+ payload.workerId,
1545
+ payload.attemptId,
1546
+ ).attempt;
1547
+ assertFoldGeneration(
1548
+ currentAttempt,
1549
+ payload.workerAttemptGeneration,
1550
+ event.supervisorGeneration,
1551
+ );
1552
+ const wasCancelled = currentAttempt.cancellation.requestedAt !== null;
1553
+ const successful = payload.exitStatus.code === 0 && payload.exitStatus.signal === null;
1554
+ const workerState = classifyObservedTermination({
1555
+ runtimeCertain: !payload.uncertainSideEffects,
1556
+ effectsUncertain: payload.uncertainSideEffects,
1557
+ cancellationRequested: wasCancelled,
1558
+ terminationObserved: true,
1559
+ failureObserved: !successful && !wasCancelled,
1560
+ });
1561
+ const transitionFacts = workerGateFacts({
1562
+ runtimeCertain: !payload.uncertainSideEffects,
1563
+ effectsUncertain: payload.uncertainSideEffects,
1564
+ cancellationRequested: wasCancelled,
1565
+ terminationObserved: true,
1566
+ failureObserved: !successful && !wasCancelled,
1567
+ });
1568
+ projection = updateAttempt(projection, payload.workerId, payload.attemptId, (attempt) => ({
1569
+ ...attempt,
1570
+ state: gatedWorkerState(attempt.state, workerState, transitionFacts),
1571
+ pidStartEvidence: {
1572
+ ...attempt.pidStartEvidence,
1573
+ observedExitAt: event.occurredAt,
1574
+ exitStatus: payload.exitStatus,
1575
+ },
1576
+ candidateOutcome: payload.candidateOutcome,
1577
+ sideEffects: {
1578
+ ...attempt.sideEffects,
1579
+ status: payload.uncertainSideEffects
1580
+ ? "uncertain"
1581
+ : attempt.sideEffects.policy === "none" ? "none" : "known",
1582
+ },
1583
+ cancellation: {
1584
+ ...attempt.cancellation,
1585
+ observedAt: wasCancelled ? event.occurredAt : attempt.cancellation.observedAt,
1586
+ },
1587
+ }), workerState);
1588
+ if (payload.ambiguityId !== null) {
1589
+ projection = appendAmbiguity(projection, {
1590
+ ambiguityId: payload.ambiguityId,
1591
+ status: "needs-reconcile",
1592
+ reason: payload.reason,
1593
+ relatedIds: [payload.workerId, payload.attemptId],
1594
+ recordedAt: event.occurredAt,
1595
+ });
1596
+ }
1597
+ break;
1598
+ }
1599
+ case "worker-needs-attention": {
1600
+ const payload = parseWorkerNeedsAttentionPayload(event.payload);
1601
+ projection = updateAttempt(projection, payload.workerId, payload.attemptId, (attempt) => {
1602
+ assertFoldGeneration(attempt, payload.workerAttemptGeneration, event.supervisorGeneration);
1603
+ return payload.ambiguous ? {
1604
+ ...attempt,
1605
+ state: gatedWorkerState(
1606
+ attempt.state,
1607
+ "ambiguous",
1608
+ workerGateFacts({ effectsUncertain: true, runtimeCertain: false }),
1609
+ ),
1610
+ sideEffects: { ...attempt.sideEffects, status: "uncertain" },
1611
+ } : {
1612
+ ...attempt,
1613
+ control: {
1614
+ status: "detached",
1615
+ reason: payload.reason,
1616
+ recordedAt: event.occurredAt,
1617
+ },
1618
+ };
1619
+ }, payload.ambiguous ? "ambiguous" : undefined);
1620
+ if (!payload.ambiguous && projection.state !== "ambiguous") {
1621
+ projection = withRunState(
1622
+ projection,
1623
+ "needs-attention",
1624
+ runGateFacts({ workersDetached: true }),
1625
+ );
1626
+ }
1627
+ if (payload.ambiguityId !== null) {
1628
+ projection = appendAmbiguity(projection, {
1629
+ ambiguityId: payload.ambiguityId,
1630
+ status: "needs-reconcile",
1631
+ reason: payload.reason,
1632
+ relatedIds: [payload.workerId, payload.attemptId],
1633
+ recordedAt: event.occurredAt,
1634
+ });
1635
+ }
1636
+ break;
1637
+ }
1638
+ case "contract-revision-requested": {
1639
+ const payload = parseContractRevisionRequestedPayload(event.payload);
1640
+ if (payload.contract.runId !== projection.runId) {
1641
+ throw new LedgerIntegrityError("Contract revision run ID mismatch");
1642
+ }
1643
+ projection = updateWorker(projection, payload.workerId, (worker) => {
1644
+ if (worker.state === "blocked") {
1645
+ requiredMatchingWait(worker, payload.correlation, true);
1646
+ }
1647
+ if (worker.contractRevisions.some(
1648
+ (revision) => revision.revisionId === payload.revisionId,
1649
+ )) {
1650
+ throw new LedgerIntegrityError(`Duplicate contract revision ${payload.revisionId}`);
1651
+ }
1652
+ if (payload.revision !== worker.contractRevisions.length + 1) {
1653
+ throw new LedgerIntegrityError("Contract revision sequence is not contiguous");
1654
+ }
1655
+ return {
1656
+ ...worker,
1657
+ contractRevisions: [
1658
+ ...worker.contractRevisions,
1659
+ {
1660
+ revisionId: payload.revisionId,
1661
+ revision: payload.revision,
1662
+ contract: payload.contract,
1663
+ status: "pending",
1664
+ correlation: payload.correlation,
1665
+ requestedAt: event.occurredAt,
1666
+ deliveredAt: null,
1667
+ },
1668
+ ],
1669
+ };
1670
+ });
1671
+ break;
1672
+ }
1673
+ case "contract-revision-delivered": {
1674
+ const payload = parseContractRevisionDeliveredPayload(event.payload);
1675
+ projection = updateWorker(projection, payload.workerId, (worker) => {
1676
+ const revision = worker.contractRevisions.find(
1677
+ (candidate) => candidate.revisionId === payload.revisionId,
1678
+ );
1679
+ if (!revision) throw new LedgerIntegrityError(`Unknown contract revision ${payload.revisionId}`);
1680
+ if (revision.correlation !== payload.correlation) {
1681
+ throw new LedgerIntegrityError("Contract revision delivery correlation mismatch");
1682
+ }
1683
+ const woken = wakeWorkerFromWait(
1684
+ worker,
1685
+ "revise_contract",
1686
+ payload.correlation,
1687
+ event.occurredAt,
1688
+ );
1689
+ return {
1690
+ ...woken,
1691
+ contractRevisions: worker.contractRevisions.map((revision) =>
1692
+ revision.revisionId === payload.revisionId
1693
+ ? { ...revision, status: "delivered" as const, deliveredAt: event.occurredAt }
1694
+ : revision),
1695
+ };
1696
+ });
1697
+ break;
1698
+ }
1699
+ case "worker-wait-declared": {
1700
+ const payload = parseWorkerWaitDeclaredPayload(event.payload);
1701
+ projection = updateWorker(projection, payload.workerId, (worker) => {
1702
+ const attempt = requiredCurrentAttempt(worker);
1703
+ assertFoldGeneration(
1704
+ attempt,
1705
+ payload.workerAttemptGeneration,
1706
+ payload.supervisorGeneration,
1707
+ );
1708
+ if (payload.supervisorGeneration !== event.supervisorGeneration) {
1709
+ throw new LedgerIntegrityError("Typed-wait supervisor generation mismatch");
1710
+ }
1711
+ if (attempt.attemptId !== payload.attemptId) {
1712
+ throw new LedgerIntegrityError("Typed-wait attempt is not current");
1713
+ }
1714
+ if (effectiveWorkerContract(worker).channel !== "rpc") {
1715
+ throw new LedgerIntegrityError("Typed wait requires an RPC contract");
1716
+ }
1717
+ if (worker.waits.some((wait) => wait.status === "waiting")) {
1718
+ throw new LedgerIntegrityError("Worker already has an active typed wait");
1719
+ }
1720
+ return {
1721
+ ...worker,
1722
+ state: gatedWorkerState(
1723
+ worker.state,
1724
+ "blocked",
1725
+ workerGateFacts({ waitDeclared: true }),
1726
+ ),
1727
+ attempts: worker.attempts.map((candidate) => candidate.attemptId === attempt.attemptId
1728
+ ? {
1729
+ ...candidate,
1730
+ state: gatedWorkerState(
1731
+ candidate.state,
1732
+ "blocked",
1733
+ workerGateFacts({ waitDeclared: true }),
1734
+ ),
1735
+ }
1736
+ : candidate),
1737
+ waits: [...worker.waits, {
1738
+ attemptId: attempt.attemptId,
1739
+ workerAttemptGeneration: attempt.workerAttemptGeneration,
1740
+ reason: payload.reason,
1741
+ correlation: payload.correlation,
1742
+ status: "waiting" as const,
1743
+ declaredAt: event.occurredAt,
1744
+ satisfiedAt: null,
1745
+ satisfiedBy: null,
1746
+ }],
1747
+ };
1748
+ });
1749
+ break;
1750
+ }
1751
+ case "collect-observed": {
1752
+ const payload = parseCollectObservedPayload(event.payload);
1753
+ projection = updateWorker(projection, payload.workerId, (worker) => {
1754
+ const previous = worker.stall.noProgress;
1755
+ const identicalRounds = previous.fingerprint === payload.payloadFingerprint
1756
+ ? previous.identicalRounds + 1
1757
+ : 1;
1758
+ const detected = identicalRounds >= payload.consecutiveRounds;
1759
+ return {
1760
+ ...worker,
1761
+ stall: {
1762
+ ...worker.stall,
1763
+ noProgress: {
1764
+ detected,
1765
+ fingerprint: payload.payloadFingerprint,
1766
+ identicalRounds,
1767
+ recordedAt: detected ? event.occurredAt : null,
1768
+ },
1769
+ },
1770
+ };
1771
+ });
1772
+ break;
1773
+ }
1774
+ case "silent-stall-recorded": {
1775
+ const payload = parseSilentStallPayload(event.payload);
1776
+ projection = updateWorker(projection, payload.workerId, (worker) => ({
1777
+ ...worker,
1778
+ stall: {
1779
+ ...worker.stall,
1780
+ silent: {
1781
+ detected: true,
1782
+ expectedCadenceMs: payload.expectedCadenceMs,
1783
+ recordedAt: event.occurredAt,
1784
+ },
1785
+ },
1786
+ }));
1787
+ break;
1788
+ }
1789
+ case "gate-evaluated": {
1790
+ const payload = parseGateEvaluatedPayload(event.payload);
1791
+ projection = updateAttempt(
1792
+ projection,
1793
+ payload.workerId,
1794
+ payload.attemptId,
1795
+ (attempt) => {
1796
+ if (attempt.state !== payload.fromState) {
1797
+ throw new LedgerIntegrityError("Gate source state does not match projection");
1798
+ }
1799
+ return payload.allowed ? {
1800
+ ...attempt,
1801
+ state: gatedWorkerState(
1802
+ attempt.state,
1803
+ "done",
1804
+ workerGateFacts({
1805
+ evidenceExists: payload.evidenceExists,
1806
+ reportSchemaDeclared: payload.reportMatches !== null,
1807
+ reportMatches: payload.reportMatches === true,
1808
+ acceptanceDeclared: payload.acceptanceCheckExitCode !== null,
1809
+ acceptanceExitCode: payload.acceptanceCheckExitCode,
1810
+ humanRequired: payload.humanApproved !== null,
1811
+ humanApproved: payload.humanApproved === true,
1812
+ budgetExceeded: false,
1813
+ }),
1814
+ ),
1815
+ } : attempt;
1816
+ },
1817
+ payload.allowed ? "done" : undefined,
1818
+ );
1819
+ projection = updateWorker(projection, payload.workerId, (worker) => ({
1820
+ ...worker,
1821
+ gate: {
1822
+ evidenceExists: payload.evidenceExists,
1823
+ reportMatches: payload.reportMatches,
1824
+ acceptanceCheckExitCode: payload.acceptanceCheckExitCode,
1825
+ humanApproved: payload.humanApproved,
1826
+ blockers: payload.blockers,
1827
+ },
1828
+ }));
1829
+ break;
1830
+ }
1831
+ case "steer-delivered": {
1832
+ const payload = parseSteerDeliveredPayload(event.payload);
1833
+ projection = updateWorker(projection, payload.workerId, (worker) =>
1834
+ wakeWorkerFromWait(worker, "steer", payload.correlation, event.occurredAt));
1835
+ break;
1836
+ }
1837
+ case "peer-message-requested": {
1838
+ const payload = parsePeerMessageRequestedPayload(event.payload);
1839
+ const sender = requiredWorker(projection, payload.senderWorkerId);
1840
+ const recipient = requiredWorker(projection, payload.recipientWorkerId);
1841
+ if (sender.workerId === recipient.workerId) {
1842
+ throw new LedgerIntegrityError("Peer message cannot target its sender");
1843
+ }
1844
+ if (!effectiveWorkerContract(sender).capabilities.includes(payload.grant)) {
1845
+ throw new LedgerIntegrityError(`Peer-message sender lacks grant ${payload.grant}`);
1846
+ }
1847
+ if (effectiveWorkerContract(sender).channel !== "rpc"
1848
+ || effectiveWorkerContract(recipient).channel !== "rpc") {
1849
+ throw new LedgerIntegrityError("Peer-message endpoints must both use RPC contracts");
1850
+ }
1851
+ if (recipient.state !== "running" && recipient.state !== "blocked") {
1852
+ throw new LedgerIntegrityError("Peer-message recipient is not running or blocked");
1853
+ }
1854
+ if (recipient.state === "blocked") {
1855
+ requiredMatchingWait(recipient, payload.correlation, true);
1856
+ }
1857
+ const grantedRecipient = resolvePeerGrantRecipient(projection, payload.grant, true);
1858
+ if (grantedRecipient.workerId !== recipient.workerId) {
1859
+ throw new LedgerIntegrityError(
1860
+ `Peer grant ${payload.grant} does not resolve to ${recipient.workerId}`,
1861
+ );
1862
+ }
1863
+ if (sender.peerMessages.some((message) => message.messageId === payload.messageId)
1864
+ || recipient.peerMessages.some((message) => message.messageId === payload.messageId)) {
1865
+ throw new LedgerIntegrityError(`Duplicate peer message ${payload.messageId}`);
1866
+ }
1867
+ const message: PeerMessageProjection = {
1868
+ messageId: payload.messageId,
1869
+ senderWorkerId: payload.senderWorkerId,
1870
+ recipientWorkerId: payload.recipientWorkerId,
1871
+ grant: payload.grant,
1872
+ correlation: payload.correlation,
1873
+ messageHash: payload.messageHash,
1874
+ status: "pending",
1875
+ requestedAt: event.occurredAt,
1876
+ deliveredAt: null,
1877
+ diagnostic: null,
1878
+ };
1879
+ projection = updateWorker(projection, sender.workerId, (worker) => ({
1880
+ ...worker,
1881
+ peerMessages: [...worker.peerMessages, message],
1882
+ }));
1883
+ projection = updateWorker(projection, recipient.workerId, (worker) => ({
1884
+ ...worker,
1885
+ lastLedgerAt: event.occurredAt,
1886
+ peerMessages: [...worker.peerMessages, message],
1887
+ }));
1888
+ break;
1889
+ }
1890
+ case "peer-message-delivery-observed": {
1891
+ const payload = parsePeerMessageDeliveryPayload(event.payload);
1892
+ const recipient = requiredWorker(projection, payload.recipientWorkerId);
1893
+ const attempt = requiredCurrentAttempt(recipient);
1894
+ if (attempt.attemptId !== payload.recipientAttemptId
1895
+ || attempt.workerAttemptGeneration !== payload.recipientWorkerAttemptGeneration) {
1896
+ throw new LedgerIntegrityError("Peer-message delivery recipient generation is stale");
1897
+ }
1898
+ const sender = requiredWorker(projection, payload.senderWorkerId);
1899
+ if (!sender.peerMessages.some((message) => message.messageId === payload.messageId)
1900
+ || !recipient.peerMessages.some((message) => message.messageId === payload.messageId)) {
1901
+ throw new LedgerIntegrityError(`Peer message ${payload.messageId} is absent from one endpoint`);
1902
+ }
1903
+ for (const workerId of [payload.senderWorkerId, payload.recipientWorkerId]) {
1904
+ projection = updateWorker(projection, workerId, (worker) => ({
1905
+ ...worker,
1906
+ ...(workerId === payload.recipientWorkerId ? { lastLedgerAt: event.occurredAt } : {}),
1907
+ peerMessages: worker.peerMessages.map((message) => message.messageId === payload.messageId
1908
+ ? {
1909
+ ...message,
1910
+ status: payload.status,
1911
+ deliveredAt: payload.status === "delivered" ? event.occurredAt : null,
1912
+ diagnostic: payload.diagnostic,
1913
+ }
1914
+ : message),
1915
+ }));
1916
+ }
1917
+ if (payload.status === "delivered") {
1918
+ projection = updateWorker(projection, payload.recipientWorkerId, (worker) =>
1919
+ wakeWorkerFromWait(worker, "peer_message", messageCorrelation(worker, payload.messageId), event.occurredAt));
1920
+ }
1921
+ break;
1922
+ }
1923
+ case "ambiguity-recorded": {
1924
+ const ambiguity = parseAmbiguityPayload(event.payload, event.occurredAt);
1925
+ if (projection.unresolvedAmbiguities.some((item) => item.ambiguityId === ambiguity.ambiguityId)) {
1926
+ throw new LedgerIntegrityError(`Duplicate ambiguity ID ${ambiguity.ambiguityId}`);
1927
+ }
1928
+ projection = {
1929
+ ...projection,
1930
+ state: gatedRunState(projection.state, "ambiguous", runGateFacts()),
1931
+ unresolvedAmbiguities: [...projection.unresolvedAmbiguities, ambiguity],
1932
+ };
1933
+ break;
1934
+ }
1935
+ case "reconciliation-recorded": {
1936
+ const payload = parseReconciliationPayload(event.payload);
1937
+ if (!projection.unresolvedAmbiguities.some(
1938
+ (item) => item.ambiguityId === payload.ambiguityId,
1939
+ )) {
1940
+ throw new LedgerIntegrityError(
1941
+ `Reconciliation references resolved or unknown ambiguity ${payload.ambiguityId}`,
1942
+ );
1943
+ }
1944
+ const unresolved = projection.unresolvedAmbiguities.filter(
1945
+ (item) => item.ambiguityId !== payload.ambiguityId,
1946
+ );
1947
+ projection = {
1948
+ ...projection,
1949
+ state: unresolved.length === 0
1950
+ ? gatedRunState(
1951
+ projection.state,
1952
+ "open",
1953
+ runGateFacts({ reconciliationRecorded: true, uncertaintyCleared: true }),
1954
+ )
1955
+ : "ambiguous",
1956
+ unresolvedAmbiguities: unresolved,
1957
+ };
1958
+ if (payload.workerId !== null && payload.attemptId !== null && payload.workerState !== null) {
1959
+ const reconciledWorker = requiredWorker(projection, payload.workerId);
1960
+ if (payload.workerState === "blocked") {
1961
+ const activeWaits = reconciledWorker.waits.filter((wait) =>
1962
+ wait.status === "waiting" && wait.attemptId === payload.attemptId);
1963
+ if (activeWaits.length !== 1) {
1964
+ throw new LedgerIntegrityError(
1965
+ "Reconciliation to blocked requires the attempt's existing typed wait",
1966
+ );
1967
+ }
1968
+ }
1969
+ projection = updateAttempt(
1970
+ projection,
1971
+ payload.workerId,
1972
+ payload.attemptId,
1973
+ (attempt) => {
1974
+ if (attempt.state !== "ambiguous") {
1975
+ throw new LedgerIntegrityError("Reconciled worker attempt is not ambiguous");
1976
+ }
1977
+ return {
1978
+ ...attempt,
1979
+ state: gatedWorkerState(
1980
+ attempt.state,
1981
+ payload.workerState!,
1982
+ workerGateFacts({
1983
+ reconciliation: true,
1984
+ uncertaintyCleared: true,
1985
+ waitDeclared: payload.workerState === "blocked",
1986
+ failureObserved: payload.workerState === "failed",
1987
+ cancellationRequested: payload.workerState === "cancelled",
1988
+ terminationObserved: payload.workerState === "cancelled",
1989
+ }),
1990
+ ),
1991
+ sideEffects: {
1992
+ ...attempt.sideEffects,
1993
+ status: attempt.sideEffects.policy === "none" ? "none" : "known",
1994
+ },
1995
+ };
1996
+ },
1997
+ payload.workerState,
1998
+ );
1999
+ }
2000
+ break;
2001
+ }
2002
+ }
2003
+ const workerId = event.payload["workerId"];
2004
+ if (typeof workerId === "string" && projection.workers[workerId]) {
2005
+ projection = updateWorker(projection, workerId as WorkerId, (worker) => ({
2006
+ ...worker,
2007
+ lastLedgerAt: event.occurredAt,
2008
+ ...(event.type === "silent-stall-recorded" ? {} : {
2009
+ stall: {
2010
+ ...worker.stall,
2011
+ silent: { detected: false, expectedCadenceMs: null, recordedAt: null },
2012
+ },
2013
+ }),
2014
+ }));
2015
+ }
2016
+ projection = {
2017
+ ...projection,
2018
+ budget: withRemainingWallClock(projection.budget, projection.createdAt, event.occurredAt),
2019
+ ledger: { throughSeq: event.seq, chainHash: event.checksum },
2020
+ updatedAt: event.occurredAt,
2021
+ };
2022
+ }
2023
+ if (!projection) throw new LedgerIntegrityError("Run ledger did not produce a projection");
2024
+ return projection;
2025
+ }
2026
+
2027
+ function createEvent(input: Omit<WorkflowLedgerEventV1, "schema" | "checksum">): WorkflowLedgerEventV1 {
2028
+ const unsigned = { schema: 1 as const, ...input };
2029
+ return { ...unsigned, checksum: fingerprint(unsigned) };
2030
+ }
2031
+
2032
+ function parseEvent(value: unknown): WorkflowLedgerEventV1 {
2033
+ const object = strictRecord(value, EVENT_KEYS, "workflow ledger event");
2034
+ if (numberValue(object, "schema") !== 1) throw new LedgerIntegrityError("Workflow event schema must be 1");
2035
+ const type = enumValue(object, "type", [
2036
+ "run-opened",
2037
+ "worker-spawn-requested",
2038
+ "attempt-spawn-intent",
2039
+ "attempt-start-observed",
2040
+ "worker-ready-observed",
2041
+ "attempt-usage-observed",
2042
+ "cancel-requested",
2043
+ "cancel-signal-sent",
2044
+ "attempt-exit-observed",
2045
+ "worker-needs-attention",
2046
+ "contract-revision-requested",
2047
+ "contract-revision-delivered",
2048
+ "collect-observed",
2049
+ "silent-stall-recorded",
2050
+ "gate-evaluated",
2051
+ "worker-wait-declared",
2052
+ "steer-delivered",
2053
+ "peer-message-requested",
2054
+ "peer-message-delivery-observed",
2055
+ "ambiguity-recorded",
2056
+ "reconciliation-recorded",
2057
+ ] as const);
2058
+ const event: WorkflowLedgerEventV1 = {
2059
+ schema: 1,
2060
+ eventId: nonEmptyString(object, "eventId"),
2061
+ seq: nonNegativeInteger(object, "seq"),
2062
+ runId: prefixedId(object, "runId", "run") as RunId,
2063
+ type,
2064
+ supervisorGeneration: nonEmptyString(object, "supervisorGeneration"),
2065
+ occurredAt: isoTimestamp(object, "occurredAt"),
2066
+ previousHash: nullableHash(object, "previousHash"),
2067
+ payload: recordValue(object, "payload"),
2068
+ checksum: hashValue(object, "checksum"),
2069
+ };
2070
+ const { checksum, ...unsigned } = event;
2071
+ if (fingerprint(unsigned) !== checksum) {
2072
+ throw new LedgerIntegrityError(`Workflow event checksum mismatch at seq ${event.seq}`);
2073
+ }
2074
+ return event;
2075
+ }
2076
+
2077
+ function parseRunOpenedPayload(value: unknown): {
2078
+ readonly requestKey: string;
2079
+ readonly requestFingerprint: string;
2080
+ readonly source: RunSource;
2081
+ readonly objective: ObjectiveContract;
2082
+ } {
2083
+ const object = strictRecord(
2084
+ value,
2085
+ ["requestKey", "requestFingerprint", "source", "objective"],
2086
+ "run-opened payload",
2087
+ );
2088
+ const payload = {
2089
+ requestKey: validateRequestKey(nonEmptyString(object, "requestKey")),
2090
+ requestFingerprint: hashValue(object, "requestFingerprint"),
2091
+ source: parseRunSource(object["source"]),
2092
+ objective: parseObjectiveContract(object["objective"]),
2093
+ };
2094
+ if (fingerprint({ source: payload.source, objective: payload.objective }) !== payload.requestFingerprint) {
2095
+ throw new LedgerIntegrityError("Run request fingerprint does not match its immutable contract");
2096
+ }
2097
+ return payload;
2098
+ }
2099
+
2100
+ function parseWorkerSpawnPayload(value: unknown): {
2101
+ readonly stepId: StepId;
2102
+ readonly workerId: WorkerId;
2103
+ readonly contractFingerprint: string;
2104
+ readonly contract: WorkerContract;
2105
+ readonly continuation: WorkerContinuationProvenance | null;
2106
+ readonly parentWorkerId: WorkerId | null;
2107
+ readonly depth: number;
2108
+ readonly attempt: WorkerAttemptProjection;
2109
+ } {
2110
+ const object = strictRecord(
2111
+ value,
2112
+ [
2113
+ "stepId", "workerId", "contractFingerprint", "contract", "continuation",
2114
+ "parentWorkerId", "depth", "attempt",
2115
+ ],
2116
+ "worker-spawn payload",
2117
+ );
2118
+ const workerId = prefixedId(object, "workerId", "worker") as WorkerId;
2119
+ const contractObject = recordValue(object, "contract");
2120
+ const request = parseWorkerRequest(contractObject, true);
2121
+ const contract: WorkerContract = {
2122
+ ...request,
2123
+ context: request.context ?? "fresh",
2124
+ runId: prefixedId(contractObject, "runId", "run") as RunId,
2125
+ workerId: prefixedId(contractObject, "workerId", "worker") as WorkerId,
2126
+ createdByDriverSessionId: nonEmptyString(contractObject, "createdByDriverSessionId"),
2127
+ };
2128
+ if (contract.workerId !== workerId) throw new LedgerIntegrityError("Worker contract ID mismatch");
2129
+ const attempt = parseWorkerAttempt(object["attempt"]);
2130
+ const continuation = object["continuation"] === undefined || object["continuation"] === null
2131
+ ? null
2132
+ : parseWorkerContinuationProvenance(object["continuation"]);
2133
+ const parentWorkerId = nullablePrefixedId(object, "parentWorkerId", "worker") as WorkerId | null;
2134
+ const depth = positiveInteger(object, "depth");
2135
+ const contractFingerprint = hashValue(object, "contractFingerprint");
2136
+ if (fingerprint({
2137
+ request,
2138
+ createdByDriverSessionId: contract.createdByDriverSessionId,
2139
+ sessionPath: attempt.session.path,
2140
+ parentWorkerId,
2141
+ depth,
2142
+ ...(continuation ? { continuation } : {}),
2143
+ }) !== contractFingerprint) {
2144
+ throw new LedgerIntegrityError("Worker contract fingerprint does not match its immutable input");
2145
+ }
2146
+ return {
2147
+ stepId: prefixedId(object, "stepId", "step") as StepId,
2148
+ workerId,
2149
+ contractFingerprint,
2150
+ contract,
2151
+ continuation,
2152
+ parentWorkerId,
2153
+ depth,
2154
+ attempt,
2155
+ };
2156
+ }
2157
+
2158
+ function parseWorkerContinuationProvenance(value: unknown): WorkerContinuationProvenance {
2159
+ const object = strictRecord(
2160
+ value,
2161
+ ["priorWorkerId", "priorAttemptId", "priorLeaseGeneration", "sessionFile"],
2162
+ "worker continuation provenance",
2163
+ );
2164
+ return {
2165
+ priorWorkerId: prefixedId(object, "priorWorkerId", "worker") as WorkerId,
2166
+ priorAttemptId: prefixedId(object, "priorAttemptId", "attempt") as AttemptId,
2167
+ priorLeaseGeneration: nonEmptyString(object, "priorLeaseGeneration"),
2168
+ sessionFile: absolutePath(object, "sessionFile"),
2169
+ };
2170
+ }
2171
+
2172
+ function parseAttemptSpawnIntentPayload(value: unknown): {
2173
+ readonly workerId: WorkerId;
2174
+ readonly attempt: WorkerAttemptProjection;
2175
+ } {
2176
+ const object = strictRecord(value, ["workerId", "attempt"], "attempt-spawn-intent payload");
2177
+ return {
2178
+ workerId: prefixedId(object, "workerId", "worker") as WorkerId,
2179
+ attempt: parseWorkerAttempt(object["attempt"]),
2180
+ };
2181
+ }
2182
+
2183
+ function parseAttemptStartPayload(value: unknown): {
2184
+ readonly workerId: WorkerId;
2185
+ readonly attemptId: AttemptId;
2186
+ readonly workerAttemptGeneration: string;
2187
+ readonly leaseGeneration: string;
2188
+ readonly processIdentity: ProcessIdentity;
2189
+ readonly launchContract: WorkerLaunchContractV1;
2190
+ } {
2191
+ const object = strictRecord(
2192
+ value,
2193
+ ["workerId", "attemptId", "workerAttemptGeneration", "leaseGeneration", "processIdentity", "launchContract"],
2194
+ "attempt-start payload",
2195
+ );
2196
+ return {
2197
+ workerId: prefixedId(object, "workerId", "worker") as WorkerId,
2198
+ attemptId: prefixedId(object, "attemptId", "attempt") as AttemptId,
2199
+ workerAttemptGeneration: nonEmptyString(object, "workerAttemptGeneration"),
2200
+ leaseGeneration: nonEmptyString(object, "leaseGeneration"),
2201
+ processIdentity: parseProcessIdentity(object["processIdentity"]),
2202
+ launchContract: parseWorkerLaunchContractV1(object["launchContract"]),
2203
+ };
2204
+ }
2205
+
2206
+ function parseWorkerReadyPayload(value: unknown): {
2207
+ readonly workerId: WorkerId;
2208
+ readonly attemptId: AttemptId;
2209
+ readonly workerAttemptGeneration: string;
2210
+ readonly sessionFile: string;
2211
+ } {
2212
+ const object = strictRecord(
2213
+ value,
2214
+ ["workerId", "attemptId", "workerAttemptGeneration", "sessionFile"],
2215
+ "worker-ready payload",
2216
+ );
2217
+ return {
2218
+ workerId: prefixedId(object, "workerId", "worker") as WorkerId,
2219
+ attemptId: prefixedId(object, "attemptId", "attempt") as AttemptId,
2220
+ workerAttemptGeneration: nonEmptyString(object, "workerAttemptGeneration"),
2221
+ sessionFile: absolutePath(object, "sessionFile"),
2222
+ };
2223
+ }
2224
+
2225
+ function parseAttemptUsagePayload(value: unknown): {
2226
+ readonly workerId: WorkerId;
2227
+ readonly attemptId: AttemptId;
2228
+ readonly workerAttemptGeneration: string;
2229
+ readonly usage: WorkerAttemptProjection["usage"];
2230
+ } {
2231
+ const object = strictRecord(
2232
+ value,
2233
+ ["workerId", "attemptId", "workerAttemptGeneration", "usage"],
2234
+ "attempt-usage payload",
2235
+ );
2236
+ return {
2237
+ workerId: prefixedId(object, "workerId", "worker") as WorkerId,
2238
+ attemptId: prefixedId(object, "attemptId", "attempt") as AttemptId,
2239
+ workerAttemptGeneration: nonEmptyString(object, "workerAttemptGeneration"),
2240
+ usage: parseUsage(object["usage"]),
2241
+ };
2242
+ }
2243
+
2244
+ function parseCancelRequestedPayload(value: unknown): {
2245
+ readonly workerId: WorkerId;
2246
+ readonly attemptId: AttemptId;
2247
+ readonly workerAttemptGeneration: string;
2248
+ readonly reason: string;
2249
+ } {
2250
+ const object = strictRecord(
2251
+ value,
2252
+ ["workerId", "attemptId", "workerAttemptGeneration", "reason"],
2253
+ "cancel-requested payload",
2254
+ );
2255
+ return {
2256
+ workerId: prefixedId(object, "workerId", "worker") as WorkerId,
2257
+ attemptId: prefixedId(object, "attemptId", "attempt") as AttemptId,
2258
+ workerAttemptGeneration: nonEmptyString(object, "workerAttemptGeneration"),
2259
+ reason: nonEmptyString(object, "reason"),
2260
+ };
2261
+ }
2262
+
2263
+ function parseCancelSignalPayload(value: unknown): ReturnType<typeof parseCancelRequestedPayload> & {
2264
+ readonly signal: string;
2265
+ } {
2266
+ const object = strictRecord(
2267
+ value,
2268
+ ["workerId", "attemptId", "workerAttemptGeneration", "signal"],
2269
+ "cancel-signal payload",
2270
+ );
2271
+ return {
2272
+ workerId: prefixedId(object, "workerId", "worker") as WorkerId,
2273
+ attemptId: prefixedId(object, "attemptId", "attempt") as AttemptId,
2274
+ workerAttemptGeneration: nonEmptyString(object, "workerAttemptGeneration"),
2275
+ reason: "signal-sent",
2276
+ signal: nonEmptyString(object, "signal"),
2277
+ };
2278
+ }
2279
+
2280
+ function parseAttemptExitPayload(value: unknown): {
2281
+ readonly workerId: WorkerId;
2282
+ readonly attemptId: AttemptId;
2283
+ readonly workerAttemptGeneration: string;
2284
+ readonly exitStatus: { readonly code: number | null; readonly signal: string | null };
2285
+ readonly candidateOutcome: WorkerAttemptProjection["candidateOutcome"];
2286
+ readonly uncertainSideEffects: boolean;
2287
+ readonly reason: string;
2288
+ readonly ambiguityId: AmbiguityId | null;
2289
+ } {
2290
+ const object = strictRecord(
2291
+ value,
2292
+ ["workerId", "attemptId", "workerAttemptGeneration", "exitStatus", "candidateOutcome", "uncertainSideEffects", "reason", "ambiguityId"],
2293
+ "attempt-exit payload",
2294
+ );
2295
+ return {
2296
+ workerId: prefixedId(object, "workerId", "worker") as WorkerId,
2297
+ attemptId: prefixedId(object, "attemptId", "attempt") as AttemptId,
2298
+ workerAttemptGeneration: nonEmptyString(object, "workerAttemptGeneration"),
2299
+ exitStatus: parseExitStatus(object["exitStatus"]),
2300
+ candidateOutcome: parseCandidateOutcome(object["candidateOutcome"]),
2301
+ uncertainSideEffects: booleanValue(object, "uncertainSideEffects"),
2302
+ reason: nonEmptyString(object, "reason"),
2303
+ ambiguityId: nullablePrefixedId(object, "ambiguityId", "ambiguity") as AmbiguityId | null,
2304
+ };
2305
+ }
2306
+
2307
+ function parseWorkerNeedsAttentionPayload(value: unknown): {
2308
+ readonly workerId: WorkerId;
2309
+ readonly attemptId: AttemptId;
2310
+ readonly workerAttemptGeneration: string;
2311
+ readonly reason: string;
2312
+ readonly ambiguous: boolean;
2313
+ readonly ambiguityId: AmbiguityId | null;
2314
+ } {
2315
+ const object = strictRecord(
2316
+ value,
2317
+ ["workerId", "attemptId", "workerAttemptGeneration", "reason", "ambiguous", "ambiguityId"],
2318
+ "worker-needs-attention payload",
2319
+ );
2320
+ return {
2321
+ workerId: prefixedId(object, "workerId", "worker") as WorkerId,
2322
+ attemptId: prefixedId(object, "attemptId", "attempt") as AttemptId,
2323
+ workerAttemptGeneration: nonEmptyString(object, "workerAttemptGeneration"),
2324
+ reason: nonEmptyString(object, "reason"),
2325
+ ambiguous: booleanValue(object, "ambiguous"),
2326
+ ambiguityId: nullablePrefixedId(object, "ambiguityId", "ambiguity") as AmbiguityId | null,
2327
+ };
2328
+ }
2329
+
2330
+ function parseContractRevisionRequestedPayload(value: unknown): {
2331
+ readonly workerId: WorkerId;
2332
+ readonly revisionId: ContractRevisionId;
2333
+ readonly revision: number;
2334
+ readonly contract: WorkerContract;
2335
+ readonly correlation: string | null;
2336
+ } {
2337
+ const object = strictRecord(
2338
+ value,
2339
+ ["workerId", "revisionId", "revision", "contract", "correlation"],
2340
+ "contract-revision-requested payload",
2341
+ );
2342
+ const workerId = prefixedId(object, "workerId", "worker") as WorkerId;
2343
+ const contract = parseWorkerContract(object["contract"]);
2344
+ if (contract.workerId !== workerId) {
2345
+ throw new LedgerIntegrityError("Contract revision worker ID mismatch");
2346
+ }
2347
+ return {
2348
+ workerId,
2349
+ revisionId: prefixedId(object, "revisionId", "revision") as ContractRevisionId,
2350
+ revision: positiveInteger(object, "revision"),
2351
+ contract,
2352
+ correlation: nullableBoundedString(object, "correlation", 256),
2353
+ };
2354
+ }
2355
+
2356
+ function parseContractRevisionDeliveredPayload(value: unknown): {
2357
+ readonly workerId: WorkerId;
2358
+ readonly revisionId: ContractRevisionId;
2359
+ readonly correlation: string | null;
2360
+ } {
2361
+ const object = strictRecord(
2362
+ value,
2363
+ ["workerId", "revisionId", "boundary", "correlation"],
2364
+ "contract-revision-delivered payload",
2365
+ );
2366
+ if (nonEmptyString(object, "boundary") !== "next-inference") {
2367
+ throw new LedgerIntegrityError("Contract revision delivery boundary is invalid");
2368
+ }
2369
+ return {
2370
+ workerId: prefixedId(object, "workerId", "worker") as WorkerId,
2371
+ revisionId: prefixedId(object, "revisionId", "revision") as ContractRevisionId,
2372
+ correlation: nullableBoundedString(object, "correlation", 256),
2373
+ };
2374
+ }
2375
+
2376
+ function parseWorkerWaitDeclaredPayload(value: unknown): {
2377
+ readonly workerId: WorkerId;
2378
+ readonly attemptId: AttemptId;
2379
+ readonly workerAttemptGeneration: string;
2380
+ readonly supervisorGeneration: string;
2381
+ readonly reason: string;
2382
+ readonly correlation: string;
2383
+ } {
2384
+ const object = strictRecord(
2385
+ value,
2386
+ [
2387
+ "workerId", "attemptId", "workerAttemptGeneration", "supervisorGeneration",
2388
+ "reason", "correlation",
2389
+ ],
2390
+ "worker-wait-declared payload",
2391
+ );
2392
+ return {
2393
+ workerId: prefixedId(object, "workerId", "worker") as WorkerId,
2394
+ attemptId: prefixedId(object, "attemptId", "attempt") as AttemptId,
2395
+ workerAttemptGeneration: nonEmptyString(object, "workerAttemptGeneration"),
2396
+ supervisorGeneration: nonEmptyString(object, "supervisorGeneration"),
2397
+ reason: boundedString(object, "reason", 2_048),
2398
+ correlation: boundedString(object, "correlation", 256),
2399
+ };
2400
+ }
2401
+
2402
+ function parseCollectObservedPayload(value: unknown): {
2403
+ readonly workerId: WorkerId;
2404
+ readonly payloadFingerprint: string;
2405
+ readonly consecutiveRounds: number;
2406
+ } {
2407
+ const object = strictRecord(
2408
+ value,
2409
+ ["workerId", "payloadFingerprint", "consecutiveRounds"],
2410
+ "collect-observed payload",
2411
+ );
2412
+ const consecutiveRounds = positiveInteger(object, "consecutiveRounds");
2413
+ if (consecutiveRounds < 2) {
2414
+ throw new LedgerIntegrityError("No-progress threshold must be at least 2");
2415
+ }
2416
+ return {
2417
+ workerId: prefixedId(object, "workerId", "worker") as WorkerId,
2418
+ payloadFingerprint: hashValue(object, "payloadFingerprint"),
2419
+ consecutiveRounds,
2420
+ };
2421
+ }
2422
+
2423
+ function parseSilentStallPayload(value: unknown): {
2424
+ readonly workerId: WorkerId;
2425
+ readonly expectedCadenceMs: number;
2426
+ } {
2427
+ const object = strictRecord(
2428
+ value,
2429
+ ["workerId", "expectedCadenceMs"],
2430
+ "silent-stall-recorded payload",
2431
+ );
2432
+ return {
2433
+ workerId: prefixedId(object, "workerId", "worker") as WorkerId,
2434
+ expectedCadenceMs: positiveInteger(object, "expectedCadenceMs"),
2435
+ };
2436
+ }
2437
+
2438
+ function parseGateEvaluatedPayload(value: unknown): {
2439
+ readonly workerId: WorkerId;
2440
+ readonly attemptId: AttemptId;
2441
+ readonly fromState: WorkerAttemptProjection["state"];
2442
+ readonly evidenceExists: boolean;
2443
+ readonly reportMatches: boolean | null;
2444
+ readonly acceptanceCheckExitCode: number | null;
2445
+ readonly humanApproved: boolean | null;
2446
+ readonly allowed: boolean;
2447
+ readonly blockers: readonly string[];
2448
+ } {
2449
+ const object = strictRecord(
2450
+ value,
2451
+ [
2452
+ "workerId", "attemptId", "fromState", "targetState", "evidenceExists",
2453
+ "reportMatches", "acceptanceCheckExitCode", "humanApproved", "budgetExceeded",
2454
+ "allowed", "blockers",
2455
+ ],
2456
+ "gate-evaluated payload",
2457
+ );
2458
+ if (nonEmptyString(object, "targetState") !== "done") {
2459
+ throw new LedgerIntegrityError("Gate target state must be done");
2460
+ }
2461
+ booleanValue(object, "budgetExceeded");
2462
+ return {
2463
+ workerId: prefixedId(object, "workerId", "worker") as WorkerId,
2464
+ attemptId: prefixedId(object, "attemptId", "attempt") as AttemptId,
2465
+ fromState: enumValue(
2466
+ object,
2467
+ "fromState",
2468
+ ["pending", "running", "blocked", "ambiguous", "done", "failed", "cancelled"] as const,
2469
+ ),
2470
+ evidenceExists: booleanValue(object, "evidenceExists"),
2471
+ reportMatches: nullableBoolean(object, "reportMatches"),
2472
+ acceptanceCheckExitCode: nullableNonNegativeInteger(object, "acceptanceCheckExitCode"),
2473
+ humanApproved: nullableBoolean(object, "humanApproved"),
2474
+ allowed: booleanValue(object, "allowed"),
2475
+ blockers: stringArray(object, "blockers"),
2476
+ };
2477
+ }
2478
+
2479
+ function parseSteerDeliveredPayload(value: unknown): {
2480
+ readonly workerId: WorkerId;
2481
+ readonly messageHash: string;
2482
+ readonly correlation: string | null;
2483
+ } {
2484
+ const object = strictRecord(
2485
+ value,
2486
+ ["workerId", "messageHash", "correlation"],
2487
+ "steer-delivered payload",
2488
+ );
2489
+ return {
2490
+ workerId: prefixedId(object, "workerId", "worker") as WorkerId,
2491
+ messageHash: hashValue(object, "messageHash"),
2492
+ correlation: nullableBoundedString(object, "correlation", 256),
2493
+ };
2494
+ }
2495
+
2496
+ function parsePeerMessageRequestedPayload(value: unknown): Omit<PeerMessageProjection, "status" | "requestedAt" | "deliveredAt" | "diagnostic"> & { readonly workerId: WorkerId } {
2497
+ const object = strictRecord(
2498
+ value,
2499
+ ["workerId", "messageId", "senderWorkerId", "recipientWorkerId", "grant", "correlation", "messageHash"],
2500
+ "peer-message-requested payload",
2501
+ );
2502
+ const senderWorkerId = prefixedId(object, "senderWorkerId", "worker") as WorkerId;
2503
+ const workerId = prefixedId(object, "workerId", "worker") as WorkerId;
2504
+ if (workerId !== senderWorkerId) throw new LedgerIntegrityError("Peer-message worker ID must be the sender");
2505
+ return {
2506
+ workerId,
2507
+ messageId: prefixedId(object, "messageId", "peer") as PeerMessageId,
2508
+ senderWorkerId,
2509
+ recipientWorkerId: prefixedId(object, "recipientWorkerId", "worker") as WorkerId,
2510
+ grant: nonEmptyString(object, "grant"),
2511
+ correlation: boundedString(object, "correlation", 256),
2512
+ messageHash: hashValue(object, "messageHash"),
2513
+ };
2514
+ }
2515
+
2516
+ function parsePeerMessageDeliveryPayload(value: unknown): {
2517
+ readonly workerId: WorkerId;
2518
+ readonly messageId: PeerMessageId;
2519
+ readonly senderWorkerId: WorkerId;
2520
+ readonly recipientWorkerId: WorkerId;
2521
+ readonly recipientAttemptId: AttemptId;
2522
+ readonly recipientWorkerAttemptGeneration: string;
2523
+ readonly status: "delivered" | "indeterminate";
2524
+ readonly diagnostic: string | null;
2525
+ } {
2526
+ const object = strictRecord(
2527
+ value,
2528
+ [
2529
+ "workerId", "messageId", "senderWorkerId", "recipientWorkerId", "recipientAttemptId",
2530
+ "recipientWorkerAttemptGeneration", "status", "diagnostic",
2531
+ ],
2532
+ "peer-message-delivery payload",
2533
+ );
2534
+ const senderWorkerId = prefixedId(object, "senderWorkerId", "worker") as WorkerId;
2535
+ const workerId = prefixedId(object, "workerId", "worker") as WorkerId;
2536
+ if (workerId !== senderWorkerId) throw new LedgerIntegrityError("Peer-message delivery worker ID must be the sender");
2537
+ return {
2538
+ workerId,
2539
+ messageId: prefixedId(object, "messageId", "peer") as PeerMessageId,
2540
+ senderWorkerId,
2541
+ recipientWorkerId: prefixedId(object, "recipientWorkerId", "worker") as WorkerId,
2542
+ recipientAttemptId: prefixedId(object, "recipientAttemptId", "attempt") as AttemptId,
2543
+ recipientWorkerAttemptGeneration: nonEmptyString(object, "recipientWorkerAttemptGeneration"),
2544
+ status: enumValue(object, "status", ["delivered", "indeterminate"] as const),
2545
+ diagnostic: nullableString(object, "diagnostic"),
2546
+ };
2547
+ }
2548
+
2549
+ function parseWorkerAttempt(value: unknown): WorkerAttemptProjection {
2550
+ const object = strictRecord(
2551
+ value,
2552
+ [
2553
+ "attemptId", "attemptNumber", "workerAttemptGeneration", "supervisorGeneration", "state",
2554
+ "session", "launchContract", "pidStartEvidence", "usage", "candidateOutcome", "sideEffects",
2555
+ "control", "cancellation", "modelSelection",
2556
+ ],
2557
+ "worker attempt",
2558
+ );
2559
+ const session = strictRecord(object["session"], ["path", "file", "leaseGeneration"], "worker session");
2560
+ const process = strictRecord(
2561
+ object["pidStartEvidence"],
2562
+ [
2563
+ "pid", "processStartIdentity", "bootIdentity", "executable", "hostname", "spawnedAt",
2564
+ "observedExitAt", "exitStatus",
2565
+ ],
2566
+ "process start evidence",
2567
+ );
2568
+ const sideEffects = strictRecord(object["sideEffects"], ["policy", "status"], "attempt side effects");
2569
+ const control = object["control"] === undefined
2570
+ ? undefined
2571
+ : strictRecord(object["control"], ["status", "reason", "recordedAt"], "attempt control");
2572
+ const cancellation = strictRecord(
2573
+ object["cancellation"],
2574
+ ["reason", "requestedAt", "sentAt", "observedAt"],
2575
+ "attempt cancellation",
2576
+ );
2577
+ const modelSelection = object["modelSelection"] === undefined
2578
+ ? undefined
2579
+ : strictRecord(object["modelSelection"], ["model", "fallbackReason"], "attempt model selection");
2580
+ return {
2581
+ attemptId: prefixedId(object, "attemptId", "attempt") as AttemptId,
2582
+ attemptNumber: positiveInteger(object, "attemptNumber"),
2583
+ workerAttemptGeneration: nonEmptyString(object, "workerAttemptGeneration"),
2584
+ supervisorGeneration: nonEmptyString(object, "supervisorGeneration"),
2585
+ state: normalizeWorkerAttemptState(object["state"]),
2586
+ session: {
2587
+ path: absolutePath(session, "path"),
2588
+ file: nullableAbsolutePath(session, "file"),
2589
+ leaseGeneration: nullableString(session, "leaseGeneration"),
2590
+ },
2591
+ launchContract: object["launchContract"] === null
2592
+ ? null
2593
+ : parseWorkerLaunchContractV1(object["launchContract"]),
2594
+ ...(modelSelection ? {
2595
+ modelSelection: {
2596
+ model: parseModelSpec(modelSelection["model"]),
2597
+ fallbackReason: nullableString(modelSelection, "fallbackReason"),
2598
+ },
2599
+ } : {}),
2600
+ pidStartEvidence: {
2601
+ pid: nullablePositiveInteger(process, "pid"),
2602
+ processStartIdentity: nullableString(process, "processStartIdentity"),
2603
+ bootIdentity: nullableString(process, "bootIdentity"),
2604
+ executable: nullableAbsolutePath(process, "executable"),
2605
+ hostname: nullableString(process, "hostname"),
2606
+ spawnedAt: nullableIsoTimestamp(process, "spawnedAt"),
2607
+ observedExitAt: nullableIsoTimestamp(process, "observedExitAt"),
2608
+ exitStatus: process["exitStatus"] === null ? null : parseExitStatus(process["exitStatus"]),
2609
+ },
2610
+ usage: parseUsage(object["usage"]),
2611
+ candidateOutcome: parseCandidateOutcome(object["candidateOutcome"]),
2612
+ sideEffects: {
2613
+ policy: enumValue(
2614
+ sideEffects,
2615
+ "policy",
2616
+ ["none", "idempotent", "reconcilable", "external"] as const,
2617
+ ),
2618
+ status: enumValue(sideEffects, "status", ["none", "known", "uncertain"] as const),
2619
+ },
2620
+ control: control === undefined ? {
2621
+ status: "attached",
2622
+ reason: null,
2623
+ recordedAt: null,
2624
+ } : {
2625
+ status: enumValue(control, "status", ["attached", "detached"] as const),
2626
+ reason: nullableString(control, "reason"),
2627
+ recordedAt: nullableIsoTimestamp(control, "recordedAt"),
2628
+ },
2629
+ cancellation: {
2630
+ reason: nullableString(cancellation, "reason"),
2631
+ requestedAt: nullableIsoTimestamp(cancellation, "requestedAt"),
2632
+ sentAt: nullableIsoTimestamp(cancellation, "sentAt"),
2633
+ observedAt: nullableIsoTimestamp(cancellation, "observedAt"),
2634
+ },
2635
+ };
2636
+ }
2637
+
2638
+ function parseProcessIdentity(value: unknown): ProcessIdentity {
2639
+ const object = strictRecord(
2640
+ value,
2641
+ ["pid", "processStartIdentity", "bootIdentity", "executable", "hostname"],
2642
+ "process identity",
2643
+ );
2644
+ return {
2645
+ pid: positiveInteger(object, "pid"),
2646
+ processStartIdentity: nonEmptyString(object, "processStartIdentity"),
2647
+ bootIdentity: nullableString(object, "bootIdentity"),
2648
+ executable: absolutePath(object, "executable"),
2649
+ hostname: nonEmptyString(object, "hostname"),
2650
+ };
2651
+ }
2652
+
2653
+ function parseUsage(value: unknown): WorkerAttemptProjection["usage"] {
2654
+ const object = strictRecord(value, ["tokens", "costUsd", "outputBytes"], "attempt usage");
2655
+ return {
2656
+ tokens: nonNegativeInteger(object, "tokens"),
2657
+ costUsd: nonNegativeNumber(object, "costUsd"),
2658
+ outputBytes: nonNegativeInteger(object, "outputBytes"),
2659
+ };
2660
+ }
2661
+
2662
+ function parseExitStatus(value: unknown): { readonly code: number | null; readonly signal: string | null } {
2663
+ const object = strictRecord(value, ["code", "signal"], "process exit status");
2664
+ const code = object["code"] === null ? null : nonNegativeInteger(object, "code");
2665
+ const signal = nullableString(object, "signal");
2666
+ if (code === null && signal === null) throw new TypeError("Process exit requires code or signal");
2667
+ return { code, signal };
2668
+ }
2669
+
2670
+ function parseCandidateOutcome(value: unknown): WorkerAttemptProjection["candidateOutcome"] {
2671
+ const object = strictRecord(value, ["valueOrRef", "hash"], "candidate outcome");
2672
+ const valueOrRef = nullableString(object, "valueOrRef");
2673
+ const hash = nullableString(object, "hash");
2674
+ return { valueOrRef, hash };
2675
+ }
2676
+
2677
+ function parseAmbiguityPayload(value: unknown, recordedAt: string): AmbiguityProjection {
2678
+ const object = strictRecord(
2679
+ value,
2680
+ ["ambiguityId", "reason", "relatedIds", "status"],
2681
+ "ambiguity payload",
2682
+ );
2683
+ if (nonEmptyString(object, "status") !== "needs-reconcile") {
2684
+ throw new LedgerIntegrityError("New ambiguity must be needs-reconcile");
2685
+ }
2686
+ return {
2687
+ ambiguityId: prefixedId(object, "ambiguityId", "ambiguity") as AmbiguityId,
2688
+ status: "needs-reconcile",
2689
+ reason: nonEmptyString(object, "reason"),
2690
+ relatedIds: stringArray(object, "relatedIds"),
2691
+ recordedAt,
2692
+ };
2693
+ }
2694
+
2695
+ function parseReconciliationPayload(value: unknown): {
2696
+ readonly ambiguityId: AmbiguityId;
2697
+ readonly decision: string;
2698
+ readonly evidenceRefs: readonly string[];
2699
+ readonly workerId: WorkerId | null;
2700
+ readonly attemptId: AttemptId | null;
2701
+ readonly workerState: Exclude<WorkerAttemptProjection["state"], "ambiguous"> | null;
2702
+ } {
2703
+ const object = strictRecord(
2704
+ value,
2705
+ ["ambiguityId", "decision", "evidenceRefs", "workerId", "attemptId", "workerState"],
2706
+ "reconciliation payload",
2707
+ );
2708
+ const workerState = object["workerState"] === undefined || object["workerState"] === null
2709
+ ? null
2710
+ : enumValue(
2711
+ object,
2712
+ "workerState",
2713
+ ["pending", "running", "blocked", "done", "failed", "cancelled"] as const,
2714
+ );
2715
+ return {
2716
+ ambiguityId: prefixedId(object, "ambiguityId", "ambiguity") as AmbiguityId,
2717
+ decision: nonEmptyString(object, "decision"),
2718
+ evidenceRefs: stringArray(object, "evidenceRefs"),
2719
+ workerId: object["workerId"] === undefined
2720
+ ? null
2721
+ : nullablePrefixedId(object, "workerId", "worker") as WorkerId | null,
2722
+ attemptId: object["attemptId"] === undefined
2723
+ ? null
2724
+ : nullablePrefixedId(object, "attemptId", "attempt") as AttemptId | null,
2725
+ workerState,
2726
+ };
2727
+ }
2728
+
2729
+ function parseRunSource(value: unknown): RunSource {
2730
+ const object = strictRecord(value, ["kind", "amqMessageId", "driverSessionId"], "run source");
2731
+ const kind = enumValue(object, "kind", ["interactive", "amq", "external-engine"] as const);
2732
+ const amqMessageId = nullableString(object, "amqMessageId");
2733
+ if (kind === "amq" && amqMessageId === null) throw new TypeError("AMQ run source requires amqMessageId");
2734
+ if (kind !== "amq" && amqMessageId !== null) {
2735
+ throw new TypeError("Only an AMQ run source may carry amqMessageId");
2736
+ }
2737
+ return { kind, amqMessageId, driverSessionId: nonEmptyString(object, "driverSessionId") };
2738
+ }
2739
+
2740
+ function parseObjectiveContract(value: unknown): ObjectiveContract {
2741
+ const object = strictRecord(
2742
+ value,
2743
+ ["schema", "goal", "acceptance", "constraints", "permissions", "budget", "evidenceGraph"],
2744
+ "objective contract",
2745
+ );
2746
+ if (numberValue(object, "schema") !== 1) throw new TypeError("Objective schema must be 1");
2747
+ const evidenceGraph = object["evidenceGraph"] === undefined
2748
+ ? undefined
2749
+ : parseEvidenceGraph(object["evidenceGraph"]);
2750
+ return {
2751
+ schema: 1,
2752
+ goal: nonEmptyString(object, "goal"),
2753
+ acceptance: nonEmptyStringArray(object, "acceptance"),
2754
+ constraints: stringArray(object, "constraints"),
2755
+ permissions: stringArray(object, "permissions"),
2756
+ budget: parseRunBudget(object["budget"]),
2757
+ ...(evidenceGraph ? { evidenceGraph } : {}),
2758
+ };
2759
+ }
2760
+
2761
+ function parseWorkerRequest(value: unknown, allowContractFields = false): WorkerRequest {
2762
+ const requestKeys = [
2763
+ "schema", "requestKey", "difficulty", "role", "proposal", "context", "forkSource", "goal", "acceptance", "constraints", "budget",
2764
+ "model", "modelFallbacks", "capabilities", "workspace", "contextRefs", "channel", "sideEffectPolicy",
2765
+ "report_schema", "requires_human_approval", "acceptance_check",
2766
+ ] as const;
2767
+ const object = strictRecord(
2768
+ value,
2769
+ allowContractFields
2770
+ ? [...requestKeys, "runId", "workerId", "createdByDriverSessionId"]
2771
+ : requestKeys,
2772
+ "worker request",
2773
+ );
2774
+ if (numberValue(object, "schema") !== 1) throw new TypeError("Worker request schema must be 1");
2775
+ const difficulty = object["difficulty"] === undefined
2776
+ ? undefined
2777
+ : enumValue(object, "difficulty", ["hard", "medium", "easy"] as const);
2778
+ const role = object["role"] === undefined ? undefined : parseWorkerRoleContract(object["role"]);
2779
+ const proposal = object["proposal"] === undefined
2780
+ ? undefined
2781
+ : parseWorkerProposalProvenance(object["proposal"]);
2782
+ const context = object["context"] === undefined
2783
+ ? "fresh"
2784
+ : enumValue(object, "context", ["fresh", "fork"] as const);
2785
+ const forkSource = object["forkSource"] === undefined
2786
+ ? undefined
2787
+ : parseWorkerForkSource(object["forkSource"]);
2788
+ if ((context === "fork") !== (forkSource !== undefined)) {
2789
+ throw new TypeError("Fork worker context requires forkSource, and fresh context forbids it");
2790
+ }
2791
+ const reportSchema = object["report_schema"] === undefined
2792
+ ? undefined
2793
+ : parseReportSchema(object["report_schema"]);
2794
+ const requiresHumanApproval = object["requires_human_approval"] === undefined
2795
+ ? undefined
2796
+ : booleanValue(object, "requires_human_approval");
2797
+ const acceptanceCheck = object["acceptance_check"] === undefined
2798
+ ? undefined
2799
+ : parseAcceptanceCheck(object["acceptance_check"]);
2800
+ const model = parseModelSpec(object["model"]);
2801
+ const modelFallbacks = object["modelFallbacks"] === undefined
2802
+ ? []
2803
+ : parseModelFallbacks(object["modelFallbacks"]);
2804
+ const modelKeys = [model, ...modelFallbacks].map((candidate) => `${candidate.provider}/${candidate.id}`);
2805
+ if (new Set(modelKeys).size !== modelKeys.length) {
2806
+ throw new TypeError("Primary and fallback model chain entries must be unique");
2807
+ }
2808
+ return {
2809
+ schema: 1,
2810
+ requestKey: validateRequestKey(nonEmptyString(object, "requestKey")),
2811
+ ...(difficulty ? { difficulty } : {}),
2812
+ ...(role ? { role } : {}),
2813
+ ...(proposal ? { proposal } : {}),
2814
+ context,
2815
+ ...(forkSource ? { forkSource } : {}),
2816
+ goal: nonEmptyString(object, "goal"),
2817
+ acceptance: nonEmptyStringArray(object, "acceptance"),
2818
+ constraints: stringArray(object, "constraints"),
2819
+ budget: parseWorkerBudget(object["budget"]),
2820
+ model,
2821
+ ...(modelFallbacks.length > 0 ? { modelFallbacks } : {}),
2822
+ capabilities: stringArray(object, "capabilities"),
2823
+ workspace: parseWorkspaceGrant(object["workspace"]),
2824
+ contextRefs: parseContentRefs(object["contextRefs"]),
2825
+ channel: enumValue(object, "channel", ["json-result", "rpc"] as const),
2826
+ sideEffectPolicy: enumValue(
2827
+ object,
2828
+ "sideEffectPolicy",
2829
+ ["none", "idempotent", "reconcilable", "external"] as const,
2830
+ ),
2831
+ ...(reportSchema ? { report_schema: reportSchema } : {}),
2832
+ ...(requiresHumanApproval === undefined
2833
+ ? {}
2834
+ : { requires_human_approval: requiresHumanApproval }),
2835
+ ...(acceptanceCheck ? { acceptance_check: acceptanceCheck } : {}),
2836
+ };
2837
+ }
2838
+
2839
+ function parseWorkerProposalProvenance(value: unknown): WorkerProposalProvenance {
2840
+ const object = strictRecord(
2841
+ value,
2842
+ ["source", "confirmSpawns", "disposition", "humanEditedFields"],
2843
+ "worker proposal provenance",
2844
+ );
2845
+ const humanEditedFields = stringArray(object, "humanEditedFields").map((field) => {
2846
+ if (!["role", "task", "model", "difficulty", "budget"].includes(field)) {
2847
+ throw new TypeError(`Unknown human-edited proposal field ${field}`);
2848
+ }
2849
+ return field as WorkerProposalProvenance["humanEditedFields"][number];
2850
+ });
2851
+ if (new Set(humanEditedFields).size !== humanEditedFields.length) {
2852
+ throw new TypeError("Human-edited proposal fields must be unique");
2853
+ }
2854
+ const source = enumValue(object, "source", ["tool", "human-edited"] as const);
2855
+ if ((source === "tool") !== (humanEditedFields.length === 0)) {
2856
+ throw new TypeError("Proposal source and human-edited fields disagree");
2857
+ }
2858
+ return {
2859
+ source,
2860
+ confirmSpawns: enumValue(object, "confirmSpawns", ["on", "multi-worker-only", "off"] as const),
2861
+ disposition: enumValue(
2862
+ object,
2863
+ "disposition",
2864
+ ["accepted", "skipped-policy-off", "skipped-single-worker"] as const,
2865
+ ),
2866
+ humanEditedFields,
2867
+ };
2868
+ }
2869
+
2870
+ function parseWorkerForkSource(value: unknown): import("./workflow-types.js").WorkerForkSource {
2871
+ const object = strictRecord(
2872
+ value,
2873
+ ["sourceSessionFile", "branchPointId", "inheritedEntries", "inheritedBytes"],
2874
+ "worker fork source",
2875
+ );
2876
+ const sourceSessionFile = nonEmptyString(object, "sourceSessionFile");
2877
+ if (!isAbsolute(sourceSessionFile)) throw new TypeError("Fork source session file must be absolute");
2878
+ return {
2879
+ sourceSessionFile: normalize(sourceSessionFile),
2880
+ branchPointId: nonEmptyString(object, "branchPointId"),
2881
+ inheritedEntries: positiveInteger(object, "inheritedEntries"),
2882
+ inheritedBytes: nonNegativeInteger(object, "inheritedBytes"),
2883
+ };
2884
+ }
2885
+
2886
+ function parseWorkerRoleContract(value: unknown): WorkerRoleContract {
2887
+ const object = strictRecord(
2888
+ value,
2889
+ ["name", "tier", "charter", "presetSource", "presetFingerprint"],
2890
+ "worker role contract",
2891
+ );
2892
+ const presetFingerprint = nonEmptyString(object, "presetFingerprint");
2893
+ if (!/^[a-f0-9]{64}$/.test(presetFingerprint)) {
2894
+ throw new TypeError("Worker role preset fingerprint must be a lowercase SHA-256 hex digest");
2895
+ }
2896
+ return {
2897
+ name: nonEmptyString(object, "name"),
2898
+ tier: nonEmptyString(object, "tier"),
2899
+ charter: nonEmptyString(object, "charter"),
2900
+ presetSource: enumValue(object, "presetSource", ["builtin", "user", "project"] as const),
2901
+ presetFingerprint,
2902
+ };
2903
+ }
2904
+
2905
+ function parseWorkerContract(value: unknown): WorkerContract {
2906
+ const object = recordValue({ value }, "value");
2907
+ const request = parseWorkerRequest(object, true);
2908
+ return {
2909
+ ...request,
2910
+ context: request.context ?? "fresh",
2911
+ runId: prefixedId(object, "runId", "run") as RunId,
2912
+ workerId: prefixedId(object, "workerId", "worker") as WorkerId,
2913
+ createdByDriverSessionId: nonEmptyString(object, "createdByDriverSessionId"),
2914
+ };
2915
+ }
2916
+
2917
+ function parseReportSchema(value: unknown): ReportSchemaV1 {
2918
+ const object = strictRecord(value, ["schema", "required", "properties"], "report schema");
2919
+ if (numberValue(object, "schema") !== 1) throw new TypeError("Report schema must be 1");
2920
+ const propertiesValue = object["properties"];
2921
+ if (typeof propertiesValue !== "object" || propertiesValue === null || Array.isArray(propertiesValue)) {
2922
+ throw new TypeError("Report schema properties must be an object");
2923
+ }
2924
+ const properties: Record<string, { readonly type: ReportSchemaV1["properties"][string]["type"] }> = {};
2925
+ for (const [name, definition] of Object.entries(propertiesValue)) {
2926
+ requireNonEmpty(name, "report schema property name");
2927
+ const field = strictRecord(definition, ["type"], `report schema property ${name}`);
2928
+ properties[name] = {
2929
+ type: enumValue(
2930
+ field,
2931
+ "type",
2932
+ ["string", "number", "boolean", "object", "array", "null"] as const,
2933
+ ),
2934
+ };
2935
+ }
2936
+ const required = stringArray(object, "required");
2937
+ for (const name of required) {
2938
+ if (!properties[name]) throw new TypeError(`Required report property ${name} is not declared`);
2939
+ }
2940
+ return { schema: 1, required, properties };
2941
+ }
2942
+
2943
+ function parseAcceptanceCheck(value: unknown): AcceptanceCheck {
2944
+ const object = strictRecord(value, ["command", "args"], "acceptance check");
2945
+ return {
2946
+ command: nonEmptyString(object, "command"),
2947
+ args: stringArray(object, "args"),
2948
+ };
2949
+ }
2950
+
2951
+ function parseRunBudget(value: unknown): RunBudget {
2952
+ const object = strictRecord(
2953
+ value,
2954
+ ["maxWorkers", "maxConcurrency", "maxDepth", "maxAttempts", "maxWallClockMs", "maxTokens", "maxCostUsd", "maxOutputBytes"],
2955
+ "run budget",
2956
+ );
2957
+ return {
2958
+ maxWorkers: parseBudgetLimit(object["maxWorkers"], "run maxWorkers", { integer: true, allowLegacyNumber: true }),
2959
+ maxConcurrency: parseBudgetLimit(object["maxConcurrency"], "run maxConcurrency", { integer: true, allowLegacyNumber: true }),
2960
+ maxDepth: parseBudgetLimit(object["maxDepth"], "run maxDepth", { integer: true, allowLegacyNumber: true }),
2961
+ maxAttempts: parseBudgetLimit(object["maxAttempts"], "run maxAttempts", { integer: true, allowLegacyNumber: true }),
2962
+ maxWallClockMs: parseBudgetLimit(object["maxWallClockMs"], "run maxWallClockMs", { integer: true, allowLegacyNumber: true }),
2963
+ maxTokens: parseBudgetLimit(object["maxTokens"], "run maxTokens", { integer: true, allowLegacyNumber: true }),
2964
+ maxCostUsd: object["maxCostUsd"] === undefined
2965
+ ? UNLIMITED
2966
+ : parseBudgetLimit(object["maxCostUsd"], "run maxCostUsd", { integer: false, allowLegacyNumber: true }),
2967
+ maxOutputBytes: object["maxOutputBytes"] === undefined
2968
+ ? UNLIMITED
2969
+ : parseBudgetLimit(object["maxOutputBytes"], "run maxOutputBytes", { integer: true, allowLegacyNumber: true }),
2970
+ };
2971
+ }
2972
+
2973
+ function parseWorkerBudget(value: unknown): WorkerBudget {
2974
+ const object = strictRecord(
2975
+ value,
2976
+ ["maxAttempts", "maxWallClockMs", "maxTokens", "maxCostUsd", "maxOutputBytes"],
2977
+ "worker budget",
2978
+ );
2979
+ return {
2980
+ maxAttempts: parseBudgetLimit(object["maxAttempts"], "worker maxAttempts", { integer: true, allowLegacyNumber: true }),
2981
+ maxWallClockMs: parseBudgetLimit(object["maxWallClockMs"], "worker maxWallClockMs", { integer: true, allowLegacyNumber: true }),
2982
+ maxTokens: parseBudgetLimit(object["maxTokens"], "worker maxTokens", { integer: true, allowLegacyNumber: true }),
2983
+ maxCostUsd: object["maxCostUsd"] === undefined
2984
+ ? UNLIMITED
2985
+ : parseBudgetLimit(object["maxCostUsd"], "worker maxCostUsd", { integer: false, allowLegacyNumber: true }),
2986
+ maxOutputBytes: object["maxOutputBytes"] === undefined
2987
+ ? UNLIMITED
2988
+ : parseBudgetLimit(object["maxOutputBytes"], "worker maxOutputBytes", { integer: true, allowLegacyNumber: true }),
2989
+ };
2990
+ }
2991
+
2992
+ function parseModelSpec(value: unknown): ModelSpec {
2993
+ const object = strictRecord(value, ["provider", "id", "thinkingLevel"], "model spec");
2994
+ const thinkingLevel = object["thinkingLevel"] === undefined
2995
+ ? undefined
2996
+ : nonEmptyString(object, "thinkingLevel");
2997
+ return {
2998
+ provider: nonEmptyString(object, "provider"),
2999
+ id: nonEmptyString(object, "id"),
3000
+ ...(thinkingLevel ? { thinkingLevel } : {}),
3001
+ };
3002
+ }
3003
+
3004
+ function parseModelFallbacks(value: unknown): readonly ModelSpec[] {
3005
+ if (!Array.isArray(value)) throw new TypeError("modelFallbacks must be an array");
3006
+ if (value.length > 4) throw new TypeError("modelFallbacks may contain at most four models");
3007
+ return value.map(parseModelSpec);
3008
+ }
3009
+
3010
+ function parseWorkspaceGrant(value: unknown): WorkspaceGrant {
3011
+ const object = strictRecord(value, ["path", "mode", "worktreePath"], "workspace grant");
3012
+ const worktreePath = object["worktreePath"] === undefined
3013
+ ? undefined
3014
+ : absolutePath(object, "worktreePath");
3015
+ return {
3016
+ path: absolutePath(object, "path"),
3017
+ mode: enumValue(object, "mode", ["read-only", "mutating"] as const),
3018
+ ...(worktreePath ? { worktreePath } : {}),
3019
+ };
3020
+ }
3021
+
3022
+ function parseContentRefs(value: unknown): readonly ContentRef[] {
3023
+ if (!Array.isArray(value)) throw new TypeError("contextRefs must be an array");
3024
+ return value.map((item) => {
3025
+ const object = strictRecord(item, ["kind", "value", "hash"], "content reference");
3026
+ const hash = object["hash"] === undefined ? undefined : nonEmptyString(object, "hash");
3027
+ return {
3028
+ kind: enumValue(object, "kind", ["file", "session", "artifact"] as const),
3029
+ value: nonEmptyString(object, "value"),
3030
+ ...(hash ? { hash } : {}),
3031
+ };
3032
+ });
3033
+ }
3034
+
3035
+ function parseEvidenceGraph(value: unknown): { readonly nodes: readonly EvidenceNode[] } {
3036
+ const object = strictRecord(value, ["nodes"], "evidence graph");
3037
+ const nodes = object["nodes"];
3038
+ if (!Array.isArray(nodes)) throw new TypeError("evidence graph nodes must be an array");
3039
+ const parsed = nodes.map((item) => {
3040
+ const node = strictRecord(
3041
+ item,
3042
+ ["id", "kind", "assertion", "verifier", "requires"],
3043
+ "evidence node",
3044
+ );
3045
+ return {
3046
+ id: nonEmptyString(node, "id"),
3047
+ kind: enumValue(node, "kind", ["claim", "artifact"] as const),
3048
+ assertion: nonEmptyString(node, "assertion"),
3049
+ verifier: nonEmptyString(node, "verifier"),
3050
+ requires: stringArray(node, "requires"),
3051
+ };
3052
+ });
3053
+ const byId = new Map<string, EvidenceNode>();
3054
+ for (const node of parsed) {
3055
+ if (byId.has(node.id)) throw new TypeError(`Duplicate evidence node ID ${node.id}`);
3056
+ byId.set(node.id, node);
3057
+ }
3058
+ for (const node of parsed) {
3059
+ for (const required of node.requires) {
3060
+ if (!byId.has(required)) {
3061
+ throw new TypeError(`Evidence node ${node.id} requires unknown node ${required}`);
3062
+ }
3063
+ }
3064
+ }
3065
+ const visiting = new Set<string>();
3066
+ const visited = new Set<string>();
3067
+ const visit = (id: string): void => {
3068
+ if (visiting.has(id)) throw new TypeError(`Evidence graph contains a cycle at ${id}`);
3069
+ if (visited.has(id)) return;
3070
+ visiting.add(id);
3071
+ for (const required of byId.get(id)!.requires) visit(required);
3072
+ visiting.delete(id);
3073
+ visited.add(id);
3074
+ };
3075
+ for (const node of parsed) visit(node.id);
3076
+ return { nodes: parsed };
3077
+ }
3078
+
3079
+ function initialAttempt(
3080
+ attemptId: AttemptId,
3081
+ attemptNumber: number,
3082
+ workerAttemptGeneration: string,
3083
+ supervisorGeneration: string,
3084
+ sessionPath: string,
3085
+ sideEffectPolicy: WorkerRequest["sideEffectPolicy"],
3086
+ model: ModelSpec,
3087
+ fallbackReason: string | null,
3088
+ ): WorkerAttemptProjection {
3089
+ return {
3090
+ attemptId,
3091
+ attemptNumber,
3092
+ workerAttemptGeneration: requireNonEmpty(workerAttemptGeneration, "worker attempt generation"),
3093
+ supervisorGeneration: requireNonEmpty(supervisorGeneration, "supervisor generation"),
3094
+ state: "pending",
3095
+ session: { path: sessionPath, file: null, leaseGeneration: null },
3096
+ launchContract: null,
3097
+ modelSelection: {
3098
+ model: parseModelSpec(model),
3099
+ fallbackReason: fallbackReason === null ? null : requireNonEmpty(fallbackReason, "fallback reason"),
3100
+ },
3101
+ pidStartEvidence: {
3102
+ pid: null,
3103
+ processStartIdentity: null,
3104
+ bootIdentity: null,
3105
+ executable: null,
3106
+ hostname: null,
3107
+ spawnedAt: null,
3108
+ observedExitAt: null,
3109
+ exitStatus: null,
3110
+ },
3111
+ usage: { tokens: 0, costUsd: 0, outputBytes: 0 },
3112
+ candidateOutcome: { valueOrRef: null, hash: null },
3113
+ sideEffects: { policy: sideEffectPolicy, status: "none" },
3114
+ control: { status: "attached", reason: null, recordedAt: null },
3115
+ cancellation: { reason: null, requestedAt: null, sentAt: null, observedAt: null },
3116
+ };
3117
+ }
3118
+
3119
+ function initialBudgetProjection(limits: RunBudget): RunBudgetProjection {
3120
+ return {
3121
+ limits,
3122
+ reserved: { workers: 0, attempts: 0, tokens: 0, costUsd: 0, outputBytes: 0 },
3123
+ spent: { tokens: 0, costUsd: 0, outputBytes: 0 },
3124
+ remaining: {
3125
+ workers: limits.maxWorkers,
3126
+ attempts: limits.maxAttempts,
3127
+ tokens: limits.maxTokens,
3128
+ wallClockMs: limits.maxWallClockMs,
3129
+ costUsd: limits.maxCostUsd,
3130
+ outputBytes: limits.maxOutputBytes,
3131
+ },
3132
+ };
3133
+ }
3134
+
3135
+ function reserveProjectionBudget(
3136
+ projection: RunProjectionV2,
3137
+ workerBudget: WorkerBudget,
3138
+ newWorker: boolean,
3139
+ ): RunProjectionV2 {
3140
+ const reserved = {
3141
+ workers: projection.budget.reserved.workers + (newWorker ? 1 : 0),
3142
+ attempts: projection.budget.reserved.attempts + 1,
3143
+ tokens: reserveBudget(projection.budget.reserved.tokens, workerBudget.maxTokens),
3144
+ costUsd: reserveBudget(projection.budget.reserved.costUsd, workerBudget.maxCostUsd),
3145
+ outputBytes: reserveBudget(projection.budget.reserved.outputBytes, workerBudget.maxOutputBytes),
3146
+ };
3147
+ return {
3148
+ ...projection,
3149
+ budget: {
3150
+ ...projection.budget,
3151
+ reserved,
3152
+ remaining: {
3153
+ ...projection.budget.remaining,
3154
+ workers: remainingBudget(projection.budget.limits.maxWorkers, reserved.workers),
3155
+ attempts: remainingBudget(projection.budget.limits.maxAttempts, reserved.attempts),
3156
+ tokens: remainingBudget(projection.budget.limits.maxTokens, reserved.tokens),
3157
+ costUsd: remainingBudget(projection.budget.limits.maxCostUsd, reserved.costUsd),
3158
+ outputBytes: remainingBudget(projection.budget.limits.maxOutputBytes, reserved.outputBytes),
3159
+ },
3160
+ },
3161
+ };
3162
+ }
3163
+
3164
+ function spendProjectionBudget(
3165
+ projection: RunProjectionV2,
3166
+ delta: { readonly tokens: number; readonly costUsd: number; readonly outputBytes: number },
3167
+ ): RunProjectionV2 {
3168
+ return {
3169
+ ...projection,
3170
+ budget: {
3171
+ ...projection.budget,
3172
+ spent: {
3173
+ tokens: projection.budget.spent.tokens + delta.tokens,
3174
+ costUsd: projection.budget.spent.costUsd + delta.costUsd,
3175
+ outputBytes: projection.budget.spent.outputBytes + delta.outputBytes,
3176
+ },
3177
+ },
3178
+ };
3179
+ }
3180
+
3181
+ function withRemainingWallClock(
3182
+ budget: RunBudgetProjection,
3183
+ createdAt: string,
3184
+ observedAt: string,
3185
+ ): RunBudgetProjection {
3186
+ const elapsed = Math.max(0, Date.parse(observedAt) - Date.parse(createdAt));
3187
+ return {
3188
+ ...budget,
3189
+ remaining: {
3190
+ ...budget.remaining,
3191
+ wallClockMs: remainingBudget(budget.limits.maxWallClockMs, elapsed),
3192
+ },
3193
+ };
3194
+ }
3195
+
3196
+ function assertWorkerCapabilitySubset(current: RunProjectionV2, request: WorkerRequest): void {
3197
+ const permissions = new Set(current.objective.permissions);
3198
+ const missing = request.capabilities.filter((capability) => {
3199
+ if (capability.startsWith("message_worker:")) {
3200
+ if (capability.slice("message_worker:".length).trim().length === 0) {
3201
+ throw new TypeError("message_worker capability requires a worker ID or role target");
3202
+ }
3203
+ return !permissions.has("message_worker");
3204
+ }
3205
+ return !permissions.has(capability);
3206
+ });
3207
+ if (missing.length > 0) {
3208
+ throw new TypeError(`Worker capabilities exceed objective permissions: ${missing.join(", ")}`);
3209
+ }
3210
+ }
3211
+
3212
+ function resolvePeerGrantRecipient(
3213
+ current: RunProjectionV2,
3214
+ grant: string,
3215
+ ledgerReplay: boolean,
3216
+ ): WorkerProjection {
3217
+ const prefix = "message_worker:";
3218
+ if (!grant.startsWith(prefix)) {
3219
+ throw peerGrantError(`Invalid peer-message grant ${grant}`, ledgerReplay);
3220
+ }
3221
+ const target = grant.slice(prefix.length);
3222
+ if (target.trim().length === 0 || target !== target.trim()) {
3223
+ throw peerGrantError("Peer-message grant requires a normalized target", ledgerReplay);
3224
+ }
3225
+ const candidates = target.startsWith("worker_")
3226
+ ? [current.workers[target as WorkerId]].filter(
3227
+ (worker): worker is WorkerProjection => worker !== undefined,
3228
+ )
3229
+ : Object.values(current.workers).filter(
3230
+ (worker) => effectiveWorkerContract(worker).role?.name === target,
3231
+ );
3232
+ if (candidates.length !== 1) {
3233
+ throw peerGrantError(
3234
+ `Peer-message grant ${grant} resolves to ${candidates.length} workers`,
3235
+ ledgerReplay,
3236
+ );
3237
+ }
3238
+ return candidates[0]!;
3239
+ }
3240
+
3241
+ function peerGrantError(message: string, ledgerReplay: boolean): Error {
3242
+ return ledgerReplay ? new LedgerIntegrityError(message) : new TypeError(message);
3243
+ }
3244
+
3245
+ function assertSpawnBudget(
3246
+ current: RunProjectionV2,
3247
+ workerBudget: WorkerBudget,
3248
+ depth: number,
3249
+ now: Date,
3250
+ ): void {
3251
+ if (budgetExhausted(current.budget.reserved.workers, current.budget.limits.maxWorkers)) {
3252
+ throw new BudgetExceededError("Run worker budget is exhausted");
3253
+ }
3254
+ assertReplacementBudget(current, workerBudget, depth, now);
3255
+ }
3256
+
3257
+ function assertReplacementBudget(
3258
+ current: RunProjectionV2,
3259
+ workerBudget: WorkerBudget,
3260
+ depth: number,
3261
+ now: Date,
3262
+ ): void {
3263
+ const active = Object.values(current.workers).filter((worker) =>
3264
+ isAttemptProcessActive(requiredCurrentAttempt(worker))).length;
3265
+ if (budgetExhausted(active, current.budget.limits.maxConcurrency)) {
3266
+ throw new BudgetExceededError("Run concurrency budget is exhausted");
3267
+ }
3268
+ if (budgetExceeded(depth, current.budget.limits.maxDepth)) {
3269
+ throw new BudgetExceededError(`Worker depth ${depth} exceeds run maxDepth`);
3270
+ }
3271
+ if (budgetExhausted(current.budget.reserved.attempts, current.budget.limits.maxAttempts)) {
3272
+ throw new BudgetExceededError("Run attempt budget is exhausted");
3273
+ }
3274
+ if (!budgetRequestFits(current.budget.limits.maxTokens, current.budget.reserved.tokens, workerBudget.maxTokens)) {
3275
+ throw new BudgetExceededError("Run token reservation budget is exhausted");
3276
+ }
3277
+ if (!budgetRequestFits(current.budget.limits.maxCostUsd, current.budget.reserved.costUsd, workerBudget.maxCostUsd)) {
3278
+ throw new BudgetExceededError("Run cost reservation budget is exhausted");
3279
+ }
3280
+ if (!budgetRequestFits(current.budget.limits.maxOutputBytes, current.budget.reserved.outputBytes, workerBudget.maxOutputBytes)) {
3281
+ throw new BudgetExceededError("Run output-byte reservation budget is exhausted");
3282
+ }
3283
+ const elapsed = Math.max(0, now.getTime() - Date.parse(current.createdAt));
3284
+ if (budgetExhausted(elapsed, current.budget.limits.maxWallClockMs)) {
3285
+ throw new BudgetExceededError("Run wall-clock budget is exhausted");
3286
+ }
3287
+ const remainingWallClock = remainingBudget(current.budget.limits.maxWallClockMs, elapsed);
3288
+ if (!budgetRequestFits(remainingWallClock, 0, workerBudget.maxWallClockMs)) {
3289
+ throw new BudgetExceededError("Worker wall-clock reservation exceeds the run deadline");
3290
+ }
3291
+ }
3292
+
3293
+ function budgetRequestFits(
3294
+ limit: import("./workflow-types.js").BudgetLimit,
3295
+ reserved: number | "unlimited",
3296
+ requested: import("./workflow-types.js").BudgetLimit,
3297
+ ): boolean {
3298
+ if (limit.kind === "unlimited") return true;
3299
+ if (reserved === "unlimited" || requested.kind === "unlimited") return false;
3300
+ return reserved + requested.value <= limit.value;
3301
+ }
3302
+
3303
+ function requiredWorker(projection: RunProjectionV2, workerId: WorkerId): WorkerProjection {
3304
+ const worker = projection.workers[workerId];
3305
+ if (!worker) throw new Error(`Unknown worker ${workerId}`);
3306
+ return worker;
3307
+ }
3308
+
3309
+ export function effectiveWorkerContract(worker: WorkerProjection): WorkerContract {
3310
+ return worker.contractRevisions.filter((revision) => revision.status === "delivered").at(-1)
3311
+ ?.contract ?? worker.contract;
3312
+ }
3313
+
3314
+ export function latestWorkerContract(worker: WorkerProjection): WorkerContract {
3315
+ return worker.contractRevisions.at(-1)?.contract ?? worker.contract;
3316
+ }
3317
+
3318
+ function optionalRevisionField(
3319
+ key: "report_schema" | "acceptance_check",
3320
+ input: {
3321
+ readonly report_schema?: ReportSchemaV1 | null;
3322
+ readonly acceptance_check?: AcceptanceCheck | null;
3323
+ },
3324
+ previous: WorkerContract,
3325
+ ): Record<string, unknown> {
3326
+ if (Object.prototype.hasOwnProperty.call(input, key)) {
3327
+ const value = input[key];
3328
+ return value === null || value === undefined ? {} : { [key]: value };
3329
+ }
3330
+ const previousValue = previous[key];
3331
+ return previousValue === undefined ? {} : { [key]: previousValue };
3332
+ }
3333
+
3334
+ function requiredCurrentAttempt(worker: WorkerProjection): WorkerAttemptProjection {
3335
+ const attempt = worker.attempts.at(-1);
3336
+ if (!attempt) throw new LedgerIntegrityError(`Worker ${worker.workerId} has no attempt`);
3337
+ return attempt;
3338
+ }
3339
+
3340
+ function requiredWorkerAttempt(
3341
+ projection: RunProjectionV2,
3342
+ workerId: WorkerId,
3343
+ attemptId: AttemptId,
3344
+ ): { readonly worker: WorkerProjection; readonly attempt: WorkerAttemptProjection } {
3345
+ const worker = requiredWorker(projection, workerId);
3346
+ const attempt = worker.attempts.find((candidate) => candidate.attemptId === attemptId);
3347
+ if (!attempt) throw new Error(`Unknown attempt ${attemptId} for worker ${workerId}`);
3348
+ return { worker, attempt };
3349
+ }
3350
+
3351
+ function updateWorker(
3352
+ projection: RunProjectionV2,
3353
+ workerId: WorkerId,
3354
+ update: (worker: WorkerProjection) => WorkerProjection,
3355
+ ): RunProjectionV2 {
3356
+ const worker = requiredWorker(projection, workerId);
3357
+ const updated = update(worker);
3358
+ return { ...projection, workers: { ...projection.workers, [workerId]: updated } };
3359
+ }
3360
+
3361
+ function workerGateFacts(input: {
3362
+ readonly evidenceExists?: boolean;
3363
+ readonly reportSchemaDeclared?: boolean;
3364
+ readonly reportMatches?: boolean;
3365
+ readonly acceptanceDeclared?: boolean;
3366
+ readonly acceptanceExitCode?: number | null;
3367
+ readonly humanRequired?: boolean;
3368
+ readonly humanApproved?: boolean;
3369
+ readonly budgetExceeded?: boolean;
3370
+ readonly runtimeCertain?: boolean;
3371
+ readonly effectsUncertain?: boolean;
3372
+ readonly supervisorLost?: boolean;
3373
+ readonly failureObserved?: boolean;
3374
+ readonly terminationObserved?: boolean;
3375
+ readonly waitDeclared?: boolean;
3376
+ readonly reconciliation?: boolean;
3377
+ readonly uncertaintyCleared?: boolean;
3378
+ readonly input?: "revise_contract" | "steer" | "peer_message";
3379
+ readonly inputCorrelationMatches?: boolean;
3380
+ readonly cancellationRequested?: boolean;
3381
+ } = {}): WorkerGateFacts {
3382
+ return {
3383
+ evidence: { exists: input.evidenceExists ?? false },
3384
+ report: {
3385
+ schemaDeclared: input.reportSchemaDeclared ?? false,
3386
+ matches: input.reportMatches ?? false,
3387
+ },
3388
+ acceptanceCheck: {
3389
+ declared: input.acceptanceDeclared ?? false,
3390
+ exitCode: input.acceptanceExitCode ?? null,
3391
+ },
3392
+ human: {
3393
+ approvalRequired: input.humanRequired ?? false,
3394
+ approved: input.humanApproved ?? false,
3395
+ },
3396
+ budget: { exceeded: input.budgetExceeded ?? false },
3397
+ runtime: {
3398
+ supervisorLost: input.supervisorLost ?? false,
3399
+ effectsUncertain: input.effectsUncertain ?? false,
3400
+ certain: input.runtimeCertain ?? !(input.supervisorLost || input.effectsUncertain),
3401
+ },
3402
+ failure: { observed: input.failureObserved ?? false },
3403
+ termination: { observed: input.terminationObserved ?? false },
3404
+ wait: { declared: input.waitDeclared ?? false },
3405
+ reconcile: {
3406
+ decisionExists: input.reconciliation ?? false,
3407
+ uncertaintyCleared: input.uncertaintyCleared ?? false,
3408
+ },
3409
+ input: {
3410
+ kind: input.input ?? null,
3411
+ correlationMatches: input.inputCorrelationMatches ?? false,
3412
+ },
3413
+ cancel: { verbReceived: input.cancellationRequested ?? false },
3414
+ };
3415
+ }
3416
+
3417
+ function requiredMatchingWait(
3418
+ worker: WorkerProjection,
3419
+ correlation: string | null,
3420
+ ledgerReplay: boolean,
3421
+ ): WorkerProjection["waits"][number] {
3422
+ const attempt = requiredCurrentAttempt(worker);
3423
+ const active = worker.waits.filter((wait) => wait.status === "waiting");
3424
+ if (active.length !== 1) {
3425
+ throw waitCorrelationError(
3426
+ `Blocked worker ${worker.workerId} must have exactly one active typed wait`,
3427
+ ledgerReplay,
3428
+ );
3429
+ }
3430
+ const wait = active[0]!;
3431
+ if (wait.attemptId !== attempt.attemptId
3432
+ || wait.workerAttemptGeneration !== attempt.workerAttemptGeneration) {
3433
+ throw waitCorrelationError("Typed wait belongs to a stale worker attempt", ledgerReplay);
3434
+ }
3435
+ if (correlation === null || wait.correlation !== correlation) {
3436
+ throw waitCorrelationError(
3437
+ `Input correlation does not match typed wait ${wait.correlation}`,
3438
+ ledgerReplay,
3439
+ );
3440
+ }
3441
+ return wait;
3442
+ }
3443
+
3444
+ function wakeWorkerFromWait(
3445
+ worker: WorkerProjection,
3446
+ input: "revise_contract" | "steer" | "peer_message",
3447
+ correlation: string | null,
3448
+ occurredAt: string,
3449
+ ): WorkerProjection {
3450
+ if (worker.state !== "blocked") return worker;
3451
+ const wait = requiredMatchingWait(worker, correlation, true);
3452
+ const attempt = requiredCurrentAttempt(worker);
3453
+ const facts = workerGateFacts({ input, inputCorrelationMatches: true });
3454
+ return {
3455
+ ...worker,
3456
+ state: gatedWorkerState(worker.state, "running", facts),
3457
+ attempts: worker.attempts.map((candidate) => candidate.attemptId === attempt.attemptId
3458
+ ? { ...candidate, state: gatedWorkerState(candidate.state, "running", facts) }
3459
+ : candidate),
3460
+ waits: worker.waits.map((candidate) => candidate === wait
3461
+ ? {
3462
+ ...candidate,
3463
+ status: "satisfied" as const,
3464
+ satisfiedAt: occurredAt,
3465
+ satisfiedBy: input,
3466
+ }
3467
+ : candidate),
3468
+ };
3469
+ }
3470
+
3471
+ function messageCorrelation(worker: WorkerProjection, messageId: PeerMessageId): string {
3472
+ const message = worker.peerMessages.find((candidate) => candidate.messageId === messageId);
3473
+ if (!message) throw new LedgerIntegrityError(`Unknown peer message ${messageId}`);
3474
+ return message.correlation;
3475
+ }
3476
+
3477
+ function waitCorrelationError(message: string, ledgerReplay: boolean): Error {
3478
+ return ledgerReplay ? new LedgerIntegrityError(message) : new RunConflictError(message);
3479
+ }
3480
+
3481
+ function gatedWorkerState(
3482
+ from: WorkerAttemptState,
3483
+ to: WorkerAttemptState,
3484
+ facts: WorkerGateFacts,
3485
+ ): WorkerAttemptState {
3486
+ if (from === to) return to;
3487
+ const result = evaluateWorkerAttemptTransition(from, to, facts);
3488
+ if (!result.allowed) {
3489
+ throw new LedgerIntegrityError(
3490
+ `Worker Gate refused ${from} -> ${to}: ${result.reason}${
3491
+ result.blockers.length > 0 ? ` (${result.blockers.join(", ")})` : ""
3492
+ }`,
3493
+ );
3494
+ }
3495
+ return to;
3496
+ }
3497
+
3498
+ function runGateFacts(input: {
3499
+ readonly workersDetached?: boolean;
3500
+ readonly reconciliationRecorded?: boolean;
3501
+ readonly uncertaintyCleared?: boolean;
3502
+ } = {}): RunGateFacts {
3503
+ return {
3504
+ objective: {
3505
+ evidenceComplete: false,
3506
+ verifierPassed: false,
3507
+ prerequisitesComplete: false,
3508
+ },
3509
+ workers: {
3510
+ active: false,
3511
+ uncertain: false,
3512
+ detached: input.workersDetached ?? false,
3513
+ },
3514
+ cancellation: { requested: false, settled: false },
3515
+ input: { delivered: false },
3516
+ reconciliation: {
3517
+ recorded: input.reconciliationRecorded ?? false,
3518
+ uncertaintyCleared: input.uncertaintyCleared ?? false,
3519
+ },
3520
+ attention: { cleared: false },
3521
+ };
3522
+ }
3523
+
3524
+ function gatedRunState(
3525
+ from: RunProjectionV2["state"],
3526
+ to: RunProjectionV2["state"],
3527
+ facts: RunGateFacts,
3528
+ ): RunProjectionV2["state"] {
3529
+ if (from === to) return to;
3530
+ const result = evaluateRunTransition(from, to, facts);
3531
+ if (!result.allowed) {
3532
+ throw new LedgerIntegrityError(
3533
+ `Run Gate refused ${from} -> ${to}: ${result.reason}${
3534
+ result.blockers.length > 0 ? ` (${result.blockers.join(", ")})` : ""
3535
+ }`,
3536
+ );
3537
+ }
3538
+ return to;
3539
+ }
3540
+
3541
+ function withRunState(
3542
+ projection: RunProjectionV2,
3543
+ state: RunProjectionV2["state"],
3544
+ facts: RunGateFacts,
3545
+ ): RunProjectionV2 {
3546
+ return { ...projection, state: gatedRunState(projection.state, state, facts) };
3547
+ }
3548
+
3549
+ function updateAttempt(
3550
+ projection: RunProjectionV2,
3551
+ workerId: WorkerId,
3552
+ attemptId: AttemptId,
3553
+ update: (attempt: WorkerAttemptProjection) => WorkerAttemptProjection,
3554
+ workerState?: WorkerProjection["state"],
3555
+ ): RunProjectionV2 {
3556
+ return updateWorker(projection, workerId, (worker) => {
3557
+ let found = false;
3558
+ const attempts = worker.attempts.map((attempt) => {
3559
+ if (attempt.attemptId !== attemptId) return attempt;
3560
+ found = true;
3561
+ return update(attempt);
3562
+ });
3563
+ if (!found) throw new LedgerIntegrityError(`Attempt ${attemptId} is missing from worker ${workerId}`);
3564
+ return { ...worker, ...(workerState ? { state: workerState } : {}), attempts };
3565
+ });
3566
+ }
3567
+
3568
+ function appendAmbiguity(
3569
+ projection: RunProjectionV2,
3570
+ ambiguity: AmbiguityProjection,
3571
+ ): RunProjectionV2 {
3572
+ if (projection.unresolvedAmbiguities.some(
3573
+ (candidate) => candidate.ambiguityId === ambiguity.ambiguityId,
3574
+ )) {
3575
+ throw new LedgerIntegrityError(`Duplicate ambiguity ID ${ambiguity.ambiguityId}`);
3576
+ }
3577
+ return {
3578
+ ...projection,
3579
+ state: gatedRunState(projection.state, "ambiguous", runGateFacts()),
3580
+ unresolvedAmbiguities: [...projection.unresolvedAmbiguities, ambiguity],
3581
+ };
3582
+ }
3583
+
3584
+ function assertAttemptGeneration(
3585
+ attempt: WorkerAttemptProjection,
3586
+ workerAttemptGeneration: string,
3587
+ supervisorGeneration: string,
3588
+ ): void {
3589
+ if (attempt.workerAttemptGeneration !== workerAttemptGeneration
3590
+ || attempt.supervisorGeneration !== supervisorGeneration) {
3591
+ throw new RunConflictError(`Attempt ${attempt.attemptId} generation is stale`);
3592
+ }
3593
+ }
3594
+
3595
+ function assertFoldGeneration(
3596
+ attempt: WorkerAttemptProjection,
3597
+ workerAttemptGeneration: string,
3598
+ supervisorGeneration: string,
3599
+ ): void {
3600
+ if (attempt.workerAttemptGeneration !== workerAttemptGeneration
3601
+ || attempt.supervisorGeneration !== supervisorGeneration) {
3602
+ throw new LedgerIntegrityError(`Attempt ${attempt.attemptId} event generation is stale`);
3603
+ }
3604
+ }
3605
+
3606
+ function isAttemptProcessActive(attempt: WorkerAttemptProjection): boolean {
3607
+ return (
3608
+ (attempt.state === "pending" || attempt.state === "running" || attempt.state === "blocked") &&
3609
+ attempt.pidStartEvidence.observedExitAt === null
3610
+ );
3611
+ }
3612
+
3613
+ function assertContinuationSource(
3614
+ projection: RunProjectionV2,
3615
+ continuation: WorkerContinuationProvenance,
3616
+ sessionPath: string,
3617
+ ): void {
3618
+ const prior = requiredWorker(projection, continuation.priorWorkerId);
3619
+ const attempt = requiredCurrentAttempt(prior);
3620
+ if (attempt.attemptId !== continuation.priorAttemptId) {
3621
+ throw new RunConflictError("Continuation must link the prior worker's current attempt");
3622
+ }
3623
+ if (prior.state !== "done" && prior.state !== "failed" && prior.state !== "cancelled") {
3624
+ throw new RunConflictError(`Continuation prior worker is not terminal: ${prior.state}`);
3625
+ }
3626
+ if (attempt.state !== prior.state) {
3627
+ throw new LedgerIntegrityError("Continuation prior worker and attempt terminal states disagree");
3628
+ }
3629
+ if (attempt.pidStartEvidence.observedExitAt === null
3630
+ || attempt.pidStartEvidence.exitStatus === null) {
3631
+ throw new RunConflictError("Continuation prior process death is not durably observed");
3632
+ }
3633
+ if (attempt.session.file !== continuation.sessionFile) {
3634
+ throw new RunConflictError("Continuation session file does not match the prior attempt");
3635
+ }
3636
+ if (attempt.session.leaseGeneration !== continuation.priorLeaseGeneration) {
3637
+ throw new RunConflictError("Continuation prior lease generation does not match the ledger");
3638
+ }
3639
+ if (attempt.session.path !== sessionPath) {
3640
+ throw new RunConflictError("Continuation session directory does not match the prior attempt");
3641
+ }
3642
+ }
3643
+
3644
+ export function expectedWorkerCadenceMs(budget: WorkerBudget): number {
3645
+ const wallClock = budgetValue(budget.maxWallClockMs);
3646
+ return wallClock === undefined ? 60_000 : Math.max(1_000, Math.min(60_000, Math.floor(wallClock / 4)));
3647
+ }
3648
+
3649
+ function workerBudgetExceeded(attempt: WorkerAttemptProjection, budget: WorkerBudget): boolean {
3650
+ return (
3651
+ budgetExceeded(attempt.usage.tokens, budget.maxTokens)
3652
+ || budgetExceeded(attempt.usage.costUsd, budget.maxCostUsd)
3653
+ || budgetExceeded(attempt.usage.outputBytes, budget.maxOutputBytes)
3654
+ );
3655
+ }
3656
+
3657
+ function existingRun(existing: RunProjectionV2, fingerprintValue: string): StartRunResult {
3658
+ if (existing.requestFingerprint !== fingerprintValue) {
3659
+ throw new RunConflictError(
3660
+ `Run request key ${existing.requestKey} already has a different immutable contract`,
3661
+ );
3662
+ }
3663
+ return { kind: "existing", projection: existing };
3664
+ }
3665
+
3666
+ function eventPath(runDirectory: string, event: WorkflowLedgerEventV1): string {
3667
+ return join(
3668
+ runDirectory,
3669
+ "events",
3670
+ `${formatSequence(event.seq)}.${safePathPart(event.eventId)}.json`,
3671
+ );
3672
+ }
3673
+
3674
+ function formatSequence(sequence: number): string {
3675
+ return sequence.toString().padStart(20, "0");
3676
+ }
3677
+
3678
+ function fingerprint(value: unknown): string {
3679
+ return sha256(canonicalJson(value));
3680
+ }
3681
+
3682
+ function validateRequestKey(value: string): string {
3683
+ requireNonEmpty(value, "request key");
3684
+ if (value.length > 512) throw new TypeError("Request key exceeds 512 characters");
3685
+ return value;
3686
+ }
3687
+
3688
+ function strictRecord(
3689
+ value: unknown,
3690
+ allowed: readonly string[],
3691
+ name: string,
3692
+ ): Record<string, unknown> {
3693
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
3694
+ throw new TypeError(`${name} must be an object`);
3695
+ }
3696
+ const object = value as Record<string, unknown>;
3697
+ const unknown = Object.keys(object).filter((key) => !allowed.includes(key));
3698
+ if (unknown.length > 0) throw new TypeError(`${name} contains unknown fields: ${unknown.join(", ")}`);
3699
+ return object;
3700
+ }
3701
+
3702
+ function recordValue(object: Record<string, unknown>, key: string): Record<string, unknown> {
3703
+ return strictRecord(object[key], Object.keys((object[key] ?? {}) as object), key);
3704
+ }
3705
+
3706
+ function nonEmptyString(object: Record<string, unknown>, key: string): string {
3707
+ const value = object[key];
3708
+ if (typeof value !== "string" || value.length === 0) throw new TypeError(`${key} must be a non-empty string`);
3709
+ return value;
3710
+ }
3711
+
3712
+ function nullableString(object: Record<string, unknown>, key: string): string | null {
3713
+ const value = object[key];
3714
+ if (value === null) return null;
3715
+ if (typeof value !== "string" || value.length === 0) throw new TypeError(`${key} must be null or a non-empty string`);
3716
+ return value;
3717
+ }
3718
+
3719
+ function boundedString(
3720
+ object: Record<string, unknown>,
3721
+ key: string,
3722
+ maxLength: number,
3723
+ ): string {
3724
+ const value = nonEmptyString(object, key);
3725
+ if (value.trim().length === 0) throw new TypeError(`${key} must not be blank`);
3726
+ if (value.length > maxLength) throw new TypeError(`${key} exceeds ${maxLength} characters`);
3727
+ return value;
3728
+ }
3729
+
3730
+ function nullableBoundedString(
3731
+ object: Record<string, unknown>,
3732
+ key: string,
3733
+ maxLength: number,
3734
+ ): string | null {
3735
+ if (object[key] === null) return null;
3736
+ return boundedString(object, key, maxLength);
3737
+ }
3738
+
3739
+ function stringArray(object: Record<string, unknown>, key: string): readonly string[] {
3740
+ const value = object[key];
3741
+ if (!Array.isArray(value)) throw new TypeError(`${key} must be an array`);
3742
+ return value.map((item) => {
3743
+ if (typeof item !== "string" || item.length === 0) throw new TypeError(`${key} entries must be non-empty strings`);
3744
+ return item;
3745
+ });
3746
+ }
3747
+
3748
+ function nonEmptyStringArray(object: Record<string, unknown>, key: string): readonly string[] {
3749
+ const value = stringArray(object, key);
3750
+ if (value.length === 0) throw new TypeError(`${key} must not be empty`);
3751
+ return value;
3752
+ }
3753
+
3754
+ function numberValue(object: Record<string, unknown>, key: string): number {
3755
+ const value = object[key];
3756
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new TypeError(`${key} must be a number`);
3757
+ return value;
3758
+ }
3759
+
3760
+ function nonNegativeInteger(object: Record<string, unknown>, key: string): number {
3761
+ const value = numberValue(object, key);
3762
+ if (!Number.isSafeInteger(value) || value < 0) throw new TypeError(`${key} must be a non-negative integer`);
3763
+ return value;
3764
+ }
3765
+
3766
+ function nonNegativeNumber(object: Record<string, unknown>, key: string): number {
3767
+ const value = numberValue(object, key);
3768
+ if (value < 0) throw new TypeError(`${key} must be non-negative`);
3769
+ return value;
3770
+ }
3771
+
3772
+ function positiveInteger(object: Record<string, unknown>, key: string): number {
3773
+ const value = numberValue(object, key);
3774
+ if (!Number.isSafeInteger(value) || value <= 0) throw new TypeError(`${key} must be a positive integer`);
3775
+ return value;
3776
+ }
3777
+
3778
+ function positiveIntegerValue(value: number, name: string): number {
3779
+ if (!Number.isSafeInteger(value) || value <= 0) {
3780
+ throw new TypeError(`${name} must be a positive integer`);
3781
+ }
3782
+ return value;
3783
+ }
3784
+
3785
+ function nullablePositiveInteger(object: Record<string, unknown>, key: string): number | null {
3786
+ if (object[key] === null) return null;
3787
+ return positiveInteger(object, key);
3788
+ }
3789
+
3790
+ function booleanValue(object: Record<string, unknown>, key: string): boolean {
3791
+ const value = object[key];
3792
+ if (typeof value !== "boolean") throw new TypeError(`${key} must be a boolean`);
3793
+ return value;
3794
+ }
3795
+
3796
+ function nullableBoolean(object: Record<string, unknown>, key: string): boolean | null {
3797
+ return object[key] === null ? null : booleanValue(object, key);
3798
+ }
3799
+
3800
+ function nullableNonNegativeInteger(
3801
+ object: Record<string, unknown>,
3802
+ key: string,
3803
+ ): number | null {
3804
+ return object[key] === null ? null : nonNegativeInteger(object, key);
3805
+ }
3806
+
3807
+ function positiveNumber(object: Record<string, unknown>, key: string): number {
3808
+ const value = numberValue(object, key);
3809
+ if (value <= 0) throw new TypeError(`${key} must be positive`);
3810
+ return value;
3811
+ }
3812
+
3813
+ function enumValue<T extends string>(
3814
+ object: Record<string, unknown>,
3815
+ key: string,
3816
+ allowed: readonly T[],
3817
+ ): T {
3818
+ const value = nonEmptyString(object, key);
3819
+ if (!allowed.includes(value as T)) throw new TypeError(`${key} is invalid`);
3820
+ return value as T;
3821
+ }
3822
+
3823
+ function prefixedId(object: Record<string, unknown>, key: string, prefix: string): string {
3824
+ const value = nonEmptyString(object, key);
3825
+ if (!value.startsWith(`${prefix}_`)) throw new TypeError(`${key} must start with ${prefix}_`);
3826
+ return value;
3827
+ }
3828
+
3829
+ function nullablePrefixedId(
3830
+ object: Record<string, unknown>,
3831
+ key: string,
3832
+ prefix: string,
3833
+ ): string | null {
3834
+ if (object[key] === null) return null;
3835
+ return prefixedId(object, key, prefix);
3836
+ }
3837
+
3838
+ function hashValue(object: Record<string, unknown>, key: string): string {
3839
+ const value = nonEmptyString(object, key);
3840
+ if (!/^sha256:[0-9a-f]{64}$/.test(value)) throw new TypeError(`${key} must be a sha256 hash`);
3841
+ return value;
3842
+ }
3843
+
3844
+ function requireSha256(value: string, label: string): string {
3845
+ const normalized = requireNonEmpty(value, label);
3846
+ if (!/^sha256:[a-f0-9]{64}$/.test(normalized)) throw new TypeError(`${label} must be a SHA-256 hash`);
3847
+ return normalized;
3848
+ }
3849
+
3850
+ function nullableHash(object: Record<string, unknown>, key: string): string | null {
3851
+ if (object[key] === null) return null;
3852
+ return hashValue(object, key);
3853
+ }
3854
+
3855
+ function isoTimestamp(object: Record<string, unknown>, key: string): string {
3856
+ const value = nonEmptyString(object, key);
3857
+ if (!Number.isFinite(Date.parse(value))) throw new TypeError(`${key} must be an ISO timestamp`);
3858
+ return value;
3859
+ }
3860
+
3861
+ function nullableIsoTimestamp(object: Record<string, unknown>, key: string): string | null {
3862
+ if (object[key] === null) return null;
3863
+ return isoTimestamp(object, key);
3864
+ }
3865
+
3866
+ function absolutePath(object: Record<string, unknown>, key: string): string {
3867
+ const value = nonEmptyString(object, key);
3868
+ if (!isAbsolute(value)) throw new TypeError(`${key} must be absolute`);
3869
+ return normalize(value);
3870
+ }
3871
+
3872
+ function nullableAbsolutePath(object: Record<string, unknown>, key: string): string | null {
3873
+ if (object[key] === null) return null;
3874
+ return absolutePath(object, key);
3875
+ }
3876
+
3877
+ function requireNonEmpty(value: string, name: string): string {
3878
+ if (value.length === 0) throw new TypeError(`${name} is required`);
3879
+ return value;
3880
+ }
3881
+
3882
+ function requireBoundedText(value: string, name: string, maxLength: number): string {
3883
+ if (value.trim().length === 0) throw new TypeError(`${name} is required`);
3884
+ if (value.length > maxLength) throw new TypeError(`${name} exceeds ${maxLength} characters`);
3885
+ return value;
3886
+ }
3887
+
3888
+ function safePathPart(value: string): string {
3889
+ return value.replace(/[^A-Za-z0-9._-]/g, "_");
3890
+ }