@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
@@ -60,8 +60,8 @@ declare const taskDiagnosticItemSchema: z.ZodUnion<readonly [z.ZodObject<{
60
60
  }>;
61
61
  state: z.ZodEnum<{
62
62
  "dead-lettered": "dead-lettered";
63
- queued: "queued";
64
63
  resolved: "resolved";
64
+ queued: "queued";
65
65
  inflight: "inflight";
66
66
  capacity: "capacity";
67
67
  }>;
@@ -122,8 +122,8 @@ declare const getTaskDiagnosticsOutput: z.ZodObject<{
122
122
  }>;
123
123
  state: z.ZodEnum<{
124
124
  "dead-lettered": "dead-lettered";
125
- queued: "queued";
126
125
  resolved: "resolved";
126
+ queued: "queued";
127
127
  inflight: "inflight";
128
128
  capacity: "capacity";
129
129
  }>;
@@ -217,7 +217,7 @@ export declare function createGetTaskDiagnosticsOperation(options?: GetTaskDiagn
217
217
  }, {
218
218
  items: ({
219
219
  kind: "dead-lettered" | "stuck-queued" | "stale-inflight" | "retry-storm" | "all-workers-at-capacity";
220
- state: "dead-lettered" | "queued" | "resolved" | "inflight" | "capacity";
220
+ state: "dead-lettered" | "resolved" | "queued" | "inflight" | "capacity";
221
221
  retryCount: number;
222
222
  requeueCount: number;
223
223
  evidence: string[];
@@ -279,7 +279,7 @@ export declare const getTaskDiagnosticsOperation: import("../operation-catalog.t
279
279
  }, {
280
280
  items: ({
281
281
  kind: "dead-lettered" | "stuck-queued" | "stale-inflight" | "retry-storm" | "all-workers-at-capacity";
282
- state: "dead-lettered" | "queued" | "resolved" | "inflight" | "capacity";
282
+ state: "dead-lettered" | "resolved" | "queued" | "inflight" | "capacity";
283
283
  retryCount: number;
284
284
  requeueCount: number;
285
285
  evidence: string[];
@@ -0,0 +1,28 @@
1
+ /**
2
+ * `weft.workflows.revisions.preload` operation + REST binding.
3
+ *
4
+ * Loads, validates, and installs one dynamic workflow source revision
5
+ * previously recorded via `engine.registerSource()` — a thin, documented
6
+ * exposure of `engine.workflows.preload()` (WFT-15/16). Modeled on
7
+ * `install-workflow-revision.ts` / `activate-workflow-revision.ts`.
8
+ *
9
+ * @module server/operations/preload-workflow-revision
10
+ */
11
+ import { z } from 'zod';
12
+ import type { WorkflowRevisionRecord } from '../../core/catalog/index.ts';
13
+ import type { UnknownRestBinding } from '../rest-bindings.ts';
14
+ declare const preloadWorkflowRevisionInput: z.ZodObject<{
15
+ name: z.ZodUnknown;
16
+ revision: z.ZodUnknown;
17
+ }, z.core.$strip>;
18
+ export type PreloadWorkflowRevisionInput = z.infer<typeof preloadWorkflowRevisionInput>;
19
+ export type PreloadWorkflowRevisionOutput = WorkflowRevisionRecord;
20
+ export declare const preloadWorkflowRevisionOperation: import("../operation-catalog.ts").OperationDefinition<{
21
+ name: unknown;
22
+ revision: unknown;
23
+ }, Readonly<{
24
+ manifest: import("../../index.ts").WorkflowRevisionManifest;
25
+ installedAt: number;
26
+ }>, unknown>;
27
+ export declare const preloadWorkflowRevisionRestBinding: UnknownRestBinding;
28
+ export {};
@@ -0,0 +1,60 @@
1
+ import { z } from "zod";
2
+ import { isWeftError } from "../../core/weft-error.js";
3
+ import { defineOperation } from "../operation-registry.js";
4
+ import {
5
+ readWorkflowCatalogRestBody,
6
+ throwWorkflowCatalogOperationFault,
7
+ validateWorkflowNameField,
8
+ validateWorkflowRevisionField,
9
+ workflowsAdminAccess
10
+ } from "./workflow-catalog-operation-helpers.js";
11
+ const preloadWorkflowRevisionInput = z.object({
12
+ name: z.unknown().describe("Workflow name. Runtime validation requires a wire-safe identifier."),
13
+ revision: z.unknown().describe("The registerSource()-registered revision to load, validate, and install. Runtime validation requires a non-empty string.")
14
+ }), preloadWorkflowRevisionOutput = z.unknown();
15
+ export const preloadWorkflowRevisionOperation = defineOperation({
16
+ name: "weft.workflows.revisions.preload",
17
+ mcpExposable: !1,
18
+ summary: "Load, validate, and install a registered dynamic workflow source revision",
19
+ description: "Load, validate, and install `(name, revision)` \u2014 previously recorded via " + "engine.registerSource() \u2014 durably into the workflow catalog. Faults NotFound when no " + "source was ever registered for this exact key, and Conflict when the load fails or the loaded module fails validation.",
20
+ destructive: !1,
21
+ tags: ["Workflow Catalog"],
22
+ inputSchema: preloadWorkflowRevisionInput,
23
+ outputSchema: preloadWorkflowRevisionOutput,
24
+ access: workflowsAdminAccess,
25
+ producibleFaults: ["InvalidParams", "NotFound", "Conflict"],
26
+ discoverable: !0,
27
+ transports: { http: !0, jsonRpcHttp: !0, jsonRpcWebSocket: !0, jsonRpcStdio: !0 },
28
+ unknownKeyPolicy: { http: "strip", jsonRpc: "reject" },
29
+ invoke: async ({ input, engine }) => {
30
+ const e = engine, name = validateWorkflowNameField(input.name), revision = validateWorkflowRevisionField(input.revision);
31
+ try {
32
+ return await e.workflows.preload(name, revision);
33
+ } catch (error) {
34
+ if (!isWeftError(error))
35
+ throw {
36
+ code: "Conflict",
37
+ message: `Dynamic workflow source "${name}" revision "${revision}" failed to load.`,
38
+ data: { reason: "load-failed" }
39
+ };
40
+ return throwWorkflowCatalogOperationFault(error);
41
+ }
42
+ }
43
+ }), preloadWorkflowRevisionRestBinding = {
44
+ method: "POST",
45
+ path: "/v1/registry/workflows/:name/preload",
46
+ pathParamNames: ["name"],
47
+ operationName: "weft.workflows.revisions.preload",
48
+ inputSources: {
49
+ name: { kind: "path", pathParam: "name" },
50
+ revision: { kind: "body-field", bodyField: "revision" }
51
+ },
52
+ extractInput: async (request, pathParams, context) => {
53
+ const body = await readWorkflowCatalogRestBody(request, context);
54
+ return {
55
+ name: pathParams.name ?? "",
56
+ revision: body.revision
57
+ };
58
+ },
59
+ success: { kind: "json", status: 200 }
60
+ };
@@ -6,6 +6,7 @@ export type ScheduleMutableOptionsInput = {
6
6
  overlap?: unknown;
7
7
  backfill?: unknown;
8
8
  jitter?: unknown;
9
+ revisionPolicy?: unknown;
9
10
  };
10
11
  /** Validate the mutable schedule options shared by create and update. */
11
12
  export declare function validateScheduleMutableOptions(input: ScheduleMutableOptionsInput): ScheduleUpdateOptions;
@@ -1,16 +1,19 @@
1
+ import { WorkflowRevisionUnavailableError } from "../../core/engine/revision-errors.js";
1
2
  import {
2
3
  isValidScheduleOverlapPolicy,
4
+ isValidScheduleRevisionPolicy,
3
5
  normalizeScheduleUpdateOptions
4
6
  } from "../../core/engine/validation/schedule.js";
5
7
  import { invalidParamsFault } from "./operation-helpers.js";
6
8
  export { isOperationFault } from "./operation-helpers.js";
7
9
  export function validateScheduleMutableOptions(input) {
8
- const description = validateScheduleDescription(input.description), overlap = validateScheduleOverlap(input.overlap), backfill = validateScheduleBackfill(input.backfill), jitter = validateScheduleJitter(input.jitter);
10
+ const description = validateScheduleDescription(input.description), overlap = validateScheduleOverlap(input.overlap), backfill = validateScheduleBackfill(input.backfill), jitter = validateScheduleJitter(input.jitter), revisionPolicy = validateScheduleRevisionPolicy(input.revisionPolicy);
9
11
  return {
10
12
  ...description !== void 0 ? { description } : {},
11
13
  ...overlap !== void 0 ? { overlap } : {},
12
14
  ...backfill !== void 0 ? { backfill } : {},
13
- ...jitter !== void 0 ? { jitter } : {}
15
+ ...jitter !== void 0 ? { jitter } : {},
16
+ ...revisionPolicy !== void 0 ? { revisionPolicy } : {}
14
17
  };
15
18
  }
16
19
  function validateScheduleDescription(value) {
@@ -27,6 +30,13 @@ function validateScheduleOverlap(value) {
27
30
  throw invalidParamsFault('Field "overlap" must be one of skip, queue, cancel-running, allow');
28
31
  return value;
29
32
  }
33
+ function validateScheduleRevisionPolicy(value) {
34
+ if (value === void 0)
35
+ return;
36
+ if (!isValidScheduleRevisionPolicy(value))
37
+ throw invalidParamsFault('Field "revisionPolicy" must be one of active-at-fire, pinned');
38
+ return value;
39
+ }
30
40
  function validateScheduleBackfill(value) {
31
41
  if (value === void 0)
32
42
  return;
@@ -51,7 +61,25 @@ function formatJitterValidationMessage(message) {
51
61
  const enginePrefix = "Invalid options.jitter: ", hasEnginePrefix = message.startsWith(enginePrefix), wireDetail = (hasEnginePrefix ? message.slice(enginePrefix.length) : message).replaceAll("options.jitter", 'Field "jitter"');
52
62
  return hasEnginePrefix ? `Field "jitter" is invalid: ${wireDetail}` : wireDetail;
53
63
  }
64
+ function mapRevisionUnavailableToFault(error) {
65
+ if (!(error instanceof WorkflowRevisionUnavailableError))
66
+ return;
67
+ return {
68
+ code: "Conflict",
69
+ message: error.message,
70
+ data: { reason: error.reason }
71
+ };
72
+ }
73
+ function isScheduleConflictMessage(normalizedMessage) {
74
+ return normalizedMessage.includes("already exists") || normalizedMessage.includes("cannot be resumed");
75
+ }
76
+ function isScheduleInvalidParamsMessage(message, normalizedMessage) {
77
+ return message.includes("Missing required field") || normalizedMessage.includes("must be") || normalizedMessage.includes("no workflow registered") || normalizedMessage.includes("cron") || normalizedMessage.includes("interval");
78
+ }
54
79
  export function mapScheduleErrorToFault(scheduleId, error) {
80
+ const revisionFault = mapRevisionUnavailableToFault(error);
81
+ if (revisionFault !== void 0)
82
+ return revisionFault;
55
83
  const message = error instanceof Error ? error.message : String(error), normalizedMessage = message.toLowerCase();
56
84
  if (normalizedMessage.includes("not found"))
57
85
  return {
@@ -59,13 +87,13 @@ export function mapScheduleErrorToFault(scheduleId, error) {
59
87
  message,
60
88
  data: { resource: "schedule", identifier: scheduleId }
61
89
  };
62
- if (normalizedMessage.includes("already exists") || normalizedMessage.includes("cannot be resumed"))
90
+ if (isScheduleConflictMessage(normalizedMessage))
63
91
  return {
64
92
  code: "Conflict",
65
93
  message,
66
94
  data: { reason: message }
67
95
  };
68
- if (message.includes("Missing required field") || normalizedMessage.includes("must be") || normalizedMessage.includes("no workflow registered") || normalizedMessage.includes("cron") || normalizedMessage.includes("interval"))
96
+ if (isScheduleInvalidParamsMessage(message, normalizedMessage))
69
97
  return {
70
98
  code: "InvalidParams",
71
99
  message,
@@ -6,6 +6,7 @@ export type SharedScheduleRestFields = {
6
6
  readonly overlap: unknown;
7
7
  readonly backfill: unknown;
8
8
  readonly jitter: unknown;
9
+ readonly revisionPolicy: unknown;
9
10
  };
10
11
  export declare function parseScheduleRestBodyRequestRecord(request: Request, context?: RestInputContext): Promise<Record<string, unknown>>;
11
12
  export declare function extractSharedScheduleRestFields(record: Record<string, unknown>): SharedScheduleRestFields;
@@ -23,6 +23,7 @@ export function extractSharedScheduleRestFields(record) {
23
23
  description: record.description,
24
24
  overlap: record.overlap,
25
25
  backfill: record.backfill,
26
- jitter: record.jitter
26
+ jitter: record.jitter,
27
+ revisionPolicy: record.revisionPolicy
27
28
  };
28
29
  }
@@ -90,6 +90,10 @@ import {
90
90
  } from "./list-workflow-revisions.js";
91
91
  import { listWorkflowsOperation, listWorkflowsRestBinding } from "./list-workflows.js";
92
92
  import { pauseScheduleOperation, pauseScheduleRestBinding } from "./pause-schedule.js";
93
+ import {
94
+ preloadWorkflowRevisionOperation,
95
+ preloadWorkflowRevisionRestBinding
96
+ } from "./preload-workflow-revision.js";
93
97
  import { purgeWorkflowsOperation, purgeWorkflowsRestBinding } from "./purge-workflows.js";
94
98
  import {
95
99
  queryWorkflowOperation,
@@ -178,6 +182,7 @@ export const STATIC_REST_BINDINGS = [
178
182
  getRegistryRestBinding,
179
183
  installWorkflowRevisionRestBinding,
180
184
  activateWorkflowRevisionRestBinding,
185
+ preloadWorkflowRevisionRestBinding,
181
186
  getWorkflowRevisionRestBinding,
182
187
  listWorkflowRevisionsRestBinding,
183
188
  getActiveWorkflowRevisionRestBinding,
@@ -247,6 +252,7 @@ export const STATIC_REST_BINDINGS = [
247
252
  getRegistryOperation,
248
253
  installWorkflowRevisionOperation,
249
254
  activateWorkflowRevisionOperation,
255
+ preloadWorkflowRevisionOperation,
250
256
  getWorkflowRevisionOperation,
251
257
  listWorkflowRevisionsOperation,
252
258
  getActiveWorkflowRevisionOperation,
@@ -8,6 +8,7 @@ declare const updateScheduleInput: z.ZodObject<{
8
8
  overlap: z.ZodOptional<z.ZodUnknown>;
9
9
  backfill: z.ZodOptional<z.ZodUnknown>;
10
10
  jitter: z.ZodOptional<z.ZodUnknown>;
11
+ revisionPolicy: z.ZodOptional<z.ZodUnknown>;
11
12
  }, z.core.$strip>;
12
13
  export type UpdateScheduleInput = z.infer<typeof updateScheduleInput>;
13
14
  export declare const updateScheduleOperation: import("../operation-catalog.ts").OperationDefinition<{
@@ -18,6 +19,7 @@ export declare const updateScheduleOperation: import("../operation-catalog.ts").
18
19
  overlap?: unknown;
19
20
  backfill?: unknown;
20
21
  jitter?: unknown;
22
+ revisionPolicy?: unknown;
21
23
  }, null, unknown>;
22
24
  export declare const updateScheduleRestBinding: UnknownRestBinding;
23
25
  export {};
@@ -16,7 +16,8 @@ const updateScheduleInput = z.object({
16
16
  description: z.unknown().optional().describe("Operator-facing schedule description. Runtime validation requires a string."),
17
17
  overlap: z.unknown().optional(),
18
18
  backfill: z.unknown().optional(),
19
- jitter: z.unknown().optional()
19
+ jitter: z.unknown().optional(),
20
+ revisionPolicy: z.unknown().optional().describe('Revision policy (WFT-20). Runtime validation requires "active-at-fire" or "pinned". Omitted preserves the current policy (and pin); "pinned" always re-resolves and re-captures the pin against the revision active right now.')
20
21
  });
21
22
  export const updateScheduleOperation = defineOperation({
22
23
  name: "weft.schedules.update",
@@ -52,7 +53,8 @@ export const updateScheduleOperation = defineOperation({
52
53
  description: { kind: "body-field", bodyField: "description" },
53
54
  overlap: { kind: "body-field", bodyField: "overlap" },
54
55
  backfill: { kind: "body-field", bodyField: "backfill" },
55
- jitter: { kind: "body-field", bodyField: "jitter" }
56
+ jitter: { kind: "body-field", bodyField: "jitter" },
57
+ revisionPolicy: { kind: "body-field", bodyField: "revisionPolicy" }
56
58
  },
57
59
  extractInput: async (request, pathParams, context) => {
58
60
  const record = await parseScheduleRestBodyRequestRecord(request, context);
@@ -5,7 +5,12 @@ import {
5
5
  } from "../../core/catalog/index.js";
6
6
  import { MAX_CONTRACT_IDENTIFIER_BYTES } from "../../core/contract/limits.js";
7
7
  import { parseWorkflowRevisionManifest } from "../../core/contract/manifest-parse.js";
8
+ import {
9
+ DynamicWorkflowSourceUnavailableError,
10
+ WorkflowSourceNotRegisteredError
11
+ } from "../../core/engine/dynamic-source-errors.js";
8
12
  import { WorkflowNotRegisteredError } from "../../core/engine/errors.js";
13
+ import { WorkflowSourceValidationError } from "../../core/source/errors.js";
9
14
  import { validateWorkflowOrActivityName } from "../../core/types/name-grammar.js";
10
15
  import { readRestJsonBody } from "../rest-body.js";
11
16
  import { invalidParamsFault, isOperationFault } from "./operation-helpers.js";
@@ -121,6 +126,28 @@ export function throwWorkflowCatalogOperationFault(error) {
121
126
  message: error.message,
122
127
  data: { reason: "catalog-conflict", weftCode: error.code }
123
128
  };
129
+ if (error instanceof WorkflowSourceNotRegisteredError)
130
+ throw {
131
+ code: "NotFound",
132
+ message: error.message,
133
+ data: { resource: "workflow-source", identifier: error.workflowType, weftCode: error.code }
134
+ };
135
+ if (error instanceof DynamicWorkflowSourceUnavailableError)
136
+ throw {
137
+ code: "Conflict",
138
+ message: error.reason === "load-failed" ? `Dynamic workflow source "${error.workflowType}"${error.revision === void 0 ? "" : ` revision "${error.revision}"`} failed to load.` : error.message,
139
+ data: { reason: error.reason, weftCode: error.code }
140
+ };
141
+ if (error instanceof WorkflowSourceValidationError)
142
+ throw {
143
+ code: "Conflict",
144
+ message: error.message,
145
+ data: {
146
+ reason: "validation-failed",
147
+ sourceValidationReasons: error.reasons,
148
+ weftCode: error.code
149
+ }
150
+ };
124
151
  throw error;
125
152
  }
126
153
  export async function readWorkflowCatalogRestBody(request, context) {
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Build the durable task-ledger envelope a fresh dispatch would create if no
3
+ * ledger record exists yet — split out of `task-dispatch.ts` purely to stay
4
+ * under this repository's 500-line-per-file ceiling, matching the existing
5
+ * `task-dispatch-revision.ts` precedent. There is no behavioral reason to
6
+ * import this module directly instead of `task-dispatch.ts`, which calls it
7
+ * from both `selectAndReserveWorker` and `enqueueTaskForLongPoll`.
8
+ *
9
+ * @module server/runtime/task-dispatch-envelope
10
+ */
11
+ import type { TaskDispatch } from '../index.ts';
12
+ import type { CreateQueuedInput } from '../task-ledger-transitions.ts';
13
+ /** The durable envelope every fresh dispatch would create if no ledger record exists yet. */
14
+ export declare function buildCreateQueuedInput(task: TaskDispatch, queue: string, visibilityTimeout: number): CreateQueuedInput;
@@ -0,0 +1,32 @@
1
+ import { isJSONValue } from "../../core/json.js";
2
+ import { isValidWorkflowRevision, REMOTE_TASK_RECORD_VERSION } from "../task-ledger.js";
3
+ function buildOptionalCreateQueuedFields(task) {
4
+ return {
5
+ ...task.workflowId !== void 0 ? { workflowId: task.workflowId } : {},
6
+ ...task.workflowExecutionToken !== void 0 ? { workflowExecutionToken: task.workflowExecutionToken } : {},
7
+ ...task.workflowRevision !== void 0 ? { workflowRevision: task.workflowRevision } : {},
8
+ ...task.priority !== void 0 ? { priority: task.priority } : {},
9
+ ...task.fairShareKey !== void 0 ? { fairShareKey: task.fairShareKey } : {},
10
+ ...task.sticky && task.workflowId !== void 0 ? { stickyWorkflowId: task.workflowId } : {},
11
+ ...task.retryPolicy !== void 0 ? { retryPolicy: task.retryPolicy } : {}
12
+ };
13
+ }
14
+ export function buildCreateQueuedInput(task, queue, visibilityTimeout) {
15
+ const input = task.input === void 0 ? null : task.input;
16
+ if (!isJSONValue(input))
17
+ throw Error(`TaskDispatch for operation "${task.operationId}" has a non-JSON-serializable "input" \u2014 the durable task ledger requires JSON-safe input.`);
18
+ if (task.workflowRevision !== void 0 && !isValidWorkflowRevision(task.workflowRevision))
19
+ throw Error(`TaskDispatch for operation "${task.operationId}" has an invalid "workflowRevision" \u2014 it must be a non-empty, bounded identifier.`);
20
+ return {
21
+ recordVersion: REMOTE_TASK_RECORD_VERSION,
22
+ operationId: task.operationId,
23
+ workflowType: task.workflowType,
24
+ activityName: task.activityName,
25
+ queue,
26
+ input,
27
+ headers: task.headers ?? {},
28
+ visibilityTimeoutMilliseconds: visibilityTimeout,
29
+ createdAt: Date.now(),
30
+ ...buildOptionalCreateQueuedFields(task)
31
+ };
32
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Dispatch-time revision staleness gate (WFT-20) — split out of
3
+ * `task-dispatch.ts` purely to stay under this repository's 500-line-per-file
4
+ * ceiling; there is no behavioral reason to import this module directly
5
+ * instead of `task-dispatch.ts`, which calls it from `dispatchTaskImpl`.
6
+ *
7
+ * @module server/runtime/task-dispatch-revision
8
+ */
9
+ import { type ConditionalBatchCondition } from '../../storage/interface.ts';
10
+ import type { ServeOptions, TaskDispatch } from '../index.ts';
11
+ /**
12
+ * Reject a dispatch whose caller-supplied `workflowRevision` disagrees with
13
+ * `task.workflowId`'s persisted `WorkflowState.revision` — a bounded,
14
+ * structured failure BEFORE any worker reservation or ledger write. A no-op
15
+ * (never reads storage) unless BOTH `workflowId` and `workflowRevision` are
16
+ * supplied; a no-op when no persisted `WorkflowState` exists for `workflowId`
17
+ * yet (nothing to compare against — the caller opted into an extra read, not
18
+ * an extra existence requirement).
19
+ *
20
+ * Returns the `conditionalBatch` precondition(s) (empty when this check was a
21
+ * no-op) that fence the CALLER's own ledger commit on the exact workflow-state
22
+ * bytes just read here. This check alone is only a pre-commit read: a
23
+ * `start-new` restart could still displace the workflow to a different
24
+ * revision in the gap between this read and the caller's later ledger write.
25
+ * The caller MUST include the returned conditions in the SAME
26
+ * `conditionalBatch` call that commits the ledger record, so that gap closes
27
+ * atomically instead of merely being read-checked up front (WFT-20).
28
+ */
29
+ export declare function assertDispatchTargetsFreshRevision(options: ServeOptions, task: TaskDispatch): Promise<ConditionalBatchCondition[]>;
@@ -0,0 +1,13 @@
1
+ import { decodeWorkflowState } from "../../core/engine/validation.js";
2
+ import { KEYS } from "../../storage/interface.js";
3
+ export async function assertDispatchTargetsFreshRevision(options, task) {
4
+ if (task.workflowId === void 0 || task.workflowRevision === void 0)
5
+ return [];
6
+ const workflowKey = KEYS.workflow(task.workflowId), persistedBytes = await options.engine.storage.get(workflowKey);
7
+ if (persistedBytes === null)
8
+ return [];
9
+ const persistedRevision = decodeWorkflowState(persistedBytes).revision;
10
+ if (persistedRevision !== task.workflowRevision)
11
+ throw Error(`TaskDispatch for operation "${task.operationId}" targets workflow "${task.workflowId}" at revision "${task.workflowRevision}", but the persisted run is pinned to revision ${persistedRevision === void 0 ? "undefined (a legacy, pre-pinning record)" : `"${persistedRevision}"`} ` + "\u2014 this dispatch is stale.");
12
+ return [{ key: workflowKey, expectedValue: persistedBytes }];
13
+ }
@@ -1,4 +1,3 @@
1
- import { isJSONValue } from "../../core/json.js";
2
1
  import { buildWorkerExecutionIdentity } from "../../worker/manifest/execution-identity.js";
3
2
  import { evictOldestAffinityEntries } from "../runtime-helpers.js";
4
3
  import {
@@ -7,9 +6,10 @@ import {
7
6
  } from "../task-ledger-transitions.js";
8
7
  import {
9
8
  decodeRemoteTaskRecord,
10
- REMOTE_TASK_RECORD_VERSION,
11
9
  taskLedgerKey
12
10
  } from "../task-ledger.js";
11
+ import { buildCreateQueuedInput } from "./task-dispatch-envelope.js";
12
+ import { assertDispatchTargetsFreshRevision } from "./task-dispatch-revision.js";
13
13
  import { commitTaskLedgerTransition } from "./task-ledger-runtime.js";
14
14
  import {
15
15
  recordTaskBacklogMetric,
@@ -65,33 +65,6 @@ function recordDispatchOutcome(context, task, workerId) {
65
65
  operationIds.add(task.operationId);
66
66
  context.operationToWorkflow.set(task.operationId, task.workflowId);
67
67
  }
68
- function buildOptionalCreateQueuedFields(task) {
69
- return {
70
- ...task.workflowId !== void 0 ? { workflowId: task.workflowId } : {},
71
- ...task.workflowExecutionToken !== void 0 ? { workflowExecutionToken: task.workflowExecutionToken } : {},
72
- ...task.priority !== void 0 ? { priority: task.priority } : {},
73
- ...task.fairShareKey !== void 0 ? { fairShareKey: task.fairShareKey } : {},
74
- ...task.sticky && task.workflowId !== void 0 ? { stickyWorkflowId: task.workflowId } : {},
75
- ...task.retryPolicy !== void 0 ? { retryPolicy: task.retryPolicy } : {}
76
- };
77
- }
78
- function buildCreateQueuedInput(task, queue, visibilityTimeout) {
79
- const input = task.input === void 0 ? null : task.input;
80
- if (!isJSONValue(input))
81
- throw Error(`TaskDispatch for operation "${task.operationId}" has a non-JSON-serializable "input" \u2014 the durable task ledger requires JSON-safe input.`);
82
- return {
83
- recordVersion: REMOTE_TASK_RECORD_VERSION,
84
- operationId: task.operationId,
85
- workflowType: task.workflowType,
86
- activityName: task.activityName,
87
- queue,
88
- input,
89
- headers: task.headers ?? {},
90
- visibilityTimeoutMilliseconds: visibilityTimeout,
91
- createdAt: Date.now(),
92
- ...buildOptionalCreateQueuedFields(task)
93
- };
94
- }
95
68
  function buildCreateThenClaimTransition(createInput, claimInput) {
96
69
  return (current, now) => {
97
70
  let queuedRecord;
@@ -114,7 +87,7 @@ function bareActivityName(qualifiedActivityName) {
114
87
  const dotIndex = qualifiedActivityName.indexOf(".");
115
88
  return dotIndex === -1 ? qualifiedActivityName : qualifiedActivityName.slice(dotIndex + 1);
116
89
  }
117
- async function selectAndReserveWorker(context, options, task, queue, visibilityTimeout) {
90
+ async function selectAndReserveWorker(context, options, task, queue, visibilityTimeout, revisionFenceConditions) {
118
91
  const routingOptions = buildRoutingOptions(context, task, queue), worker = context.registry.findWorker(task.activityName, routingOptions);
119
92
  if (!worker)
120
93
  return !1;
@@ -128,7 +101,7 @@ async function selectAndReserveWorker(context, options, task, queue, visibilityT
128
101
  workflowType: task.workflowType,
129
102
  activityName: bareActivityName(task.activityName)
130
103
  });
131
- context.registry.assignTask(worker.id, task.operationId, visibilityTimeout, task.fairShareKey, attemptToken);
104
+ context.registry.assignTask(worker.id, task.operationId, visibilityTimeout, task.fairShareKey, attemptToken, task.workflowRevision);
132
105
  let result;
133
106
  try {
134
107
  result = await commitTaskLedgerTransition(options.engine.storage, task.operationId, buildCreateThenClaimTransition(createInput, {
@@ -136,7 +109,7 @@ async function selectAndReserveWorker(context, options, task, queue, visibilityT
136
109
  workerSessionId: worker.id,
137
110
  ...executionIdentity !== void 0 ? { executionIdentity } : {},
138
111
  leaseDurationMilliseconds: visibilityTimeout
139
- }), 1);
112
+ }), 1, revisionFenceConditions);
140
113
  } catch (error) {
141
114
  context.registry.releaseReservation(task.operationId);
142
115
  throw error;
@@ -159,6 +132,7 @@ async function selectAndReserveWorker(context, options, task, queue, visibilityT
159
132
  ...task.workflowExecutionToken !== void 0 && {
160
133
  workflowExecutionToken: task.workflowExecutionToken
161
134
  },
135
+ ...task.workflowRevision !== void 0 && { workflowRevision: task.workflowRevision },
162
136
  ...task.headers ? { headers: task.headers } : {}
163
137
  }));
164
138
  recordTaskQueueLatencyMetric(context.metricsCollector, {
@@ -169,11 +143,11 @@ async function selectAndReserveWorker(context, options, task, queue, visibilityT
169
143
  recordDispatchOutcome(context, task, worker.id);
170
144
  return !0;
171
145
  }
172
- async function enqueueTaskForLongPoll(context, options, task, queue, visibilityTimeout, resolvedPriority) {
146
+ async function enqueueTaskForLongPoll(context, options, task, queue, visibilityTimeout, resolvedPriority, revisionFenceConditions) {
173
147
  const storage = options.engine.storage, createInput = buildCreateQueuedInput(task, queue, visibilityTimeout), rawExisting = await storage.get(taskLedgerKey(task.operationId)), current = decodeRemoteTaskRecord(rawExisting);
174
148
  let queuedRecord;
175
149
  if (current === null) {
176
- const result = await commitTaskLedgerTransition(storage, task.operationId, (freshCurrent, now) => createQueued(freshCurrent, createInput, now), 1);
150
+ const result = await commitTaskLedgerTransition(storage, task.operationId, (freshCurrent, now) => createQueued(freshCurrent, createInput, now), 1, revisionFenceConditions);
177
151
  if (!result.ok) {
178
152
  const raced = decodeRemoteTaskRecord(await storage.get(taskLedgerKey(task.operationId)));
179
153
  if (raced === null || raced.state !== "queued")
@@ -194,6 +168,7 @@ async function enqueueTaskForLongPoll(context, options, task, queue, visibilityT
194
168
  visibilityTimeout,
195
169
  workflowId: task.workflowId,
196
170
  workflowExecutionToken: task.workflowExecutionToken,
171
+ workflowRevision: queuedRecord.workflowRevision,
197
172
  firstQueuedAt: queuedRecord.firstQueuedAt,
198
173
  lastQueuedAt: queuedRecord.lastQueuedAt,
199
174
  lastDispatchedAt: queuedRecord.lastDispatchedAt,
@@ -214,15 +189,15 @@ export async function dispatchTaskImpl(context, options, task) {
214
189
  }
215
190
  if (!task.workflowType)
216
191
  throw Error(`TaskDispatch for operation "${task.operationId}" is missing required field "workflowType".`);
217
- const dotIndex = task.activityName.indexOf(".");
192
+ const revisionFenceConditions = await assertDispatchTargetsFreshRevision(options, task), dotIndex = task.activityName.indexOf(".");
218
193
  if (dotIndex !== -1 && task.activityName.slice(0, dotIndex) !== task.workflowType)
219
194
  throw Error(`TaskDispatch for operation "${task.operationId}" has activityName "${task.activityName}" whose qualifier does not match workflowType "${task.workflowType}".`);
220
195
  const queue = task.queue ?? "default", visibilityTimeout = clampVisibilityTimeout(task.visibilityTimeout), resolvedPriority = resolveTaskPriority(context, options, task);
221
196
  if (!validateTaskEnvelope(context, task))
222
197
  return !1;
223
- if (await selectAndReserveWorker(context, options, task, queue, visibilityTimeout))
198
+ if (await selectAndReserveWorker(context, options, task, queue, visibilityTimeout, revisionFenceConditions))
224
199
  return !0;
225
- return enqueueTaskForLongPoll(context, options, task, queue, visibilityTimeout, resolvedPriority);
200
+ return enqueueTaskForLongPoll(context, options, task, queue, visibilityTimeout, resolvedPriority, revisionFenceConditions);
226
201
  }
227
202
  export function cancelTask(context, operationId) {
228
203
  const task = context.registry.getTask(operationId);
@@ -16,7 +16,7 @@ function rebuildWorkflowIndex(context, operationId, workflowId) {
16
16
  }
17
17
  function rehydrateWorkerOwnership(context, record, now) {
18
18
  const remaining = Math.max(0, record.leaseDeadline - now);
19
- context.registry.assignTask(record.workerSessionId, record.operationId, remaining, record.fairShareKey, record.attemptToken);
19
+ context.registry.assignTask(record.workerSessionId, record.operationId, remaining, record.fairShareKey, record.attemptToken, record.workflowRevision);
20
20
  const tracked = context.registry.getWorkerTasks(record.workerSessionId).find((task) => task.operationId === record.operationId);
21
21
  if (tracked)
22
22
  tracked.visibilityTimeout = record.visibilityTimeoutMilliseconds;
@@ -16,7 +16,7 @@
16
16
  *
17
17
  * @module server/runtime/task-ledger-runtime
18
18
  */
19
- import { type Storage } from '../../storage/interface.ts';
19
+ import { type ConditionalBatchCondition, type Storage } from '../../storage/interface.ts';
20
20
  import type { TaskLedgerPreconditionResult, TaskLedgerTransitionResult } from '../task-ledger-transitions.ts';
21
21
  import { type RemoteTaskRecord } from '../task-ledger.ts';
22
22
  export type TaskLedgerCommitResult<T extends RemoteTaskRecord> = Readonly<{
@@ -46,7 +46,19 @@ export type TaskLedgerDeleteResult = Readonly<{
46
46
  * reflects reality and re-attempting the identical transition cannot change
47
47
  * the outcome.
48
48
  */
49
- export declare function commitTaskLedgerTransition<T extends RemoteTaskRecord>(storage: Storage, operationId: string, transitionFn: (current: RemoteTaskRecord | null, now: number) => TaskLedgerTransitionResult<T>, maxAttempts?: number): Promise<TaskLedgerCommitResult<T>>;
49
+ export declare function commitTaskLedgerTransition<T extends RemoteTaskRecord>(storage: Storage, operationId: string, transitionFn: (current: RemoteTaskRecord | null, now: number) => TaskLedgerTransitionResult<T>, maxAttempts?: number,
50
+ /**
51
+ * Extra same-transaction preconditions to fence the commit on — for
52
+ * example, "the target workflow's persisted state is still exactly the
53
+ * bytes read at dispatch time" (WFT-20's revision-staleness gate). Checked
54
+ * in the SAME `conditionalBatch` call as the ledger key's own CAS, so a
55
+ * concurrent write to any of these keys between read and commit loses the
56
+ * whole batch atomically, closing the gap a standalone pre-check
57
+ * (read-then-later-write, with unrelated work in between) cannot close.
58
+ * Re-supplied by the caller on each retry attempt since the caller, not
59
+ * this loop, owns re-reading whatever these conditions guard.
60
+ */
61
+ additionalConditions?: readonly ConditionalBatchCondition[]): Promise<TaskLedgerCommitResult<T>>;
50
62
  /**
51
63
  * Read the current ledger record, check a delete-only precondition (WFT-24
52
64
  * retention: {@link canDeleteRetainedTerminalTask}), and conditionally delete
@@ -1,16 +1,18 @@
1
- import { storageConditionalBatch } from "../../storage/interface.js";
1
+ import {
2
+ storageConditionalBatch
3
+ } from "../../storage/interface.js";
2
4
  import {
3
5
  decodeRemoteTaskRecord,
4
6
  encodeRemoteTaskRecord,
5
7
  taskLedgerKey
6
8
  } from "../task-ledger.js";
7
- export async function commitTaskLedgerTransition(storage, operationId, transitionFn, maxAttempts = 1) {
9
+ export async function commitTaskLedgerTransition(storage, operationId, transitionFn, maxAttempts = 1, additionalConditions = []) {
8
10
  const key = taskLedgerKey(operationId);
9
11
  for (let attempt = 1;attempt <= maxAttempts; attempt++) {
10
12
  const rawExisting = await storage.get(key), current = decodeRemoteTaskRecord(rawExisting), result = transitionFn(current, Date.now());
11
13
  if (!result.ok)
12
14
  return result;
13
- if (await storageConditionalBatch(storage, [{ key, expectedValue: rawExisting }], [{ type: "put", key, value: encodeRemoteTaskRecord(result.nextRecord) }]))
15
+ if (await storageConditionalBatch(storage, [{ key, expectedValue: rawExisting }, ...additionalConditions], [{ type: "put", key, value: encodeRemoteTaskRecord(result.nextRecord) }]))
14
16
  return { ok: !0, record: result.nextRecord };
15
17
  }
16
18
  return {