@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
@@ -17,7 +17,21 @@ async function isWorkflowTypeRegistered(storage, workflowId, isTypeRegistered) {
17
17
  if (bytes === null)
18
18
  return !1;
19
19
  try {
20
- return isTypeRegistered(decodeWorkflowState(bytes).type);
20
+ const state = decodeWorkflowState(bytes);
21
+ return isTypeRegistered(state.type, state.revision);
22
+ } catch {
23
+ return !1;
24
+ }
25
+ }
26
+ async function isWorkflowStillRunningAndEligible(storage, workflowId, isTypeRegistered) {
27
+ const bytes = await storage.get(KEYS.workflow(workflowId));
28
+ if (bytes === null)
29
+ return !1;
30
+ try {
31
+ const state = decodeWorkflowState(bytes);
32
+ if (state.status !== "running")
33
+ return !1;
34
+ return isTypeRegistered === void 0 || isTypeRegistered(state.type, state.revision);
21
35
  } catch {
22
36
  return !1;
23
37
  }
@@ -39,14 +53,14 @@ export function createWorkflowClaimReclaimTarget(registry, storage, metrics, onR
39
53
  throw error;
40
54
  }
41
55
  }
42
- async function releaseClaimAcquiredWhileDisposing(workflowId) {
43
- await registry.release(workflowId);
56
+ async function releaseClaimAcquiredWhileDisposing(workflowId, acquiredEpoch) {
57
+ await registry.release(workflowId, acquiredEpoch);
44
58
  }
45
59
  async function confirmStillRunningOrReleaseFreshClaim(workflowId, acquiredEpoch) {
46
- if (await isWorkflowStillRunning(storage, workflowId))
60
+ if (await isWorkflowStillRunningAndEligible(storage, workflowId, isTypeRegistered))
47
61
  return !0;
48
62
  if (registry.currentEpoch(workflowId) === acquiredEpoch)
49
- await registry.release(workflowId);
63
+ await registry.release(workflowId, acquiredEpoch);
50
64
  return !1;
51
65
  }
52
66
  async function redriveAlreadyHeldClaim(workflowId, expectedEpoch) {
@@ -55,7 +69,7 @@ export function createWorkflowClaimReclaimTarget(registry, storage, metrics, onR
55
69
  if (!await isWorkflowStillRunning(storage, workflowId)) {
56
70
  pendingRedriveWorkflowIds.delete(workflowId);
57
71
  if (registry.currentEpoch(workflowId) === expectedEpoch)
58
- await registry.release(workflowId);
72
+ await registry.release(workflowId, expectedEpoch);
59
73
  return { status: "not-eligible" };
60
74
  }
61
75
  if (disposing)
@@ -76,7 +90,7 @@ export function createWorkflowClaimReclaimTarget(registry, storage, metrics, onR
76
90
  }
77
91
  metrics.recordClaimAttempt("acquired");
78
92
  if (disposing) {
79
- await releaseClaimAcquiredWhileDisposing(workflowId);
93
+ await releaseClaimAcquiredWhileDisposing(workflowId, acquireResult.epoch);
80
94
  return { status: "not-eligible" };
81
95
  }
82
96
  if (!await confirmStillRunningOrReleaseFreshClaim(workflowId, acquireResult.epoch))
@@ -86,7 +100,7 @@ export function createWorkflowClaimReclaimTarget(registry, storage, metrics, onR
86
100
  async function handleTakeoverAcquired(workflowId, acquiredEpoch) {
87
101
  metrics.recordClaimAttempt("takeover");
88
102
  if (disposing) {
89
- await releaseClaimAcquiredWhileDisposing(workflowId);
103
+ await releaseClaimAcquiredWhileDisposing(workflowId, acquiredEpoch);
90
104
  return { status: "not-eligible" };
91
105
  }
92
106
  if (!await confirmStillRunningOrReleaseFreshClaim(workflowId, acquiredEpoch))
@@ -8,39 +8,43 @@
8
8
  * fragments; this module is the thin, stateful layer around them that reads
9
9
  * storage, executes `storageConditionalBatch`, and tracks — for every claim
10
10
  * THIS engine currently holds — the exact epoch and holder bytes it last
11
- * wrote. Renewal and release condition on those exact bytes, extracted from
12
- * the fragment they were just written by rather than re-encoded from the
13
- * fields the registry happens to know, mirroring `lease-manager.ts`'s
14
- * "never round-trip encode(decode(raw))" discipline.
11
+ * wrote, extracted from the fragment they were just written by rather than
12
+ * re-encoded, mirroring `lease-manager.ts`'s "never round-trip
13
+ * encode(decode(raw))" discipline.
15
14
  *
16
15
  * **Scope.** The unit itself: acquire, renew, release, takeover, and
17
16
  * release-all — plus, additively, {@link WorkflowClaimRegistry.prepareAcquireFragment}
18
17
  * and {@link WorkflowClaimRegistry.recordFoldedAcquire}, the two-step seam a
19
- * caller uses to fold `acquire` into ITS OWN atomic enabling write (a create
20
- * batch, a delayed-start pending→running transition, a failed-workflow
21
- * reactivation) instead of committing the fragment through this registry's
22
- * own `acquire()`. Driving `renew` and the reclaim scan from a lifecycle
23
- * task, and turning a lost `acquire`/`takeover` into
24
- * `WorkflowClaimUnavailableError` for explicit single-workflow callers, are
25
- * still each call site's own responsibility this registry never throws
26
- * that error itself. Per the ADR, background scanning never throws it either
27
- * it skips the workflow and continues — so every method here returns a
28
- * discriminated result instead of throwing on a lost CAS, leaving that
29
- * decision to the caller — `takeover` also gates on a per-workflow-id
30
- * anti-thrash cooldown ({@link WorkflowClaimTakeoverCooldown}). Still out of
31
- * scope: `weft_workflow_claim_*` metrics, `wakeOwnershipCheck`, and external
32
- * terminal-transition rotation (any engine may commit those unconditioned).
18
+ * caller uses to fold `acquire` into ITS OWN atomic enabling write instead of
19
+ * committing the fragment through this registry's own `acquire()`. Driving
20
+ * `renew`/the reclaim scan, and turning a lost `acquire`/`takeover` into
21
+ * `WorkflowClaimUnavailableError` for explicit single-workflow callers, stay
22
+ * each call site's responsibility this registry never throws that error,
23
+ * and per the ADR background scanning never does either, so every method
24
+ * returns a discriminated result instead. `takeover` also gates on a
25
+ * per-workflow-id anti-thrash cooldown ({@link WorkflowClaimTakeoverCooldown}).
26
+ * Still out of scope: `weft_workflow_claim_*` metrics, `wakeOwnershipCheck`,
27
+ * and external terminal-transition rotation.
33
28
  *
34
29
  * **Renewal-vs-release serialization.** A per-workflow in-flight-renewal
35
30
  * promise (mirroring `lease-manager.ts`'s single `inFlightRenewal`) lets
36
31
  * `release()` await a renewal already in progress before reading the cached
37
- * bytes it conditions on; otherwise both race the same holder bytes and
38
- * whichever commits second loses its CAS. A `releasing` set stops a NEW
39
- * renewal from starting once release has begun, so a `renew()` arriving
40
- * mid-release fails fast as `'not-held'`. A renewal that THROWS (a transient
41
- * storage error, not a CAS-false result) is not a lost claim: it propagates
42
- * to its caller leaving cached bytes untouched, and `release()`'s wait
43
- * swallows it — a storage hiccup must not fail a terminal or shutdown release.
32
+ * bytes it conditions on; otherwise both race the same holder bytes. A
33
+ * `releasing` set stops a NEW renewal from starting once release has begun,
34
+ * so a `renew()` arriving mid-release fails fast as `'not-held'`. A renewal
35
+ * that THROWS (transient, not a CAS-false result) is not a lost claim: it
36
+ * propagates leaving cached bytes untouched, and `release()`'s wait swallows
37
+ * it a storage hiccup must not fail a terminal or shutdown release.
38
+ *
39
+ * **Epoch-guarded release (WFT-134).** `release(workflowId, expectedEpoch?)`
40
+ * takes an OPTIONAL epoch a caller itself acquired; without it, `release`
41
+ * drops whatever entry is CURRENTLY tracked — correct only when nothing else
42
+ * could have replaced the claim meanwhile. A caller undoing ITS OWN
43
+ * acquisition must pass that epoch: a `recordFoldedAcquire` replacement can
44
+ * land even during this call's own in-flight-renewal await, and
45
+ * `expectedEpoch` is re-checked against the freshly-read entry AFTER that
46
+ * await, so a stale pre-await snapshot never authorizes releasing a newer,
47
+ * live claim.
44
48
  *
45
49
  * @module core/engine/workflow-claim-registry
46
50
  */
@@ -56,10 +60,7 @@ export type WorkflowClaimRegistryOptions = {
56
60
  getNow: () => number;
57
61
  /** `workflowClaimTtl` (ms), resolved by `ownership-options.ts`. */
58
62
  claimTtlMs: number;
59
- /**
60
- * `workflowClaimRenewInterval` (ms), resolved by `ownership-options.ts` —
61
- * feeds `isWorkflowClaimExpired`'s grace term.
62
- */
63
+ /** `workflowClaimRenewInterval` (ms) — feeds `isWorkflowClaimExpired`'s grace term. */
63
64
  claimRenewIntervalMs: number;
64
65
  /** Operator-warning seam; defaults to `process.emitWarning` via {@link emitWorkflowClaimLostWarning}. */
65
66
  warn?: EmitWorkflowLeaseWarning;
@@ -98,8 +99,8 @@ export type WorkflowClaimReleaseResult = {
98
99
  };
99
100
  /**
100
101
  * A prepared, not-yet-committed `acquire` — the output of
101
- * {@link WorkflowClaimRegistry.prepareAcquireFragment}, meant to be merged
102
- * into a caller's own atomic enabling write and then handed back to
102
+ * {@link WorkflowClaimRegistry.prepareAcquireFragment}, merged into a
103
+ * caller's own atomic write and handed to
103
104
  * {@link WorkflowClaimRegistry.recordFoldedAcquire} once that write commits.
104
105
  */
105
106
  export type WorkflowClaimAcquirePreparation = {
@@ -130,8 +131,8 @@ export type WorkflowClaimTakeoverResult = {
130
131
  };
131
132
  /**
132
133
  * Owns this engine's per-workflow ownership claims: reads storage, executes
133
- * the pure transition fragments, and tracks the exact bytes it last wrote for
134
- * every claim it currently holds. See the module doc for scope.
134
+ * the pure transition fragments, and tracks the exact bytes last written for
135
+ * every held claim. See the module doc for scope.
135
136
  */
136
137
  export declare class WorkflowClaimRegistry {
137
138
  #private;
@@ -142,25 +143,20 @@ export declare class WorkflowClaimRegistry {
142
143
  get engineId(): string;
143
144
  /**
144
145
  * Every workflow id this engine currently tracks a live claim for — active
145
- * or parked. A defensive-copy snapshot, mirroring `releaseAll`'s own
146
- * `[...this.#claims.keys()]` read: callers (the claim-renewal task, an
147
- * active-claims metrics gauge) must not observe mutations to this registry's
148
- * internal map while iterating a snapshot they already took.
146
+ * or parked. A defensive-copy snapshot: callers (the claim-renewal task, an
147
+ * active-claims metrics gauge) must not observe mutations to this
148
+ * registry's internal map while iterating a snapshot they already took.
149
149
  */
150
150
  listHeldWorkflowIds(): readonly string[];
151
151
  /**
152
152
  * Defensive copy of the epoch bytes this engine last wrote for
153
- * `workflowId`, for fencing durable writes — `null` if untracked. A copy so
154
- * a caller mutating the returned buffer cannot corrupt this registry's
155
- * cached fencing token.
153
+ * `workflowId`, for fencing durable writes — `null` if untracked.
156
154
  */
157
155
  currentEpochBytes(workflowId: string): Uint8Array | null;
158
156
  /**
159
- * `acquire`: always reads both keys fresh never assumes absence — then
160
- * builds and attempts the fragment from what it read. On a lost CAS,
161
- * resolves `heldBy` from the holder bytes already read when they were
162
- * non-null; otherwise (a competitor raced in between the read and the
163
- * write) re-reads to report the true current holder.
157
+ * `acquire`: always reads both keys fresh, then builds and attempts the
158
+ * fragment from what it read. On a lost CAS, resolves `heldBy` from the
159
+ * already-read holder bytes when non-null; otherwise re-reads.
164
160
  */
165
161
  acquire(workflowId: string): Promise<WorkflowClaimAcquireResult>;
166
162
  /**
@@ -168,60 +164,68 @@ export declare class WorkflowClaimRegistry {
168
164
  * `acquire` fragment WITHOUT committing it or updating this registry's
169
165
  * tracking — for a caller that folds the fragment into ITS OWN atomic
170
166
  * enabling write instead of letting {@link acquire} commit it alone. The
171
- * caller merges `fragment.conditions`/`fragment.operations` into its own
172
- * operation list, commits ONE atomic `storageConditionalBatch`, and —
173
- * ONLY on success — calls {@link recordFoldedAcquire} with this SAME
174
- * preparation. Safe to call again on every retry attempt: this always
175
- * re-reads fresh bytes, so a stale epoch from an earlier attempt never
176
- * dooms a later one.
167
+ * caller merges the fragment into its own operation list, commits ONE
168
+ * atomic `storageConditionalBatch`, and — ONLY on success — calls
169
+ * {@link recordFoldedAcquire} with this SAME preparation. Safe to call
170
+ * again on every retry: it always re-reads fresh bytes.
177
171
  */
178
172
  prepareAcquireFragment(workflowId: string): Promise<WorkflowClaimAcquirePreparation>;
179
173
  /**
180
174
  * Install the tracking entry for a claim acquired via a FOLDED enabling
181
175
  * write (see {@link prepareAcquireFragment}) — call ONLY after the
182
- * caller's own atomic commit that included `preparation.fragment`'s
183
- * conditions and operations has actually succeeded. Extracts the exact
184
- * bytes the fragment wrote using the same "never round-trip
185
- * encode(decode(raw))" discipline every other grant path in this class
186
- * uses.
176
+ * caller's own atomic commit of `preparation.fragment` has succeeded.
177
+ * Extracts the exact written bytes, the same "never round-trip
178
+ * encode(decode(raw))" discipline every other grant path here uses.
187
179
  */
188
180
  recordFoldedAcquire(workflowId: string, preparation: WorkflowClaimAcquirePreparation): void;
189
181
  /**
190
182
  * `renew`: conditions on the exact holder bytes this engine last wrote.
191
183
  * Concurrent calls for the same id share the one in-flight promise. A
192
184
  * CAS-false result marks the claim lost locally and emits
193
- * `WeftWorkflowClaimLostWarning` — losing one workflow's claim never
194
- * touches any other tracked claim.
185
+ * `WeftWorkflowClaimLostWarning`.
195
186
  */
196
187
  renew(workflowId: string): Promise<WorkflowClaimRenewResult>;
197
188
  /**
198
189
  * `release`: stops new renewals for `workflowId` and awaits any renewal
199
- * already in flight (swallowing a thrown rejection — best-effort, never
200
- * reject on a renewal's storage error) before building the expected bytes,
201
- * so the two can never race the same holder bytes. Deletes only the holder
202
- * key — the epoch key is never touched, so a successor's next `acquire`
203
- * reads the true prior epoch. A lost CAS means this engine was already
204
- * fenced out; the local entry is dropped either way, since there is
205
- * nothing left to protect.
190
+ * already in flight (best-effort — swallows a thrown rejection) before
191
+ * building the expected bytes, so the two never race the same holder
192
+ * bytes. Deletes only the holder key, never the epoch key, so a
193
+ * successor's next `acquire` reads the true prior epoch.
194
+ *
195
+ * `expectedEpoch`, when given, makes this a no-op (`'not-held'`) against
196
+ * any entry but the exact generation the caller means to undo — see the
197
+ * module doc's "Epoch-guarded release". A thrown storage error during the
198
+ * CAS still forgets the LOCAL entry (identity-guarded like the
199
+ * success/failure paths below) before rethrowing, so a caller giving up
200
+ * after a failed release does not leave the renewal task renewing forever
201
+ * (WFT-134).
206
202
  */
207
- release(workflowId: string): Promise<WorkflowClaimReleaseResult>;
203
+ release(workflowId: string, expectedEpoch?: number): Promise<WorkflowClaimReleaseResult>;
208
204
  /**
209
205
  * `takeover`: reads the holder and epoch keys fresh, and only attempts the
210
206
  * CAS once the holder is not live — either its grace-adjusted `expiresAt`
211
207
  * has passed ({@link isWorkflowClaimExpired}), or the holder bytes are
212
208
  * foreign/undecodable garbage no valid engine could have written (mirrors
213
- * `lease-manager.ts`'s "garbage is not a live owner" treatment: not live,
214
- * so it can be stolen via CAS on its exact observed bytes). A holder
215
- * present with no epoch key violates the write invariant the two are
216
- * always written together, and the epoch key is never deleted — so it is
217
- * treated defensively as nothing safe to fence a takeover against.
209
+ * `lease-manager.ts`'s "garbage is not a live owner" treatment). A holder
210
+ * with no epoch key violates the write invariant the two are always
211
+ * written together, and the epoch key is never deletedso it is treated
212
+ * defensively as nothing safe to fence a takeover against.
218
213
  */
219
214
  takeover(workflowId: string): Promise<WorkflowClaimTakeoverResult>;
220
215
  /**
221
- * Best-effort release of every claim this engine currently tracks, for
222
- * graceful shutdown. A failed release (thrown or lost-race) is swallowed
223
- * per workflow so shutdown proceeds the reclaim scan (a later stage)
224
- * collects any stranded claim once its grace-adjusted expiry passes.
216
+ * Forget this engine's LOCAL tracking entry for `workflowId` no durable
217
+ * write, just `this.#claims.delete`. Caller: `suspendWorkflow` (WFT-134),
218
+ * whose commit already durably deletes `wf-owner-holder:<id>`, leaving only
219
+ * this stale cache entry else {@link currentEpoch} stays non-null for a
220
+ * gone holder, routing a same-engine `resume()` onto the stale-cache fast
221
+ * path in `acquireStandaloneClaimBeforeResume`. No-op on an untracked id;
222
+ * check {@link currentEpoch} first to avoid clobbering a newer generation.
223
+ */
224
+ forgetLocalClaim(workflowId: string): void;
225
+ /**
226
+ * Best-effort release of every claim this engine tracks, for graceful
227
+ * shutdown. A failed release (thrown or lost-race) is swallowed per
228
+ * workflow — the reclaim scan later collects any stranded claim.
225
229
  */
226
230
  releaseAll(): Promise<void>;
227
231
  }
@@ -125,7 +125,7 @@ export class WorkflowClaimRegistry {
125
125
  this.#claims.set(workflowId, { ...entry, holderBytes });
126
126
  return { status: "renewed", workflowId };
127
127
  }
128
- async release(workflowId) {
128
+ async release(workflowId, expectedEpoch) {
129
129
  this.#releasing.add(workflowId);
130
130
  try {
131
131
  const inFlight = this.#inFlightRenewals.get(workflowId);
@@ -136,11 +136,21 @@ export class WorkflowClaimRegistry {
136
136
  const entry = this.#claims.get(workflowId);
137
137
  if (entry === void 0)
138
138
  return { status: "not-held", workflowId };
139
+ if (expectedEpoch !== void 0 && entry.epoch !== expectedEpoch)
140
+ return { status: "not-held", workflowId };
139
141
  const fragment = buildWorkflowClaimReleaseTransition({
140
142
  workflowId,
141
143
  currentEpochBytes: entry.epochBytes,
142
144
  currentHolderBytes: entry.holderBytes
143
- }), committed = await storageConditionalBatch(this.#claimStorage, fragment.conditions, fragment.operations);
145
+ });
146
+ let committed;
147
+ try {
148
+ committed = await storageConditionalBatch(this.#claimStorage, fragment.conditions, fragment.operations);
149
+ } catch (error) {
150
+ if (this.#claims.get(workflowId) === entry)
151
+ this.#claims.delete(workflowId);
152
+ throw error;
153
+ }
144
154
  if (this.#claims.get(workflowId) === entry)
145
155
  this.#claims.delete(workflowId);
146
156
  return { status: committed ? "released" : "lost-race", workflowId };
@@ -187,6 +197,9 @@ export class WorkflowClaimRegistry {
187
197
  this.#takeoverCooldown.clear(workflowId);
188
198
  return { status: "acquired", workflowId, epoch };
189
199
  }
200
+ forgetLocalClaim(workflowId) {
201
+ this.#claims.delete(workflowId);
202
+ }
190
203
  async releaseAll() {
191
204
  const workflowIds = [...this.#claims.keys()];
192
205
  await Promise.all(workflowIds.map(async (workflowId) => {
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Bump helpers for the durable per-workflow-id generation counter
3
+ * (`wf-gen:<id>`, WFT-153). See `storage/generation-keys.ts` for the
4
+ * keyspace's full rationale.
5
+ *
6
+ * Mirrors `workflow-claim-transitions.ts`'s epoch-rotation fragment shape
7
+ * (`nextEpochFromObservedBytes` / `buildWorkflowClaimExternalTerminalRotationTransition`)
8
+ * deliberately: both are "mint `(observed ?? 0) + 1`, never a literal"
9
+ * permanently-retained counters bumped in the same atomic batch as the
10
+ * transition that motivates the bump.
11
+ *
12
+ * @module core/engine/workflow-generation-fence
13
+ */
14
+ import { type BatchOperation } from '../../storage/interface.ts';
15
+ import type { EngineInternals } from './internals.ts';
16
+ import type { WorkflowClaimTransitionFragment } from './workflow-claim-transitions.ts';
17
+ /** Mint the next generation from bytes just read: `(decode(bytes) ?? 0) + 1`, never a literal. */
18
+ export declare function nextGenerationFromObservedBytes(observedGenerationBytes: Uint8Array | null): number;
19
+ /**
20
+ * Build just the `wf-gen:<id>` bump PUT operation from an ALREADY-OBSERVED
21
+ * value — no storage read of its own. Used by the `onTerminalConflict:
22
+ * 'start-new'` restart path (`start-terminal-conflict-purge.ts`), which folds
23
+ * this operation into its create batch and relies on that batch's own outer
24
+ * `duplicateIdGenerationCondition` — built from the SAME observed bytes this
25
+ * function bumps from — as the CAS fence, rather than a second, independent
26
+ * condition here. Reusing one observed value for both the fence and the bump
27
+ * amount is what makes the restart's own CAS trivially self-consistent: it
28
+ * can never fence itself out on its own legitimate restart, because the
29
+ * value it bumps from is exactly the value its own precondition checks.
30
+ */
31
+ export declare function buildWorkflowGenerationBumpOperation(workflowId: string, observedGenerationBytes: Uint8Array | null): BatchOperation;
32
+ /**
33
+ * Read the current `wf-gen:<id>` value, build its bump PUT operation and (CAS
34
+ * condition permitting) fold BOTH into `base` — the caller's own fragment, in
35
+ * `purgeWorkflow` the `wf-owner-epoch` rotation fragment
36
+ * `buildExternalTerminalRotationFragment` already built — for a STANDALONE
37
+ * purge/retention commit that, unlike the `'start-new'` restart path above,
38
+ * has no pre-existing duplicate-id-style condition of its own to piggyback
39
+ * on. Folding happens HERE, not at the call site, so `purgeWorkflow` stays a
40
+ * flat read-then-commit without its own merge step.
41
+ *
42
+ * RESOLVED (chatgpt-codex-connector review, WFT-153): this bump PUT was
43
+ * previously unconditioned, on the theory that a lost update between two
44
+ * concurrent purges of the same id was harmless — either purge's bump moves
45
+ * the value away from whatever an earlier duplicate-id read observed. That
46
+ * theory misses a THIRD purge landing in between: purge A reads generation
47
+ * N and (slowly) prepares a bump to N+1; purge B — a later purge of the
48
+ * SAME id, after the id was reused and purged again — reads the CURRENT
49
+ * value N+1 and commits a bump to N+2; if A's stale N+1 write then commits
50
+ * UNCONDITIONED, it overwrites B's N+2 with A's own N+1, rolling the
51
+ * "monotonic" counter backward. A cross-engine start that captured the
52
+ * intermediate absent/N+1 pair during the window before B's purge could then
53
+ * pass its `duplicateIdGenerationCondition` re-check after the rollback, even
54
+ * though a run genuinely executed and was purged in between — reopening the
55
+ * exact ABA this key exists to close.
56
+ *
57
+ * The condition folded in now closes that: `expectedValue` is the SAME
58
+ * `observedGenerationBytes` the bump amount is minted from, so a lost race
59
+ * (another purge already changed `wf-gen:<id>` since this read) fails the
60
+ * CAS instead of overwriting a newer generation with a stale one. Gated on
61
+ * `internals.storage.capabilities().conditionalBatch` (the same
62
+ * capability-conditioned pattern `buildWorkflowStateCommit` uses in
63
+ * `storage-io.ts`) rather than required unconditionally: a backend that
64
+ * honestly reports no `conditionalBatch` support keeps the pre-existing
65
+ * unconditioned bump (a residual, capability-limited ABA window, not a new
66
+ * regression) instead of newly requiring a capability purge never required
67
+ * before this fix — purge must keep working, degraded, on such backends.
68
+ *
69
+ * Runs under EVERY ownership mode — unlike the `wf-owner-epoch` rotation
70
+ * fragment, which is a no-op outside `ownership: 'workflow-lease'`, this
71
+ * always reads and bumps: the ABA hole it closes exists under `'none'` and
72
+ * `'lease'` too.
73
+ */
74
+ export declare function foldWorkflowGenerationBumpForPurge(internals: EngineInternals, workflowId: string, base: WorkflowClaimTransitionFragment): Promise<WorkflowClaimTransitionFragment>;
@@ -0,0 +1,19 @@
1
+ import { KEYS } from "../../storage/interface.js";
2
+ import { decodeGeneration, encodeGeneration } from "./generation-codec.js";
3
+ export function nextGenerationFromObservedBytes(observedGenerationBytes) {
4
+ return ((observedGenerationBytes === null ? null : decodeGeneration(observedGenerationBytes)) ?? 0) + 1;
5
+ }
6
+ export function buildWorkflowGenerationBumpOperation(workflowId, observedGenerationBytes) {
7
+ return {
8
+ type: "put",
9
+ key: KEYS.workflowGeneration(workflowId),
10
+ value: encodeGeneration(nextGenerationFromObservedBytes(observedGenerationBytes))
11
+ };
12
+ }
13
+ export async function foldWorkflowGenerationBumpForPurge(internals, workflowId, base) {
14
+ const key = KEYS.workflowGeneration(workflowId), observedGenerationBytes = await internals.storage.get(key), operation = buildWorkflowGenerationBumpOperation(workflowId, observedGenerationBytes), condition = internals.storage.capabilities().conditionalBatch ? { key, expectedValue: observedGenerationBytes } : void 0;
15
+ return {
16
+ operations: [...base.operations, operation],
17
+ conditions: base.conditions.concat(condition ?? [])
18
+ };
19
+ }
@@ -0,0 +1,16 @@
1
+ import type { WorkflowState } from '../types.ts';
2
+ import type { EngineInternals } from './internals.ts';
3
+ /**
4
+ * The timestamp at which `state` becomes eligible for retention-driven
5
+ * purge, or `null` when it is non-terminal, has no applicable retention
6
+ * window, or (WFT-19 review round 2) its own pinned revision's retention
7
+ * policy cannot be resolved on this process right now — an unresolvable pin
8
+ * is NOT purge-eligible under the engine default; it is re-examined on a
9
+ * later sweep once the pin becomes resolvable, rather than risking an
10
+ * irreversible early purge against a shorter policy than the run's own.
11
+ * Falls back to the resolved dynamic definition's own retention policy for a
12
+ * `registerSource()`-registered type (see `resolveRetentionPolicyForState()`),
13
+ * then the engine-wide default for any other type — mirroring `retention.ts`'s
14
+ * `resolveWorkflowTypeRetention`.
15
+ */
16
+ export declare function getWorkflowRetentionDeadline(internals: EngineInternals, state: WorkflowState): Promise<number | null>;
@@ -0,0 +1,33 @@
1
+ import { DynamicWorkflowSourceUnavailableError } from "./dynamic-source-errors.js";
2
+ import {
3
+ getResolvedDynamicRegistration,
4
+ resolveExecutableRegistrationForRevision
5
+ } from "./dynamic-source-execution.js";
6
+ import { WorkflowRevisionUnavailableError } from "./revision-errors.js";
7
+ import { isTerminalWorkflowStatus, resolveRetentionForStatus } from "./validation.js";
8
+ async function resolveRetentionPolicyForState(internals, state) {
9
+ const eagerOrAlreadyResolved = getResolvedDynamicRegistration(internals, state.type, state.revision)?.retention;
10
+ if (eagerOrAlreadyResolved !== void 0)
11
+ return { kind: "resolved", policy: eagerOrAlreadyResolved };
12
+ if (!internals.sources.byName.has(state.type))
13
+ return { kind: "resolved", policy: void 0 };
14
+ try {
15
+ const { entry } = await resolveExecutableRegistrationForRevision(internals.engine, internals, state.type, state.revision);
16
+ return { kind: "resolved", policy: entry.retention };
17
+ } catch (error) {
18
+ if (error instanceof DynamicWorkflowSourceUnavailableError || error instanceof WorkflowRevisionUnavailableError)
19
+ return { kind: "unresolvable" };
20
+ throw error;
21
+ }
22
+ }
23
+ export async function getWorkflowRetentionDeadline(internals, state) {
24
+ if (!isTerminalWorkflowStatus(state.status))
25
+ return null;
26
+ const resolution = await resolveRetentionPolicyForState(internals, state);
27
+ if (resolution.kind === "unresolvable")
28
+ return null;
29
+ const policy = resolution.policy ?? internals.options.retention, retentionMs = resolveRetentionForStatus(policy, state.status);
30
+ if (retentionMs === void 0)
31
+ return null;
32
+ return state.updatedAt + retentionMs;
33
+ }
@@ -7,6 +7,7 @@ import type { SignalDeliveredEvent, SignalReceivedEvent } from './signal-events.
7
7
  import type { AlertFiredEvent, AlertResolvedEvent, CheckpointSizeWarningEvent, CleanupWarningEvent, ConstraintViolatedEvent, DevelopmentWarningEvent, StorageSizeReportedEvent, WorkerConnectedEvent, WorkerDisconnectedEvent } from './system-events.ts';
8
8
  import type { UpdateCompletedEvent, UpdateReceivedEvent } from './update-events.ts';
9
9
  import type { WorkflowCancelledEvent, WorkflowCompletedEvent, WorkflowDefinitionRegisteredEvent, WorkflowFailedEvent, WorkflowRecoverySkippedEvent, WorkflowResumedEvent, WorkflowStartedEvent, WorkflowSuspendedEvent, WorkflowTeardownEvent, WorkflowTimedOutEvent } from './workflow-events.ts';
10
+ import type { WorkflowSourceLoadCancelledEvent, WorkflowSourceLoadFailedEvent, WorkflowSourceLoadReadyEvent, WorkflowSourceLoadStartedEvent } from './workflow-source-events.ts';
10
11
  /**
11
12
  * Record mapping each event-name string the {@link Engine} dispatches to its
12
13
  * corresponding typed `Event` subclass.
@@ -63,6 +64,10 @@ export type WeftEventMap = {
63
64
  'catalog:activation-rejected': WorkflowRevisionActivationRejectedEvent;
64
65
  'catalog:revision-draining': WorkflowRevisionDrainingEvent;
65
66
  'catalog:revision-removed': WorkflowRevisionRemovedEvent;
67
+ 'workflow-source:load-started': WorkflowSourceLoadStartedEvent;
68
+ 'workflow-source:load-ready': WorkflowSourceLoadReadyEvent;
69
+ 'workflow-source:load-failed': WorkflowSourceLoadFailedEvent;
70
+ 'workflow-source:load-cancelled': WorkflowSourceLoadCancelledEvent;
66
71
  };
67
72
  /**
68
73
  * Typed version of the `EventTarget` interface that constrains
@@ -7,3 +7,4 @@ export * from './signal-events.ts';
7
7
  export * from './system-events.ts';
8
8
  export * from './update-events.ts';
9
9
  export * from './workflow-events.ts';
10
+ export * from './workflow-source-events.ts';
@@ -7,3 +7,4 @@ export * from "./signal-events.js";
7
7
  export * from "./system-events.js";
8
8
  export * from "./update-events.js";
9
9
  export * from "./workflow-events.js";
10
+ export * from "./workflow-source-events.js";
@@ -0,0 +1,102 @@
1
+ import type { FailureCategory } from '../types/identity.ts';
2
+ type WorkflowSourceKind = import('../source/index.ts').WorkflowSourceKind;
3
+ /**
4
+ * Fired on the {@link Engine} when a dynamic workflow source's single-flight
5
+ * load for `(workflowType, revision)` starts — the loader is about to be
6
+ * invoked. Never fired for a cache hit (an already-installed revision
7
+ * `resolveWorkflowSource()` returns without invoking the loader).
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { Engine, WorkflowSourceLoadStartedEvent } from '@lostgradient/weft';
12
+ *
13
+ * const engine = new Engine();
14
+ * engine.addEventListener(WorkflowSourceLoadStartedEvent.type, (event) => {
15
+ * console.log('loading:', event.workflowType, event.revision);
16
+ * });
17
+ * ```
18
+ */
19
+ export declare class WorkflowSourceLoadStartedEvent extends Event {
20
+ static readonly type: "workflow-source:load-started";
21
+ readonly workflowType: string;
22
+ readonly revision: string;
23
+ readonly kind: WorkflowSourceKind;
24
+ constructor(workflowType: string, revision: string, kind: WorkflowSourceKind);
25
+ }
26
+ /**
27
+ * Fired on the {@link Engine} when a dynamic workflow source's load for
28
+ * `(workflowType, revision)` completes successfully — the loader ran,
29
+ * validation passed, and the manifest is durably installed.
30
+ * `loadDurationMs` is measured via the engine's own injected clock.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * import { Engine, WorkflowSourceLoadReadyEvent } from '@lostgradient/weft';
35
+ *
36
+ * const engine = new Engine();
37
+ * engine.addEventListener(WorkflowSourceLoadReadyEvent.type, (event) => {
38
+ * console.log('ready:', event.workflowType, event.revision, event.loadDurationMs);
39
+ * });
40
+ * ```
41
+ */
42
+ export declare class WorkflowSourceLoadReadyEvent extends Event {
43
+ static readonly type: "workflow-source:load-ready";
44
+ readonly workflowType: string;
45
+ readonly revision: string;
46
+ readonly kind: WorkflowSourceKind;
47
+ readonly loadDurationMs: number;
48
+ constructor(workflowType: string, revision: string, kind: WorkflowSourceKind, loadDurationMs: number);
49
+ }
50
+ /**
51
+ * Fired on the {@link Engine} when a dynamic workflow source's load for
52
+ * `(workflowType, revision)` fails — the loader threw, validation rejected
53
+ * the loaded module, or the durable install failed. `failureCategory` is
54
+ * the closed, low-cardinality {@link FailureCategory} classification, safe
55
+ * to use as a metric label; the underlying error itself is never included.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * import { Engine, WorkflowSourceLoadFailedEvent } from '@lostgradient/weft';
60
+ *
61
+ * const engine = new Engine();
62
+ * engine.addEventListener(WorkflowSourceLoadFailedEvent.type, (event) => {
63
+ * console.log('failed:', event.workflowType, event.revision, event.failureCategory);
64
+ * });
65
+ * ```
66
+ */
67
+ export declare class WorkflowSourceLoadFailedEvent extends Event {
68
+ static readonly type: "workflow-source:load-failed";
69
+ readonly workflowType: string;
70
+ readonly revision: string;
71
+ readonly kind: WorkflowSourceKind;
72
+ readonly loadDurationMs: number;
73
+ readonly failureCategory: FailureCategory;
74
+ constructor(workflowType: string, revision: string, kind: WorkflowSourceKind, loadDurationMs: number, failureCategory: FailureCategory);
75
+ }
76
+ /**
77
+ * Fired on the {@link Engine} when the LAST outstanding
78
+ * `resolveWorkflowSource()` waiter for `(workflowType, revision)` releases
79
+ * (its own abort, or engine disposal) while the shared load is still
80
+ * unsettled. The shared load itself is never aborted — a fresh caller
81
+ * starting a new attempt for the same key re-fires
82
+ * {@link WorkflowSourceLoadStartedEvent}, not this event again for the
83
+ * orphaned attempt.
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * import { Engine, WorkflowSourceLoadCancelledEvent } from '@lostgradient/weft';
88
+ *
89
+ * const engine = new Engine();
90
+ * engine.addEventListener(WorkflowSourceLoadCancelledEvent.type, (event) => {
91
+ * console.log('cancelled:', event.workflowType, event.revision);
92
+ * });
93
+ * ```
94
+ */
95
+ export declare class WorkflowSourceLoadCancelledEvent extends Event {
96
+ static readonly type: "workflow-source:load-cancelled";
97
+ readonly workflowType: string;
98
+ readonly revision: string;
99
+ readonly kind: WorkflowSourceKind;
100
+ constructor(workflowType: string, revision: string, kind: WorkflowSourceKind);
101
+ }
102
+ export {};