@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
@@ -6,6 +6,10 @@ import { buildIndexOperations, validateAttributeType } from "../search-attribute
6
6
  import { normalizeWorkflowTags } from "../workflow-tags.js";
7
7
  import { mutateWorkflowTags, validateAttributeValueSizes } from "./attributes-tags.js";
8
8
  import { CONSTRAINED_ID_CHUNK_SIZE } from "./candidate-read-batching.js";
9
+ import {
10
+ getResolvedDynamicRegistration,
11
+ resolveExecutableRegistrationForRevision
12
+ } from "./dynamic-source-execution.js";
9
13
  import { resolveListCandidateIds } from "./list-candidate-resolution.js";
10
14
  import {
11
15
  readSearchAttributesForFilter,
@@ -95,6 +99,7 @@ function summaryFromState(state, attributeFailureCategory) {
95
99
  status: state.status,
96
100
  ...state.tags !== void 0 && { tags: state.tags },
97
101
  version: state.versionTuple.workflowVersion,
102
+ ...state.revision !== void 0 && { revision: state.revision },
98
103
  createdAt: state.createdAt,
99
104
  updatedAt: state.updatedAt,
100
105
  ...state.executionDeadline !== void 0 && { executionDeadline: state.executionDeadline },
@@ -164,10 +169,19 @@ export async function getAttributes(internals, workflowId) {
164
169
  return null;
165
170
  return decoded;
166
171
  }
172
+ async function resolveAttributeSchemaRegistration(internals, state) {
173
+ const eagerOrAlreadyResolved = getResolvedDynamicRegistration(internals, state.type, state.revision);
174
+ if (eagerOrAlreadyResolved !== void 0)
175
+ return eagerOrAlreadyResolved;
176
+ if (!internals.sources.byName.has(state.type))
177
+ return;
178
+ const { entry } = await resolveExecutableRegistrationForRevision(internals.engine, internals, state.type, state.revision);
179
+ return entry;
180
+ }
167
181
  export async function setAttributes(internals, workflowId, attributes) {
168
182
  const stateBytes = await internals.storage.get(KEYS.workflow(workflowId));
169
183
  if (stateBytes) {
170
- const state = decodeWorkflowState(stateBytes), registration = internals.registrations.get(state.type);
184
+ const state = decodeWorkflowState(stateBytes), registration = await resolveAttributeSchemaRegistration(internals, state);
171
185
  if (registration?.searchAttributes) {
172
186
  const schema = registration.searchAttributes;
173
187
  for (const [key, value] of Object.entries(attributes)) {
@@ -45,7 +45,10 @@ class MemoDurableActivityScope {
45
45
  this.#activityCallbacks = activityCallbacks;
46
46
  this.#persistCheckpoint = persistCheckpoint;
47
47
  this.#identityPrefix = `memo:${operation.step}:${hashString(operation.key)}`;
48
- this.#internals.workflowTypeByWorkflowId.set(workflowId, context.workflowType);
48
+ this.#internals.workflowTypeByWorkflowId.set(workflowId, {
49
+ type: context.workflowType,
50
+ revision: this.#internals.workflowTypeByWorkflowId.get(workflowId)?.revision
51
+ });
49
52
  }
50
53
  async run(execute) {
51
54
  const cleanupAbortForwarding = this.#forwardAbortSignals();
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Count non-terminal (`running`, `pending`, or `suspended`) workflow runs
3
+ * pinned to an exact `(type, revision)` (WFT-17). Wired into
4
+ * `catalog-removal.ts`'s `countWorkflowRevisionReferences()`, replacing the
5
+ * `nonTerminalRuns: 0` stub WFT-12 left in place pending this batch (see
6
+ * that changelog entry: "stays 0 until run-level revision pinning (WFT-17)
7
+ * exists to count against").
8
+ *
9
+ * Mirrors `diagnostics/version-check.ts`'s `groupActiveWorkflowsByType()`
10
+ * bounded-scan shape: only top-level `wf:{id}` records are workflow states
11
+ * (`wf:{id}:ckpt`, `:offload`, `:archive`, `:timeline:`, and index keys all
12
+ * share the `wf:` prefix but are not).
13
+ *
14
+ * @module core/engine/nonterminal-revision-count
15
+ */
16
+ import type { Storage } from '../../storage/interface.ts';
17
+ /**
18
+ * Scan `storage` for non-terminal workflow states whose `type` and
19
+ * `revision` match exactly, returning the count. A legacy record with no
20
+ * persisted `revision` never matches (a defined `revision` argument cannot
21
+ * equal `undefined`), consistent with recovery's own "a legacy record is a
22
+ * distinct, unresolved pin" treatment — it is not silently folded into
23
+ * whichever revision happens to be active.
24
+ */
25
+ export declare function countNonTerminalRunsForRevision(storage: Storage, type: string, revision: string): Promise<number>;
@@ -0,0 +1,16 @@
1
+ import { decodeWorkflowState, isTerminalWorkflowStatus } from "./validation.js";
2
+ import { isTopLevelWorkflowStateKey } from "./workflow-state-stream.js";
3
+ export async function countNonTerminalRunsForRevision(storage, type, revision) {
4
+ let count = 0;
5
+ for await (const [key, bytes] of storage.scan("wf:")) {
6
+ if (!isTopLevelWorkflowStateKey(key))
7
+ continue;
8
+ const state = decodeWorkflowState(bytes);
9
+ if (state.type !== type || state.revision !== revision)
10
+ continue;
11
+ if (isTerminalWorkflowStatus(state.status))
12
+ continue;
13
+ count += 1;
14
+ }
15
+ return count;
16
+ }
@@ -1,31 +1,13 @@
1
1
  import type { ContextOperationRequest } from '../context.ts';
2
- import type { Checkpoint, Duration, StartOptions, TimerEntry, WorkflowState } from '../types.ts';
3
- import type { WorkflowVersionTuple } from '../workflow-version-tuple.ts';
2
+ import type { StartOptions, TimerEntry } from '../types.ts';
4
3
  import type { EngineInternals } from './internals.ts';
5
- type RegistrationEntry = EngineInternals['registrations'] extends Map<string, infer Entry> ? Entry : never;
4
+ import { type TimeOperationCallbacks } from './time-operation-callbacks.ts';
6
5
  type SleepOperation = Extract<ContextOperationRequest, {
7
6
  type: 'sleep';
8
7
  }>;
9
- export type TimeOperationCallbacks = {
10
- completeOperation: (workflowId: string, value: unknown) => void;
11
- dispatchEvent: (event: Event) => void;
12
- loadWorkflowState: (workflowId: string) => Promise<WorkflowState | null>;
13
- failWorkflow: (workflowId: string, error: Error) => Promise<void>;
14
- runSerializedWorkflowStateWrite: <Result>(workflowId: string, writeOperation: () => Promise<Result>) => Promise<Result>;
15
- beginWorkflowExecution: (workflowId: string, workflowExecutionToken: string | undefined, workflowType: string, input: unknown, checkpoint: Checkpoint, executionDeadline: number | undefined, executionStateOwnerId: string, registration: RegistrationEntry) => void;
16
- workflowVersionTupleFromState: (state: WorkflowState) => WorkflowVersionTuple;
17
- setWorkflowStartHeaders: (workflowId: string, headers: Map<string, string> | undefined) => void;
18
- loadWorkflowStartHeaders: (workflowId: string) => Promise<Map<string, string> | undefined>;
19
- parseStartOptionDuration: (value: Duration, fieldName: 'options.executionTimeout' | 'options.startAfter') => number;
20
- runDeferredTerminalCleanup: (workflowId: string, timerId: string) => Promise<void>;
21
- runWorkflowFinalizer: (workflowId: string, timerId: string) => Promise<void>;
22
- handleScheduleTimer: (entry: TimerEntry) => Promise<void>;
23
- timeout: (workflowId: string) => Promise<void>;
24
- handleCleanupError: (source: string, error: unknown, workflowId: string) => void;
25
- };
8
+ export type { TimeOperationCallbacks };
26
9
  export declare function createDelayedStartTimerEntry(_internals: EngineInternals, workflowId: string, scheduledStartAt: number, options: StartOptions | undefined, callbacks: Pick<TimeOperationCallbacks, 'parseStartOptionDuration'>): TimerEntry;
27
10
  export declare function processSleepOperation(internals: EngineInternals, workflowId: string, operation: SleepOperation, callbacks: Pick<TimeOperationCallbacks, 'completeOperation' | 'loadWorkflowState'>): Promise<void>;
28
11
  export declare function registerSleepResolver(internals: EngineInternals, workflowId: string, operationId: string, resolve: () => void, scheduledFireAt: number): void;
29
- export declare function startDelayedWorkflow(internals: EngineInternals, entry: TimerEntry, callbacks: Pick<TimeOperationCallbacks, 'beginWorkflowExecution' | 'dispatchEvent' | 'failWorkflow' | 'handleCleanupError' | 'loadWorkflowStartHeaders' | 'loadWorkflowState' | 'runSerializedWorkflowStateWrite' | 'setWorkflowStartHeaders' | 'workflowVersionTupleFromState'>): Promise<void>;
30
- export declare function handleTimerFired(internals: EngineInternals, entry: TimerEntry, callbacks: Pick<TimeOperationCallbacks, 'failWorkflow' | 'handleCleanupError' | 'loadWorkflowStartHeaders' | 'loadWorkflowState' | 'runDeferredTerminalCleanup' | 'runWorkflowFinalizer' | 'runSerializedWorkflowStateWrite' | 'handleScheduleTimer' | 'setWorkflowStartHeaders' | 'timeout' | 'beginWorkflowExecution' | 'dispatchEvent' | 'workflowVersionTupleFromState'>): Promise<void>;
31
- export {};
12
+ export declare function startDelayedWorkflow(internals: EngineInternals, entry: TimerEntry, callbacks: Pick<TimeOperationCallbacks, 'beginWorkflowExecution' | 'dispatchEvent' | 'failWorkflow' | 'handleCleanupError' | 'loadWorkflowStartHeaders' | 'loadWorkflowState' | 'resolveExecutableRegistrationForRevision' | 'runSerializedWorkflowStateWrite' | 'setWorkflowStartHeaders' | 'workflowVersionTupleFromState'>): Promise<void>;
13
+ export declare function handleTimerFired(internals: EngineInternals, entry: TimerEntry, callbacks: Pick<TimeOperationCallbacks, 'failWorkflow' | 'handleCleanupError' | 'loadWorkflowStartHeaders' | 'loadWorkflowState' | 'runDeferredTerminalCleanup' | 'runWorkflowFinalizer' | 'runSerializedWorkflowStateWrite' | 'handleScheduleTimer' | 'setWorkflowStartHeaders' | 'timeout' | 'beginWorkflowExecution' | 'dispatchEvent' | 'resolveExecutableRegistrationForRevision' | 'workflowVersionTupleFromState'>): Promise<void>;
@@ -4,7 +4,9 @@ import { encode } from "../codec.js";
4
4
  import { buildTimerBatchOperations, normalizeStorageTimestamp } from "../scheduler.js";
5
5
  import { notifyConditionWaiters, notifyConditionWaitersForTimerFire } from "./condition-waiters.js";
6
6
  import { commitFencedEngineWrite } from "./fenced-write.js";
7
+ import { resolveDelayedStartRegistrationOrFail } from "./lifecycle/delayed-start-registration.js";
7
8
  import { reprovideRecoveredServices } from "./lifecycle/recovered-services.js";
9
+ import { ensureDelayedStartClaimAndCleanupBeforeFailure } from "./lifecycle/standalone-claim-acquire.js";
8
10
  import {
9
11
  acknowledgeSupersededSleepTimers,
10
12
  handleSleepTimerWithAcknowledgement,
@@ -88,23 +90,22 @@ export async function startDelayedWorkflow(internals, entry, callbacks) {
88
90
  const checkpoint = await loadDelayedWorkflowCheckpoint(internals, entry, callbacks);
89
91
  if (!checkpoint)
90
92
  return;
91
- const registration = internals.registrations.get(state.type);
92
- if (!registration) {
93
- await callbacks.failWorkflow(entry.workflowId, Error(`No workflow registered with name "${state.type}"`));
93
+ const resolvedRegistration = await resolveDelayedStartRegistrationOrFail(internals, entry, state.type, state.revision, callbacks);
94
+ if (resolvedRegistration === null)
94
95
  return;
95
- }
96
- const now = internals.options.getNow(), executionDeadline = await resolveDelayedExecutionDeadline(entry, now, callbacks);
96
+ const { entry: registration, revision: resolvedRevision } = resolvedRegistration, now = internals.options.getNow(), executionDeadline = await resolveDelayedExecutionDeadline(internals, entry, now, callbacks);
97
97
  if (executionDeadline === "invalid")
98
98
  return;
99
99
  const runningState = await callbacks.runSerializedWorkflowStateWrite(entry.workflowId, async () => {
100
100
  const latestState = await callbacks.loadWorkflowState(entry.workflowId);
101
101
  if (!latestState || latestState.status !== "pending")
102
102
  return null;
103
- const nextRunningState = {
103
+ const effectiveRevision = latestState.revision ?? resolvedRevision, nextRunningState = {
104
104
  ...latestState,
105
105
  status: "running",
106
106
  startedAt: now,
107
107
  updatedAt: now,
108
+ ...effectiveRevision !== void 0 && { revision: effectiveRevision },
108
109
  ...executionDeadline !== void 0 && { executionDeadline }
109
110
  }, operations = [
110
111
  {
@@ -136,7 +137,7 @@ export async function startDelayedWorkflow(internals, entry, callbacks) {
136
137
  internals.checkpoints.set(entry.workflowId, checkpoint);
137
138
  internals.workflowVersionTuples.set(entry.workflowId, callbacks.workflowVersionTupleFromState(runningState));
138
139
  callbacks.setWorkflowStartHeaders(entry.workflowId, await callbacks.loadWorkflowStartHeaders(entry.workflowId));
139
- callbacks.beginWorkflowExecution(entry.workflowId, runningState.workflowExecutionToken, runningState.type, runningState.input, checkpoint, executionDeadline, runningState.executionStateOwnerId ?? entry.workflowId, registration);
140
+ callbacks.beginWorkflowExecution(entry.workflowId, runningState.workflowExecutionToken, runningState.type, runningState.revision, runningState.input, checkpoint, executionDeadline, runningState.executionStateOwnerId ?? entry.workflowId, registration);
140
141
  }
141
142
  async function loadDelayedWorkflowCheckpoint(internals, entry, callbacks) {
142
143
  const checkpointBytes = await internals.storage.get(KEYS.checkpoint(entry.workflowId));
@@ -146,21 +147,22 @@ async function loadDelayedWorkflowCheckpoint(internals, entry, callbacks) {
146
147
  }
147
148
  return deserializeCheckpoint(checkpointBytes);
148
149
  }
149
- async function resolveDelayedExecutionDeadline(entry, now, callbacks) {
150
+ async function resolveDelayedExecutionDeadline(internals, entry, now, callbacks) {
150
151
  if (entry.executionTimeoutMs === void 0)
151
152
  return;
152
153
  if (!Number.isFinite(entry.executionTimeoutMs) || entry.executionTimeoutMs < 0) {
153
- await failInvalidDelayedExecutionTimeout(entry, callbacks);
154
+ await failInvalidDelayedExecutionTimeout(internals, entry, callbacks);
154
155
  return "invalid";
155
156
  }
156
157
  try {
157
158
  return normalizeStorageTimestamp(now + entry.executionTimeoutMs, `Delayed execution timeout for workflow "${entry.workflowId}"`);
158
159
  } catch {
159
- await failInvalidDelayedExecutionTimeout(entry, callbacks);
160
+ await failInvalidDelayedExecutionTimeout(internals, entry, callbacks);
160
161
  return "invalid";
161
162
  }
162
163
  }
163
- async function failInvalidDelayedExecutionTimeout(entry, callbacks) {
164
+ async function failInvalidDelayedExecutionTimeout(internals, entry, callbacks) {
165
+ await ensureDelayedStartClaimAndCleanupBeforeFailure(internals, entry.workflowId);
164
166
  await callbacks.failWorkflow(entry.workflowId, Error(`Invalid delayed execution timeout for workflow "${entry.workflowId}"`));
165
167
  }
166
168
  export async function handleTimerFired(internals, entry, callbacks) {
@@ -85,7 +85,7 @@ export type WorkflowLeaseOwnershipBootstrapOptions = {
85
85
  * doc for why an engine that cannot execute a candidate's registered type
86
86
  * must never win its claim in the first place.
87
87
  */
88
- isWorkflowTypeRegistered?: (workflowType: string) => boolean;
88
+ isWorkflowTypeRegistered?: (workflowType: string, revision: string | undefined) => boolean;
89
89
  };
90
90
  /**
91
91
  * Everything `Engine` needs to hold on `EngineInternals` once bootstrap
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Count schedules pinned to an exact `(type, revision)` (WFT-20). Wired into
3
+ * `catalog-removal.ts`'s `countWorkflowRevisionReferences()`, replacing the
4
+ * `pinnedSchedules: 0` stub WFT-12 left in place pending this batch (see
5
+ * that field's own doc comment: "always 0 until WFT-20").
6
+ *
7
+ * Mirrors `nonterminal-revision-count.ts`'s bounded-scan shape exactly: only
8
+ * top-level `schedule:{id}` records are schedule states — `schedule:{id}:*`
9
+ * suffix keys (timer index, run metadata) share the `schedule:` prefix but
10
+ * are not, the same skip `listSchedules`/`recoverOrphanedScheduleTimers`
11
+ * already apply.
12
+ *
13
+ * @module core/engine/pinned-schedule-revision-count
14
+ */
15
+ import type { Storage } from '../../storage/interface.ts';
16
+ /**
17
+ * Scan `storage` for schedules whose `workflowType` and `pinnedRevision`
18
+ * match exactly, returning the count. Only `revisionPolicy: 'pinned'`
19
+ * schedules ever count — an `'active-at-fire'` schedule against the same
20
+ * `(type, revision)` never blocks removal, since it resolves whatever is
21
+ * active at each future fire regardless of what is being removed now. A
22
+ * `'cancelled'` pinned schedule is excluded too: it will never fire again,
23
+ * so it must not block removal of the revision it once pinned. A `'paused'`
24
+ * pinned schedule still counts — it can be resumed and fire again later.
25
+ */
26
+ export declare function countPinnedSchedulesForRevision(storage: Storage, type: string, revision: string): Promise<number>;
@@ -0,0 +1,19 @@
1
+ import { decodeScheduleState } from "./validation/schedule.js";
2
+ export async function countPinnedSchedulesForRevision(storage, type, revision) {
3
+ let count = 0;
4
+ for await (const [key, bytes] of storage.scan("schedule:")) {
5
+ if (key.slice(9).includes(":"))
6
+ continue;
7
+ const state = decodeScheduleState(bytes);
8
+ if (!state)
9
+ continue;
10
+ if (state.workflowType !== type)
11
+ continue;
12
+ if (state.revisionPolicy !== "pinned" || state.pinnedRevision !== revision)
13
+ continue;
14
+ if (state.status === "cancelled")
15
+ continue;
16
+ count += 1;
17
+ }
18
+ return count;
19
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Revision resolution for a `revisionPolicy: 'pinned'` schedule (WFT-20):
3
+ * capture-time resolution (what revision would run right now, recorded on
4
+ * the schedule) and fire-time resolution (resolve and reserve EXACTLY the
5
+ * captured revision, enforcing a real, checkable commitment for an eager
6
+ * workflow type rather than silently degrading to active-at-fire behavior).
7
+ *
8
+ * Fire-time resolution deliberately does NOT reuse
9
+ * `resolveExecutableRegistrationForRevision()` unchanged for an eager type:
10
+ * that resolver's documented "eager ignores the pin" rule is correct for
11
+ * run RECOVERY (a process only ever runs the code it has loaded, so a stale
12
+ * pin on an eager type is harmless) but wrong for a schedule's
13
+ * forward-looking commitment — silently falling back to whatever is active
14
+ * would make a "pinned" schedule lie about what it guarantees, and would
15
+ * make `pinnedSchedules` reference-count accounting
16
+ * ({@link import('./pinned-schedule-revision-count.ts').countPinnedSchedulesForRevision})
17
+ * block removal of a revision nothing will ever actually run again.
18
+ *
19
+ * @module core/engine/pinned-schedule-revision
20
+ */
21
+ import type { ConditionalBatchCondition } from '../../storage/interface.ts';
22
+ import type { ScheduleRevisionPolicy } from '../types.ts';
23
+ import { type ExecutableRegistration } from './dynamic-source-execution.ts';
24
+ import type { Engine } from './index.ts';
25
+ import type { EngineInternals } from './internals.ts';
26
+ /**
27
+ * Capture-time resolution: the revision that would run RIGHT NOW for `type`,
28
+ * reusing the same resolver (and revision-selection rule) a fresh
29
+ * `engine.start()` call would use. Called by `schedule()`/`updateSchedule()`
30
+ * when a caller requests `revisionPolicy: 'pinned'`, so the captured
31
+ * {@link import('../types/schedules.ts').ScheduleMetadata.pinnedRevision} is
32
+ * exactly what would have run had the schedule fired at this instant. No
33
+ * `inFlightStartsByRevision` reservation here — this is not launching a
34
+ * workflow, only recording a decision; the write that persists the pin is
35
+ * separately fenced against a concurrent `removeWorkflowRevision()` via
36
+ * `buildCatalogEntryRevisionCondition` at the `writeScheduleState` call site.
37
+ */
38
+ export declare function resolveScheduleRevisionForPin(engine: Engine, internals: EngineInternals, type: string): Promise<string>;
39
+ /**
40
+ * `schedule()`'s create-time revision resolution — split out to keep that
41
+ * function under the complexity ceiling. For `revisionPolicy: 'pinned'`,
42
+ * captures and returns the current revision via
43
+ * {@link resolveScheduleRevisionForPin} (which already resolves, and for a
44
+ * lazy type loads, `type` internally). For `'active-at-fire'`, returns
45
+ * `undefined` and — for a lazy type not yet eagerly registered — resolves
46
+ * once here purely to trigger the loader at schedule-creation time, exactly
47
+ * as before this revision-policy field existed.
48
+ */
49
+ export declare function resolveScheduleCreationRevision(internals: EngineInternals, type: string, revisionPolicy: ScheduleRevisionPolicy): Promise<string | undefined>;
50
+ /**
51
+ * Fire-time resolve-and-reserve for a pinned schedule occurrence — the
52
+ * pinned-revision counterpart of
53
+ * `catalog-removal.ts`'s `resolveAndReserveExecutableRegistration`, reusing
54
+ * the identical early/late `inFlightStartsByRevision` reservation shape so a
55
+ * concurrent `removeWorkflowRevision()` sees an in-flight reference the same
56
+ * way it would for an ordinary start.
57
+ */
58
+ export declare function resolveAndReservePinnedExecutableRegistration(engine: Engine, internals: EngineInternals, type: string, revision: string): Promise<{
59
+ registration: ExecutableRegistration['entry'];
60
+ inFlightRevision: string | undefined;
61
+ resolvedRevision: string | undefined;
62
+ }>;
63
+ /**
64
+ * The `writeScheduleState` `extraConditions`/`onExtraConditionsLost` options
65
+ * that fence a pinned schedule's create/update write against a concurrent
66
+ * `removeWorkflowRevision()` landing between {@link resolveScheduleRevisionForPin}'s
67
+ * capture and this commit — see `storage-io.ts`'s `writeScheduleState` doc for
68
+ * why this is necessary (the scan-based `pinnedSchedules` reference count
69
+ * cannot see an uncommitted schedule write, so `removeWorkflowRevision()`'s
70
+ * own reference check cannot block it on its own). `undefined` (no fencing)
71
+ * when `pinnedRevision` is `undefined` — this write is not pinning.
72
+ */
73
+ export declare function buildPinnedRevisionWriteOptions(internals: EngineInternals, type: string, pinnedRevision: string | undefined): Promise<{
74
+ extraConditions: ConditionalBatchCondition[];
75
+ onExtraConditionsLost: () => Error;
76
+ } | undefined>;
@@ -0,0 +1,53 @@
1
+ import { releaseInFlightStart, reserveInFlightStart } from "./catalog-removal.js";
2
+ import {
3
+ resolveExecutableRegistration,
4
+ resolveExecutableRegistrationForRevision
5
+ } from "./dynamic-source-execution.js";
6
+ import { buildCatalogEntryRevisionCondition } from "./lifecycle/start-commit.js";
7
+ import {
8
+ resolveCachedStartRevision,
9
+ resolveStartRevisionUncached
10
+ } from "./lifecycle/start-revision-resolution.js";
11
+ import { WorkflowRevisionUnavailableError } from "./revision-errors.js";
12
+ export async function resolveScheduleRevisionForPin(engine, internals, type) {
13
+ const { revision: resolvedRevision } = await resolveExecutableRegistration(engine, internals, type);
14
+ return resolveCachedStartRevision(internals, type, resolvedRevision) ?? await resolveStartRevisionUncached(internals, type);
15
+ }
16
+ async function resolvePinnedExecutableRegistration(engine, internals, type, revision, onRevisionChosen) {
17
+ const eager = internals.registrations.get(type);
18
+ if (eager !== void 0) {
19
+ if (internals.registeredCatalogRevisions.get(type) !== revision)
20
+ throw new WorkflowRevisionUnavailableError(type, revision, "not-registered");
21
+ onRevisionChosen?.(revision);
22
+ return { entry: eager, revision: void 0 };
23
+ }
24
+ onRevisionChosen?.(revision);
25
+ return resolveExecutableRegistrationForRevision(engine, internals, type, revision);
26
+ }
27
+ export async function resolveScheduleCreationRevision(internals, type, revisionPolicy) {
28
+ if (revisionPolicy === "pinned")
29
+ return resolveScheduleRevisionForPin(internals.engine, internals, type);
30
+ if (!internals.registrations.has(type))
31
+ await resolveExecutableRegistration(internals.engine, internals, type);
32
+ return;
33
+ }
34
+ export async function resolveAndReservePinnedExecutableRegistration(engine, internals, type, revision) {
35
+ let earlyReservation;
36
+ try {
37
+ const { entry: registration, revision: resolvedRevision } = await resolvePinnedExecutableRegistration(engine, internals, type, revision, (chosen) => {
38
+ earlyReservation = reserveInFlightStart(internals, type, chosen);
39
+ }), inFlightRevision = earlyReservation !== void 0 ? earlyReservation : reserveInFlightStart(internals, type, resolvedRevision);
40
+ return { registration, inFlightRevision, resolvedRevision };
41
+ } catch (error) {
42
+ releaseInFlightStart(internals, type, earlyReservation);
43
+ throw error;
44
+ }
45
+ }
46
+ export async function buildPinnedRevisionWriteOptions(internals, type, pinnedRevision) {
47
+ if (pinnedRevision === void 0)
48
+ return;
49
+ return {
50
+ extraConditions: [await buildCatalogEntryRevisionCondition(internals, type, pinnedRevision)],
51
+ onExtraConditionsLost: () => new WorkflowRevisionUnavailableError(type, pinnedRevision, "not-installed")
52
+ };
53
+ }
@@ -6,6 +6,13 @@ export type RegistrationCallbacks = {
6
6
  ensureRetentionSweepInterval: () => void;
7
7
  dispatchEvent: (event: Event) => void;
8
8
  };
9
+ /**
10
+ * Reject constraints on a workflow definition when the engine has no
11
+ * inline execution strategy. Exported so `dynamic-source-execution.ts`
12
+ * (WFT-15/16) can apply the identical guard to a dynamically-resolved
13
+ * definition — see that module's own call site.
14
+ */
15
+ export declare function assertConstraintsSupported(internals: EngineInternals, name: string, registration: WorkflowDefinition): void;
9
16
  /**
10
17
  * Build the `RegistrationEntry` `engine.register()` would store for
11
18
  * `registration`, without committing anything — pure normalization
@@ -28,6 +35,29 @@ export type RegistrationCallbacks = {
28
35
  * ```
29
36
  */
30
37
  export declare function buildRegistrationEntry(name: string, registration: WorkflowDefinition): RegistrationEntry;
38
+ /**
39
+ * Visit every RESOLVED `registerSource()`-registered type's normalized
40
+ * retention policy (via {@link buildRegistrationEntry}) — the dynamic-source
41
+ * counterpart to walking `internals.registrations.values()` directly for an
42
+ * eager type. Used by `bulk-operations-purge.ts` so a dynamic workflow's own
43
+ * (possibly shorter) retention window narrows the purge sweep's scan bound
44
+ * the same way an eager registration's does.
45
+ */
46
+ export declare function forEachResolvedDynamicRetentionPolicy(internals: EngineInternals, visit: (policy: RegistrationEntry['retention']) => void): void;
47
+ /**
48
+ * True when at least one `registerSource()`-registered type has a
49
+ * registered candidate revision this process has NOT yet resolved — its
50
+ * retention policy (and everything else `buildRegistrationEntry` would
51
+ * derive) is genuinely unknown, so `getMinimumRetentionMs()`'s scan-bound
52
+ * optimization in `bulk-operations-purge.ts` cannot be trusted (WFT-19
53
+ * review round 1): it silently excluded exactly this class of workflow
54
+ * from the periodic retention sweep's scan bound, since an unresolved
55
+ * candidate's possibly-shorter-than-default retention window was invisible
56
+ * to the minimum computation. Used to fall back to an unbounded terminal
57
+ * scan while any such candidate remains unresolved — self-healing once
58
+ * `shouldPurgeWorkflowState`'s own per-run resolve installs it.
59
+ */
60
+ export declare function hasUnresolvedDynamicSourceCandidate(internals: EngineInternals): boolean;
31
61
  /**
32
62
  * Structural predicate for `BuiltWorkflowDefinition` — the runtime shape
33
63
  * `workflow({ name }).execute(...)` returns. Exported so the dynamic
@@ -15,7 +15,7 @@ function copiedTags(tags) {
15
15
  function isWorkflowDefinition(value) {
16
16
  return typeof value === "object" && value !== null && "name" in value && "handler" in value && typeof value.handler === "function";
17
17
  }
18
- function assertConstraintsSupported(internals, name, registration) {
18
+ export function assertConstraintsSupported(internals, name, registration) {
19
19
  if (!registration.constraints || registration.constraints.length === 0)
20
20
  return;
21
21
  if (internals.inlineStrategy === null)
@@ -68,6 +68,20 @@ export function buildRegistrationEntry(name, registration) {
68
68
  }
69
69
  return entry;
70
70
  }
71
+ export function forEachResolvedDynamicRetentionPolicy(internals, visit) {
72
+ for (const [type, revisions] of internals.sources.resolved)
73
+ for (const resolved of revisions.values())
74
+ visit(buildRegistrationEntry(type, resolved.definition).retention);
75
+ }
76
+ export function hasUnresolvedDynamicSourceCandidate(internals) {
77
+ for (const [type, byRevision] of internals.sources.byName) {
78
+ const resolvedForType = internals.sources.resolved.get(type);
79
+ for (const revision of byRevision.keys())
80
+ if (resolvedForType?.has(revision) !== !0)
81
+ return !0;
82
+ }
83
+ return !1;
84
+ }
71
85
  function normalizeMessageDefinitions(workflowType, messageKind, definitions) {
72
86
  const normalized = Object.create(null), sortedDefinitions = Object.values(definitions).toSorted((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
73
87
  for (const definition of sortedDefinitions) {
@@ -80,13 +94,14 @@ function normalizeMessageDefinitions(workflowType, messageKind, definitions) {
80
94
  function commitWorkflowDefinition(internals, definition, callbacks) {
81
95
  const name = definition.name;
82
96
  validateWorkflowOrActivityName(name, "workflow");
83
- if (internals.workflowSourcesByName.has(name))
97
+ if (internals.sources.byName.has(name))
84
98
  throw Error(`Cannot register("${name}"): "${name}" is already registered as a dynamic workflow source via engine.registerSource(). A workflow name may not be both eagerly registered and a dynamic source.`);
85
99
  assertConstraintsSupported(internals, name, definition);
86
100
  const entry = buildRegistrationEntry(name, definition);
87
101
  internals.registrations.set(name, entry);
88
102
  callbacks.ensureRetentionSweepInterval();
89
103
  internals.workflowTypesByHandler.set(definition.handler, name);
104
+ internals.registeredCatalogRevisions.delete(name);
90
105
  if (!internals.pendingCatalogInstalls.includes(name))
91
106
  internals.pendingCatalogInstalls.push(name);
92
107
  callbacks.dispatchEvent(new WorkflowDefinitionRegisteredEvent(name));
@@ -1,10 +1,15 @@
1
1
  import { purgeInternal } from "./bulk-operations.js";
2
+ import { resolveLastKnownDynamicRegistration } from "./dynamic-source-execution.js";
2
3
  export function hasConfiguredRetention(internals) {
3
4
  if (internals.options.retention !== null)
4
5
  return !0;
5
6
  for (const registration of internals.registrations.values())
6
7
  if (registration.retention !== void 0 && registration.retention !== null)
7
8
  return !0;
9
+ for (const revisions of internals.sources.resolved.values())
10
+ for (const resolved of revisions.values())
11
+ if (resolved.definition.retention !== void 0 && resolved.definition.retention !== null)
12
+ return !0;
8
13
  return !1;
9
14
  }
10
15
  export function setNextRetentionSweepAt(internals) {
@@ -49,7 +54,7 @@ export async function runRetentionSweep(internals, handleCleanupError, cleanupWa
49
54
  }
50
55
  }
51
56
  export function resolveWorkflowTypeRetention(internals, type) {
52
- const registration = internals.registrations.get(type);
57
+ const registration = resolveLastKnownDynamicRegistration(internals, type);
53
58
  if (registration?.retention)
54
59
  return {
55
60
  type,
@@ -69,7 +74,9 @@ export function resolveWorkflowTypeRetention(internals, type) {
69
74
  };
70
75
  }
71
76
  export function getRetentionOverview(internals, resolveRetentionForType = (type) => resolveWorkflowTypeRetention(internals, type)) {
72
- const workflowTypes = [...internals.registrations.keys()].toSorted().map((type) => resolveRetentionForType(type));
77
+ const workflowTypes = [
78
+ ...new Set([...internals.registrations.keys(), ...internals.sources.byName.keys()])
79
+ ].toSorted().map((type) => resolveRetentionForType(type));
73
80
  return {
74
81
  defaultRetention: internals.options.retention,
75
82
  sweepIntervalMs: internals.options.retentionSweepIntervalMs,
@@ -0,0 +1,71 @@
1
+ /**
2
+ * {@link WorkflowRevisionUnavailableError} (WFT-17/WFT-18) — thrown by
3
+ * {@link import('./dynamic-source-execution.ts').resolveExecutableRegistrationForRevision}
4
+ * when a resumed run's persisted, EXACT pinned `WorkflowState.revision` cannot
5
+ * be resolved in this process. Distinct from
6
+ * {@link import('./dynamic-source-errors.ts').DynamicWorkflowSourceUnavailableError},
7
+ * which covers a dynamic source whose ACTIVE revision is ambiguous or fails
8
+ * to load — this error instead covers "the run's own pin cannot be honored",
9
+ * which can also happen for a legacy (pre-revision-pinning) record on a
10
+ * dynamic source with two or more registered candidates, where there is no
11
+ * pin at all and the ambiguity cannot be resolved by falling back to
12
+ * whichever revision happens to be active (that fallback is exactly the bug
13
+ * this batch closes — see `documentation/guides/recovery-and-deploys.md`).
14
+ *
15
+ * @module core/engine/revision-errors
16
+ */
17
+ import { WeftError } from '../weft-error.ts';
18
+ /**
19
+ * Thrown when a workflow run's pinned {@link import('../types/state.ts').WorkflowState.revision}
20
+ * cannot be resolved to an executable registration in this process:
21
+ *
22
+ * - `reason: 'not-registered'` — the run pins a specific revision (a
23
+ * dynamic-source candidate this process has never `registerSource()`-registered,
24
+ * including the case where the pin no longer matches a dynamic source's sole
25
+ * remaining candidate).
26
+ * - `reason: 'legacy-ambiguous'` — the run predates revision pinning (no
27
+ * `revision` was ever persisted for it) and its type is a dynamic source
28
+ * with two or more registered candidates, so there is no way to tell which
29
+ * one it actually started against.
30
+ * - `reason: 'not-installed'` — thrown at commit-time ADMISSION, not
31
+ * recovery, for either a fresh `start()` or a checkpoint-backed failed-run
32
+ * `retryFailedAll()` reactivation (WFT-17/WFT-18 Codex review on PR #958):
33
+ * the resolved (or pinned) revision's durable catalog entry was
34
+ * concurrently removed — `removeWorkflowRevision()` racing a `start()` on
35
+ * a DIFFERENT process under a lease ownership mode, or racing a
36
+ * `retryFailedAll()` on ANY process/mode, since a failed run contributes no
37
+ * `inFlightStartsByRevision` reservation of its own — between this
38
+ * process resolving the revision and that commit batch actually landing.
39
+ * Fails the one call outright — never retried inside the engine — so the
40
+ * caller re-issues it, which re-resolves against whatever is actually
41
+ * still installed.
42
+ *
43
+ * `engine.recoverAll()` classifies the affected `(type, revision)` group
44
+ * `unavailable`: only the runs in that group fail (with this error as their
45
+ * `system` failure cause, delivered via `failWorkflow()`/`onRecoveredWorkflow`,
46
+ * never thrown to `recoverAll()`'s own caller); sibling groups — including
47
+ * OTHER revisions of the same dynamic-source type — continue recovering
48
+ * normally. A standalone `engine.resume(workflowId)` call has no such
49
+ * isolation to fall back on — there is only the one run — so it throws this
50
+ * error directly to its caller instead.
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * import { Engine, WorkflowRevisionUnavailableError } from '@lostgradient/weft';
55
+ *
56
+ * const engine = new Engine();
57
+ * try {
58
+ * await engine.resume('workflow-id');
59
+ * } catch (err) {
60
+ * if (err instanceof WorkflowRevisionUnavailableError) {
61
+ * console.error(err.workflowType, err.revision, err.reason);
62
+ * }
63
+ * }
64
+ * ```
65
+ */
66
+ export declare class WorkflowRevisionUnavailableError extends WeftError<'WorkflowRevisionUnavailableError'> {
67
+ readonly workflowType: string;
68
+ readonly revision: string | undefined;
69
+ readonly reason: 'not-registered' | 'legacy-ambiguous' | 'not-installed';
70
+ constructor(workflowType: string, revision: string | undefined, reason: 'not-registered' | 'legacy-ambiguous' | 'not-installed');
71
+ }
@@ -0,0 +1,13 @@
1
+ import { WeftError } from "../weft-error.js";
2
+
3
+ export class WorkflowRevisionUnavailableError extends WeftError {
4
+ workflowType;
5
+ revision;
6
+ reason;
7
+ constructor(workflowType, revision, reason) {
8
+ super("WorkflowRevisionUnavailableError", reason === "legacy-ambiguous" ? `Cannot recover workflow type "${workflowType}": this run predates revision pinning and the type is a dynamic source with multiple registered revisions, so which one it started against cannot be determined.` : reason === "not-installed" ? `Cannot admit workflow type "${workflowType}": revision${revision === void 0 ? "" : ` "${revision}"`} is no longer installed in the durable catalog (removed concurrently). Retry the operation.` : `Cannot recover workflow type "${workflowType}": its pinned revision${revision === void 0 ? "" : ` "${revision}"`} is not registered in this process. Register the exact revision this run started against before retrying.`);
9
+ this.workflowType = workflowType;
10
+ this.revision = revision;
11
+ this.reason = reason;
12
+ }
13
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The pinned-vs-active-at-fire branch point for a scheduled occurrence
3
+ * (WFT-20), split into its own tiny module so `schedule-run.ts` — a
4
+ * deliberately small, purpose-built module — has one obvious call site
5
+ * instead of inlining the branch.
6
+ *
7
+ * @module core/engine/schedule-revision-fire
8
+ */
9
+ import type { ScheduleState } from '../types.ts';
10
+ /**
11
+ * The revision override `startScheduledRun` should pass to
12
+ * `ScheduleCallbacks.startWorkflow` for this occurrence: `state.pinnedRevision`
13
+ * when `state.revisionPolicy === 'pinned'`, else `undefined` (an
14
+ * `'active-at-fire'` schedule resolves whatever revision is active at fire
15
+ * time, unchanged from pre-WFT-20 behavior).
16
+ */
17
+ export declare function buildPinnedRevisionOverride(state: ScheduleState): string | undefined;
@@ -0,0 +1,3 @@
1
+ export function buildPinnedRevisionOverride(state) {
2
+ return state.revisionPolicy === "pinned" ? state.pinnedRevision : void 0;
3
+ }