@lostgradient/weft 0.24.0 → 0.24.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (283) hide show
  1. package/README.md +2 -2
  2. package/dist/cli/conformance.js +5 -2
  3. package/dist/cli/generated/operation-catalog.snapshot.json +95 -1
  4. package/dist/cli/generated/operation-client.generated.d.ts +28 -2
  5. package/dist/cli/generated/operation-client.generated.js +2 -0
  6. package/dist/cli/parse-schedule-arguments.js +15 -3
  7. package/dist/cli/schedule.js +2 -1
  8. package/dist/cli/types.d.ts +2 -1
  9. package/dist/client/http-client-requests.js +6 -9
  10. package/dist/client/start-body.js +1 -0
  11. package/dist/core/catalog/index.d.ts +2 -2
  12. package/dist/core/catalog/index.js +9 -2
  13. package/dist/core/catalog/reference-counts.d.ts +29 -14
  14. package/dist/core/catalog/removal.d.ts +63 -10
  15. package/dist/core/catalog/removal.js +18 -2
  16. package/dist/core/catalog/storage-io.d.ts +23 -0
  17. package/dist/core/catalog/storage-io.js +1 -1
  18. package/dist/core/engine/activity-resolution.js +17 -17
  19. package/dist/core/engine/bulk-operations-purge.d.ts +1 -1
  20. package/dist/core/engine/bulk-operations-purge.js +17 -22
  21. package/dist/core/engine/bulk-operations-retry.d.ts +24 -0
  22. package/dist/core/engine/bulk-operations-retry.js +190 -0
  23. package/dist/core/engine/bulk-operations-shared.d.ts +18 -0
  24. package/dist/core/engine/bulk-operations-shared.js +14 -0
  25. package/dist/core/engine/bulk-operations.d.ts +3 -2
  26. package/dist/core/engine/bulk-operations.js +10 -169
  27. package/dist/core/engine/callback-creators-bundles.js +5 -3
  28. package/dist/core/engine/callback-creators-core.js +9 -1
  29. package/dist/core/engine/callback-creators-schedule.js +2 -2
  30. package/dist/core/engine/catalog-readiness.js +2 -0
  31. package/dist/core/engine/catalog-removal.d.ts +107 -3
  32. package/dist/core/engine/catalog-removal.js +66 -13
  33. package/dist/core/engine/catalog-tombstone-recovery.d.ts +56 -0
  34. package/dist/core/engine/catalog-tombstone-recovery.js +39 -0
  35. package/dist/core/engine/child-workflow.d.ts +13 -1
  36. package/dist/core/engine/child-workflow.js +22 -4
  37. package/dist/core/engine/constraints.js +5 -1
  38. package/dist/core/engine/construction.d.ts +1 -1
  39. package/dist/core/engine/decode-revision.d.ts +32 -0
  40. package/dist/core/engine/decode-revision.js +9 -0
  41. package/dist/core/engine/disposal.d.ts +6 -2
  42. package/dist/core/engine/disposal.js +24 -10
  43. package/dist/core/engine/dynamic-source-errors.d.ts +81 -0
  44. package/dist/core/engine/dynamic-source-errors.js +25 -0
  45. package/dist/core/engine/dynamic-source-execution.d.ts +173 -0
  46. package/dist/core/engine/dynamic-source-execution.js +108 -0
  47. package/dist/core/engine/engine-internal-types.d.ts +16 -0
  48. package/dist/core/engine/engine-workflows-namespace.d.ts +10 -0
  49. package/dist/core/engine/engine-workflows-namespace.js +3 -1
  50. package/dist/core/engine/generation-codec.d.ts +22 -0
  51. package/dist/core/engine/generation-codec.js +2 -0
  52. package/dist/core/engine/index.d.ts +18 -10
  53. package/dist/core/engine/index.js +22 -11
  54. package/dist/core/engine/inline-launch-queue.js +1 -1
  55. package/dist/core/engine/internals.d.ts +26 -30
  56. package/dist/core/engine/lifecycle/checkpoint-launch.d.ts +29 -0
  57. package/dist/core/engine/lifecycle/checkpoint-launch.js +75 -0
  58. package/dist/core/engine/lifecycle/delayed-start-registration.d.ts +30 -0
  59. package/dist/core/engine/lifecycle/delayed-start-registration.js +11 -0
  60. package/dist/core/engine/lifecycle/fork-helpers.d.ts +33 -1
  61. package/dist/core/engine/lifecycle/fork-helpers.js +3 -1
  62. package/dist/core/engine/lifecycle/recovery-isolation.d.ts +20 -0
  63. package/dist/core/engine/lifecycle/recovery-isolation.js +26 -0
  64. package/dist/core/engine/lifecycle/recovery-revision-groups.d.ts +75 -0
  65. package/dist/core/engine/lifecycle/recovery-revision-groups.js +64 -0
  66. package/dist/core/engine/lifecycle/resume-body.d.ts +44 -0
  67. package/dist/core/engine/lifecycle/resume-body.js +211 -0
  68. package/dist/core/engine/lifecycle/resume-generation-guard.d.ts +58 -0
  69. package/dist/core/engine/lifecycle/resume-generation-guard.js +11 -0
  70. package/dist/core/engine/lifecycle/resume.d.ts +46 -3
  71. package/dist/core/engine/lifecycle/resume.js +21 -201
  72. package/dist/core/engine/lifecycle/shared.d.ts +43 -0
  73. package/dist/core/engine/lifecycle/standalone-claim-acquire.d.ts +80 -2
  74. package/dist/core/engine/lifecycle/standalone-claim-acquire.js +20 -4
  75. package/dist/core/engine/lifecycle/start-commit-errors.d.ts +12 -0
  76. package/dist/core/engine/lifecycle/start-commit-errors.js +6 -0
  77. package/dist/core/engine/lifecycle/start-commit.d.ts +24 -7
  78. package/dist/core/engine/lifecycle/start-commit.js +57 -15
  79. package/dist/core/engine/lifecycle/start-exec.d.ts +18 -2
  80. package/dist/core/engine/lifecycle/start-exec.js +7 -5
  81. package/dist/core/engine/lifecycle/start-or-signal-create.js +1 -3
  82. package/dist/core/engine/lifecycle/start-or-signal-resolution.d.ts +8 -2
  83. package/dist/core/engine/lifecycle/start-or-signal-resolution.js +11 -3
  84. package/dist/core/engine/lifecycle/start-or-signal.js +1 -1
  85. package/dist/core/engine/lifecycle/start-precondition-attribution.d.ts +55 -0
  86. package/dist/core/engine/lifecycle/start-precondition-attribution.js +13 -0
  87. package/dist/core/engine/lifecycle/start-revision-resolution.d.ts +34 -0
  88. package/dist/core/engine/lifecycle/start-revision-resolution.js +11 -0
  89. package/dist/core/engine/lifecycle/start-schedule-timing.d.ts +11 -0
  90. package/dist/core/engine/lifecycle/start-schedule-timing.js +21 -0
  91. package/dist/core/engine/lifecycle/start-state.d.ts +16 -0
  92. package/dist/core/engine/lifecycle/start-state.js +60 -0
  93. package/dist/core/engine/lifecycle/start-terminal-conflict-purge.d.ts +146 -4
  94. package/dist/core/engine/lifecycle/start-terminal-conflict-purge.js +33 -4
  95. package/dist/core/engine/lifecycle/start.d.ts +54 -7
  96. package/dist/core/engine/lifecycle/start.js +53 -90
  97. package/dist/core/engine/lifecycle/transition.d.ts +12 -4
  98. package/dist/core/engine/lifecycle/transition.js +40 -95
  99. package/dist/core/engine/lifecycle.d.ts +5 -2
  100. package/dist/core/engine/lifecycle.js +7 -6
  101. package/dist/core/engine/listing.js +15 -1
  102. package/dist/core/engine/memo-durable-activity.js +4 -1
  103. package/dist/core/engine/nonterminal-revision-count.d.ts +25 -0
  104. package/dist/core/engine/nonterminal-revision-count.js +16 -0
  105. package/dist/core/engine/operations-time.d.ts +5 -23
  106. package/dist/core/engine/operations-time.js +13 -11
  107. package/dist/core/engine/ownership-bootstrap.d.ts +1 -1
  108. package/dist/core/engine/pinned-schedule-revision-count.d.ts +26 -0
  109. package/dist/core/engine/pinned-schedule-revision-count.js +19 -0
  110. package/dist/core/engine/pinned-schedule-revision.d.ts +76 -0
  111. package/dist/core/engine/pinned-schedule-revision.js +53 -0
  112. package/dist/core/engine/registration.d.ts +30 -0
  113. package/dist/core/engine/registration.js +17 -2
  114. package/dist/core/engine/retention.js +9 -2
  115. package/dist/core/engine/revision-errors.d.ts +71 -0
  116. package/dist/core/engine/revision-errors.js +13 -0
  117. package/dist/core/engine/schedule-overlap.js +2 -1
  118. package/dist/core/engine/schedule-revision-fire.d.ts +17 -0
  119. package/dist/core/engine/schedule-revision-fire.js +3 -0
  120. package/dist/core/engine/schedule-run.d.ts +10 -0
  121. package/dist/core/engine/schedule-run.js +2 -1
  122. package/dist/core/engine/schedules.d.ts +11 -1
  123. package/dist/core/engine/schedules.js +28 -9
  124. package/dist/core/engine/source-diagnostics.d.ts +76 -0
  125. package/dist/core/engine/source-diagnostics.js +105 -0
  126. package/dist/core/engine/source-registration.js +4 -2
  127. package/dist/core/engine/source-resolution.d.ts +25 -9
  128. package/dist/core/engine/source-resolution.js +49 -14
  129. package/dist/core/engine/source-runtime-state.d.ts +92 -0
  130. package/dist/core/engine/source-runtime-state.js +36 -0
  131. package/dist/core/engine/storage-io.d.ts +19 -2
  132. package/dist/core/engine/storage-io.js +21 -2
  133. package/dist/core/engine/termination/complete.js +1 -1
  134. package/dist/core/engine/termination/finalizer-registration.d.ts +18 -0
  135. package/dist/core/engine/termination/finalizer-registration.js +21 -0
  136. package/dist/core/engine/termination/finalizer.d.ts +16 -19
  137. package/dist/core/engine/termination/finalizer.js +3 -3
  138. package/dist/core/engine/termination/suspend.js +3 -0
  139. package/dist/core/engine/time-operation-callbacks.d.ts +32 -0
  140. package/dist/core/engine/time-operation-callbacks.js +0 -0
  141. package/dist/core/engine/validation/schedule-cadence.d.ts +16 -0
  142. package/dist/core/engine/validation/schedule-cadence.js +20 -0
  143. package/dist/core/engine/validation/schedule-options.d.ts +4 -1
  144. package/dist/core/engine/validation/schedule-options.js +14 -0
  145. package/dist/core/engine/validation/schedule-revision.d.ts +24 -0
  146. package/dist/core/engine/validation/schedule-revision.js +21 -0
  147. package/dist/core/engine/validation/schedule-warnings.d.ts +11 -0
  148. package/dist/core/engine/validation/schedule-warnings.js +5 -0
  149. package/dist/core/engine/validation/schedule.d.ts +29 -4
  150. package/dist/core/engine/validation/schedule.js +19 -36
  151. package/dist/core/engine/validation.js +13 -17
  152. package/dist/core/engine/workflow-claim-reclaim-target.d.ts +1 -1
  153. package/dist/core/engine/workflow-claim-reclaim-target.js +22 -8
  154. package/dist/core/engine/workflow-claim-registry.d.ts +79 -75
  155. package/dist/core/engine/workflow-claim-registry.js +15 -2
  156. package/dist/core/engine/workflow-generation-fence.d.ts +74 -0
  157. package/dist/core/engine/workflow-generation-fence.js +19 -0
  158. package/dist/core/engine/workflow-retention-deadline.d.ts +16 -0
  159. package/dist/core/engine/workflow-retention-deadline.js +33 -0
  160. package/dist/core/events/event-map.d.ts +5 -0
  161. package/dist/core/events/index.d.ts +1 -0
  162. package/dist/core/events/index.js +1 -0
  163. package/dist/core/events/workflow-source-events.d.ts +102 -0
  164. package/dist/core/events/workflow-source-events.js +57 -0
  165. package/dist/core/execution-strategy.d.ts +7 -0
  166. package/dist/core/inline-execution-strategy.context-options.d.ts +1 -1
  167. package/dist/core/inline-execution-strategy.js +1 -1
  168. package/dist/core/start-workflow-validation.d.ts +15 -0
  169. package/dist/core/start-workflow-validation.js +11 -1
  170. package/dist/core/types/checkpoint.d.ts +14 -0
  171. package/dist/core/types/schedules.d.ts +52 -1
  172. package/dist/core/types/state.d.ts +16 -0
  173. package/dist/core/weft-error.d.ts +1 -1
  174. package/dist/core/weft-error.js +4 -1
  175. package/dist/core/worker-execution-strategy.d.ts +1 -0
  176. package/dist/core/worker-execution-strategy.js +13 -1
  177. package/dist/core/worker-fault-handling.d.ts +10 -0
  178. package/dist/core/worker-fault-handling.js +1 -0
  179. package/dist/core/worker-inbound-message.d.ts +1 -0
  180. package/dist/core/worker-inbound-message.js +1 -0
  181. package/dist/core/worker-protocol-guard.js +4 -0
  182. package/dist/core/worker-protocol.d.ts +7 -0
  183. package/dist/core/worker-protocol.js +2 -1
  184. package/dist/core/worker-turn-watchdog.d.ts +8 -1
  185. package/dist/core/worker-turn-watchdog.js +3 -2
  186. package/dist/core/workflow-identifiers.d.ts +37 -0
  187. package/dist/core/workflow-identifiers.js +19 -1
  188. package/dist/diagnostics/format.js +5 -0
  189. package/dist/diagnostics/types.d.ts +24 -0
  190. package/dist/diagnostics/version-check.js +8 -2
  191. package/dist/http.js +2 -2
  192. package/dist/index.d.ts +3 -3
  193. package/dist/index.js +7 -0
  194. package/dist/indexeddb.js +1 -1
  195. package/dist/json-schema.js +3 -3
  196. package/dist/server/fault-to-json-rpc.js +2 -1
  197. package/dist/server/fleet-event-feed.js +6 -6
  198. package/dist/server/index.d.ts +11 -0
  199. package/dist/server/operation-fault.d.ts +2 -0
  200. package/dist/server/operation-fault.js +2 -1
  201. package/dist/server/operations/create-schedule.d.ts +2 -0
  202. package/dist/server/operations/create-schedule.js +10 -6
  203. package/dist/server/operations/get-catalog-diagnostics.d.ts +28 -0
  204. package/dist/server/operations/get-catalog-diagnostics.js +10 -2
  205. package/dist/server/operations/get-task-detail-schema.d.ts +8 -0
  206. package/dist/server/operations/get-task-detail-schema.js +1 -0
  207. package/dist/server/operations/get-task-detail.d.ts +8 -0
  208. package/dist/server/operations/get-task-detail.js +1 -0
  209. package/dist/server/operations/get-task-diagnostics.d.ts +4 -4
  210. package/dist/server/operations/preload-workflow-revision.d.ts +28 -0
  211. package/dist/server/operations/preload-workflow-revision.js +60 -0
  212. package/dist/server/operations/schedule-faults.d.ts +1 -0
  213. package/dist/server/operations/schedule-faults.js +32 -4
  214. package/dist/server/operations/schedule-rest-body.d.ts +1 -0
  215. package/dist/server/operations/schedule-rest-body.js +2 -1
  216. package/dist/server/operations/static-registrations.js +6 -0
  217. package/dist/server/operations/update-schedule.d.ts +2 -0
  218. package/dist/server/operations/update-schedule.js +4 -2
  219. package/dist/server/operations/workflow-catalog-operation-helpers.js +27 -0
  220. package/dist/server/rest-binding.d.ts +11 -0
  221. package/dist/server/runtime/task-dispatch-envelope.d.ts +41 -0
  222. package/dist/server/runtime/task-dispatch-envelope.js +54 -0
  223. package/dist/server/runtime/task-dispatch-revision.d.ts +29 -0
  224. package/dist/server/runtime/task-dispatch-revision.js +13 -0
  225. package/dist/server/runtime/task-dispatch.d.ts +3 -1
  226. package/dist/server/runtime/task-dispatch.js +20 -44
  227. package/dist/server/runtime/task-ledger-recovery.js +1 -1
  228. package/dist/server/runtime/task-ledger-runtime.d.ts +14 -2
  229. package/dist/server/runtime/task-ledger-runtime.js +5 -3
  230. package/dist/server/runtime/task-polling.js +9 -3
  231. package/dist/server/runtime/task-reconciliation.js +2 -1
  232. package/dist/server/runtime/websocket-worker.js +9 -0
  233. package/dist/server/task-ledger-codec.d.ts +28 -0
  234. package/dist/server/task-ledger-codec.js +7 -1
  235. package/dist/server/task-ledger-transition-helpers.js +1 -0
  236. package/dist/server/task-ledger-types.d.ts +9 -0
  237. package/dist/server/task-ledger.d.ts +1 -1
  238. package/dist/server/task-ledger.js +3 -1
  239. package/dist/server/task-queue-types.d.ts +2 -0
  240. package/dist/storage/bun-sql.js +135 -100
  241. package/dist/storage/catalog-keys.d.ts +19 -0
  242. package/dist/storage/catalog-keys.js +3 -1
  243. package/dist/storage/compressed-storage.js +1 -1
  244. package/dist/storage/generation-keys.d.ts +52 -0
  245. package/dist/storage/generation-keys.js +4 -0
  246. package/dist/storage/index.d.ts +76 -63
  247. package/dist/storage/interface.d.ts +24 -230
  248. package/dist/storage/interface.js +1 -1
  249. package/dist/storage/key-prefixes.d.ts +1 -1
  250. package/dist/storage/key-prefixes.js +2 -0
  251. package/dist/storage/lease-keys.d.ts +48 -0
  252. package/dist/storage/lease-keys.js +8 -0
  253. package/dist/storage/lmdb.d.ts +30 -2
  254. package/dist/storage/lmdb.js +1 -1
  255. package/dist/storage/memory.js +1 -1
  256. package/dist/storage/neon.js +3 -3
  257. package/dist/storage/node-sqlite.js +135 -100
  258. package/dist/storage/postgres.js +3 -3
  259. package/dist/storage/resolve.js +1 -1
  260. package/dist/storage/scoped-storage.js +1 -1
  261. package/dist/storage/signal-keys.d.ts +23 -0
  262. package/dist/storage/signal-keys.js +9 -0
  263. package/dist/storage/testing.js +1 -1
  264. package/dist/storage/turso.js +2 -2
  265. package/dist/storage/workflow-lifecycle-keys.d.ts +128 -0
  266. package/dist/storage/workflow-lifecycle-keys.js +46 -0
  267. package/dist/storage/workflow-record-keys.d.ts +98 -0
  268. package/dist/storage/workflow-record-keys.js +47 -0
  269. package/dist/version.d.ts +1 -1
  270. package/dist/version.js +1 -1
  271. package/dist/web-extension.js +1 -1
  272. package/dist/worker/protocol-messages.d.ts +17 -0
  273. package/dist/worker/protocol-schemas.d.ts +32 -0
  274. package/dist/worker/protocol-schemas.js +7 -3
  275. package/dist/worker/protocol-task-result.js +35 -3
  276. package/dist/worker/protocol.js +1 -1
  277. package/dist/worker/registry/types.d.ts +2 -0
  278. package/dist/worker/registry.d.ts +1 -1
  279. package/dist/worker/registry.js +3 -1
  280. package/dist/workers/workflow-runner.d.ts +9 -0
  281. package/dist/workers/workflow-runner.js +9 -2
  282. package/dist/workers/workflow-worker-entry.js +9 -4
  283. package/package.json +2 -2
@@ -20,13 +20,14 @@ export function runWorkflowStartInterceptor(_internals, workflowId, workflowType
20
20
  });
21
21
  return capturedHeaders;
22
22
  }
23
- export function startWorkflowExecution(internals, workflowId, workflowExecutionToken, workflowType, input, checkpoint, nestingDepth, executionDeadline, executionStateOwnerId, _callbacks) {
23
+ export function startWorkflowExecution(internals, workflowId, workflowExecutionToken, workflowType, revision, input, checkpoint, nestingDepth, executionDeadline, executionStateOwnerId, _callbacks) {
24
24
  if (nestingDepth !== 0)
25
25
  internals.workflowNestingDepths.set(workflowId, nestingDepth);
26
- internals.workflowTypeByWorkflowId.set(workflowId, workflowType);
26
+ internals.workflowTypeByWorkflowId.set(workflowId, { type: workflowType, revision });
27
27
  internals.strategy.startWorkflow({
28
28
  workflowId,
29
29
  ...workflowExecutionToken !== void 0 && { workflowExecutionToken },
30
+ ...revision !== void 0 && { revision },
30
31
  workflowType,
31
32
  input,
32
33
  checkpoint: serializeCheckpoint(checkpoint),
@@ -40,7 +41,7 @@ export function startWorkflowExecution(internals, workflowId, workflowExecutionT
40
41
  }
41
42
  });
42
43
  }
43
- export function beginWorkflowExecution(internals, workflowId, workflowExecutionToken, workflowType, input, checkpoint, executionDeadline, executionStateOwnerId, _registration, callbacks, onStarted) {
44
+ export function beginWorkflowExecution(internals, workflowId, workflowExecutionToken, workflowType, revision, input, checkpoint, executionDeadline, executionStateOwnerId, _registration, callbacks, onStarted) {
44
45
  const nestingDepth = internals.pendingNestingDepth ?? 0;
45
46
  internals.pendingNestingDepth = void 0;
46
47
  if (internals.inlineStrategy !== null) {
@@ -48,6 +49,7 @@ export function beginWorkflowExecution(internals, workflowId, workflowExecutionT
48
49
  workflowId,
49
50
  ...workflowExecutionToken !== void 0 && { workflowExecutionToken },
50
51
  workflowType,
52
+ revision,
51
53
  input,
52
54
  checkpoint,
53
55
  nestingDepth,
@@ -58,7 +60,7 @@ export function beginWorkflowExecution(internals, workflowId, workflowExecutionT
58
60
  return;
59
61
  }
60
62
  callbacks.dispatchEvent(new WorkflowStartedEvent(workflowId, workflowType, input));
61
- startWorkflowExecution(internals, workflowId, workflowExecutionToken, workflowType, input, checkpoint, nestingDepth, executionDeadline, executionStateOwnerId, callbacks);
63
+ startWorkflowExecution(internals, workflowId, workflowExecutionToken, workflowType, revision, input, checkpoint, nestingDepth, executionDeadline, executionStateOwnerId, callbacks);
62
64
  onStarted?.();
63
65
  }
64
66
  export function assertDeferSupported(internals, options, isDelayedStart) {
@@ -73,7 +75,7 @@ export async function beginExecutionAwaitingLiveness(internals, params, workflow
73
75
  if (params.isDelayed)
74
76
  return;
75
77
  const liveness = params.options?.defer === !1 ? Promise.withResolvers() : void 0;
76
- beginWorkflowExecution(internals, workflowId, params.state.workflowExecutionToken, params.type, params.input, params.checkpoint, params.state.executionDeadline, params.state.executionStateOwnerId ?? workflowId, params.registration, callbacks, liveness ? () => liveness.resolve() : void 0);
78
+ beginWorkflowExecution(internals, workflowId, params.state.workflowExecutionToken, params.type, params.state.revision, params.input, params.checkpoint, params.state.executionDeadline, params.state.executionStateOwnerId ?? workflowId, params.registration, callbacks, liveness ? () => liveness.resolve() : void 0);
77
79
  if (liveness)
78
80
  await liveness.promise;
79
81
  }
@@ -2,9 +2,7 @@ import { KEYS } from "../../../storage/interface.js";
2
2
  import { encode } from "../../codec.js";
3
3
  import { WorkflowAlreadyExistsError } from "../errors.js";
4
4
  import { buildCreateBatchSignalOperations } from "../signals.js";
5
- import {
6
- StartIdempotencyRaceLostError
7
- } from "./start-commit.js";
5
+ import { StartIdempotencyRaceLostError } from "./start-commit-errors.js";
8
6
  import {
9
7
  requireWinnerId,
10
8
  resolveCallerIdWinnerOrRetry,
@@ -41,11 +41,17 @@ export declare function resolveExistingRunOrThrowPurged(internals: EngineInterna
41
41
  * resolves it instead of racing it to a terminal-conflict). Only when the record is
42
42
  * absent do we wait for the reservation to clear and read once more to discriminate:
43
43
  *
44
- * - **record present** — the winner committed: signal it (or conflict if terminal)
45
- * and return the handle.
44
+ * - **record present** — the winner committed: signal it (or conflict if terminal).
46
45
  * - **record absent after the reservation clears** — the winner aborted before
47
46
  * committing (storage failure, oversized payload, throwing start interceptor): no
48
47
  * run exists, so return `undefined` and let the caller retry its own create.
48
+ *
49
+ * Either terminal-conflict branch is checked against {@link wasSignalAcceptedByWinner}
50
+ * before throwing: a fast workflow can reach a terminal status between this
51
+ * loser's reads, entirely independent of how much slower or faster the winner's
52
+ * OWN commit path happens to be — so a same-`signalId` convergent caller (the
53
+ * documented "concurrent absent-target callers" contract) must not depend on
54
+ * catching the winner mid-flight to avoid a spurious conflict.
49
55
  */
50
56
  export declare function resolveCallerIdWinnerOrRetry(internals: EngineInternals, winnerId: string, signalSpec: StartOrSignalSignal, signalId: string, callbacks: StartOrSignalCallbacks, allowTerminalRestart?: boolean): Promise<WorkflowHandle | undefined>;
51
57
  /**
@@ -1,5 +1,5 @@
1
1
  import { sleep } from "../../../runtime/portable.js";
2
- import { KEYS } from "../../../storage/interface.js";
2
+ import { KEYS, storageHas } from "../../../storage/interface.js";
3
3
  import { decode } from "../../codec.js";
4
4
  import { IdempotencyKeyPurgedError, StartOrSignalConflictError } from "../errors.js";
5
5
  import { loadWorkflowState } from "../storage-io.js";
@@ -23,6 +23,14 @@ async function awaitReservationCleared(internals, workflowId) {
23
23
  await sleep(RESERVATION_CLEAR_RETRY_DELAY_MS);
24
24
  }
25
25
  }
26
+ async function wasSignalAcceptedByWinner(internals, workflowId, signalName, signalId) {
27
+ return storageHas(internals.storage, KEYS.signalAcceptedResponse(workflowId, signalName, signalId));
28
+ }
29
+ async function resolveTerminalConflictOrConvergence(internals, winnerId, status, signalSpec, signalId, callbacks) {
30
+ if (await wasSignalAcceptedByWinner(internals, winnerId, signalSpec.name, signalId))
31
+ return callbacks.getHandle(winnerId);
32
+ throw new StartOrSignalConflictError(winnerId, status);
33
+ }
26
34
  export async function resolveCallerIdWinnerOrRetry(internals, winnerId, signalSpec, signalId, callbacks, allowTerminalRestart = !1) {
27
35
  const state = await loadWorkflowState(internals, winnerId);
28
36
  if (state !== null) {
@@ -31,7 +39,7 @@ export async function resolveCallerIdWinnerOrRetry(internals, winnerId, signalSp
31
39
  return callbacks.getHandle(winnerId);
32
40
  }
33
41
  if (!allowTerminalRestart)
34
- throw new StartOrSignalConflictError(winnerId, state.status);
42
+ return resolveTerminalConflictOrConvergence(internals, winnerId, state.status, signalSpec, signalId, callbacks);
35
43
  }
36
44
  await awaitReservationCleared(internals, winnerId);
37
45
  const stateAfterReservation = await loadWorkflowState(internals, winnerId);
@@ -40,7 +48,7 @@ export async function resolveCallerIdWinnerOrRetry(internals, winnerId, signalSp
40
48
  if (isTerminalWorkflowStatus(stateAfterReservation.status)) {
41
49
  if (state !== null && isSameTerminalRun(state, stateAfterReservation))
42
50
  return;
43
- throw new StartOrSignalConflictError(winnerId, stateAfterReservation.status);
51
+ return resolveTerminalConflictOrConvergence(internals, winnerId, stateAfterReservation.status, signalSpec, signalId, callbacks);
44
52
  }
45
53
  await callbacks.signalExistingWorkflow(winnerId, signalSpec.name, signalSpec.payload, signalId);
46
54
  return callbacks.getHandle(winnerId);
@@ -6,7 +6,7 @@ import {
6
6
  StartWorkflowValidationError
7
7
  } from "../../start-workflow-validation.js";
8
8
  import { IdempotencyKeyPurgedError, StartOrSignalConflictError } from "../errors.js";
9
- import { StartIdempotencyRaceLostError } from "./start-commit.js";
9
+ import { StartIdempotencyRaceLostError } from "./start-commit-errors.js";
10
10
  import {
11
11
  createWithSignalOrFallback,
12
12
  idempotentStartOperationsFor
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Decide what a lost start compare-and-swap MEANS, once every positively-detectable
3
+ * cause has already been ruled out by the caller — an idempotency precondition, a
4
+ * catalog-entry removal, and a lost workflow claim are each re-checked and raise
5
+ * their own error before this runs.
6
+ *
7
+ * Throws when the loss is terminal; returns normally when the caller should retry
8
+ * workflow-concurrency admission.
9
+ *
10
+ * WFT-152: a caller-supplied id that lost its duplicate-id condition means another
11
+ * engine sharing this store committed a create for the same id after this start's
12
+ * duplicate-id read. It raises the SAME {@link WorkflowAlreadyExistsError} the
13
+ * in-engine `pendingStarts` guard raises for the identical collision, so a
14
+ * cross-engine duplicate id is indistinguishable from an in-engine one at the call
15
+ * site.
16
+ *
17
+ * Attribution is by ELIMINATION, never by re-reading the duplicate-id key. A
18
+ * re-read is unsound: the winning run can complete and be purged (or swept by
19
+ * retention) between the failed compare-and-swap and the check, restoring the
20
+ * workflow record to the very value the condition expected, so the conflict reads
21
+ * back as "no conflict". With no workflow-concurrency conditions in the batch,
22
+ * nothing else is left for the lost outcome to mean.
23
+ *
24
+ * When concurrency conditions ARE present, retry only on positive evidence that the
25
+ * retryable one is what missed — hence `hasWorkflowConcurrencyConflict`, invoked
26
+ * lazily so the extra storage read happens only on that path.
27
+ *
28
+ * That evidence is NOT proof the duplicate id was fine, and deliberately is not
29
+ * treated as such. The concurrency precondition is a monotonic atomic-state VERSION
30
+ * key (`buildWorkflowConcurrencyStartOperations` conditions on `snapshot.version`
31
+ * and writes `version + 1`; releasing the slot increments again rather than
32
+ * restoring), so once a same-id winner acquires and releases, that condition stays
33
+ * mismatched forever and reports a conflict regardless of what else missed. What
34
+ * makes retrying safe here is the caller's own earlier check: it re-reads the
35
+ * duplicate-id key positively and raises `WorkflowAlreadyExistsError` before this
36
+ * runs, so reaching this point means the workflow record currently matches the
37
+ * expectation — the id is free right now, and a retry re-conditions on that same
38
+ * still-matching value.
39
+ *
40
+ * The pre-compare-and-swap purge ABA — a winner purged between the read and the
41
+ * commit, making an absent record look never-used to a value-comparing condition
42
+ * alone — no longer reaches this function's elimination logic (WFT-153, closing
43
+ * the residual this comment used to describe as untracked). The caller now checks
44
+ * a SECOND, positive-evidence condition first (`'duplicate-id-generation'` in
45
+ * `buildAndCommitStartBatch`) on a durable per-id generation counter that a purge
46
+ * bumps but never resets, so that case throws `WorkflowAlreadyExistsError` before
47
+ * reaching here. This function still does not claim exclusive attribution for
48
+ * every conceivable cause — only that the two positively-detectable duplicate-id
49
+ * causes are ruled out ahead of it.
50
+ *
51
+ * With no concurrency evidence at all, fail closed: something missed, nothing
52
+ * retryable explains it, and a spurious `WorkflowAlreadyExistsError` is public,
53
+ * non-destructive, and retryable by the caller.
54
+ */
55
+ export declare function attributeLostStartPreconditionOrRetry(workflowId: string, hasDuplicateIdCondition: boolean, hasWorkflowConcurrency: boolean, hasWorkflowConcurrencyConflict: () => Promise<boolean>): Promise<void>;
@@ -0,0 +1,13 @@
1
+ import { WorkflowAlreadyExistsError } from "../errors.js";
2
+ import { StartIdempotencyRaceLostError } from "./start-commit-errors.js";
3
+ export async function attributeLostStartPreconditionOrRetry(workflowId, hasDuplicateIdCondition, hasWorkflowConcurrency, hasWorkflowConcurrencyConflict) {
4
+ if (!hasDuplicateIdCondition) {
5
+ if (!hasWorkflowConcurrency)
6
+ throw new StartIdempotencyRaceLostError;
7
+ return;
8
+ }
9
+ if (!hasWorkflowConcurrency)
10
+ throw new WorkflowAlreadyExistsError(workflowId);
11
+ if (!await hasWorkflowConcurrencyConflict())
12
+ throw new WorkflowAlreadyExistsError(workflowId);
13
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Resolve the exact revision a fresh `startWorkflow()` call — or a schedule's
3
+ * pin capture (WFT-20, `pinned-schedule-revision.ts`) — is about to commit
4
+ * to. Extracted out of `lifecycle/start.ts` so both that module and the
5
+ * schedule pin-capture path import the SAME logic instead of duplicating the
6
+ * `registeredCatalogRevisions`-fallback + `ensureWorkflowCatalogReady()`
7
+ * recheck rule; `start.ts` imports these back unchanged.
8
+ *
9
+ * @module core/engine/lifecycle/start-revision-resolution
10
+ */
11
+ import type { EngineInternals } from '../internals.ts';
12
+ /**
13
+ * The exact executable artifact a start is about to run: the resolved
14
+ * dynamic-source candidate revision, or — for an eager registration, which
15
+ * never populates `resolvedRevision` — this process's own
16
+ * `registeredCatalogRevisions` entry for `type` (the revision of the code
17
+ * actually loaded here, NOT `inFlightRevision`, which for an eager type
18
+ * falls back to the catalog's cached ACTIVE pointer and can name a revision
19
+ * this process never loaded under a multi-engine deployment). Synchronous,
20
+ * on purpose: every top-level engine.* method already awaits
21
+ * `ensureWorkflowCatalogReady()` before reaching a call site that needs this,
22
+ * so this map is populated by the time the overwhelmingly common case gets
23
+ * here. `undefined` means "genuinely not cached yet"; the caller falls back
24
+ * to {@link resolveStartRevisionUncached}.
25
+ */
26
+ export declare function resolveCachedStartRevision(internals: EngineInternals, type: string, resolvedRevision: string | undefined): string | undefined;
27
+ /**
28
+ * The rare fallback {@link resolveCachedStartRevision} defers to: a fired
29
+ * schedule occurrence or a delayed-start timer calls `startWorkflow`
30
+ * directly from background scheduler code, with no top-level
31
+ * `ensureWorkflowCatalogReady()` gate already awaited. Re-checks catalog
32
+ * readiness once, then re-reads the cache.
33
+ */
34
+ export declare function resolveStartRevisionUncached(internals: EngineInternals, type: string): Promise<string>;
@@ -0,0 +1,11 @@
1
+ import { ensureWorkflowCatalogReady } from "../catalog-readiness.js";
2
+ export function resolveCachedStartRevision(internals, type, resolvedRevision) {
3
+ return resolvedRevision ?? internals.registeredCatalogRevisions.get(type);
4
+ }
5
+ export async function resolveStartRevisionUncached(internals, type) {
6
+ await ensureWorkflowCatalogReady(internals.engine);
7
+ const afterReadiness = internals.registeredCatalogRevisions.get(type);
8
+ if (afterReadiness !== void 0)
9
+ return afterReadiness;
10
+ throw Error(`Cannot start workflow "${type}": no catalog revision is registered for this eagerly-registered type, even after re-checking catalog readiness. This should be unreachable.`);
11
+ }
@@ -0,0 +1,11 @@
1
+ import type { StartOptions } from '../../types.ts';
2
+ import type { EngineInternals } from '../internals.ts';
3
+ import { type LifecycleCallbacks } from './shared.ts';
4
+ /**
5
+ * Resolve a start's effective delayed-start timestamp from
6
+ * `options.startAt`/`options.startAfter`, or `undefined` for an immediate
7
+ * start. Split out of `start.ts` to keep that file under the repository's
8
+ * line-count ceiling; this is a pure resolution step with no dependency on
9
+ * `startWorkflow`'s own reservation/commit state.
10
+ */
11
+ export declare function resolveScheduledStartAt(internals: EngineInternals, options: StartOptions | undefined, submissionTime: number, callbacks: LifecycleCallbacks): number | undefined;
@@ -0,0 +1,21 @@
1
+ import { normalizeStorageTimestamp } from "../../scheduler.js";
2
+ import {
3
+ assertExclusiveStartWorkflowOptions,
4
+ coerceStartWorkflowTimestamp,
5
+ StartWorkflowValidationError
6
+ } from "../../start-workflow-validation.js";
7
+ import { parseStartOptionDuration } from "./start-state.js";
8
+ export function resolveScheduledStartAt(internals, options, submissionTime, callbacks) {
9
+ assertExclusiveStartWorkflowOptions(options?.startAt, options?.startAfter);
10
+ if (options?.startAt !== void 0)
11
+ return coerceStartWorkflowTimestamp(options.startAt, "options.startAt");
12
+ if (options?.startAfter !== void 0) {
13
+ const startAfterMilliseconds = parseStartOptionDuration(internals, options.startAfter, "options.startAfter", callbacks);
14
+ try {
15
+ return normalizeStorageTimestamp(submissionTime + startAfterMilliseconds, "options.startAfter");
16
+ } catch {
17
+ throw new StartWorkflowValidationError("options.startAfter must resolve to a finite, non-negative start time");
18
+ }
19
+ }
20
+ return;
21
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Build the initial in-memory {@link WorkflowState} and {@link Checkpoint}
3
+ * for a fresh `start()` — split out of `start.ts`, which has no headroom
4
+ * under the repository's 500-line implementation-file ceiling for this
5
+ * logic inline.
6
+ *
7
+ * @module core/engine/lifecycle/start-state
8
+ */
9
+ import type { Checkpoint, Duration, StartOptions, TimerEntry, WorkflowState } from '../../types.ts';
10
+ import { type WorkflowVersionTuple } from '../../workflow-version-tuple.ts';
11
+ import type { EngineInternals } from '../internals.ts';
12
+ import { type LifecycleCallbacks } from './shared.ts';
13
+ export declare function parseStartOptionDuration(_internals: EngineInternals, duration: Duration, fieldName: 'options.executionTimeout' | 'options.startAfter', _callbacks: LifecycleCallbacks): number;
14
+ export declare function createInitialWorkflowState(internals: EngineInternals, workflowId: string, type: string, input: unknown, versionTuple: WorkflowVersionTuple, revision: string, options: StartOptions | undefined, tags: string[] | undefined, executionStateOwnerId: string | undefined, parentWorkflowId: string | undefined, parentWorkflowExecutionToken: string | undefined, delayedStartTimer: TimerEntry | undefined, callbacks: LifecycleCallbacks): WorkflowState;
15
+ export declare function createInitialCheckpoint(internals: EngineInternals, workflowId: string, workflowVersion: string, options: StartOptions | undefined, _callbacks: LifecycleCallbacks): Checkpoint;
16
+ export declare function applyRestartLineage(state: WorkflowState, displacedState: WorkflowState | null): void;
@@ -0,0 +1,60 @@
1
+ import { createCheckpoint } from "../../checkpoint.js";
2
+ import { normalizeStorageTimestamp } from "../../scheduler.js";
3
+ import {
4
+ StartWorkflowValidationError,
5
+ parseStartWorkflowDuration
6
+ } from "../../start-workflow-validation.js";
7
+ export function parseStartOptionDuration(_internals, duration, fieldName, _callbacks) {
8
+ return parseStartWorkflowDuration(duration, fieldName);
9
+ }
10
+ function buildInitialIdentitySlice(workflowId, type, input, versionTuple, revision, executionStateOwnerId, parentWorkflowId, parentWorkflowExecutionToken, delayedStartTimer, now, tags) {
11
+ return {
12
+ id: workflowId,
13
+ type,
14
+ status: delayedStartTimer ? "pending" : "running",
15
+ input,
16
+ versionTuple,
17
+ revision,
18
+ workflowExecutionToken: crypto.randomUUID(),
19
+ ...executionStateOwnerId !== void 0 && { executionStateOwnerId },
20
+ ...parentWorkflowId !== void 0 && { parentWorkflowId },
21
+ ...parentWorkflowExecutionToken !== void 0 && { parentWorkflowExecutionToken },
22
+ createdAt: now,
23
+ ...!delayedStartTimer && { startedAt: now },
24
+ updatedAt: now,
25
+ ...tags !== void 0 && { tags }
26
+ };
27
+ }
28
+ function resolveInitialExecutionDeadline(internals, options, delayedStartTimer, now, callbacks) {
29
+ if (options?.executionTimeout === void 0 || delayedStartTimer)
30
+ return;
31
+ const executionTimeoutMilliseconds = parseStartOptionDuration(internals, options.executionTimeout, "options.executionTimeout", callbacks);
32
+ try {
33
+ return normalizeStorageTimestamp(now + executionTimeoutMilliseconds, "options.executionTimeout");
34
+ } catch {
35
+ throw new StartWorkflowValidationError("options.executionTimeout must resolve to a finite, non-negative deadline");
36
+ }
37
+ }
38
+ export function createInitialWorkflowState(internals, workflowId, type, input, versionTuple, revision, options, tags, executionStateOwnerId, parentWorkflowId, parentWorkflowExecutionToken, delayedStartTimer, callbacks) {
39
+ const now = internals.options.getNow(), state = buildInitialIdentitySlice(workflowId, type, input, versionTuple, revision, executionStateOwnerId, parentWorkflowId, parentWorkflowExecutionToken, delayedStartTimer, now, tags), executionDeadline = resolveInitialExecutionDeadline(internals, options, delayedStartTimer, now, callbacks);
40
+ if (executionDeadline !== void 0)
41
+ state.executionDeadline = executionDeadline;
42
+ return state;
43
+ }
44
+ export function createInitialCheckpoint(internals, workflowId, workflowVersion, options, _callbacks) {
45
+ const checkpoint = createCheckpoint(workflowId, workflowVersion, internals.options.getNow());
46
+ if (options?.searchAttributes)
47
+ checkpoint.searchAttributes = { ...options.searchAttributes };
48
+ return checkpoint;
49
+ }
50
+ export function applyRestartLineage(state, displacedState) {
51
+ if (displacedState === null)
52
+ return;
53
+ state.restartedFrom = {
54
+ workflowId: displacedState.id,
55
+ ...displacedState.workflowExecutionToken !== void 0 && {
56
+ workflowExecutionToken: displacedState.workflowExecutionToken
57
+ },
58
+ replacedAt: state.createdAt
59
+ };
60
+ }
@@ -1,7 +1,91 @@
1
- import type { BatchOperation } from '../../../storage/interface.ts';
1
+ import type { BatchOperation, ConditionalBatchCondition } from '../../../storage/interface.ts';
2
2
  import type { StartWorkflowOptions, WorkflowState } from '../../types.ts';
3
3
  import type { EngineInternals } from '../internals.ts';
4
4
  import { type LifecycleCallbacks } from './shared.ts';
5
+ /**
6
+ * What the duplicate-id read decided about a caller-supplied workflow id.
7
+ */
8
+ export type StartDuplicateIdDecision = {
9
+ /** A prior terminal run this start will displace, or `null` for a fresh id. */
10
+ terminalRunToPurge: WorkflowState | null;
11
+ /**
12
+ * Compare-and-swap precondition that makes the duplicate-id check atomic with
13
+ * the create commit (WFT-152).
14
+ *
15
+ * The check below reads `KEYS.workflow(workflowId)` and decides whether the id
16
+ * is free, but that read and the create batch are separated by every build step
17
+ * in between. Within ONE engine `pendingStarts` holds the id across that window;
18
+ * two engines sharing a store share no such memory, so both previously committed
19
+ * blind and the second silently overwrote the first — leaving the loser with a
20
+ * run whose terminal transition happens on the other engine and therefore never
21
+ * settles its `result()` waiter. Conditioning the batch on the exact value seen
22
+ * here collapses that window: the loser's batch does not commit, and
23
+ * `buildAndCommitStartBatch` surfaces {@link WorkflowAlreadyExistsError} — the
24
+ * same error the in-engine `pendingStarts` guard already throws for the same
25
+ * collision.
26
+ *
27
+ * `expectedValue` is the RAW observed bytes, deliberately not a re-encoding of
28
+ * the decoded state: re-encoding is not guaranteed to round-trip byte-identically,
29
+ * and a condition built from one would fail against a record nothing had touched.
30
+ * `null` (id absent) and a prior terminal run's bytes (an
31
+ * `onTerminalConflict: 'start-new'` restart) are both valid expected values, so a
32
+ * restart is equally protected against a concurrent engine displacing the same
33
+ * terminal run.
34
+ *
35
+ * No `conditionalBatch` capability gate is needed. Reaching this code means a
36
+ * workflow is registered, and registration drains through
37
+ * `WorkflowCatalog#activateRegistered`, which already hard-requires that
38
+ * capability at `Engine.create()`. A store that cannot honour this condition
39
+ * cannot host an engine that could start a workflow in the first place.
40
+ *
41
+ * RESOLVED (PR #959 review; closed by WFT-153). This condition alone compares
42
+ * only a VALUE, so on its own it cannot distinguish "this id was never used"
43
+ * from "a run existed here and was purged" — a racing winner that commits,
44
+ * completes, and is purged (or swept by retention) before this batch commits
45
+ * would make `wf:<id>` absent again, matching `expectedValue: null`. That gap
46
+ * is now closed by {@link duplicateIdGenerationCondition} below, an ADDITIONAL
47
+ * condition on a durable per-id generation counter a purge bumps but never
48
+ * resets — see its own doc for the mechanism.
49
+ */
50
+ duplicateIdCondition: ConditionalBatchCondition;
51
+ /**
52
+ * ADDITIONAL compare-and-swap precondition on the observed `wf-gen:<id>`
53
+ * bytes, closing the residual ABA hole {@link duplicateIdCondition} cannot
54
+ * detect (WFT-153, following WFT-152). `duplicateIdCondition` compares a
55
+ * VALUE, so it cannot tell "this id was never used" from "a run existed
56
+ * here and was purged" — if a racing winner completes and is purged before
57
+ * this batch commits, `wf:<id>` looks absent again and `duplicateIdCondition`
58
+ * alone would match. `wf-gen:<id>` is bumped in the SAME atomic batch that
59
+ * deletes `wf:<id>` on purge (`workflow-generation-fence.ts`) and is never
60
+ * otherwise touched, so a purge landing in the read-to-commit gap changes
61
+ * this key even though `wf:<id>` reads the same — the stale loser's
62
+ * condition on the pre-purge generation bytes fails where the value-only
63
+ * comparison could not detect it. See `storage/generation-keys.ts`.
64
+ */
65
+ duplicateIdGenerationCondition: ConditionalBatchCondition;
66
+ /**
67
+ * The exact `wf-gen:<id>` bytes {@link duplicateIdGenerationCondition} was
68
+ * built from, threaded through to {@link prepareTerminalRunPurge} so a
69
+ * `'start-new'` restart's own displacing purge bumps the generation from
70
+ * this SAME observed value rather than a second, independent read. This is
71
+ * what makes the restart's own CAS trivially self-consistent — it can never
72
+ * fence itself out on its own legitimate restart, because the value it
73
+ * bumps from is exactly the value its own precondition checks.
74
+ */
75
+ observedGenerationBytes: Uint8Array | null;
76
+ };
77
+ /**
78
+ * The decision for a start whose id was GENERATED rather than caller-supplied. A
79
+ * v4 UUID is effectively unique, so the duplicate-id read is skipped entirely and
80
+ * there is no observed value to condition on — the start keeps the unconditioned
81
+ * single-write hot path.
82
+ */
83
+ export declare const GENERATED_ID_START_DECISION: {
84
+ readonly terminalRunToPurge: null;
85
+ readonly duplicateIdCondition: undefined;
86
+ readonly duplicateIdGenerationCondition: undefined;
87
+ readonly observedGenerationBytes: null;
88
+ };
5
89
  /**
6
90
  * Decide what a caller-supplied workflow id that already has a persisted record
7
91
  * means, WITHOUT performing any destructive action. Only invoked when the caller
@@ -23,9 +107,58 @@ import { type LifecycleCallbacks } from './shared.ts';
23
107
  * delete the finalizer payload before the resource is torn down, leaking it, so
24
108
  * the restart is refused until teardown settles (which clears the marker).
25
109
  *
26
- * Returns `null` when there is no existing record (the create proceeds normally).
110
+ * This read is only a point-in-time observation: another engine sharing the store
111
+ * can commit a create for the same id in the window between it and the create
112
+ * batch. The returned {@link StartDuplicateIdDecision} therefore carries a
113
+ * `duplicateIdCondition` holding the exact bytes seen here, so that batch can be
114
+ * conditioned on them — turning that window into a lost compare-and-swap rather
115
+ * than a blind overwrite (WFT-152). It also reads `wf-gen:<id>` in the SAME pass
116
+ * and carries `duplicateIdGenerationCondition`/`observedGenerationBytes` (WFT-153),
117
+ * closing the residual ABA window a value-only condition cannot detect — see that
118
+ * field's own doc.
27
119
  */
28
- export declare function resolveTerminalConflictForRestart(internals: EngineInternals, workflowId: string, options: StartWorkflowOptions | undefined): Promise<WorkflowState | null>;
120
+ export declare function resolveTerminalConflictForRestart(internals: EngineInternals, workflowId: string, options: StartWorkflowOptions | undefined): Promise<StartDuplicateIdDecision>;
121
+ /**
122
+ * The `'reattach-only'`/`'bulk-retry-only'` fence for `startWorkflow`'s
123
+ * `skipAdmissionIdCheck` parameter (WFT-95 TOCTOU fix). Call immediately
124
+ * after {@link resolveTerminalConflictForRestart} resolves.
125
+ *
126
+ * Both call sites reach `startWorkflow` after a separate, non-atomic read
127
+ * confirmed an already-persisted record they mean to replay or replace — but
128
+ * that confirmation and `resolveTerminalConflictForRestart`'s own atomic read
129
+ * are not the same read:
130
+ *
131
+ * - `dispatchChildWorkflowStart()`'s crash-reattach retry confirms a matching
132
+ * persisted child via `loadWorkflowState()`, then leaves
133
+ * `options.onTerminalConflict` unset — so `resolveTerminalConflictForRestart`
134
+ * either finds the record still there (throws {@link WorkflowAlreadyExistsError},
135
+ * this function is never reached: the expected, unraced reattach) or gone
136
+ * (`terminalRunToPurge: null`, the ordinary fresh-create branch).
137
+ * - `retryFailedWorkflow()`'s checkpoint-absent fallback (`bulk-operations-retry.ts`)
138
+ * confirms a `failed` record via its own `loadWorkflowState()`, then always
139
+ * sets `options.onTerminalConflict: 'start-new'` — so
140
+ * `resolveTerminalConflictForRestart` either finds the SAME terminal record
141
+ * still there (returns its bytes as `terminalRunToPurge`, the expected,
142
+ * unraced purge-and-replace) or finds it gone (`terminalRunToPurge: null`,
143
+ * the same ordinary fresh-create branch).
144
+ *
145
+ * Under `ownership: 'workflow-lease'`, another engine can purge the matched
146
+ * record in the window between either call site's own confirmation read and
147
+ * `resolveTerminalConflictForRestart`'s. Being called with a `'reattach-only'`
148
+ * or `'bulk-retry-only'` `skipAdmissionIdCheck` and a `null` `terminalRunToPurge`
149
+ * therefore means the race happened: there is nothing left to reattach to or
150
+ * replace, so this re-runs strict admission rather than let a bypassed
151
+ * `.`/`..` id fall through into a genuinely fresh create. `coerceStartWorkflowId`
152
+ * throws the same `StartWorkflowValidationError` strict admission would have
153
+ * thrown on the caller's very first (non-retry) attempt — a clean,
154
+ * deterministic rejection instead of a silently created reserved-id run.
155
+ *
156
+ * A no-op for every other `skipAdmissionIdCheck` value: `true` (schedule
157
+ * drain) applies unconditionally and never calls this, and `undefined`
158
+ * (every public start surface) already went through strict admission in
159
+ * `prepareStartWorkflow`.
160
+ */
161
+ export declare function enforceReplayOnlyIdFence(skipAdmissionIdCheck: boolean | 'reattach-only' | 'bulk-retry-only' | undefined, workflowId: string, terminalRunToPurge: WorkflowState | null): void;
29
162
  /**
30
163
  * Prepare a prior terminal run for displacement by a `'start-new'` restart WITHOUT
31
164
  * committing the destructive delete. Returns the storage delete operations (for the
@@ -35,5 +168,14 @@ export declare function resolveTerminalConflictForRestart(internals: EngineInter
35
168
  * entries. `clearPurgedWorkflowInMemoryState` runs `cleanupWaiters` to settle the
36
169
  * old run's pending signal/update/sleep waiters; it only needs
37
170
  * `swallowPromiseRejection`, which `LifecycleCallbacks` already exposes.
171
+ *
172
+ * Also appends the `wf-gen:<id>` bump PUT operation (WFT-153), built from
173
+ * `observedGenerationBytes` — the SAME bytes `resolveTerminalConflictForRestart`
174
+ * already read for `duplicateIdGenerationCondition` — rather than a second,
175
+ * independent read. Reusing that one observed value for both the outer CAS
176
+ * condition and the bump amount is what makes this restart's own commit
177
+ * trivially self-consistent: it can never fence itself out on its own
178
+ * legitimate restart, because the value it bumps from is exactly the value its
179
+ * own precondition checks (see `duplicateIdGenerationCondition`'s doc).
38
180
  */
39
- export declare function prepareTerminalRunPurge(internals: EngineInternals, state: WorkflowState, callbacks: LifecycleCallbacks): Promise<BatchOperation[]>;
181
+ export declare function prepareTerminalRunPurge(internals: EngineInternals, state: WorkflowState, callbacks: LifecycleCallbacks, observedGenerationBytes: Uint8Array | null): Promise<BatchOperation[]>;
@@ -1,4 +1,5 @@
1
1
  import { KEYS, storageHas } from "../../../storage/interface.js";
2
+ import { coerceStartWorkflowId } from "../../start-workflow-validation.js";
2
3
  import {
3
4
  clearPurgedWorkflowInMemoryState,
4
5
  collectWorkflowPurgeDeleteOperations
@@ -6,10 +7,28 @@ import {
6
7
  import { WorkflowAlreadyExistsError, WorkflowTeardownPendingError } from "../errors.js";
7
8
  import { cleanupWaiters } from "../termination/cleanup.js";
8
9
  import { decodeWorkflowState, isTerminalWorkflowStatus } from "../validation.js";
10
+ import { buildWorkflowGenerationBumpOperation } from "../workflow-generation-fence.js";
11
+ export const GENERATED_ID_START_DECISION = {
12
+ terminalRunToPurge: null,
13
+ duplicateIdCondition: void 0,
14
+ duplicateIdGenerationCondition: void 0,
15
+ observedGenerationBytes: null
16
+ };
9
17
  export async function resolveTerminalConflictForRestart(internals, workflowId, options) {
10
- const existingBytes = await internals.storage.get(KEYS.workflow(workflowId));
18
+ const key = KEYS.workflow(workflowId), generationKey = KEYS.workflowGeneration(workflowId), [existingBytes, observedGenerationBytes] = await Promise.all([
19
+ internals.storage.get(key),
20
+ internals.storage.get(generationKey)
21
+ ]), duplicateIdGenerationCondition = {
22
+ key: generationKey,
23
+ expectedValue: observedGenerationBytes
24
+ };
11
25
  if (existingBytes === null)
12
- return null;
26
+ return {
27
+ terminalRunToPurge: null,
28
+ duplicateIdCondition: { key, expectedValue: null },
29
+ duplicateIdGenerationCondition,
30
+ observedGenerationBytes
31
+ };
13
32
  if (options?.onTerminalConflict !== "start-new")
14
33
  throw new WorkflowAlreadyExistsError(workflowId);
15
34
  const existingState = decodeWorkflowState(existingBytes);
@@ -17,12 +36,22 @@ export async function resolveTerminalConflictForRestart(internals, workflowId, o
17
36
  throw new WorkflowAlreadyExistsError(workflowId);
18
37
  if (await storageHas(internals.storage, KEYS.teardownOwed(workflowId)))
19
38
  throw new WorkflowTeardownPendingError(workflowId);
20
- return existingState;
39
+ return {
40
+ terminalRunToPurge: existingState,
41
+ duplicateIdCondition: { key, expectedValue: existingBytes },
42
+ duplicateIdGenerationCondition,
43
+ observedGenerationBytes
44
+ };
21
45
  }
22
- export async function prepareTerminalRunPurge(internals, state, callbacks) {
46
+ export function enforceReplayOnlyIdFence(skipAdmissionIdCheck, workflowId, terminalRunToPurge) {
47
+ if ((skipAdmissionIdCheck === "reattach-only" || skipAdmissionIdCheck === "bulk-retry-only") && terminalRunToPurge === null)
48
+ coerceStartWorkflowId(workflowId, "options.id");
49
+ }
50
+ export async function prepareTerminalRunPurge(internals, state, callbacks, observedGenerationBytes) {
23
51
  const cleanupWaitersForStart = (id) => cleanupWaiters(internals, id, {
24
52
  swallowPromiseRejection: callbacks.swallowPromiseRejection
25
53
  }), deleteOperations = await collectWorkflowPurgeDeleteOperations(internals, state);
54
+ deleteOperations.push(buildWorkflowGenerationBumpOperation(state.id, observedGenerationBytes));
26
55
  clearPurgedWorkflowInMemoryState(internals, state.id, cleanupWaitersForStart);
27
56
  return deleteOperations;
28
57
  }