@evomap/evolver-core 2.0.0-beta.2 → 2.0.0-beta.22

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 (240) hide show
  1. package/assets/gep/genes.jsonl +5 -5
  2. package/dist/algo/bans.d.ts +10 -1
  3. package/dist/algo/bans.js +56 -6
  4. package/dist/algo/candidateAssembly.d.ts +25 -2
  5. package/dist/algo/candidateAssembly.js +171 -26
  6. package/dist/algo/capabilityCandidates.js +10 -0
  7. package/dist/algo/conversationSniffer.js +25 -8
  8. package/dist/algo/cycleEngine.d.ts +63 -5
  9. package/dist/algo/cycleEngine.js +320 -42
  10. package/dist/algo/cycleFailureClassifier.d.ts +1 -1
  11. package/dist/algo/evolutionEvent.d.ts +26 -0
  12. package/dist/algo/evolutionEvent.js +32 -0
  13. package/dist/algo/exploration.d.ts +7 -0
  14. package/dist/algo/exploration.js +16 -3
  15. package/dist/algo/geneHealth.d.ts +36 -3
  16. package/dist/algo/geneHealth.js +47 -4
  17. package/dist/algo/geneIntake.d.ts +46 -1
  18. package/dist/algo/geneIntake.js +121 -10
  19. package/dist/algo/geneSelection.d.ts +126 -6
  20. package/dist/algo/geneSelection.js +479 -36
  21. package/dist/algo/index.d.ts +6 -1
  22. package/dist/algo/index.js +6 -1
  23. package/dist/algo/kautoProjection.d.ts +41 -0
  24. package/dist/algo/kautoProjection.js +95 -0
  25. package/dist/algo/kautoValidator.d.ts +68 -0
  26. package/dist/algo/kautoValidator.js +256 -0
  27. package/dist/algo/memoryGraph.d.ts +62 -0
  28. package/dist/algo/memoryGraph.js +86 -0
  29. package/dist/algo/orchestrator.d.ts +17 -1
  30. package/dist/algo/orchestrator.js +30 -4
  31. package/dist/algo/publishEligibility.d.ts +34 -0
  32. package/dist/algo/publishEligibility.js +52 -0
  33. package/dist/algo/solidify.d.ts +11 -2
  34. package/dist/algo/solidify.js +37 -7
  35. package/dist/algo/ucb1.d.ts +53 -0
  36. package/dist/algo/ucb1.js +156 -0
  37. package/dist/assetrepair/hubRejection.d.ts +12 -0
  38. package/dist/assetrepair/hubRejection.js +109 -0
  39. package/dist/assetrepair/index.d.ts +2 -0
  40. package/dist/assetrepair/index.js +2 -0
  41. package/dist/assetrepair/repair.d.ts +33 -0
  42. package/dist/assetrepair/repair.js +155 -0
  43. package/dist/assetstore/assetSidecarRecords.d.ts +28 -0
  44. package/dist/assetstore/assetSidecarRecords.js +384 -0
  45. package/dist/assetstore/assetSidecarRecovery.d.ts +48 -0
  46. package/dist/assetstore/assetSidecarRecovery.js +288 -0
  47. package/dist/assetstore/assetStoreHealth.d.ts +81 -0
  48. package/dist/assetstore/assetStoreHealth.js +319 -0
  49. package/dist/assetstore/assetStoreLayout.d.ts +2 -0
  50. package/dist/assetstore/assetStoreLayout.js +6 -0
  51. package/dist/assetstore/assetStoreStorage.d.ts +42 -0
  52. package/dist/assetstore/assetStoreStorage.js +336 -0
  53. package/dist/assetstore/assetSyncLedger.d.ts +91 -1
  54. package/dist/assetstore/assetSyncLedger.js +718 -59
  55. package/dist/assetstore/foreignJsonlSource.d.ts +48 -0
  56. package/dist/assetstore/foreignJsonlSource.js +150 -0
  57. package/dist/assetstore/index.d.ts +5 -0
  58. package/dist/assetstore/index.js +5 -0
  59. package/dist/assetstore/learningHistory.js +3 -3
  60. package/dist/assetstore/localAssetStoreSnapshot.d.ts +51 -0
  61. package/dist/assetstore/localAssetStoreSnapshot.js +329 -0
  62. package/dist/assetstore/localJsonl.d.ts +12 -1
  63. package/dist/assetstore/localJsonl.js +251 -42
  64. package/dist/assetstore/provenance.d.ts +93 -4
  65. package/dist/assetstore/provenance.js +368 -84
  66. package/dist/assetstore/provider.d.ts +63 -0
  67. package/dist/assetstore/provider.js +97 -6
  68. package/dist/assetstore/reviewFilter.d.ts +19 -1
  69. package/dist/assetstore/reviewFilter.js +39 -1
  70. package/dist/assetstore/reviewLedger.d.ts +8 -2
  71. package/dist/assetstore/reviewLedger.js +71 -45
  72. package/dist/assetstore/unionReadStore.d.ts +25 -0
  73. package/dist/assetstore/unionReadStore.js +119 -0
  74. package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
  75. package/dist/benchmark/antiGeneBenchmark.js +4 -3
  76. package/dist/benchmark/antiGeneRollout.d.ts +2 -0
  77. package/dist/benchmark/antiGeneRollout.js +4 -3
  78. package/dist/benchmark/index.d.ts +3 -1
  79. package/dist/benchmark/index.js +3 -1
  80. package/dist/benchmark/selectionFlatAbstention.d.ts +152 -0
  81. package/dist/benchmark/selectionFlatAbstention.js +481 -0
  82. package/dist/benchmark/triggerShift.d.ts +62 -0
  83. package/dist/benchmark/triggerShift.js +106 -0
  84. package/dist/bootstrap/envFingerprint.d.ts +9 -0
  85. package/dist/bootstrap/envFingerprint.js +5 -0
  86. package/dist/bootstrap/index.d.ts +3 -1
  87. package/dist/bootstrap/index.js +3 -1
  88. package/dist/bootstrap/lifecycleBootstrap.d.ts +111 -0
  89. package/dist/bootstrap/lifecycleBootstrap.js +433 -0
  90. package/dist/bootstrap/v1EnvCompat.d.ts +113 -0
  91. package/dist/bootstrap/v1EnvCompat.js +303 -0
  92. package/dist/events/eventArchive.d.ts +2 -0
  93. package/dist/events/eventArchive.js +13 -3
  94. package/dist/events/eventSchema.d.ts +7 -7
  95. package/dist/events/eventStore.d.ts +2 -0
  96. package/dist/events/eventStore.js +5 -1
  97. package/dist/events/ingest.d.ts +2 -1
  98. package/dist/events/ingest.js +15 -0
  99. package/dist/events/paths.d.ts +12 -10
  100. package/dist/events/paths.js +24 -20
  101. package/dist/events/public.d.ts +2 -2
  102. package/dist/events/public.js +2 -2
  103. package/dist/events/reports.d.ts +2 -0
  104. package/dist/events/reports.js +4 -0
  105. package/dist/exec/autoExec.d.ts +68 -4
  106. package/dist/exec/autoExec.js +392 -33
  107. package/dist/exec/autonomousCycle.d.ts +31 -4
  108. package/dist/exec/autonomousCycle.js +71 -13
  109. package/dist/exec/claudeBridge.d.ts +80 -15
  110. package/dist/exec/claudeBridge.js +845 -76
  111. package/dist/exec/executionBinding.d.ts +414 -0
  112. package/dist/exec/executionBinding.js +588 -0
  113. package/dist/exec/index.d.ts +1 -0
  114. package/dist/exec/index.js +1 -0
  115. package/dist/exec/openPrRegistry.d.ts +8 -2
  116. package/dist/exec/openPrRegistry.js +32 -22
  117. package/dist/exec/prompt.js +14 -1
  118. package/dist/exec/proofOfWork.d.ts +1 -1
  119. package/dist/exec/proofOfWork.js +2 -2
  120. package/dist/exec/runnerRegistry.d.ts +153 -36
  121. package/dist/exec/runnerRegistry.js +848 -65
  122. package/dist/exec/selfPr.js +1 -7
  123. package/dist/feedback/envelope.d.ts +61 -0
  124. package/dist/feedback/envelope.js +168 -0
  125. package/dist/feedback/index.d.ts +1 -0
  126. package/dist/feedback/index.js +1 -0
  127. package/dist/hooks/hooks.js +1 -0
  128. package/dist/hub/assetCallLog.d.ts +35 -1
  129. package/dist/hub/assetCallLog.js +124 -1
  130. package/dist/hub/bindings.d.ts +8 -1
  131. package/dist/hub/bindings.js +29 -8
  132. package/dist/hub/capability.d.ts +130 -4
  133. package/dist/hub/conversationDistiller.d.ts +19 -0
  134. package/dist/hub/conversationDistiller.js +115 -37
  135. package/dist/hub/fake.d.ts +3 -2
  136. package/dist/hub/fake.js +2 -1
  137. package/dist/hub/index.d.ts +1 -0
  138. package/dist/hub/index.js +1 -0
  139. package/dist/hub/questionGenerator.d.ts +5 -1
  140. package/dist/hub/questionGenerator.js +8 -6
  141. package/dist/hub/recipeCompose.d.ts +27 -0
  142. package/dist/hub/recipeCompose.js +90 -0
  143. package/dist/hub/sanitize.js +122 -7
  144. package/dist/index.d.ts +6 -1
  145. package/dist/index.js +7 -1
  146. package/dist/issueReporter/index.d.ts +156 -0
  147. package/dist/issueReporter/index.js +1688 -0
  148. package/dist/mailbox/dispatch.d.ts +1 -1
  149. package/dist/mailbox/dispatch.js +22 -6
  150. package/dist/mailbox/envelope.d.ts +7 -1
  151. package/dist/mailbox/envelope.js +9 -2
  152. package/dist/mailbox/ipcServer.d.ts +12 -2
  153. package/dist/mailbox/ipcServer.js +183 -13
  154. package/dist/mailbox/store.d.ts +89 -3
  155. package/dist/mailbox/store.js +895 -41
  156. package/dist/modelCompatibility.d.ts +164 -0
  157. package/dist/modelCompatibility.js +309 -0
  158. package/dist/observers/valueDigestObserver.d.ts +9 -0
  159. package/dist/observers/valueDigestObserver.js +35 -2
  160. package/dist/ops/cleanup.js +1 -1
  161. package/dist/ops/evolutionGraphProjection.d.ts +20 -0
  162. package/dist/ops/evolutionGraphProjection.js +315 -0
  163. package/dist/ops/index.d.ts +2 -1
  164. package/dist/ops/index.js +2 -1
  165. package/dist/ops/savingsCore.js +1 -2
  166. package/dist/ops/selfUpdate.d.ts +18 -1
  167. package/dist/ops/selfUpdate.js +88 -23
  168. package/dist/ops/valueOutreach.d.ts +3 -1
  169. package/dist/ops/valueOutreach.js +5 -1
  170. package/dist/personality/schema.d.ts +24 -24
  171. package/dist/schema/evolutionGraph.d.ts +784 -0
  172. package/dist/schema/evolutionGraph.js +187 -0
  173. package/dist/schema/index.d.ts +1 -0
  174. package/dist/schema/index.js +1 -0
  175. package/dist/schema/proofOfWork.d.ts +125 -6
  176. package/dist/schema/proofOfWork.js +102 -4
  177. package/dist/schema/signal.d.ts +3 -3
  178. package/dist/schema/signal.js +1 -1
  179. package/dist/shadow/shadowHub.js +1 -0
  180. package/dist/signals/curriculum.d.ts +55 -0
  181. package/dist/signals/curriculum.js +202 -0
  182. package/dist/signals/cycleHistoryFromEvents.js +17 -8
  183. package/dist/signals/expand.d.ts +15 -1
  184. package/dist/signals/expand.js +169 -1
  185. package/dist/signals/extractor.d.ts +2 -2
  186. package/dist/signals/extractor.js +31 -6
  187. package/dist/signals/index.d.ts +4 -1
  188. package/dist/signals/index.js +4 -1
  189. package/dist/signals/metaSignals.d.ts +4 -0
  190. package/dist/signals/metaSignals.js +42 -0
  191. package/dist/signals/scopeVocabulary.d.ts +75 -0
  192. package/dist/signals/scopeVocabulary.js +91 -0
  193. package/dist/signals/signalGate.js +1 -1
  194. package/dist/signals/taskDomain.d.ts +22 -0
  195. package/dist/signals/taskDomain.js +43 -0
  196. package/dist/strategy/constraintAblation.d.ts +64 -0
  197. package/dist/strategy/constraintAblation.js +2820 -0
  198. package/dist/strategy/constraintAblationPredicates.d.ts +31 -0
  199. package/dist/strategy/constraintAblationPredicates.js +339 -0
  200. package/dist/strategy/index.d.ts +2 -1
  201. package/dist/strategy/index.js +2 -1
  202. package/dist/trace/index.d.ts +3 -1
  203. package/dist/trace/index.js +3 -1
  204. package/dist/trace/learningTrace.d.ts +216 -0
  205. package/dist/trace/learningTrace.js +298 -0
  206. package/dist/trace/proxyTurns.d.ts +31 -0
  207. package/dist/trace/proxyTurns.js +137 -0
  208. package/dist/trace/trajectory.d.ts +8 -0
  209. package/dist/trace/trajectory.js +14 -2
  210. package/dist/util/fetchPort.d.ts +1 -0
  211. package/dist/util/fetchPort.js +11 -0
  212. package/dist/util/fileLock.d.ts +113 -7
  213. package/dist/util/fileLock.js +1035 -80
  214. package/dist/util/index.d.ts +3 -1
  215. package/dist/util/index.js +2 -1
  216. package/dist/verify/index.d.ts +2 -1
  217. package/dist/verify/index.js +1 -1
  218. package/dist/verify/sandboxRunner.d.ts +30 -0
  219. package/dist/verify/sandboxRunner.js +370 -27
  220. package/dist/verify/sandboxedValidation.d.ts +16 -2
  221. package/dist/verify/sandboxedValidation.js +200 -22
  222. package/dist/verify/validation.d.ts +35 -4
  223. package/dist/verify/validation.js +164 -16
  224. package/dist/wire/geneHints.d.ts +88 -16
  225. package/dist/wire/geneHints.js +124 -15
  226. package/dist/wire/index.d.ts +8 -3
  227. package/dist/wire/index.js +2 -2
  228. package/dist/wire/schemaGate.d.ts +21 -0
  229. package/dist/wire/schemaGate.js +119 -10
  230. package/dist/workflow/dsl.d.ts +24 -3
  231. package/dist/workflow/dsl.js +4 -0
  232. package/dist/workflow/engine.d.ts +5 -1
  233. package/dist/workflow/engine.js +3 -0
  234. package/dist/workflow/index.d.ts +3 -1
  235. package/dist/workflow/index.js +3 -1
  236. package/dist/workflow/runtime.d.ts +110 -0
  237. package/dist/workflow/runtime.js +1298 -0
  238. package/dist/workflow/stateStore.d.ts +172 -0
  239. package/dist/workflow/stateStore.js +1044 -0
  240. package/package.json +12 -4
@@ -0,0 +1,1298 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import { monotonicFactory } from 'ulid';
4
+ import { containsWorkflowSensitiveText, CORE_WHITELIST, isRef, } from './dsl.js';
5
+ import { assertStableId, deriveWorkflowDefinitionDigest, findWorkflowJsonViolation, WORKFLOW_DEFINITION_SCHEMA_VERSION, WORKFLOW_RUN_SCHEMA_VERSION, WorkflowStateError, WorkflowStateStore, } from './stateStore.js';
6
+ const makeUlid = monotonicFactory();
7
+ const MAX_RETRY_ATTEMPTS = 10;
8
+ const MAX_BACKOFF_MS = 300_000;
9
+ const DEFAULT_MAX_CONCURRENT_RUNS = 4;
10
+ const MAX_CONCURRENT_RUNS = 64;
11
+ const DEFAULT_MAX_QUEUED_RUNS = 256;
12
+ const MAX_QUEUED_RUNS = 4_096;
13
+ const DEFAULT_MAX_STEP_EXECUTIONS = 10_000;
14
+ const MAX_STEP_EXECUTIONS = 100_000;
15
+ const CONTROL_POLL_INTERVAL_MS = 50;
16
+ export const MAX_WORKFLOW_DEFINITION_STEPS = 256;
17
+ export const MAX_WORKFLOW_NESTING_DEPTH = 16;
18
+ export const MAX_WORKFLOW_FOREACH_ITEMS = 1_000;
19
+ const SENSITIVE_KEY_RE = /(?:password|passwd|secret|token|api[_-]?key|authorization|cookie|private[_-]?key|credentials?|access[_-]?key)(?:[_-]?(?:id|file|path))?$/i;
20
+ const NON_SECRET_CONTAINER_KEYS = new Set(['input', 'output', 'outputs']);
21
+ export class WorkflowRuntimeError extends Error {
22
+ code;
23
+ constructor(message, code) {
24
+ super(message);
25
+ this.code = code;
26
+ this.name = 'WorkflowRuntimeError';
27
+ }
28
+ }
29
+ export class WorkflowRunFailedError extends Error {
30
+ runId;
31
+ status;
32
+ errorClass;
33
+ constructor(runId, status, errorClass) {
34
+ super(`workflow run ${runId} ended with status ${status} (${errorClass ?? 'unknown'})`);
35
+ this.runId = runId;
36
+ this.status = status;
37
+ this.errorClass = errorClass;
38
+ this.name = 'WorkflowRunFailedError';
39
+ }
40
+ }
41
+ export class ClassifiedWorkflowError extends Error {
42
+ errorClass;
43
+ constructor(errorClass, message = 'workflow step failed') {
44
+ super(message);
45
+ this.errorClass = errorClass;
46
+ this.name = 'ClassifiedWorkflowError';
47
+ }
48
+ }
49
+ class DeterministicWorkflowError extends Error {
50
+ constructor(message) {
51
+ super(message);
52
+ this.name = 'DeterministicWorkflowError';
53
+ }
54
+ }
55
+ class WorkflowSuspendedError extends Error {
56
+ constructor() {
57
+ super('workflow execution suspended at a durable operator boundary');
58
+ this.name = 'WorkflowSuspendedError';
59
+ }
60
+ }
61
+ class CheckpointInterruptionError extends Error {
62
+ interruption;
63
+ constructor(interruption) {
64
+ super(interruption instanceof Error ? interruption.message : 'workflow checkpoint hook interrupted execution');
65
+ this.interruption = interruption;
66
+ this.name = 'CheckpointInterruptionError';
67
+ }
68
+ }
69
+ class BoundedScheduler {
70
+ limit;
71
+ maxQueuedRuns;
72
+ active = 0;
73
+ queue = [];
74
+ admissionWaiters = new Set();
75
+ constructor(limit, maxQueuedRuns) {
76
+ this.limit = limit;
77
+ this.maxQueuedRuns = maxQueuedRuns;
78
+ }
79
+ schedule(run) {
80
+ if (!this.hasAdmissionCapacity()) {
81
+ throw new WorkflowRuntimeError('workflow scheduler waiting queue limit exceeded', 'RESOURCE_LIMIT');
82
+ }
83
+ return new Promise((resolve, reject) => {
84
+ this.queue.push({ run, resolve: resolve, reject });
85
+ this.drain();
86
+ });
87
+ }
88
+ async waitForAdmission() {
89
+ while (!this.hasAdmissionCapacity()) {
90
+ await new Promise((resolve) => { this.admissionWaiters.add(resolve); });
91
+ }
92
+ }
93
+ hasAdmissionCapacity() {
94
+ return this.active < this.limit || this.queue.length < this.maxQueuedRuns;
95
+ }
96
+ notifyAdmissionWaiters() {
97
+ if (!this.hasAdmissionCapacity())
98
+ return;
99
+ const waiters = [...this.admissionWaiters];
100
+ this.admissionWaiters.clear();
101
+ for (const resolve of waiters)
102
+ resolve();
103
+ }
104
+ drain() {
105
+ while (this.active < this.limit) {
106
+ const job = this.queue.shift();
107
+ if (!job)
108
+ return;
109
+ this.notifyAdmissionWaiters();
110
+ this.active += 1;
111
+ void job.run()
112
+ .then(job.resolve, job.reject)
113
+ .finally(() => {
114
+ this.active -= 1;
115
+ this.notifyAdmissionWaiters();
116
+ this.drain();
117
+ });
118
+ }
119
+ }
120
+ }
121
+ export function defaultWorkflowStateDir() {
122
+ return join(homedir(), '.evolver', 'workflows');
123
+ }
124
+ function stableJson(value) {
125
+ if (Array.isArray(value))
126
+ return `[${value.map(stableJson).join(',')}]`;
127
+ if (typeof value === 'object' && value !== null) {
128
+ return `{${Object.entries(value)
129
+ .filter(([key]) => key !== 'workflowId')
130
+ .sort(([a], [b]) => a.localeCompare(b))
131
+ .map(([key, child]) => `${JSON.stringify(key)}:${stableJson(child)}`).join(',')}}`;
132
+ }
133
+ return JSON.stringify(value) ?? 'null';
134
+ }
135
+ export function deriveWorkflowId(spec) {
136
+ const violation = findWorkflowJsonViolation(spec);
137
+ if (violation)
138
+ throw new WorkflowRuntimeError(`workflow definition contains a non-JSON value at ${violation}`, 'INVALID_STATE');
139
+ if (spec.workflowId !== undefined) {
140
+ assertStableId(spec.workflowId, 'workflowId');
141
+ return spec.workflowId;
142
+ }
143
+ return `wf_${deriveWorkflowDefinitionDigest(spec).slice(0, 24)}`;
144
+ }
145
+ function findSensitivePath(value, path = '$', seen = new Set()) {
146
+ if (typeof value === 'string')
147
+ return containsWorkflowSensitiveText(value) ? path : null;
148
+ if (typeof value !== 'object' || value === null)
149
+ return null;
150
+ if (seen.has(value))
151
+ return null;
152
+ seen.add(value);
153
+ if (Array.isArray(value)) {
154
+ for (let index = 0; index < value.length; index += 1) {
155
+ const found = findSensitivePath(value[index], `${path}[${index}]`, seen);
156
+ if (found)
157
+ return found;
158
+ }
159
+ return null;
160
+ }
161
+ for (const [key, child] of Object.entries(value)) {
162
+ const childPath = `${path}.${key}`;
163
+ if (!NON_SECRET_CONTAINER_KEYS.has(key) && SENSITIVE_KEY_RE.test(key) && child !== undefined && child !== null && child !== '')
164
+ return childPath;
165
+ const found = findSensitivePath(child, childPath, seen);
166
+ if (found)
167
+ return found;
168
+ }
169
+ return null;
170
+ }
171
+ function assertPersistable(value) {
172
+ const sensitivePath = findSensitivePath(value);
173
+ if (sensitivePath) {
174
+ throw new WorkflowRuntimeError(`refusing to persist sensitive workflow data at ${sensitivePath}`, 'SENSITIVE_STATE');
175
+ }
176
+ const violation = findWorkflowJsonViolation(value);
177
+ if (violation)
178
+ throw new WorkflowRuntimeError(`workflow state contains a non-JSON value at ${violation}`, 'INVALID_STATE');
179
+ }
180
+ function invalidWorkflowSpec(path) {
181
+ throw new WorkflowRuntimeError(`workflow definition is malformed at ${path}`, 'INVALID_STATE');
182
+ }
183
+ function isRecord(value) {
184
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
185
+ }
186
+ function assertValueShape(value, path) {
187
+ if (value === null || typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number')
188
+ return;
189
+ if (!isRecord(value) || typeof value['ref'] !== 'string')
190
+ invalidWorkflowSpec(path);
191
+ }
192
+ function assertRetryShape(value, path) {
193
+ if (value === undefined)
194
+ return;
195
+ if (!isRecord(value) || typeof value['maxAttempts'] !== 'number')
196
+ invalidWorkflowSpec(path);
197
+ for (const key of ['initialDelayMs', 'maxDelayMs', 'multiplier']) {
198
+ if (value[key] !== undefined && typeof value[key] !== 'number')
199
+ invalidWorkflowSpec(`${path}.${key}`);
200
+ }
201
+ }
202
+ function workflowResourceLimit(message) {
203
+ throw new WorkflowRuntimeError(message, 'RESOURCE_LIMIT');
204
+ }
205
+ function assertStepArrayShape(value, path, ancestors = new Set(), budget = { steps: 0 }, depth = 0) {
206
+ if (depth > MAX_WORKFLOW_NESTING_DEPTH) {
207
+ workflowResourceLimit(`workflow definition nesting exceeds ${MAX_WORKFLOW_NESTING_DEPTH} at ${path}`);
208
+ }
209
+ if (!Array.isArray(value) || ancestors.has(value))
210
+ invalidWorkflowSpec(path);
211
+ ancestors.add(value);
212
+ for (let index = 0; index < value.length; index += 1) {
213
+ budget.steps += 1;
214
+ if (budget.steps > MAX_WORKFLOW_DEFINITION_STEPS) {
215
+ workflowResourceLimit(`workflow definition exceeds ${MAX_WORKFLOW_DEFINITION_STEPS} steps`);
216
+ }
217
+ const stepPath = `${path}[${index}]`;
218
+ const candidate = value[index];
219
+ if (!isRecord(candidate) || typeof candidate['id'] !== 'string' || typeof candidate['kind'] !== 'string') {
220
+ invalidWorkflowSpec(stepPath);
221
+ }
222
+ const kind = candidate['kind'];
223
+ if (kind === 'script' || kind === 'agent') {
224
+ if (candidate['idempotency'] !== undefined
225
+ && candidate['idempotency'] !== 'idempotent'
226
+ && candidate['idempotency'] !== 'non_idempotent') {
227
+ invalidWorkflowSpec(`${stepPath}.idempotency`);
228
+ }
229
+ assertRetryShape(candidate['retry'], `${stepPath}.retry`);
230
+ }
231
+ if (kind === 'script') {
232
+ const call = candidate['call'];
233
+ if (typeof candidate['setOutput'] !== 'string' || !isRecord(call)
234
+ || typeof call['fn'] !== 'string' || !Array.isArray(call['args'])) {
235
+ invalidWorkflowSpec(stepPath);
236
+ }
237
+ for (let arg = 0; arg < call['args'].length; arg += 1) {
238
+ assertValueShape(call['args'][arg], `${stepPath}.call.args[${arg}]`);
239
+ }
240
+ }
241
+ else if (kind === 'agent') {
242
+ if (typeof candidate['prompt'] !== 'string'
243
+ || (candidate['outputKey'] !== undefined && typeof candidate['outputKey'] !== 'string')) {
244
+ invalidWorkflowSpec(stepPath);
245
+ }
246
+ }
247
+ else if (kind === 'approval') {
248
+ if (candidate['label'] !== undefined && typeof candidate['label'] !== 'string')
249
+ invalidWorkflowSpec(stepPath);
250
+ }
251
+ else if (kind === 'if') {
252
+ assertValueShape(candidate['cond'], `${stepPath}.cond`);
253
+ assertStepArrayShape(candidate['then'], `${stepPath}.then`, ancestors, budget, depth + 1);
254
+ if (candidate['else'] !== undefined) {
255
+ assertStepArrayShape(candidate['else'], `${stepPath}.else`, ancestors, budget, depth + 1);
256
+ }
257
+ }
258
+ else if (kind === 'foreach') {
259
+ if (!isRecord(candidate['over']) || typeof candidate['over']['ref'] !== 'string'
260
+ || typeof candidate['as'] !== 'string'
261
+ || (candidate['parallel'] !== undefined && typeof candidate['parallel'] !== 'boolean')
262
+ || (candidate['collect'] !== undefined && typeof candidate['collect'] !== 'string')) {
263
+ invalidWorkflowSpec(stepPath);
264
+ }
265
+ assertStepArrayShape(candidate['body'], `${stepPath}.body`, ancestors, budget, depth + 1);
266
+ }
267
+ else {
268
+ invalidWorkflowSpec(`${stepPath}.kind`);
269
+ }
270
+ }
271
+ ancestors.delete(value);
272
+ }
273
+ export function assertValidWorkflowSpec(value) {
274
+ if (!isRecord(value) || typeof value['name'] !== 'string'
275
+ || (value['workflowId'] !== undefined && typeof value['workflowId'] !== 'string')
276
+ || (value['input'] !== undefined && !isRecord(value['input']))) {
277
+ invalidWorkflowSpec('$');
278
+ }
279
+ if (value['output'] !== undefined
280
+ && (!isRecord(value['output']) || typeof value['output']['ref'] !== 'string')) {
281
+ invalidWorkflowSpec('$.output');
282
+ }
283
+ assertStepArrayShape(value['steps'], '$.steps');
284
+ validateSteps(value['steps']);
285
+ }
286
+ function resolveRef(ref, ctx) {
287
+ const [head, ...rest] = ref.split('.');
288
+ let base;
289
+ if (head === 'input')
290
+ base = ctx.input;
291
+ else if (head === 'steps') {
292
+ const stepId = rest.shift();
293
+ base = stepId !== undefined ? ctx.steps[stepId] : undefined;
294
+ }
295
+ else if (head === 'item')
296
+ base = ctx.item;
297
+ else
298
+ throw new DeterministicWorkflowError(`invalid workflow reference root: ${head}`);
299
+ for (const key of rest) {
300
+ if (base == null)
301
+ return undefined;
302
+ if (typeof base !== 'object')
303
+ return undefined;
304
+ base = base[key];
305
+ }
306
+ return base;
307
+ }
308
+ function evalValue(value, ctx) {
309
+ return isRef(value) ? resolveRef(value.ref, ctx) : value;
310
+ }
311
+ function evalCore(call, ctx, core) {
312
+ const fn = core[call.fn];
313
+ if (!fn)
314
+ throw new DeterministicWorkflowError(`core function is not allowlisted: ${call.fn}`);
315
+ return fn(...call.args.map((arg) => evalValue(arg, ctx)));
316
+ }
317
+ function retrySettings(step) {
318
+ const retry = step.retry;
319
+ const maxAttempts = retry?.maxAttempts ?? 1;
320
+ const initialDelayMs = retry?.initialDelayMs ?? 1_000;
321
+ const maxDelayMs = retry?.maxDelayMs ?? 30_000;
322
+ const multiplier = retry?.multiplier ?? 2;
323
+ if (!Number.isInteger(maxAttempts) || maxAttempts < 1 || maxAttempts > MAX_RETRY_ATTEMPTS
324
+ || !Number.isFinite(initialDelayMs) || initialDelayMs < 0 || initialDelayMs > MAX_BACKOFF_MS
325
+ || !Number.isFinite(maxDelayMs) || maxDelayMs < initialDelayMs || maxDelayMs > MAX_BACKOFF_MS
326
+ || !Number.isFinite(multiplier) || multiplier < 1 || multiplier > 10) {
327
+ throw new WorkflowRuntimeError('invalid bounded workflow retry policy', 'INVALID_RETRY');
328
+ }
329
+ return { maxAttempts, initialDelayMs, maxDelayMs, multiplier };
330
+ }
331
+ function backoffMs(settings, attempts) {
332
+ return Math.min(settings.maxDelayMs, Math.round(settings.initialDelayMs * settings.multiplier ** Math.max(0, attempts - 1)));
333
+ }
334
+ function defaultClassifyError(error) {
335
+ if (error instanceof ClassifiedWorkflowError)
336
+ return error.errorClass;
337
+ const code = typeof error === 'object' && error !== null ? error.code : undefined;
338
+ return code && ['ETIMEDOUT', 'ECONNRESET', 'ECONNREFUSED', 'EAI_AGAIN', 'ENETUNREACH'].includes(code) ? 'transient' : 'unknown';
339
+ }
340
+ function stepIdempotency(step) {
341
+ return step.idempotency ?? (step.kind === 'script' ? 'idempotent' : 'non_idempotent');
342
+ }
343
+ function validateSteps(steps, ids = new Set()) {
344
+ for (const step of steps) {
345
+ assertStableId(step.id, 'stepId');
346
+ if (ids.has(step.id))
347
+ throw new WorkflowRuntimeError(`duplicate workflow step id: ${step.id}`, 'RUN_NOT_RESUMABLE');
348
+ ids.add(step.id);
349
+ if (step.kind === 'script' || step.kind === 'agent') {
350
+ const settings = retrySettings(step);
351
+ if (stepIdempotency(step) === 'non_idempotent' && settings.maxAttempts > 1) {
352
+ throw new WorkflowRuntimeError('non-idempotent workflow steps cannot be retried without an idempotency guarantee', 'INVALID_RETRY');
353
+ }
354
+ }
355
+ else if (step.kind === 'if') {
356
+ const beforeBranches = new Set(ids);
357
+ const thenIds = new Set(ids);
358
+ const elseIds = new Set(ids);
359
+ validateSteps(step.then, thenIds);
360
+ validateSteps(step.else ?? [], elseIds);
361
+ for (const id of [...thenIds, ...elseIds]) {
362
+ if (!beforeBranches.has(id))
363
+ ids.add(id);
364
+ }
365
+ }
366
+ else if (step.kind === 'foreach') {
367
+ if (step.parallel) {
368
+ throw new WorkflowRuntimeError('durable foreach parallelism is deferred beyond phase one', 'UNSUPPORTED_PARALLELISM');
369
+ }
370
+ validateSteps(step.body, ids);
371
+ }
372
+ }
373
+ }
374
+ function escapeRegExp(value) {
375
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
376
+ }
377
+ function collectCanonicalMetadata(steps, prefix = 'root', output = []) {
378
+ for (const step of steps) {
379
+ const executionPrefix = `${prefix}/${escapeRegExp(step.id)}`;
380
+ if (step.kind === 'script' || step.kind === 'agent') {
381
+ output.push({
382
+ pattern: new RegExp(`^${executionPrefix}$`),
383
+ stepId: step.id,
384
+ kind: step.kind,
385
+ idempotency: stepIdempotency(step),
386
+ maxAttempts: retrySettings(step).maxAttempts,
387
+ });
388
+ }
389
+ else if (step.kind === 'approval') {
390
+ output.push({
391
+ pattern: new RegExp(`^${executionPrefix}$`),
392
+ stepId: step.id,
393
+ kind: 'approval',
394
+ idempotency: 'idempotent',
395
+ maxAttempts: 1,
396
+ });
397
+ }
398
+ else if (step.kind === 'if') {
399
+ collectCanonicalMetadata(step.then, `${executionPrefix}:then`, output);
400
+ collectCanonicalMetadata(step.else ?? [], `${executionPrefix}:else`, output);
401
+ }
402
+ else {
403
+ collectCanonicalMetadata(step.body, `${executionPrefix}\\[\\d+\\]`, output);
404
+ }
405
+ }
406
+ return output;
407
+ }
408
+ function validateResumeState(state) {
409
+ assertValidWorkflowSpec(state.spec);
410
+ assertPersistable(state.spec);
411
+ if (state.spec.workflowId !== state.workflowId || deriveWorkflowId(state.spec) !== state.workflowId) {
412
+ throw new WorkflowRuntimeError('persisted workflow identity does not match its definition', 'RUN_NOT_RESUMABLE');
413
+ }
414
+ if (state.definitionDigest !== deriveWorkflowDefinitionDigest(state.spec)) {
415
+ throw new WorkflowRuntimeError('persisted workflow definition does not match its digest', 'RUN_NOT_RESUMABLE');
416
+ }
417
+ if (stableJson(state.context.input) !== stableJson(state.spec.input ?? {})) {
418
+ throw new WorkflowRuntimeError('persisted workflow input does not match its definition', 'RUN_NOT_RESUMABLE');
419
+ }
420
+ const canonical = collectCanonicalMetadata(state.spec.steps);
421
+ for (const [executionId, persisted] of Object.entries(state.steps)) {
422
+ const expected = canonical.find((candidate) => candidate.pattern.test(executionId));
423
+ if (!expected
424
+ || persisted.executionId !== executionId
425
+ || persisted.stepId !== expected.stepId
426
+ || persisted.kind !== expected.kind
427
+ || persisted.idempotency !== expected.idempotency
428
+ || persisted.maxAttempts !== expected.maxAttempts
429
+ || persisted.attempts > expected.maxAttempts) {
430
+ throw new WorkflowRuntimeError(`persisted workflow step metadata is inconsistent: ${executionId}`, 'RUN_NOT_RESUMABLE');
431
+ }
432
+ }
433
+ if (state.currentStep !== undefined) {
434
+ const current = state.steps[state.currentStep];
435
+ if (!current || !canonical.some((candidate) => candidate.pattern.test(state.currentStep ?? ''))) {
436
+ throw new WorkflowRuntimeError('persisted currentStep is inconsistent with the workflow definition', 'RUN_NOT_RESUMABLE');
437
+ }
438
+ }
439
+ if (state.status === 'waiting_approval') {
440
+ const approval = state.approval;
441
+ const current = approval ? state.steps[approval.executionId] : undefined;
442
+ if (!approval || state.currentStep !== approval.executionId || current?.kind !== 'approval'
443
+ || current.status !== 'waiting_approval' || current.stepId !== approval.gateId) {
444
+ throw new WorkflowRuntimeError('persisted approval state is inconsistent', 'RUN_NOT_RESUMABLE');
445
+ }
446
+ }
447
+ }
448
+ function validateConcurrency(value) {
449
+ const configured = value ?? DEFAULT_MAX_CONCURRENT_RUNS;
450
+ if (!Number.isSafeInteger(configured) || configured < 1 || configured > MAX_CONCURRENT_RUNS) {
451
+ throw new WorkflowRuntimeError('maxConcurrentRuns must be an integer between 1 and 64', 'INVALID_CONCURRENCY');
452
+ }
453
+ return configured;
454
+ }
455
+ function validateMaxQueuedRuns(value) {
456
+ const configured = value ?? DEFAULT_MAX_QUEUED_RUNS;
457
+ if (!Number.isSafeInteger(configured) || configured < 0 || configured > MAX_QUEUED_RUNS) {
458
+ throw new WorkflowRuntimeError('maxQueuedRuns must be an integer between 0 and 4096', 'INVALID_CONCURRENCY');
459
+ }
460
+ return configured;
461
+ }
462
+ function validateMaxStepExecutions(value) {
463
+ const configured = value ?? DEFAULT_MAX_STEP_EXECUTIONS;
464
+ if (!Number.isSafeInteger(configured) || configured < 1 || configured > MAX_STEP_EXECUTIONS) {
465
+ throw new WorkflowRuntimeError('maxStepExecutions must be an integer between 1 and 100000', 'RESOURCE_LIMIT');
466
+ }
467
+ return configured;
468
+ }
469
+ function isTerminal(status) {
470
+ return ['cancelled', 'succeeded', 'failed', 'unsafe_to_resume'].includes(status);
471
+ }
472
+ function canBypassHistoryForSafety(error) {
473
+ return error instanceof WorkflowStateError
474
+ && ['RESOURCE_LIMIT', 'CORRUPT_STATE', 'UNSAFE_PATH', 'INSECURE_PERMISSIONS'].includes(error.code);
475
+ }
476
+ export class DurableWorkflowRuntime {
477
+ deps;
478
+ store;
479
+ core;
480
+ now;
481
+ sleep;
482
+ classifyError;
483
+ generateRunId;
484
+ scheduler;
485
+ maxConcurrentRuns;
486
+ maxStepExecutions;
487
+ inFlight = new Map();
488
+ constructor(deps) {
489
+ this.deps = deps;
490
+ this.store = new WorkflowStateStore(deps.stateDir ?? defaultWorkflowStateDir(), {
491
+ lock: deps.lock,
492
+ onTransactionPhase: deps.onTransactionPhase,
493
+ });
494
+ this.core = { ...CORE_WHITELIST, ...(deps.core ?? {}) };
495
+ this.now = deps.now ?? (() => Date.now());
496
+ this.sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
497
+ this.classifyError = deps.classifyError ?? defaultClassifyError;
498
+ this.generateRunId = deps.generateRunId ?? (() => `run_${makeUlid()}`);
499
+ this.maxConcurrentRuns = validateConcurrency(deps.maxConcurrentRuns);
500
+ this.maxStepExecutions = validateMaxStepExecutions(deps.maxStepExecutions);
501
+ const sharedLimit = this.store.ensureConcurrencyLimit(this.maxConcurrentRuns);
502
+ if (sharedLimit !== this.maxConcurrentRuns) {
503
+ throw new WorkflowRuntimeError(`state directory maxConcurrentRuns is ${sharedLimit}, not ${this.maxConcurrentRuns}`, 'INVALID_CONCURRENCY');
504
+ }
505
+ this.scheduler = new BoundedScheduler(this.maxConcurrentRuns, validateMaxQueuedRuns(deps.maxQueuedRuns));
506
+ }
507
+ async start(spec, options = {}) {
508
+ assertValidWorkflowSpec(spec);
509
+ assertPersistable(spec);
510
+ const workflowId = deriveWorkflowId(spec);
511
+ const runId = typeof options === 'string' ? options : (options.runId ?? this.generateRunId());
512
+ assertStableId(runId, 'runId');
513
+ const timestamp = this.timestamp();
514
+ const state = {
515
+ schemaVersion: WORKFLOW_RUN_SCHEMA_VERSION,
516
+ workflowSchemaVersion: WORKFLOW_DEFINITION_SCHEMA_VERSION,
517
+ workflowId,
518
+ definitionDigest: deriveWorkflowDefinitionDigest(spec),
519
+ runId,
520
+ workflowName: spec.name,
521
+ status: 'queued',
522
+ spec: { ...spec, workflowId },
523
+ context: { input: spec.input ?? {}, steps: {} },
524
+ steps: {},
525
+ createdAt: timestamp,
526
+ updatedAt: timestamp,
527
+ };
528
+ await this.withLockedRun(runId, async () => {
529
+ try {
530
+ this.store.readStored(runId);
531
+ throw new WorkflowRuntimeError(`workflow run already exists: ${runId}`, 'RUN_NOT_RESUMABLE');
532
+ }
533
+ catch (error) {
534
+ if (!(error instanceof WorkflowStateError) || error.code !== 'STATE_NOT_FOUND')
535
+ throw error;
536
+ }
537
+ await this.checkpoint(state, [
538
+ this.historyEvent(state, 'run_created'),
539
+ this.historyEvent(state, 'run_queued'),
540
+ ]);
541
+ });
542
+ let scheduled;
543
+ try {
544
+ // Keep admission separate from await so execution failures are not mistaken for synchronous queue rejection.
545
+ scheduled = this.scheduleRun(runId, 'start');
546
+ }
547
+ catch (error) {
548
+ if (error instanceof WorkflowRuntimeError && error.code === 'RESOURCE_LIMIT') {
549
+ await this.withLockedRun(runId, async () => {
550
+ const rejected = this.store.readStored(runId);
551
+ if (rejected.status === 'queued')
552
+ await this.failState(rejected, 'resource_limit');
553
+ });
554
+ }
555
+ throw error;
556
+ }
557
+ return await scheduled;
558
+ }
559
+ async resume(runId, options) {
560
+ if (options !== undefined)
561
+ return await this.operatorResume(runId, options);
562
+ return await this.scheduleRun(runId, 'recovery');
563
+ }
564
+ async operatorResume(runId, options) {
565
+ const existing = this.inFlight.get(runId);
566
+ if (existing)
567
+ await existing;
568
+ await this.withLockedRun(runId, async () => {
569
+ const state = this.store.readStored(runId);
570
+ await this.validateResumeStateOrFail(state);
571
+ if (state.status !== 'paused' && state.status !== 'waiting_approval') {
572
+ throw new WorkflowStateError('workflow run is not paused or waiting for approval', 'INVALID_TRANSITION');
573
+ }
574
+ if (state.status === 'waiting_approval') {
575
+ const control = this.store.readControl(runId);
576
+ const executionId = state.approval?.executionId;
577
+ const decision = executionId ? control.approvals[executionId] : undefined;
578
+ if (!decision) {
579
+ throw new WorkflowStateError('workflow approval decision is still pending', 'INVALID_TRANSITION');
580
+ }
581
+ this.ensureApprovalDecisionHistory(state, decision);
582
+ }
583
+ state.status = 'queued';
584
+ state.updatedAt = this.timestamp();
585
+ try {
586
+ await this.checkpoint(state, [this.historyEvent(state, 'resume_requested', options)], { pause: null });
587
+ }
588
+ catch (error) {
589
+ if (!canBypassHistoryForSafety(error))
590
+ throw error;
591
+ await this.safetyCheckpoint(state, { pause: null });
592
+ }
593
+ });
594
+ return await this.scheduleRun(runId, 'operator');
595
+ }
596
+ async pause(runId, options) {
597
+ const at = this.timestamp();
598
+ this.store.requestPauseWithHistory(runId, options, at);
599
+ return this.result(this.store.read(runId));
600
+ }
601
+ async cancel(runId, options) {
602
+ const state = this.store.readStored(runId);
603
+ const at = this.timestamp();
604
+ this.store.requestCancelWithHistory(runId, options, at);
605
+ if (state.status !== 'paused' && state.status !== 'waiting_approval') {
606
+ return this.result(this.store.read(runId));
607
+ }
608
+ return await this.withLockedRun(runId, async () => {
609
+ const lockedState = this.store.readStored(runId);
610
+ if (isTerminal(lockedState.status))
611
+ return this.result(lockedState);
612
+ const cancel = this.store.readControl(runId).cancel;
613
+ if (!cancel)
614
+ return this.result(lockedState);
615
+ await this.finalizeCancellation(lockedState, cancel);
616
+ return this.result(lockedState);
617
+ });
618
+ }
619
+ async approve(runId, gateId, options) {
620
+ const at = this.timestamp();
621
+ await this.withLockedRun(runId, async () => {
622
+ const state = this.store.readStored(runId);
623
+ await this.validateResumeStateOrFail(state);
624
+ this.store.approve(runId, gateId, options, at);
625
+ });
626
+ return await this.operatorResume(runId, options);
627
+ }
628
+ async reject(runId, gateId, options) {
629
+ const at = this.timestamp();
630
+ await this.withLockedRun(runId, async () => {
631
+ const state = this.store.readStored(runId);
632
+ await this.validateResumeStateOrFail(state);
633
+ this.store.reject(runId, gateId, options, at);
634
+ });
635
+ return await this.operatorResume(runId, options);
636
+ }
637
+ status(runId) {
638
+ return this.store.read(runId);
639
+ }
640
+ history(runId) {
641
+ return this.store.readHistory(runId);
642
+ }
643
+ async recoverPending() {
644
+ return await this.recoverRuns((runId) => this.isRecoveryCandidate(this.store.readStored(runId), this.store.readControl(runId)), (runId, error) => { this.reportRecoveryFailure(runId, error); });
645
+ }
646
+ isRecoveryCandidate(state, control) {
647
+ const hasRecoverableCancel = (state.status === 'paused' || state.status === 'waiting_approval')
648
+ && control.cancel !== undefined;
649
+ const approvalExecutionId = state.status === 'waiting_approval' ? state.approval?.executionId : undefined;
650
+ const hasRecoverableDecision = approvalExecutionId !== undefined
651
+ && control.approvals[approvalExecutionId] !== undefined;
652
+ return hasRecoverableCancel || hasRecoverableDecision
653
+ || ['queued', 'pending', 'running', 'retry_wait'].includes(state.status);
654
+ }
655
+ async recoverRuns(shouldRecover, reportFailure) {
656
+ const runIds = this.store.listRunIds();
657
+ const results = new Array(runIds.length);
658
+ let nextIndex = 0;
659
+ const worker = async () => {
660
+ while (nextIndex < runIds.length) {
661
+ const index = nextIndex;
662
+ nextIndex += 1;
663
+ const runId = runIds[index];
664
+ try {
665
+ if (await shouldRecover(runId))
666
+ results[index] = await this.scheduleRecoveryRun(runId);
667
+ }
668
+ catch (error) {
669
+ reportFailure(runId, error);
670
+ }
671
+ }
672
+ };
673
+ const workerCount = Math.min(this.maxConcurrentRuns, runIds.length);
674
+ await Promise.all(Array.from({ length: workerCount }, worker));
675
+ return results.filter((result) => result !== undefined);
676
+ }
677
+ async scheduleRecoveryRun(runId) {
678
+ while (true) {
679
+ await this.scheduler.waitForAdmission();
680
+ let scheduled;
681
+ try {
682
+ scheduled = this.scheduleRun(runId, 'recovery');
683
+ }
684
+ catch (error) {
685
+ if (error instanceof WorkflowRuntimeError && error.code === 'RESOURCE_LIMIT')
686
+ continue;
687
+ throw error;
688
+ }
689
+ return await scheduled;
690
+ }
691
+ }
692
+ scheduleRun(runId, mode) {
693
+ const existing = this.inFlight.get(runId);
694
+ if (existing)
695
+ return existing;
696
+ const scheduled = this.scheduler.schedule(async () => await this.withConcurrencySlot(async () => await this.resumeLocked(runId, mode)));
697
+ this.inFlight.set(runId, scheduled);
698
+ void scheduled.finally(() => {
699
+ if (this.inFlight.get(runId) === scheduled)
700
+ this.inFlight.delete(runId);
701
+ }).catch(() => undefined);
702
+ return scheduled;
703
+ }
704
+ async resumeLocked(runId, mode) {
705
+ return await this.withLockedRun(runId, async () => {
706
+ const state = this.store.readStored(runId);
707
+ await this.validateResumeStateOrFail(state);
708
+ if (isTerminal(state.status))
709
+ return this.result(state);
710
+ const control = this.store.readControl(runId);
711
+ const cancel = control.cancel;
712
+ if (cancel) {
713
+ await this.finalizeCancellation(state, cancel);
714
+ return this.result(state);
715
+ }
716
+ if (state.status === 'paused')
717
+ return this.result(state);
718
+ if (state.status === 'waiting_approval') {
719
+ const executionId = state.approval?.executionId;
720
+ const decision = executionId ? control.approvals[executionId] : undefined;
721
+ if (!decision)
722
+ return this.result(state);
723
+ this.ensureApprovalDecisionHistory(state, decision);
724
+ }
725
+ if (mode === 'recovery') {
726
+ this.appendHistoryUnlessUnavailable(this.historyEvent(state, 'recovery_started'));
727
+ }
728
+ const interrupted = state.currentStep ? state.steps[state.currentStep] : undefined;
729
+ if (interrupted?.status === 'running') {
730
+ if (interrupted.idempotency === 'non_idempotent') {
731
+ interrupted.status = 'failed';
732
+ interrupted.lastErrorClass = 'interrupted_non_idempotent';
733
+ state.status = 'unsafe_to_resume';
734
+ state.lastErrorClass = 'interrupted_non_idempotent';
735
+ state.updatedAt = this.timestamp();
736
+ try {
737
+ await this.checkpoint(state, [
738
+ this.historyEvent(state, 'unsafe_to_resume', {
739
+ executionId: interrupted.executionId,
740
+ stepId: interrupted.stepId,
741
+ errorClass: 'interrupted_non_idempotent',
742
+ }),
743
+ ]);
744
+ }
745
+ catch (error) {
746
+ if (!canBypassHistoryForSafety(error))
747
+ throw error;
748
+ await this.safetyCheckpoint(state);
749
+ }
750
+ return this.result(state);
751
+ }
752
+ interrupted.status = 'pending';
753
+ interrupted.attempts = Math.max(0, interrupted.attempts - 1);
754
+ delete interrupted.startedAt;
755
+ delete interrupted.completedAt;
756
+ delete interrupted.nextAttemptAt;
757
+ }
758
+ return await this.execute(state);
759
+ });
760
+ }
761
+ async withLockedRun(runId, fn) {
762
+ this.store.acquireRunLock(runId);
763
+ try {
764
+ return await fn();
765
+ }
766
+ finally {
767
+ this.store.releaseRunLock(runId);
768
+ }
769
+ }
770
+ async withConcurrencySlot(fn) {
771
+ let slot = null;
772
+ while (slot === null) {
773
+ slot = this.store.tryAcquireConcurrencySlot(this.maxConcurrentRuns);
774
+ if (slot === null)
775
+ await new Promise((resolve) => setTimeout(resolve, 10));
776
+ }
777
+ try {
778
+ return await fn();
779
+ }
780
+ finally {
781
+ this.store.releaseConcurrencySlot(slot);
782
+ }
783
+ }
784
+ async validateResumeStateOrFail(state) {
785
+ try {
786
+ validateResumeState(state);
787
+ }
788
+ catch (error) {
789
+ if (error instanceof WorkflowRuntimeError
790
+ && ['RUN_NOT_RESUMABLE', 'INVALID_STATE', 'INVALID_RETRY', 'UNSUPPORTED_PARALLELISM', 'RESOURCE_LIMIT'].includes(error.code)
791
+ && !isTerminal(state.status)) {
792
+ await this.failState(state, error.code === 'RESOURCE_LIMIT' ? 'resource_limit' : 'invalid_control_flow');
793
+ }
794
+ throw error;
795
+ }
796
+ }
797
+ async execute(state) {
798
+ try {
799
+ await this.enforceControlBoundary(state);
800
+ state.status = 'running';
801
+ state.updatedAt = this.timestamp();
802
+ await this.checkpoint(state, [this.historyEvent(state, 'run_started')]);
803
+ const ctx = { input: state.context.input, steps: state.context.steps };
804
+ await this.runSteps(state, state.spec.steps, ctx, 'root', { remaining: this.maxStepExecutions });
805
+ await this.enforceControlBoundary(state);
806
+ state.context = { input: ctx.input, steps: ctx.steps };
807
+ delete state.currentStep;
808
+ delete state.approval;
809
+ state.status = 'succeeded';
810
+ state.completedAt = this.timestamp();
811
+ state.updatedAt = state.completedAt;
812
+ await this.checkpointCompletion(state, [this.historyEvent(state, 'run_succeeded')]);
813
+ return this.result(state);
814
+ }
815
+ catch (error) {
816
+ if (error instanceof WorkflowSuspendedError)
817
+ return this.result(this.store.readStored(state.runId));
818
+ if (error instanceof CheckpointInterruptionError)
819
+ throw error;
820
+ if (error instanceof DeterministicWorkflowError) {
821
+ await this.failState(state, 'invalid_control_flow');
822
+ throw new WorkflowRunFailedError(state.runId, 'failed', 'invalid_control_flow');
823
+ }
824
+ const latest = this.store.readStored(state.runId);
825
+ if (latest.status === 'failed' || latest.status === 'unsafe_to_resume') {
826
+ throw new WorkflowRunFailedError(latest.runId, latest.status, latest.lastErrorClass);
827
+ }
828
+ if ((error instanceof WorkflowRuntimeError
829
+ && (error.code === 'SENSITIVE_STATE' || error.code === 'INVALID_STATE' || error.code === 'RESOURCE_LIMIT'))
830
+ || canBypassHistoryForSafety(error)) {
831
+ const errorClass = error instanceof WorkflowStateError
832
+ ? (error.code === 'RESOURCE_LIMIT' ? 'resource_limit' : 'invalid_control_flow')
833
+ : (error.code === 'SENSITIVE_STATE'
834
+ ? 'sensitive_output'
835
+ : (error.code === 'RESOURCE_LIMIT' ? 'resource_limit' : 'invalid_output'));
836
+ await this.failState(latest, errorClass);
837
+ throw new WorkflowRunFailedError(latest.runId, 'failed', errorClass);
838
+ }
839
+ throw error;
840
+ }
841
+ }
842
+ async runSteps(state, steps, ctx, prefix, budget) {
843
+ for (const step of steps) {
844
+ await this.enforceControlBoundary(state);
845
+ await this.runStep(state, step, ctx, prefix, budget);
846
+ }
847
+ }
848
+ async runStep(state, step, ctx, prefix, budget) {
849
+ if (budget.remaining <= 0)
850
+ workflowResourceLimit('workflow expanded step execution limit exceeded');
851
+ budget.remaining -= 1;
852
+ const executionId = `${prefix}/${step.id}`;
853
+ if (step.kind === 'script' || step.kind === 'agent') {
854
+ const output = await this.runLeaf(state, step, ctx, executionId);
855
+ ctx.steps[step.id] = output;
856
+ state.context = { input: state.context.input, steps: ctx.steps };
857
+ return;
858
+ }
859
+ if (step.kind === 'approval') {
860
+ await this.runApproval(state, step, ctx, executionId);
861
+ return;
862
+ }
863
+ if (step.kind === 'if') {
864
+ const branchName = evalValue(step.cond, ctx) ? 'then' : 'else';
865
+ await this.runSteps(state, branchName === 'then' ? step.then : (step.else ?? []), ctx, `${executionId}:${branchName}`, budget);
866
+ return;
867
+ }
868
+ const items = resolveRef(step.over.ref, ctx);
869
+ if (!Array.isArray(items))
870
+ throw new DeterministicWorkflowError(`foreach reference is not an array: ${step.over.ref}`);
871
+ if (items.length > MAX_WORKFLOW_FOREACH_ITEMS) {
872
+ workflowResourceLimit(`workflow foreach exceeds ${MAX_WORKFLOW_FOREACH_ITEMS} items`);
873
+ }
874
+ const results = [];
875
+ for (let index = 0; index < items.length; index += 1) {
876
+ const sub = { input: ctx.input, steps: { ...ctx.steps }, item: items[index] };
877
+ await this.runSteps(state, step.body, sub, `${executionId}[${index}]`, budget);
878
+ results.push(this.lastStepOutput(step.body, sub));
879
+ }
880
+ const collected = { [step.collect ?? 'results']: results };
881
+ assertPersistable(collected);
882
+ ctx.steps[step.id] = collected;
883
+ state.context = { input: state.context.input, steps: ctx.steps };
884
+ state.updatedAt = this.timestamp();
885
+ await this.checkpoint(state);
886
+ }
887
+ lastStepOutput(steps, ctx) {
888
+ const last = steps.at(-1);
889
+ if (!last)
890
+ return null;
891
+ if (last.kind !== 'if')
892
+ return ctx.steps[last.id] ?? null;
893
+ const branch = evalValue(last.cond, ctx) ? last.then : (last.else ?? []);
894
+ return this.lastStepOutput(branch, ctx);
895
+ }
896
+ async failState(state, errorClass) {
897
+ const completedAt = this.timestamp();
898
+ const current = state.currentStep ? state.steps[state.currentStep] : undefined;
899
+ if (current && current.status !== 'succeeded' && current.status !== 'failed') {
900
+ current.status = 'failed';
901
+ current.lastErrorClass = errorClass;
902
+ current.completedAt = completedAt;
903
+ }
904
+ state.status = 'failed';
905
+ state.lastErrorClass = errorClass;
906
+ state.completedAt = completedAt;
907
+ state.updatedAt = completedAt;
908
+ delete state.approval;
909
+ try {
910
+ await this.checkpoint(state, [this.historyEvent(state, 'run_failed', { errorClass })]);
911
+ }
912
+ catch (error) {
913
+ if (!canBypassHistoryForSafety(error))
914
+ throw error;
915
+ await this.safetyCheckpoint(state);
916
+ }
917
+ }
918
+ async runApproval(state, step, ctx, executionId) {
919
+ const existing = state.steps[executionId];
920
+ if (existing?.status === 'succeeded') {
921
+ ctx.steps[step.id] = existing.output ?? { approved: true };
922
+ return;
923
+ }
924
+ const stepState = existing ?? {
925
+ executionId,
926
+ stepId: step.id,
927
+ kind: 'approval',
928
+ idempotency: 'idempotent',
929
+ status: 'pending',
930
+ attempts: 0,
931
+ maxAttempts: 1,
932
+ };
933
+ state.steps[executionId] = stepState;
934
+ const control = this.store.readControl(state.runId);
935
+ const decision = control.approvals[executionId];
936
+ if (decision?.decision === 'rejected') {
937
+ stepState.status = 'failed';
938
+ stepState.lastErrorClass = 'approval_rejected';
939
+ stepState.completedAt = this.timestamp();
940
+ state.currentStep = executionId;
941
+ state.status = 'failed';
942
+ state.lastErrorClass = 'approval_rejected';
943
+ state.completedAt = stepState.completedAt;
944
+ state.updatedAt = stepState.completedAt;
945
+ delete state.approval;
946
+ await this.checkpoint(state, [
947
+ this.historyEvent(state, 'step_failed', {
948
+ executionId,
949
+ stepId: step.id,
950
+ errorClass: 'approval_rejected',
951
+ }),
952
+ this.historyEvent(state, 'run_failed', { errorClass: 'approval_rejected' }),
953
+ ]);
954
+ throw new ClassifiedWorkflowError('permanent', 'workflow approval was rejected');
955
+ }
956
+ if (decision?.decision === 'approved') {
957
+ const output = { approved: true };
958
+ stepState.output = output;
959
+ stepState.status = 'succeeded';
960
+ stepState.completedAt = this.timestamp();
961
+ state.currentStep = executionId;
962
+ state.updatedAt = stepState.completedAt;
963
+ delete state.approval;
964
+ ctx.steps[step.id] = output;
965
+ state.context = { input: state.context.input, steps: ctx.steps };
966
+ await this.checkpoint(state, [this.historyEvent(state, 'step_succeeded', { executionId, stepId: step.id })]);
967
+ return;
968
+ }
969
+ const requestedAt = this.timestamp();
970
+ stepState.status = 'waiting_approval';
971
+ stepState.startedAt ??= requestedAt;
972
+ state.currentStep = executionId;
973
+ state.approval = { gateId: step.id, executionId, requestedAt };
974
+ state.status = 'waiting_approval';
975
+ state.updatedAt = requestedAt;
976
+ await this.checkpoint(state, [
977
+ this.historyEvent(state, 'approval_waiting', { executionId, stepId: step.id, gateId: step.id }),
978
+ ]);
979
+ throw new WorkflowSuspendedError();
980
+ }
981
+ async runLeaf(state, step, ctx, executionId) {
982
+ const settings = retrySettings(step);
983
+ const idempotency = stepIdempotency(step);
984
+ const existing = state.steps[executionId];
985
+ if (existing?.status === 'succeeded')
986
+ return existing.output;
987
+ const stepState = existing ?? {
988
+ executionId,
989
+ stepId: step.id,
990
+ kind: step.kind,
991
+ idempotency,
992
+ status: 'pending',
993
+ attempts: 0,
994
+ maxAttempts: settings.maxAttempts,
995
+ };
996
+ state.steps[executionId] = stepState;
997
+ while (stepState.attempts < stepState.maxAttempts) {
998
+ await this.enforceControlBoundary(state);
999
+ if (stepState.status === 'retry_wait' && stepState.nextAttemptAt) {
1000
+ const remaining = Date.parse(stepState.nextAttemptAt) - this.now();
1001
+ if (remaining > 0)
1002
+ await this.sleep(remaining);
1003
+ await this.enforceControlBoundary(state);
1004
+ }
1005
+ stepState.attempts += 1;
1006
+ stepState.status = 'running';
1007
+ stepState.startedAt = this.timestamp();
1008
+ delete stepState.nextAttemptAt;
1009
+ state.currentStep = executionId;
1010
+ state.status = 'running';
1011
+ state.updatedAt = stepState.startedAt;
1012
+ await this.checkpoint(state, [
1013
+ this.historyEvent(state, 'step_started', {
1014
+ executionId,
1015
+ stepId: step.id,
1016
+ attempt: stepState.attempts,
1017
+ }),
1018
+ ]);
1019
+ let output;
1020
+ try {
1021
+ output = step.kind === 'script'
1022
+ ? { [step.setOutput]: evalCore(step.call, ctx, this.core) }
1023
+ : { [step.outputKey ?? 'output']: await this.runAgentAttempt(state.runId, step.prompt, ctx) };
1024
+ assertPersistable(output);
1025
+ }
1026
+ catch (error) {
1027
+ const persistError = error instanceof WorkflowRuntimeError
1028
+ && (error.code === 'SENSITIVE_STATE' || error.code === 'INVALID_STATE');
1029
+ if (persistError) {
1030
+ const errorClass = error.code === 'SENSITIVE_STATE' ? 'sensitive_output' : 'invalid_output';
1031
+ stepState.status = 'failed';
1032
+ stepState.lastErrorClass = errorClass;
1033
+ state.status = stepState.idempotency === 'non_idempotent' ? 'unsafe_to_resume' : 'failed';
1034
+ state.lastErrorClass = errorClass;
1035
+ }
1036
+ else {
1037
+ const errorClass = this.classifyError(error);
1038
+ stepState.lastErrorClass = errorClass;
1039
+ state.lastErrorClass = errorClass;
1040
+ if (errorClass === 'transient' && stepState.attempts < stepState.maxAttempts) {
1041
+ const delay = backoffMs(settings, stepState.attempts);
1042
+ stepState.status = 'retry_wait';
1043
+ stepState.nextAttemptAt = new Date(this.now() + delay).toISOString();
1044
+ state.status = 'retry_wait';
1045
+ state.updatedAt = this.timestamp();
1046
+ await this.checkpointCompletion(state, [
1047
+ this.historyEvent(state, 'step_retry_scheduled', {
1048
+ executionId,
1049
+ stepId: step.id,
1050
+ attempt: stepState.attempts,
1051
+ errorClass,
1052
+ }),
1053
+ ]);
1054
+ continue;
1055
+ }
1056
+ stepState.status = 'failed';
1057
+ state.status = stepState.idempotency === 'non_idempotent'
1058
+ && (errorClass === 'unknown' || errorClass === 'permanent')
1059
+ ? 'unsafe_to_resume'
1060
+ : 'failed';
1061
+ }
1062
+ stepState.completedAt = this.timestamp();
1063
+ state.completedAt = stepState.completedAt;
1064
+ state.updatedAt = stepState.completedAt;
1065
+ await this.checkpointCompletion(state, [
1066
+ this.historyEvent(state, 'step_failed', {
1067
+ executionId,
1068
+ stepId: step.id,
1069
+ attempt: stepState.attempts,
1070
+ ...(stepState.lastErrorClass ? { errorClass: stepState.lastErrorClass } : {}),
1071
+ }),
1072
+ state.status === 'unsafe_to_resume'
1073
+ ? this.historyEvent(state, 'unsafe_to_resume', {
1074
+ executionId,
1075
+ stepId: step.id,
1076
+ ...(stepState.lastErrorClass ? { errorClass: stepState.lastErrorClass } : {}),
1077
+ })
1078
+ : this.historyEvent(state, 'run_failed', {
1079
+ ...(stepState.lastErrorClass ? { errorClass: stepState.lastErrorClass } : {}),
1080
+ }),
1081
+ ]);
1082
+ throw error;
1083
+ }
1084
+ stepState.output = output;
1085
+ stepState.status = 'succeeded';
1086
+ stepState.completedAt = this.timestamp();
1087
+ delete stepState.lastErrorClass;
1088
+ delete state.lastErrorClass;
1089
+ state.updatedAt = stepState.completedAt;
1090
+ await this.checkpointCompletion(state, [
1091
+ this.historyEvent(state, 'step_succeeded', {
1092
+ executionId,
1093
+ stepId: step.id,
1094
+ attempt: stepState.attempts,
1095
+ }),
1096
+ ]);
1097
+ return output;
1098
+ }
1099
+ state.status = 'failed';
1100
+ state.updatedAt = this.timestamp();
1101
+ await this.checkpoint(state, [this.historyEvent(state, 'run_failed')]);
1102
+ throw new WorkflowRuntimeError('workflow retry attempts exhausted', 'RUN_NOT_RESUMABLE');
1103
+ }
1104
+ async runAgentAttempt(runId, prompt, ctx) {
1105
+ const controller = new AbortController();
1106
+ const observer = this.observeCancellation(runId, controller);
1107
+ try {
1108
+ const execution = Promise.resolve().then(async () => {
1109
+ if (controller.signal.aborted)
1110
+ throw new WorkflowSuspendedError();
1111
+ return await this.deps.agent(prompt, ctx, { signal: controller.signal });
1112
+ });
1113
+ return await Promise.race([execution, observer.failure]);
1114
+ }
1115
+ finally {
1116
+ observer.stop();
1117
+ }
1118
+ }
1119
+ observeCancellation(runId, controller) {
1120
+ let stopped = false;
1121
+ let timer;
1122
+ let rejectFailure;
1123
+ const failure = new Promise((_resolve, reject) => { rejectFailure = reject; });
1124
+ const poll = () => {
1125
+ if (stopped)
1126
+ return;
1127
+ try {
1128
+ if (this.store.readControl(runId).cancel) {
1129
+ controller.abort();
1130
+ return;
1131
+ }
1132
+ timer = setTimeout(poll, CONTROL_POLL_INTERVAL_MS);
1133
+ }
1134
+ catch (error) {
1135
+ controller.abort();
1136
+ rejectFailure(error);
1137
+ }
1138
+ };
1139
+ poll();
1140
+ return {
1141
+ failure,
1142
+ stop: () => {
1143
+ stopped = true;
1144
+ if (timer !== undefined)
1145
+ clearTimeout(timer);
1146
+ },
1147
+ };
1148
+ }
1149
+ async checkpointCompletion(state, history) {
1150
+ assertPersistable(state);
1151
+ const cancellation = this.store.commitUnlessCancelled(state, history);
1152
+ if (cancellation) {
1153
+ const durableState = this.store.readStored(state.runId);
1154
+ await this.finalizeCancellation(durableState, cancellation);
1155
+ throw new WorkflowSuspendedError();
1156
+ }
1157
+ try {
1158
+ await this.deps.afterCheckpoint?.(state);
1159
+ }
1160
+ catch (error) {
1161
+ throw new CheckpointInterruptionError(error);
1162
+ }
1163
+ }
1164
+ async enforceControlBoundary(state) {
1165
+ const control = this.store.readControl(state.runId);
1166
+ if (control.cancel) {
1167
+ await this.finalizeCancellation(state, control.cancel);
1168
+ throw new WorkflowSuspendedError();
1169
+ }
1170
+ if (control.pause) {
1171
+ state.status = 'paused';
1172
+ state.updatedAt = this.timestamp();
1173
+ await this.checkpoint(state, [
1174
+ this.historyEvent(state, 'paused', {
1175
+ actor: control.pause.actor,
1176
+ ...(control.pause.reason ? { reason: control.pause.reason } : {}),
1177
+ }),
1178
+ ]);
1179
+ throw new WorkflowSuspendedError();
1180
+ }
1181
+ }
1182
+ async finalizeCancellation(state, request) {
1183
+ if (state.status === 'cancelled')
1184
+ return;
1185
+ const completedAt = this.timestamp();
1186
+ state.status = 'cancelled';
1187
+ state.completedAt = completedAt;
1188
+ state.updatedAt = completedAt;
1189
+ delete state.approval;
1190
+ try {
1191
+ await this.checkpoint(state, [
1192
+ this.historyEvent(state, 'cancelled', {
1193
+ actor: request.actor,
1194
+ ...(request.reason ? { reason: request.reason } : {}),
1195
+ at: completedAt,
1196
+ }),
1197
+ ], { pause: null, cancel: null });
1198
+ }
1199
+ catch (error) {
1200
+ if (!canBypassHistoryForSafety(error))
1201
+ throw error;
1202
+ // Retain cancel as the bounded durable audit when operation history cannot accept another event.
1203
+ await this.safetyCheckpoint(state, { pause: null });
1204
+ }
1205
+ }
1206
+ async checkpoint(state, history = [], controlPatch = {}) {
1207
+ assertPersistable(state);
1208
+ this.store.commit(state, history, controlPatch);
1209
+ try {
1210
+ await this.deps.afterCheckpoint?.(state);
1211
+ }
1212
+ catch (error) {
1213
+ throw new CheckpointInterruptionError(error);
1214
+ }
1215
+ }
1216
+ async safetyCheckpoint(state, controlPatch = {}) {
1217
+ assertPersistable(state);
1218
+ this.store.commitSafetyState(state, controlPatch);
1219
+ try {
1220
+ await this.deps.afterCheckpoint?.(state);
1221
+ }
1222
+ catch (error) {
1223
+ throw new CheckpointInterruptionError(error);
1224
+ }
1225
+ }
1226
+ ensureApprovalDecisionHistory(state, decision) {
1227
+ try {
1228
+ const type = decision.decision === 'approved' ? 'approval_approved' : 'approval_rejected';
1229
+ const recorded = this.store.readHistory(state.runId).some((event) => event.type === type
1230
+ && event.gateId === decision.gateId
1231
+ && event.at === decision.requestedAt
1232
+ && event.actor === decision.actor
1233
+ && event.reason === decision.reason
1234
+ && (event.executionId === undefined || event.executionId === decision.executionId));
1235
+ if (recorded)
1236
+ return;
1237
+ this.store.appendHistory(this.historyEvent(state, type, {
1238
+ at: decision.requestedAt,
1239
+ executionId: decision.executionId,
1240
+ gateId: decision.gateId,
1241
+ actor: decision.actor,
1242
+ ...(decision.reason !== undefined ? { reason: decision.reason } : {}),
1243
+ }));
1244
+ }
1245
+ catch (error) {
1246
+ if (!canBypassHistoryForSafety(error))
1247
+ throw error;
1248
+ }
1249
+ }
1250
+ appendHistoryUnlessUnavailable(event) {
1251
+ try {
1252
+ this.store.appendHistory(event);
1253
+ }
1254
+ catch (error) {
1255
+ if (!canBypassHistoryForSafety(error))
1256
+ throw error;
1257
+ }
1258
+ }
1259
+ historyEvent(state, type, metadata = {}) {
1260
+ const { at, ...rest } = metadata;
1261
+ return {
1262
+ runId: state.runId,
1263
+ workflowId: state.workflowId,
1264
+ type,
1265
+ status: state.status,
1266
+ at: at ?? this.timestamp(),
1267
+ ...rest,
1268
+ };
1269
+ }
1270
+ timestamp() {
1271
+ return new Date(this.now()).toISOString();
1272
+ }
1273
+ result(state) {
1274
+ const ctx = { input: state.context.input, steps: state.context.steps };
1275
+ return {
1276
+ runId: state.runId,
1277
+ workflowId: state.workflowId,
1278
+ status: state.status,
1279
+ output: state.status === 'succeeded' && state.spec.output ? resolveRef(state.spec.output.ref, ctx) : undefined,
1280
+ ctx,
1281
+ };
1282
+ }
1283
+ reportRecoveryFailure(runId, error) {
1284
+ try {
1285
+ this.deps.onRecoveryFailure?.({ runId, code: recoveryFailureCode(error) });
1286
+ }
1287
+ catch {
1288
+ // Startup recovery must continue even if an optional diagnostic sink fails.
1289
+ }
1290
+ }
1291
+ }
1292
+ function recoveryFailureCode(error) {
1293
+ if (error instanceof WorkflowStateError)
1294
+ return error.code;
1295
+ if (error instanceof WorkflowRunFailedError)
1296
+ return 'RUN_FAILED';
1297
+ return 'RECOVERY_FAILED';
1298
+ }