@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
@@ -1,5 +1,6 @@
1
1
  import {
2
- KEYS
2
+ KEYS,
3
+ storageValuesEqual
3
4
  } from "../../storage/interface.js";
4
5
  import { encode } from "../codec.js";
5
6
  import { buildTimerBatchOperations } from "../scheduler.js";
@@ -8,7 +9,10 @@ import {
8
9
  clearPendingAtomicWorkflowCommitSideEffects,
9
10
  takePendingAtomicWorkflowCommitSideEffects
10
11
  } from "./checkpoint-side-effects.js";
11
- import { commitFencedEngineWrite } from "./fenced-write.js";
12
+ import {
13
+ commitFencedEngineWrite,
14
+ commitFencedEngineWriteAllowingPreconditionFailure
15
+ } from "./fenced-write.js";
12
16
  import { getWorkflowExecutionStartedAt } from "./handles.js";
13
17
  import { resolveEffectiveScheduleFireAt } from "./schedule-jitter.js";
14
18
  import { createScheduleTimerId, decodeWorkflowStartHeaders } from "./state-utilities.js";
@@ -150,8 +154,23 @@ export async function writeScheduleState(internals, state, options) {
150
154
  ], includeTimer = options?.includeTimer ?? state.status === "active";
151
155
  operations.push(...buildScheduleTimerReplacementOperations(state, includeTimer, options?.replaceTimerFrom));
152
156
  operations.push(...options?.additionalOperations ?? []);
157
+ const extraConditions = options?.extraConditions ?? [];
158
+ if (extraConditions.length > 0) {
159
+ await commitScheduleStateWithExtraConditions(internals, state, operations, extraConditions, options?.onExtraConditionsLost);
160
+ return;
161
+ }
153
162
  await commitFencedEngineWrite(internals, null, operations, [], () => Error(`Schedule state commit for schedule "${state.id}" lost its precondition.`));
154
163
  }
164
+ async function commitScheduleStateWithExtraConditions(internals, state, operations, extraConditions, onExtraConditionsLost) {
165
+ if (await commitFencedEngineWriteAllowingPreconditionFailure(internals, null, operations, extraConditions))
166
+ return;
167
+ for (const condition of extraConditions) {
168
+ const currentValue = await internals.storage.get(condition.key);
169
+ if (!storageValuesEqual(currentValue, condition.expectedValue))
170
+ throw onExtraConditionsLost?.() ?? Error(`Schedule state commit for schedule "${state.id}" lost its precondition.`);
171
+ }
172
+ throw Error(`Schedule state commit for schedule "${state.id}" lost its precondition.`);
173
+ }
155
174
  export async function loadWorkflowStartHeaders(internals, workflowId) {
156
175
  const bytes = await internals.storage.get(KEYS.workflowHeaders(workflowId));
157
176
  if (!bytes)
@@ -177,7 +177,7 @@ async function releaseWorkflowClaimAfterTerminalSettlement(internals, workflowId
177
177
  if (registry.currentEpoch(workflowId) !== capturedEpoch)
178
178
  return;
179
179
  try {
180
- await registry.release(workflowId);
180
+ await registry.release(workflowId, capturedEpoch);
181
181
  } catch {}
182
182
  }
183
183
  export async function completeWorkflow(internals, workflowId, result, callbacks) {
@@ -0,0 +1,18 @@
1
+ import type { EngineInternals } from '../internals.ts';
2
+ import type { RunnableFinalizer } from './finalizer-activity.ts';
3
+ /**
4
+ * Resolve `type`'s `finalizer` against the workflow instance's own EXACT
5
+ * pinned `revision` (WFT-17/WFT-19), awaiting a full dynamic-source resolve
6
+ * when it is `registerSource()`-registered but unresolved on this process —
7
+ * {@link getResolvedDynamicRegistration}'s sync-only lookup cannot close
8
+ * that gap. Without this, a type never started/resumed/recovered here after
9
+ * a fresh restart (only torn down) rearms its `wf-teardown:` timer forever,
10
+ * since nothing else triggers the resolve `lastResolvedRevisionByName`
11
+ * needs. Returns `undefined` (rearm) when `type` has no registration at
12
+ * all, when the pinned revision cannot be resolved on this process
13
+ * ({@link WorkflowRevisionUnavailableError}), or when the resolve itself
14
+ * fails ({@link DynamicWorkflowSourceUnavailableError}) — a load failure
15
+ * here doesn't fail the (already terminal) workflow, it just defers to the
16
+ * next self-heal attempt.
17
+ */
18
+ export declare function resolveFinalizerRegistration(internals: EngineInternals, type: string, revision: string | undefined): Promise<RunnableFinalizer | undefined>;
@@ -0,0 +1,21 @@
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
+ export async function resolveFinalizerRegistration(internals, type, revision) {
8
+ const eagerOrAlreadyResolved = getResolvedDynamicRegistration(internals, type, revision)?.finalizer;
9
+ if (eagerOrAlreadyResolved !== void 0)
10
+ return eagerOrAlreadyResolved;
11
+ if (!internals.sources.byName.has(type))
12
+ return;
13
+ try {
14
+ const { entry } = await resolveExecutableRegistrationForRevision(internals.engine, internals, type, revision);
15
+ return entry.finalizer;
16
+ } catch (error) {
17
+ if (error instanceof DynamicWorkflowSourceUnavailableError || error instanceof WorkflowRevisionUnavailableError)
18
+ return;
19
+ throw error;
20
+ }
21
+ }
@@ -3,29 +3,26 @@
3
3
  * Dispatched by the scheduler when a `wf-teardown:` timer fires, it drives a
4
4
  * workflow's definition-level `finalizer` to durable completion after a
5
5
  * `cancelled`/`timed-out` terminal — claiming the durable teardown marker, running
6
- * the finalizer activity (via {@link runFinalizerActivity}), and then clearing,
7
- * backing off, or dead-lettering based on the outcome. The byte-level claim mechanics
8
- * (CAS, settle, re-arm, dead-letter, the stale horizon and backoff) live in
9
- * `./finalizer-claim.ts`; this module is the orchestration that decides which to call.
6
+ * the finalizer activity (via {@link runFinalizerActivity}), and then clearing, backing
7
+ * off, or dead-lettering based on the outcome. The byte-level claim mechanics (CAS,
8
+ * settle, re-arm, dead-letter, stale horizon, backoff) live in `./finalizer-claim.ts`;
9
+ * this module is the orchestration that decides which to call.
10
10
  *
11
- * Concurrency model — a single durable, TIME-based claim:
12
- * the durable `teardownOwed` marker carries a `{ status, attempts, token, claimedAt }`
13
- * claim ({@link TeardownClaim}). A holder fenced-CAS's `owed → running` (stamping
14
- * `claimedAt`) before running, and settle-CAS's the exact `running` bytes it wrote when
15
- * clearing or rescheduling so a concurrent reclaimer can never clobber a fresher
16
- * claim. Liveness is decided purely by the clock: a `running` claim is reclaimable once
17
- * `claimedAt` is older than {@link teardownStaleThresholdMs} (the finalizer's per-attempt
18
- * timeout plus a margin). There is NO in-memory liveness set and NO epoch in the record;
19
- * crash recovery is an ordinary stale-claim retry driven by the timer that survived the
20
- * terminal batch. The tradeoff is that a finalizer running past the stale threshold may
21
- * be re-driven concurrently, which is why workflow finalizers must be idempotent (a
22
- * contract of the #446 design).
11
+ * Concurrency model — a single durable, TIME-based claim: the durable `teardownOwed`
12
+ * marker carries a `{ status, attempts, token, claimedAt }` claim ({@link TeardownClaim}).
13
+ * A holder fenced-CAS's `owed → running` (stamping `claimedAt`) before running, and
14
+ * settle-CAS's the exact `running` bytes it wrote when clearing or rescheduling — so a
15
+ * concurrent reclaimer can never clobber a fresher claim. Liveness is decided purely by
16
+ * the clock: a `running` claim is reclaimable once `claimedAt` is older than
17
+ * {@link teardownStaleThresholdMs}. There is NO in-memory liveness set and NO epoch in
18
+ * the record; crash recovery is an ordinary stale-claim retry driven by the timer that
19
+ * survived the terminal batch the tradeoff is a finalizer running past the stale
20
+ * threshold may be re-driven concurrently, which is why finalizers must be idempotent.
23
21
  *
24
22
  * Self-heal invariant: every exit that does NOT settle the claim (a lost claim CAS, a
25
23
  * presumed-live `running` claim, a shutdown-aborted attempt, or a missing registration)
26
- * re-arms a future `wf-teardown:` timer before returning, because the scheduler deletes
27
- * the fired timer once this returns without throwing. A non-settling exit that forgot to
28
- * re-arm would strand the marker with no timer to re-drive it.
24
+ * re-arms a future `wf-teardown:` timer before returning the scheduler deletes the fired
25
+ * timer on return, so a non-settling exit that forgot to re-arm strands the marker.
29
26
  *
30
27
  * @module core/engine/termination/finalizer
31
28
  */
@@ -18,6 +18,7 @@ import {
18
18
  teardownStaleThresholdMs,
19
19
  teardownTimerOperations
20
20
  } from "./finalizer-claim.js";
21
+ import { resolveFinalizerRegistration } from "./finalizer-registration.js";
21
22
  export { teardownStaleThresholdMs } from "./finalizer-claim.js";
22
23
  const TERMINAL_STATUSES_OWED_TEARDOWN = new Set([
23
24
  "cancelled",
@@ -53,10 +54,9 @@ async function resolveTeardownDrive(internals, workflowId, token, callbacks) {
53
54
  const state = await callbacks.loadWorkflowState(workflowId);
54
55
  if (state === null || !TERMINAL_STATUSES_OWED_TEARDOWN.has(state.status))
55
56
  return clearOrRearm(internals, workflowId, token, markerBytes);
56
- const registeredFinalizer = internals.registrations.get(state.type)?.finalizer;
57
- if (registeredFinalizer === void 0)
57
+ const finalizer = await resolveFinalizerRegistration(internals, state.type, state.revision);
58
+ if (finalizer === void 0)
58
59
  return { kind: "rearm", token };
59
- const finalizer = registeredFinalizer;
60
60
  if (!runningClaimIsStale(internals, claim, finalizer))
61
61
  return { kind: "rearm", token };
62
62
  const finalizerStateBytes = await internals.storage.get(KEYS.finalizerState(workflowId));
@@ -6,6 +6,7 @@ import { dropQueuedInlineWorkflowStart } from "../inline-launch-queue.js";
6
6
  import { buildWorkflowVisibilityIndexTransition } from "../workflow-indexes.js";
7
7
  import { evictSuspendedWorkflowWaiters } from "./cleanup.js";
8
8
  export async function suspendWorkflow(internals, workflowId, callbacks) {
9
+ const capturedClaimEpochForForget = internals.workflowClaimRegistry?.currentEpoch(workflowId) ?? null;
9
10
  if (!await callbacks.runSerializedWorkflowStateWrite(workflowId, async () => {
10
11
  const state = await callbacks.loadWorkflowState(workflowId);
11
12
  if (!state || state.status !== "running")
@@ -26,6 +27,8 @@ export async function suspendWorkflow(internals, workflowId, callbacks) {
26
27
  return !0;
27
28
  }))
28
29
  return;
30
+ if (internals.workflowClaimRegistry !== null && capturedClaimEpochForForget !== null && internals.workflowClaimRegistry.currentEpoch(workflowId) === capturedClaimEpochForForget)
31
+ internals.workflowClaimRegistry.forgetLocalClaim(workflowId);
29
32
  const event = new WorkflowSuspendedEvent(workflowId);
30
33
  callbacks.dispatchEvent(event);
31
34
  callbacks.forwardEventToHandle(workflowId, event);
@@ -0,0 +1,32 @@
1
+ /**
2
+ * {@link TimeOperationCallbacks} — the callback bundle `operations-time.ts`'s
3
+ * timer-fire handlers (delayed start, sleep, deadline, schedule) take. Split
4
+ * out of that file, which has no headroom under the repository's 500-line
5
+ * implementation-file ceiling for this type inline.
6
+ *
7
+ * @module core/engine/time-operation-callbacks
8
+ */
9
+ import type { Checkpoint, Duration, TimerEntry, WorkflowState } from '../types.ts';
10
+ import type { WorkflowVersionTuple } from '../workflow-version-tuple.ts';
11
+ import type { ExecutableRegistration } from './dynamic-source-execution.ts';
12
+ type RegistrationEntry = ExecutableRegistration['entry'];
13
+ export type TimeOperationCallbacks = {
14
+ completeOperation: (workflowId: string, value: unknown) => void;
15
+ dispatchEvent: (event: Event) => void;
16
+ loadWorkflowState: (workflowId: string) => Promise<WorkflowState | null>;
17
+ failWorkflow: (workflowId: string, error: Error) => Promise<void>;
18
+ runSerializedWorkflowStateWrite: <Result>(workflowId: string, writeOperation: () => Promise<Result>) => Promise<Result>;
19
+ beginWorkflowExecution: (workflowId: string, workflowExecutionToken: string | undefined, workflowType: string, revision: string | undefined, input: unknown, checkpoint: Checkpoint, executionDeadline: number | undefined, executionStateOwnerId: string, registration: RegistrationEntry) => void;
20
+ workflowVersionTupleFromState: (state: WorkflowState) => WorkflowVersionTuple;
21
+ setWorkflowStartHeaders: (workflowId: string, headers: Map<string, string> | undefined) => void;
22
+ loadWorkflowStartHeaders: (workflowId: string) => Promise<Map<string, string> | undefined>;
23
+ parseStartOptionDuration: (value: Duration, fieldName: 'options.executionTimeout' | 'options.startAfter') => number;
24
+ runDeferredTerminalCleanup: (workflowId: string, timerId: string) => Promise<void>;
25
+ runWorkflowFinalizer: (workflowId: string, timerId: string) => Promise<void>;
26
+ handleScheduleTimer: (entry: TimerEntry) => Promise<void>;
27
+ timeout: (workflowId: string) => Promise<void>;
28
+ handleCleanupError: (source: string, error: unknown, workflowId: string) => void;
29
+ /** Resolve `type` against an EXACT pinned `WorkflowState.revision` (WFT-17, `undefined` for a legacy record) — never the catalog's active pointer. Used by a delayed-start fire, matching `resumeWorkflowFromStorage()`. */
30
+ resolveExecutableRegistrationForRevision: (type: string, revision: string | undefined) => Promise<ExecutableRegistration>;
31
+ };
32
+ export {};
File without changes
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Decode a persisted schedule record's cadence fields (`cronExpression` XOR
3
+ * `intervalMs`). Split out of `schedule.ts` purely to stay under this
4
+ * repository's 500-line-per-file ceiling (the same precedent
5
+ * `lifecycle/start.ts`/`decode-revision.ts` set for WFT-17/18) — there is no
6
+ * behavioral reason to import this module directly instead of `schedule.ts`,
7
+ * which re-exports nothing from here because `decodeScheduleCadence` is
8
+ * consumed only by `decodeScheduleIdentityFields` in that file.
9
+ *
10
+ * @module core/engine/validation/schedule-cadence
11
+ */
12
+ /** Decode the cadence fields (`cronExpression` XOR `intervalMs`) of a persisted schedule record, or `null` (and warn) on a malformed record. */
13
+ export declare function decodeScheduleCadence(decoded: Record<string, unknown>, scheduleId: string): {
14
+ cronExpression?: string;
15
+ intervalMs?: number;
16
+ } | null;
@@ -0,0 +1,20 @@
1
+ import { parseCronExpression } from "../../schedule.js";
2
+ import { rejectInvalidScheduleRecord } from "./schedule-warnings.js";
3
+ export function decodeScheduleCadence(decoded, scheduleId) {
4
+ const { cronExpression, intervalMs } = decoded, hasCron = cronExpression !== void 0, hasInterval = intervalMs !== void 0;
5
+ if (hasCron === hasInterval)
6
+ return rejectInvalidScheduleRecord(scheduleId, "with conflicting or missing cadence (expected exactly one of cronExpression or intervalMs)");
7
+ if (hasInterval) {
8
+ if (typeof intervalMs !== "number" || !Number.isSafeInteger(intervalMs) || intervalMs <= 0)
9
+ return rejectInvalidScheduleRecord(scheduleId, "with invalid intervalMs");
10
+ return { intervalMs };
11
+ }
12
+ if (typeof cronExpression !== "string")
13
+ return rejectInvalidScheduleRecord(scheduleId, "with invalid cronExpression");
14
+ try {
15
+ parseCronExpression(cronExpression);
16
+ } catch {
17
+ return rejectInvalidScheduleRecord(scheduleId, "with invalid cronExpression");
18
+ }
19
+ return { cronExpression };
20
+ }
@@ -1,15 +1,18 @@
1
- import type { ScheduleOptions, ScheduleOverlapPolicy, ScheduleUpdateOptions } from '../../types.ts';
1
+ import type { ScheduleOptions, ScheduleOverlapPolicy, ScheduleRevisionPolicy, ScheduleUpdateOptions } from '../../types.ts';
2
2
  export declare const SCHEDULE_OVERLAP_POLICIES: Set<ScheduleOverlapPolicy>;
3
+ export declare const SCHEDULE_REVISION_POLICIES: Set<ScheduleRevisionPolicy>;
3
4
  type NormalizedScheduleOptions = Required<Pick<ScheduleOptions, 'overlap' | 'backfill'>> & {
4
5
  id?: string;
5
6
  description?: string;
6
7
  jitterMs?: number;
8
+ revisionPolicy?: ScheduleRevisionPolicy;
7
9
  };
8
10
  type NormalizedScheduleUpdateOptions = {
9
11
  description?: string;
10
12
  overlap?: ScheduleOverlapPolicy;
11
13
  backfill?: boolean;
12
14
  jitterMs?: number;
15
+ revisionPolicy?: ScheduleRevisionPolicy;
13
16
  };
14
17
  export declare function normalizeScheduleOptions(options: ScheduleOptions | undefined): NormalizedScheduleOptions;
15
18
  export declare function normalizeScheduleUpdateOptions(options: ScheduleUpdateOptions | undefined): NormalizedScheduleUpdateOptions;
@@ -5,6 +5,9 @@ export const SCHEDULE_OVERLAP_POLICIES = new Set([
5
5
  "queue",
6
6
  "cancel-running",
7
7
  "allow"
8
+ ]), SCHEDULE_REVISION_POLICIES = new Set([
9
+ "active-at-fire",
10
+ "pinned"
8
11
  ]);
9
12
  export function normalizeScheduleOptions(options) {
10
13
  if (options === void 0)
@@ -20,6 +23,9 @@ export function normalizeScheduleOptions(options) {
20
23
  },
21
24
  ...options.jitter !== void 0 && {
22
25
  jitterMs: normalizeScheduleJitter(options.jitter, "options.jitter")
26
+ },
27
+ ...options.revisionPolicy !== void 0 && {
28
+ revisionPolicy: normalizeScheduleRevisionPolicy(options.revisionPolicy, "options.revisionPolicy")
23
29
  }
24
30
  };
25
31
  }
@@ -38,9 +44,17 @@ export function normalizeScheduleUpdateOptions(options) {
38
44
  },
39
45
  ...options.jitter !== void 0 && {
40
46
  jitterMs: normalizeScheduleJitter(options.jitter, "options.jitter")
47
+ },
48
+ ...options.revisionPolicy !== void 0 && {
49
+ revisionPolicy: normalizeScheduleRevisionPolicy(options.revisionPolicy, "options.revisionPolicy")
41
50
  }
42
51
  };
43
52
  }
53
+ function normalizeScheduleRevisionPolicy(value, fieldName) {
54
+ if (!SCHEDULE_REVISION_POLICIES.has(value))
55
+ throw Error(`${fieldName} must be one of ${[...SCHEDULE_REVISION_POLICIES].join(", ")}`);
56
+ return value;
57
+ }
44
58
  function normalizeScheduleDescription(description) {
45
59
  if (typeof description !== "string")
46
60
  throw Error("options.description must be a string when provided");
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Decode a persisted schedule record's revision-policy fields
3
+ * (`revisionPolicy` / `pinnedRevision`, WFT-20). Split out of `schedule.ts`
4
+ * purely to stay under this repository's 500-line-per-file ceiling — see
5
+ * that file's own precedent (`lifecycle/start.ts`/`decode-revision.ts` for
6
+ * WFT-17/18) — there is no behavioral reason to import this module directly
7
+ * instead of `schedule.ts`, which composes it into `decodeScheduleState`.
8
+ *
9
+ * @module core/engine/validation/schedule-revision
10
+ */
11
+ import type { ScheduleState } from '../../types.ts';
12
+ type ScheduleRevisionFields = Pick<ScheduleState, 'revisionPolicy' | 'pinnedRevision'>;
13
+ /**
14
+ * Decode `revisionPolicy`/`pinnedRevision` from a persisted schedule record.
15
+ * A record with NO `revisionPolicy` field predates WFT-20 and decodes as
16
+ * `'active-at-fire'` — the same "absent means legacy, not corrupt" treatment
17
+ * `WorkflowState.revision` got (WFT-17/18) — never rejected as malformed. An
18
+ * explicit but unrecognized `revisionPolicy` string, or a `pinnedRevision`
19
+ * present while `revisionPolicy !== 'pinned'` (or absent while it IS
20
+ * `'pinned'`), both reject the whole record via
21
+ * {@link rejectInvalidScheduleRecord} rather than silently coercing.
22
+ */
23
+ export declare function decodeScheduleRevisionPolicyFields(decoded: Record<string, unknown>, scheduleId: string): ScheduleRevisionFields | null;
24
+ export {};
@@ -0,0 +1,21 @@
1
+ import { SCHEDULE_REVISION_POLICIES } from "./schedule-options.js";
2
+ import { rejectInvalidScheduleRecord } from "./schedule-warnings.js";
3
+ export function decodeScheduleRevisionPolicyFields(decoded, scheduleId) {
4
+ const rawRevisionPolicy = decoded.revisionPolicy;
5
+ if (rawRevisionPolicy === void 0) {
6
+ if (decoded.pinnedRevision !== void 0)
7
+ return rejectInvalidScheduleRecord(scheduleId, "with a pinnedRevision but no revisionPolicy");
8
+ return { revisionPolicy: "active-at-fire" };
9
+ }
10
+ if (!SCHEDULE_REVISION_POLICIES.has(rawRevisionPolicy))
11
+ return rejectInvalidScheduleRecord(scheduleId, "with invalid revisionPolicy");
12
+ const revisionPolicy = rawRevisionPolicy, pinnedRevision = decoded.pinnedRevision, hasPinnedRevision = pinnedRevision !== void 0;
13
+ if (revisionPolicy === "pinned") {
14
+ if (!hasPinnedRevision || typeof pinnedRevision !== "string" || pinnedRevision.length === 0)
15
+ return rejectInvalidScheduleRecord(scheduleId, 'with revisionPolicy "pinned" but a missing or invalid pinnedRevision');
16
+ return { revisionPolicy, pinnedRevision };
17
+ }
18
+ if (hasPinnedRevision)
19
+ return rejectInvalidScheduleRecord(scheduleId, 'with a pinnedRevision but revisionPolicy is not "pinned"');
20
+ return { revisionPolicy };
21
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The shared "ignore this malformed persisted schedule record" warning
3
+ * helper, split into its own leaf module so both `schedule.ts` and its
4
+ * decode helpers (`schedule-cadence.ts`, `schedule-revision.ts`) can import
5
+ * it without an import cycle — `schedule.ts` re-exports it so every existing
6
+ * caller keeps importing it from there.
7
+ *
8
+ * @module core/engine/validation/schedule-warnings
9
+ */
10
+ /** Warn and return `null` for a persisted schedule record that failed decode validation. */
11
+ export declare function rejectInvalidScheduleRecord(scheduleId: string | undefined, message: string): null;
@@ -0,0 +1,5 @@
1
+ export function rejectInvalidScheduleRecord(scheduleId, message) {
2
+ const prefix = scheduleId === void 0 ? "[weft] Ignoring malformed schedule record" : `[weft] Ignoring malformed schedule "${scheduleId}"`;
3
+ console.warn(`${prefix} ${message}.`);
4
+ return null;
5
+ }
@@ -1,11 +1,37 @@
1
- import type { ScheduleFilter, ScheduleOverlapPolicy, ScheduleSpec, ScheduleState, ScheduleStatus } from '../../types.ts';
2
- export { normalizeScheduleOptions, normalizeScheduleUpdateOptions, SCHEDULE_OVERLAP_POLICIES, } from './schedule-options.ts';
1
+ import type { ScheduleFilter, ScheduleOverlapPolicy, ScheduleRevisionPolicy, ScheduleSpec, ScheduleState, ScheduleStatus } from '../../types.ts';
2
+ export { normalizeScheduleOptions, normalizeScheduleUpdateOptions, SCHEDULE_OVERLAP_POLICIES, SCHEDULE_REVISION_POLICIES, } from './schedule-options.ts';
3
+ export { rejectInvalidScheduleRecord } from './schedule-warnings.ts';
3
4
  export declare const SCHEDULE_STATUSES: Set<ScheduleStatus>;
4
5
  export declare function isValidScheduleTimestamp(value: unknown): value is number;
5
6
  export declare function isValidScheduleStatus(value: unknown): value is ScheduleStatus;
6
7
  export declare function isValidScheduleOverlapPolicy(value: unknown): value is ScheduleOverlapPolicy;
8
+ export declare function isValidScheduleRevisionPolicy(value: unknown): value is ScheduleRevisionPolicy;
9
+ /**
10
+ * Whether `value` is a schedule-related workflow id that decoded persisted
11
+ * data may still carry. Deliberately reuses the pre-WFT-95 decode predicate
12
+ * (not the fresh-admission `.`/`..` rejection), because every caller of this
13
+ * function — `decodeScheduleIdentityFields`, `decodeScheduleRunMetadata`,
14
+ * and the persisted `currentWorkflowId`/`queuedRuns[].workflowId` fields in
15
+ * this module — reads already-persisted data. A schedule created before
16
+ * WFT-95 with `id: '.'` or `'..'` must remain decodable and keep firing
17
+ * after upgrade.
18
+ */
7
19
  export declare function isValidScheduleIdentifier(value: unknown): value is string;
8
- export declare function coerceScheduleId(scheduleId: string, fieldName: string): string;
20
+ /**
21
+ * Coerce a caller-supplied `scheduleId` used to look up or control an
22
+ * already-persisted schedule — `getSchedule`, `pauseSchedule`,
23
+ * `resumeSchedule`, `cancelSchedule`, and `updateSchedule` all call this,
24
+ * never schedule creation (that admission path is
25
+ * `normalizeScheduleOptions`'s direct `coerceStartWorkflowId(options.id, …)`
26
+ * call, which keeps the strict `.`/`..` rejection). Deliberately uses the
27
+ * decode-compatible `assertDecodableWorkflowId`, not the admission-only
28
+ * `.`/`..` rejection (WFT-95 review): a schedule created before that
29
+ * rejection landed may already carry `id: '.'` or `'..'`, and callers must
30
+ * still be able to inspect, pause, resume, cancel, or update it instead of
31
+ * having every control operation reject a schedule they can't otherwise
32
+ * reach.
33
+ */
34
+ export declare function coerceScheduleId(scheduleId: unknown, fieldName: string): string;
9
35
  /**
10
36
  * A recurrence specification normalized into the discriminated cadence the
11
37
  * engine persists. Interval periods are resolved to whole milliseconds.
@@ -24,7 +50,6 @@ export type NormalizedScheduleSpec = {
24
50
  */
25
51
  export declare function normalizeScheduleSpec(spec: string | ScheduleSpec): NormalizedScheduleSpec;
26
52
  export declare function normalizeScheduleFilter(filter: ScheduleFilter | undefined): ScheduleFilter | undefined;
27
- export declare function rejectInvalidScheduleRecord(scheduleId: string | undefined, message: string): null;
28
53
  export declare function decodeScheduleIdentityFields(decoded: Record<string, unknown>): (Pick<ScheduleState, 'id' | 'workflowType' | 'status' | 'overlap'> & {
29
54
  cronExpression?: string;
30
55
  intervalMs?: number;
@@ -2,13 +2,18 @@ import { decode } from "../../codec.js";
2
2
  import { isRecord } from "../../debug-output.js";
3
3
  import { parseCronExpression } from "../../schedule.js";
4
4
  import { parseDuration } from "../../scheduler.js";
5
- import { coerceStartWorkflowId } from "../../start-workflow-validation.js";
6
- import { SCHEDULE_OVERLAP_POLICIES } from "./schedule-options.js";
5
+ import { assertDecodableWorkflowId, isDecodableWorkflowId } from "../../workflow-identifiers.js";
6
+ import { decodeScheduleCadence } from "./schedule-cadence.js";
7
+ import { SCHEDULE_OVERLAP_POLICIES, SCHEDULE_REVISION_POLICIES } from "./schedule-options.js";
8
+ import { decodeScheduleRevisionPolicyFields } from "./schedule-revision.js";
9
+ import { rejectInvalidScheduleRecord } from "./schedule-warnings.js";
7
10
  export {
8
11
  normalizeScheduleOptions,
9
12
  normalizeScheduleUpdateOptions,
10
- SCHEDULE_OVERLAP_POLICIES
13
+ SCHEDULE_OVERLAP_POLICIES,
14
+ SCHEDULE_REVISION_POLICIES
11
15
  } from "./schedule-options.js";
16
+ export { rejectInvalidScheduleRecord } from "./schedule-warnings.js";
12
17
  export const SCHEDULE_STATUSES = new Set(["active", "paused", "cancelled"]);
13
18
  export function isValidScheduleTimestamp(value) {
14
19
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
@@ -19,18 +24,15 @@ export function isValidScheduleStatus(value) {
19
24
  export function isValidScheduleOverlapPolicy(value) {
20
25
  return typeof value === "string" && SCHEDULE_OVERLAP_POLICIES.has(value);
21
26
  }
27
+ export function isValidScheduleRevisionPolicy(value) {
28
+ return typeof value === "string" && SCHEDULE_REVISION_POLICIES.has(value);
29
+ }
22
30
  export function isValidScheduleIdentifier(value) {
23
- if (typeof value !== "string")
24
- return !1;
25
- try {
26
- coerceStartWorkflowId(value, "schedule id");
27
- return !0;
28
- } catch {
29
- return !1;
30
- }
31
+ return typeof value === "string" && isDecodableWorkflowId(value);
31
32
  }
32
33
  export function coerceScheduleId(scheduleId, fieldName) {
33
- return coerceStartWorkflowId(scheduleId, fieldName);
34
+ assertDecodableWorkflowId(scheduleId, fieldName);
35
+ return scheduleId;
34
36
  }
35
37
  function normalizeIntervalEvery(every) {
36
38
  if (typeof every !== "string" && typeof every !== "number")
@@ -94,29 +96,6 @@ export function normalizeScheduleFilter(filter) {
94
96
  validateScheduleFilterBound(filter.offset, "offset");
95
97
  return filter;
96
98
  }
97
- export function rejectInvalidScheduleRecord(scheduleId, message) {
98
- const prefix = scheduleId === void 0 ? "[weft] Ignoring malformed schedule record" : `[weft] Ignoring malformed schedule "${scheduleId}"`;
99
- console.warn(`${prefix} ${message}.`);
100
- return null;
101
- }
102
- function decodeScheduleCadence(decoded, scheduleId) {
103
- const { cronExpression, intervalMs } = decoded, hasCron = cronExpression !== void 0, hasInterval = intervalMs !== void 0;
104
- if (hasCron === hasInterval)
105
- return rejectInvalidScheduleRecord(scheduleId, "with conflicting or missing cadence (expected exactly one of cronExpression or intervalMs)");
106
- if (hasInterval) {
107
- if (typeof intervalMs !== "number" || !Number.isSafeInteger(intervalMs) || intervalMs <= 0)
108
- return rejectInvalidScheduleRecord(scheduleId, "with invalid intervalMs");
109
- return { intervalMs };
110
- }
111
- if (typeof cronExpression !== "string")
112
- return rejectInvalidScheduleRecord(scheduleId, "with invalid cronExpression");
113
- try {
114
- parseCronExpression(cronExpression);
115
- } catch {
116
- return rejectInvalidScheduleRecord(scheduleId, "with invalid cronExpression");
117
- }
118
- return { cronExpression };
119
- }
120
99
  export function decodeScheduleIdentityFields(decoded) {
121
100
  const id = decoded.id;
122
101
  if (!isValidScheduleIdentifier(id))
@@ -307,9 +286,13 @@ export function decodeScheduleState(bytes) {
307
286
  const runtime = decodeScheduleRuntimeFields(decoded, identity.id);
308
287
  if (!runtime)
309
288
  return null;
289
+ const revisionFields = decodeScheduleRevisionPolicyFields(decoded, identity.id);
290
+ if (!revisionFields)
291
+ return null;
310
292
  return {
311
293
  ...identity,
312
294
  input: decoded.input,
313
- ...runtime
295
+ ...runtime,
296
+ ...revisionFields
314
297
  };
315
298
  }
@@ -1,9 +1,11 @@
1
1
  import { decode } from "../codec.js";
2
2
  import { isRecord } from "../debug-output.js";
3
3
  import { normalizeFailureCategory } from "../failure-categories.js";
4
- import { coerceStartWorkflowId, parseStartWorkflowDuration } from "../start-workflow-validation.js";
4
+ import { parseStartWorkflowDuration } from "../start-workflow-validation.js";
5
5
  import { DEFAULT_WORKFLOW_VERSION } from "../versioning.js";
6
+ import { isDecodableWorkflowId } from "../workflow-identifiers.js";
6
7
  import { isWorkflowTagArray } from "../workflow-tags.js";
8
+ import { sanitizeDecodedRevision } from "./decode-revision.js";
7
9
  import {
8
10
  MAX_TIMELINE_COORDINATOR_DETAILS,
9
11
  MAX_TIMELINE_DETAIL_STRING_LENGTH
@@ -30,6 +32,7 @@ const WORKFLOW_STATE_FIELD_NAMES = new Set(defineCompleteWorkflowStateFieldNames
30
32
  "failureCategory",
31
33
  "terminationReason",
32
34
  "versionTuple",
35
+ "revision",
33
36
  "workflowExecutionToken",
34
37
  "executionStateOwnerId",
35
38
  "parentWorkflowId",
@@ -131,24 +134,18 @@ export function decodeWorkflowState(bytes) {
131
134
  else
132
135
  state.failureCategory = normalizedFailureCategory;
133
136
  }
134
- if (state.executionStateOwnerId !== void 0)
135
- try {
136
- coerceStartWorkflowId(state.executionStateOwnerId, "executionStateOwnerId");
137
- } catch {
138
- console.warn(`[weft] Decoded workflow state for "${state.id}" has an invalid executionStateOwnerId field; falling back to the workflow id as the execution owner. This usually indicates corruption or tampering of the storage record.`);
139
- delete state.executionStateOwnerId;
140
- }
137
+ if (state.executionStateOwnerId !== void 0 && !isDecodableWorkflowId(state.executionStateOwnerId)) {
138
+ console.warn(`[weft] Decoded workflow state for "${state.id}" has an invalid executionStateOwnerId field; falling back to the workflow id as the execution owner. This usually indicates corruption or tampering of the storage record.`);
139
+ delete state.executionStateOwnerId;
140
+ }
141
141
  sanitizeDecodedParentLineage(state);
142
142
  sanitizeDecodedRestartLineage(state);
143
+ sanitizeDecodedRevision(state);
143
144
  return decodedRecord === void 0 ? state : stripUnknownWorkflowStateFields(state, decodedRecord);
144
145
  }
145
146
  function sanitizeDecodedParentLineage(state) {
146
- if (state.parentWorkflowId !== void 0)
147
- try {
148
- coerceStartWorkflowId(state.parentWorkflowId, "parentWorkflowId");
149
- } catch {
150
- delete state.parentWorkflowId;
151
- }
147
+ if (state.parentWorkflowId !== void 0 && !isDecodableWorkflowId(state.parentWorkflowId))
148
+ delete state.parentWorkflowId;
152
149
  if (typeof state.parentWorkflowExecutionToken !== "string" || state.parentWorkflowExecutionToken.length === 0 || state.parentWorkflowId === void 0)
153
150
  delete state.parentWorkflowExecutionToken;
154
151
  }
@@ -158,9 +155,8 @@ function sanitizeDecodedRestartLineage(state) {
158
155
  delete state.restartedFrom;
159
156
  return;
160
157
  }
161
- try {
162
- coerceStartWorkflowId(restartedFrom.workflowId, "restartedFrom.workflowId");
163
- } catch {
158
+ const restartWorkflowId = restartedFrom.workflowId;
159
+ if (typeof restartWorkflowId !== "string" || !isDecodableWorkflowId(restartWorkflowId)) {
164
160
  delete state.restartedFrom;
165
161
  return;
166
162
  }
@@ -104,4 +104,4 @@ export declare function createWorkflowClaimReclaimTarget(registry: WorkflowClaim
104
104
  * every existing caller/test that does not care about mixed workflow-type
105
105
  * fleets keeps working unchanged.
106
106
  */
107
- isTypeRegistered?: (workflowType: string) => boolean): WorkflowClaimReclaimTargetHandle;
107
+ isTypeRegistered?: (workflowType: string, revision: string | undefined) => boolean): WorkflowClaimReclaimTargetHandle;