@lostgradient/weft 0.24.0 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) 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.js +14 -20
  20. package/dist/core/engine/bulk-operations-retry.d.ts +9 -0
  21. package/dist/core/engine/bulk-operations-retry.js +177 -0
  22. package/dist/core/engine/bulk-operations-shared.d.ts +18 -0
  23. package/dist/core/engine/bulk-operations-shared.js +14 -0
  24. package/dist/core/engine/bulk-operations.js +8 -167
  25. package/dist/core/engine/callback-creators-bundles.js +4 -2
  26. package/dist/core/engine/callback-creators-core.js +9 -1
  27. package/dist/core/engine/callback-creators-schedule.js +2 -2
  28. package/dist/core/engine/catalog-readiness.js +2 -0
  29. package/dist/core/engine/catalog-removal.d.ts +107 -3
  30. package/dist/core/engine/catalog-removal.js +66 -13
  31. package/dist/core/engine/catalog-tombstone-recovery.d.ts +56 -0
  32. package/dist/core/engine/catalog-tombstone-recovery.js +39 -0
  33. package/dist/core/engine/constraints.js +5 -1
  34. package/dist/core/engine/construction.d.ts +1 -1
  35. package/dist/core/engine/decode-revision.d.ts +32 -0
  36. package/dist/core/engine/decode-revision.js +9 -0
  37. package/dist/core/engine/disposal.d.ts +6 -2
  38. package/dist/core/engine/disposal.js +24 -10
  39. package/dist/core/engine/dynamic-source-errors.d.ts +81 -0
  40. package/dist/core/engine/dynamic-source-errors.js +25 -0
  41. package/dist/core/engine/dynamic-source-execution.d.ts +173 -0
  42. package/dist/core/engine/dynamic-source-execution.js +108 -0
  43. package/dist/core/engine/engine-internal-types.d.ts +16 -0
  44. package/dist/core/engine/engine-workflows-namespace.d.ts +10 -0
  45. package/dist/core/engine/engine-workflows-namespace.js +3 -1
  46. package/dist/core/engine/index.d.ts +18 -10
  47. package/dist/core/engine/index.js +21 -10
  48. package/dist/core/engine/inline-launch-queue.js +1 -1
  49. package/dist/core/engine/internals.d.ts +26 -30
  50. package/dist/core/engine/lifecycle/checkpoint-launch.d.ts +29 -0
  51. package/dist/core/engine/lifecycle/checkpoint-launch.js +75 -0
  52. package/dist/core/engine/lifecycle/delayed-start-registration.d.ts +30 -0
  53. package/dist/core/engine/lifecycle/delayed-start-registration.js +11 -0
  54. package/dist/core/engine/lifecycle/fork-helpers.d.ts +33 -1
  55. package/dist/core/engine/lifecycle/fork-helpers.js +3 -1
  56. package/dist/core/engine/lifecycle/recovery-revision-groups.d.ts +75 -0
  57. package/dist/core/engine/lifecycle/recovery-revision-groups.js +64 -0
  58. package/dist/core/engine/lifecycle/resume-generation-guard.d.ts +58 -0
  59. package/dist/core/engine/lifecycle/resume-generation-guard.js +11 -0
  60. package/dist/core/engine/lifecycle/resume.js +18 -9
  61. package/dist/core/engine/lifecycle/shared.d.ts +43 -0
  62. package/dist/core/engine/lifecycle/standalone-claim-acquire.d.ts +12 -0
  63. package/dist/core/engine/lifecycle/standalone-claim-acquire.js +6 -0
  64. package/dist/core/engine/lifecycle/start-commit-errors.d.ts +12 -0
  65. package/dist/core/engine/lifecycle/start-commit-errors.js +6 -0
  66. package/dist/core/engine/lifecycle/start-commit.d.ts +22 -7
  67. package/dist/core/engine/lifecycle/start-commit.js +49 -15
  68. package/dist/core/engine/lifecycle/start-exec.d.ts +18 -2
  69. package/dist/core/engine/lifecycle/start-exec.js +7 -5
  70. package/dist/core/engine/lifecycle/start-or-signal-create.js +1 -3
  71. package/dist/core/engine/lifecycle/start-or-signal.js +1 -1
  72. package/dist/core/engine/lifecycle/start-precondition-attribution.d.ts +49 -0
  73. package/dist/core/engine/lifecycle/start-precondition-attribution.js +13 -0
  74. package/dist/core/engine/lifecycle/start-revision-resolution.d.ts +34 -0
  75. package/dist/core/engine/lifecycle/start-revision-resolution.js +11 -0
  76. package/dist/core/engine/lifecycle/start-state.d.ts +16 -0
  77. package/dist/core/engine/lifecycle/start-state.js +60 -0
  78. package/dist/core/engine/lifecycle/start-terminal-conflict-purge.d.ts +66 -3
  79. package/dist/core/engine/lifecycle/start-terminal-conflict-purge.js +13 -3
  80. package/dist/core/engine/lifecycle/start.d.ts +16 -6
  81. package/dist/core/engine/lifecycle/start.js +38 -68
  82. package/dist/core/engine/lifecycle/transition.d.ts +3 -4
  83. package/dist/core/engine/lifecycle/transition.js +30 -80
  84. package/dist/core/engine/lifecycle.d.ts +4 -2
  85. package/dist/core/engine/lifecycle.js +6 -6
  86. package/dist/core/engine/listing.js +15 -1
  87. package/dist/core/engine/memo-durable-activity.js +4 -1
  88. package/dist/core/engine/nonterminal-revision-count.d.ts +25 -0
  89. package/dist/core/engine/nonterminal-revision-count.js +16 -0
  90. package/dist/core/engine/operations-time.d.ts +5 -23
  91. package/dist/core/engine/operations-time.js +13 -11
  92. package/dist/core/engine/ownership-bootstrap.d.ts +1 -1
  93. package/dist/core/engine/pinned-schedule-revision-count.d.ts +26 -0
  94. package/dist/core/engine/pinned-schedule-revision-count.js +19 -0
  95. package/dist/core/engine/pinned-schedule-revision.d.ts +76 -0
  96. package/dist/core/engine/pinned-schedule-revision.js +53 -0
  97. package/dist/core/engine/registration.d.ts +30 -0
  98. package/dist/core/engine/registration.js +17 -2
  99. package/dist/core/engine/retention.js +9 -2
  100. package/dist/core/engine/revision-errors.d.ts +71 -0
  101. package/dist/core/engine/revision-errors.js +13 -0
  102. package/dist/core/engine/schedule-revision-fire.d.ts +17 -0
  103. package/dist/core/engine/schedule-revision-fire.js +3 -0
  104. package/dist/core/engine/schedule-run.js +2 -1
  105. package/dist/core/engine/schedules.d.ts +2 -1
  106. package/dist/core/engine/schedules.js +28 -9
  107. package/dist/core/engine/source-diagnostics.d.ts +76 -0
  108. package/dist/core/engine/source-diagnostics.js +105 -0
  109. package/dist/core/engine/source-registration.js +4 -2
  110. package/dist/core/engine/source-resolution.d.ts +25 -9
  111. package/dist/core/engine/source-resolution.js +49 -14
  112. package/dist/core/engine/source-runtime-state.d.ts +92 -0
  113. package/dist/core/engine/source-runtime-state.js +36 -0
  114. package/dist/core/engine/storage-io.d.ts +19 -2
  115. package/dist/core/engine/storage-io.js +21 -2
  116. package/dist/core/engine/termination/finalizer-registration.d.ts +18 -0
  117. package/dist/core/engine/termination/finalizer-registration.js +21 -0
  118. package/dist/core/engine/termination/finalizer.d.ts +16 -19
  119. package/dist/core/engine/termination/finalizer.js +3 -3
  120. package/dist/core/engine/time-operation-callbacks.d.ts +32 -0
  121. package/dist/core/engine/time-operation-callbacks.js +0 -0
  122. package/dist/core/engine/validation/schedule-cadence.d.ts +16 -0
  123. package/dist/core/engine/validation/schedule-cadence.js +20 -0
  124. package/dist/core/engine/validation/schedule-options.d.ts +4 -1
  125. package/dist/core/engine/validation/schedule-options.js +14 -0
  126. package/dist/core/engine/validation/schedule-revision.d.ts +24 -0
  127. package/dist/core/engine/validation/schedule-revision.js +21 -0
  128. package/dist/core/engine/validation/schedule-warnings.d.ts +11 -0
  129. package/dist/core/engine/validation/schedule-warnings.js +5 -0
  130. package/dist/core/engine/validation/schedule.d.ts +4 -3
  131. package/dist/core/engine/validation/schedule.js +15 -26
  132. package/dist/core/engine/validation.js +3 -0
  133. package/dist/core/engine/workflow-claim-reclaim-target.d.ts +1 -1
  134. package/dist/core/engine/workflow-claim-reclaim-target.js +16 -2
  135. package/dist/core/engine/workflow-retention-deadline.d.ts +16 -0
  136. package/dist/core/engine/workflow-retention-deadline.js +33 -0
  137. package/dist/core/events/event-map.d.ts +5 -0
  138. package/dist/core/events/index.d.ts +1 -0
  139. package/dist/core/events/index.js +1 -0
  140. package/dist/core/events/workflow-source-events.d.ts +102 -0
  141. package/dist/core/events/workflow-source-events.js +57 -0
  142. package/dist/core/execution-strategy.d.ts +7 -0
  143. package/dist/core/inline-execution-strategy.context-options.d.ts +1 -1
  144. package/dist/core/inline-execution-strategy.js +1 -1
  145. package/dist/core/types/checkpoint.d.ts +14 -0
  146. package/dist/core/types/schedules.d.ts +52 -1
  147. package/dist/core/types/state.d.ts +16 -0
  148. package/dist/core/weft-error.d.ts +1 -1
  149. package/dist/core/weft-error.js +4 -1
  150. package/dist/core/worker-execution-strategy.d.ts +1 -0
  151. package/dist/core/worker-execution-strategy.js +13 -1
  152. package/dist/core/worker-fault-handling.d.ts +10 -0
  153. package/dist/core/worker-fault-handling.js +1 -0
  154. package/dist/core/worker-inbound-message.d.ts +1 -0
  155. package/dist/core/worker-inbound-message.js +1 -0
  156. package/dist/core/worker-protocol-guard.js +4 -0
  157. package/dist/core/worker-protocol.d.ts +7 -0
  158. package/dist/core/worker-protocol.js +2 -1
  159. package/dist/core/worker-turn-watchdog.d.ts +8 -1
  160. package/dist/core/worker-turn-watchdog.js +3 -2
  161. package/dist/diagnostics/format.js +5 -0
  162. package/dist/diagnostics/types.d.ts +24 -0
  163. package/dist/diagnostics/version-check.js +8 -2
  164. package/dist/http.js +1 -1
  165. package/dist/index.d.ts +3 -3
  166. package/dist/index.js +7 -0
  167. package/dist/indexeddb.js +1 -1
  168. package/dist/json-schema.js +3 -3
  169. package/dist/server/fault-to-json-rpc.js +2 -1
  170. package/dist/server/index.d.ts +11 -0
  171. package/dist/server/operation-fault.d.ts +2 -0
  172. package/dist/server/operation-fault.js +2 -1
  173. package/dist/server/operations/create-schedule.d.ts +2 -0
  174. package/dist/server/operations/create-schedule.js +10 -6
  175. package/dist/server/operations/get-catalog-diagnostics.d.ts +28 -0
  176. package/dist/server/operations/get-catalog-diagnostics.js +10 -2
  177. package/dist/server/operations/get-task-detail-schema.d.ts +8 -0
  178. package/dist/server/operations/get-task-detail-schema.js +1 -0
  179. package/dist/server/operations/get-task-detail.d.ts +8 -0
  180. package/dist/server/operations/get-task-detail.js +1 -0
  181. package/dist/server/operations/get-task-diagnostics.d.ts +4 -4
  182. package/dist/server/operations/preload-workflow-revision.d.ts +28 -0
  183. package/dist/server/operations/preload-workflow-revision.js +60 -0
  184. package/dist/server/operations/schedule-faults.d.ts +1 -0
  185. package/dist/server/operations/schedule-faults.js +32 -4
  186. package/dist/server/operations/schedule-rest-body.d.ts +1 -0
  187. package/dist/server/operations/schedule-rest-body.js +2 -1
  188. package/dist/server/operations/static-registrations.js +6 -0
  189. package/dist/server/operations/update-schedule.d.ts +2 -0
  190. package/dist/server/operations/update-schedule.js +4 -2
  191. package/dist/server/operations/workflow-catalog-operation-helpers.js +27 -0
  192. package/dist/server/runtime/task-dispatch-envelope.d.ts +14 -0
  193. package/dist/server/runtime/task-dispatch-envelope.js +32 -0
  194. package/dist/server/runtime/task-dispatch-revision.d.ts +29 -0
  195. package/dist/server/runtime/task-dispatch-revision.js +13 -0
  196. package/dist/server/runtime/task-dispatch.js +12 -37
  197. package/dist/server/runtime/task-ledger-recovery.js +1 -1
  198. package/dist/server/runtime/task-ledger-runtime.d.ts +14 -2
  199. package/dist/server/runtime/task-ledger-runtime.js +5 -3
  200. package/dist/server/runtime/task-polling.js +9 -3
  201. package/dist/server/runtime/task-reconciliation.js +2 -1
  202. package/dist/server/runtime/websocket-worker.js +9 -0
  203. package/dist/server/task-ledger-codec.d.ts +9 -0
  204. package/dist/server/task-ledger-codec.js +4 -1
  205. package/dist/server/task-ledger-transition-helpers.js +1 -0
  206. package/dist/server/task-ledger-types.d.ts +9 -0
  207. package/dist/server/task-ledger.d.ts +1 -1
  208. package/dist/server/task-ledger.js +2 -1
  209. package/dist/server/task-queue-types.d.ts +2 -0
  210. package/dist/storage/bun-sql.js +3 -1
  211. package/dist/storage/catalog-keys.d.ts +19 -0
  212. package/dist/storage/catalog-keys.js +3 -1
  213. package/dist/storage/compressed-storage.js +1 -1
  214. package/dist/storage/index.d.ts +3 -1
  215. package/dist/storage/interface.d.ts +2 -0
  216. package/dist/storage/interface.js +1 -1
  217. package/dist/storage/key-prefixes.d.ts +1 -1
  218. package/dist/storage/key-prefixes.js +1 -0
  219. package/dist/storage/lmdb.d.ts +30 -2
  220. package/dist/storage/lmdb.js +1 -1
  221. package/dist/storage/memory.js +1 -1
  222. package/dist/storage/neon.js +2 -2
  223. package/dist/storage/node-sqlite.js +3 -1
  224. package/dist/storage/postgres.js +2 -2
  225. package/dist/storage/resolve.js +1 -1
  226. package/dist/storage/scoped-storage.js +1 -1
  227. package/dist/storage/testing.js +1 -1
  228. package/dist/storage/turso.js +1 -1
  229. package/dist/version.d.ts +1 -1
  230. package/dist/version.js +1 -1
  231. package/dist/web-extension.js +1 -1
  232. package/dist/worker/protocol-messages.d.ts +17 -0
  233. package/dist/worker/protocol-schemas.d.ts +32 -0
  234. package/dist/worker/protocol-schemas.js +7 -3
  235. package/dist/worker/protocol-task-result.js +35 -3
  236. package/dist/worker/protocol.js +1 -1
  237. package/dist/worker/registry/types.d.ts +2 -0
  238. package/dist/worker/registry.d.ts +1 -1
  239. package/dist/worker/registry.js +3 -1
  240. package/dist/workers/workflow-runner.d.ts +9 -0
  241. package/dist/workers/workflow-runner.js +9 -2
  242. package/dist/workers/workflow-worker-entry.js +9 -4
  243. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- import { type BatchOperation } from '../../storage/interface.ts';
1
+ import { type BatchOperation, type ConditionalBatchCondition } from '../../storage/interface.ts';
2
2
  import type { ScheduleState, WorkflowState } from '../types.ts';
3
3
  import type { EngineInternals } from './internals.ts';
4
4
  import { type WorkflowClaimTransitionFragment } from './workflow-claim-transitions.ts';
@@ -84,11 +84,28 @@ export declare function commitExternalTerminalWorkflowStateOperations(internals:
84
84
  export declare function loadScheduleState(internals: EngineInternals, scheduleId: string): Promise<ScheduleState | null>;
85
85
  /** Load a schedule state. */
86
86
  export declare function requireScheduleState(internals: EngineInternals, scheduleId: string): Promise<ScheduleState>;
87
- /** Persist schedule state and optionally write the next schedule timer. */
87
+ /**
88
+ * Persist schedule state and optionally write the next schedule timer.
89
+ *
90
+ * `extraConditions` (WFT-20) additionally fences the commit on caller-supplied
91
+ * preconditions — used by a `revisionPolicy: 'pinned'` create/update to fence
92
+ * the write on `buildCatalogEntryRevisionCondition(type, pinnedRevision)` so a
93
+ * concurrent `removeWorkflowRevision()` landing between pin capture and this
94
+ * commit cannot let a pin to an already-removed revision durably land. When
95
+ * the commit's `extraConditions` are the ones that lost the CAS,
96
+ * `onExtraConditionsLost` (required whenever `extraConditions` is non-empty)
97
+ * supplies the thrown error instead of this function's own generic
98
+ * lost-precondition error, so the caller sees an actionable, typed failure
99
+ * (e.g. `WorkflowRevisionUnavailableError`) rather than a bare "lost its
100
+ * precondition" message. Every existing caller passes neither option and gets
101
+ * byte-for-byte the pre-WFT-20 commit shape.
102
+ */
88
103
  export declare function writeScheduleState(internals: EngineInternals, state: ScheduleState, options?: {
89
104
  includeTimer?: boolean;
90
105
  replaceTimerFrom?: ScheduleState;
91
106
  additionalOperations?: BatchOperation[];
107
+ extraConditions?: ConditionalBatchCondition[];
108
+ onExtraConditionsLost?: () => Error;
92
109
  }): Promise<void>;
93
110
  /** Load persisted workflow start headers by workflow ID. */
94
111
  export declare function loadWorkflowStartHeaders(internals: EngineInternals, workflowId: string): Promise<Map<string, string> | undefined>;
@@ -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)
@@ -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));
@@ -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,9 +1,11 @@
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;
7
9
  export declare function isValidScheduleIdentifier(value: unknown): value is string;
8
10
  export declare function coerceScheduleId(scheduleId: string, fieldName: string): string;
9
11
  /**
@@ -24,7 +26,6 @@ export type NormalizedScheduleSpec = {
24
26
  */
25
27
  export declare function normalizeScheduleSpec(spec: string | ScheduleSpec): NormalizedScheduleSpec;
26
28
  export declare function normalizeScheduleFilter(filter: ScheduleFilter | undefined): ScheduleFilter | undefined;
27
- export declare function rejectInvalidScheduleRecord(scheduleId: string | undefined, message: string): null;
28
29
  export declare function decodeScheduleIdentityFields(decoded: Record<string, unknown>): (Pick<ScheduleState, 'id' | 'workflowType' | 'status' | 'overlap'> & {
29
30
  cronExpression?: string;
30
31
  intervalMs?: number;
@@ -3,12 +3,17 @@ import { isRecord } from "../../debug-output.js";
3
3
  import { parseCronExpression } from "../../schedule.js";
4
4
  import { parseDuration } from "../../scheduler.js";
5
5
  import { coerceStartWorkflowId } from "../../start-workflow-validation.js";
6
- import { SCHEDULE_OVERLAP_POLICIES } from "./schedule-options.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,6 +24,9 @@ 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
31
  if (typeof value !== "string")
24
32
  return !1;
@@ -94,29 +102,6 @@ export function normalizeScheduleFilter(filter) {
94
102
  validateScheduleFilterBound(filter.offset, "offset");
95
103
  return filter;
96
104
  }
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
105
  export function decodeScheduleIdentityFields(decoded) {
121
106
  const id = decoded.id;
122
107
  if (!isValidScheduleIdentifier(id))
@@ -307,9 +292,13 @@ export function decodeScheduleState(bytes) {
307
292
  const runtime = decodeScheduleRuntimeFields(decoded, identity.id);
308
293
  if (!runtime)
309
294
  return null;
295
+ const revisionFields = decodeScheduleRevisionPolicyFields(decoded, identity.id);
296
+ if (!revisionFields)
297
+ return null;
310
298
  return {
311
299
  ...identity,
312
300
  input: decoded.input,
313
- ...runtime
301
+ ...runtime,
302
+ ...revisionFields
314
303
  };
315
304
  }
@@ -4,6 +4,7 @@ import { normalizeFailureCategory } from "../failure-categories.js";
4
4
  import { coerceStartWorkflowId, parseStartWorkflowDuration } from "../start-workflow-validation.js";
5
5
  import { DEFAULT_WORKFLOW_VERSION } from "../versioning.js";
6
6
  import { isWorkflowTagArray } from "../workflow-tags.js";
7
+ import { sanitizeDecodedRevision } from "./decode-revision.js";
7
8
  import {
8
9
  MAX_TIMELINE_COORDINATOR_DETAILS,
9
10
  MAX_TIMELINE_DETAIL_STRING_LENGTH
@@ -30,6 +31,7 @@ const WORKFLOW_STATE_FIELD_NAMES = new Set(defineCompleteWorkflowStateFieldNames
30
31
  "failureCategory",
31
32
  "terminationReason",
32
33
  "versionTuple",
34
+ "revision",
33
35
  "workflowExecutionToken",
34
36
  "executionStateOwnerId",
35
37
  "parentWorkflowId",
@@ -140,6 +142,7 @@ export function decodeWorkflowState(bytes) {
140
142
  }
141
143
  sanitizeDecodedParentLineage(state);
142
144
  sanitizeDecodedRestartLineage(state);
145
+ sanitizeDecodedRevision(state);
143
146
  return decodedRecord === void 0 ? state : stripUnknownWorkflowStateFields(state, decodedRecord);
144
147
  }
145
148
  function sanitizeDecodedParentLineage(state) {
@@ -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;
@@ -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
  }
@@ -43,7 +57,7 @@ export function createWorkflowClaimReclaimTarget(registry, storage, metrics, onR
43
57
  await registry.release(workflowId);
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
63
  await registry.release(workflowId);
@@ -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';