@lostgradient/weft 0.24.1 → 0.24.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +1 -1
  2. package/dist/core/engine/bulk-operations-purge.d.ts +1 -1
  3. package/dist/core/engine/bulk-operations-purge.js +3 -2
  4. package/dist/core/engine/bulk-operations-retry.d.ts +16 -1
  5. package/dist/core/engine/bulk-operations-retry.js +19 -6
  6. package/dist/core/engine/bulk-operations.d.ts +3 -2
  7. package/dist/core/engine/bulk-operations.js +2 -2
  8. package/dist/core/engine/callback-creators-bundles.js +1 -1
  9. package/dist/core/engine/callback-creators-schedule.js +2 -2
  10. package/dist/core/engine/child-workflow.d.ts +13 -1
  11. package/dist/core/engine/child-workflow.js +22 -4
  12. package/dist/core/engine/generation-codec.d.ts +22 -0
  13. package/dist/core/engine/generation-codec.js +2 -0
  14. package/dist/core/engine/index.js +1 -1
  15. package/dist/core/engine/lifecycle/recovery-isolation.d.ts +20 -0
  16. package/dist/core/engine/lifecycle/recovery-isolation.js +26 -0
  17. package/dist/core/engine/lifecycle/resume-body.d.ts +44 -0
  18. package/dist/core/engine/lifecycle/resume-body.js +211 -0
  19. package/dist/core/engine/lifecycle/resume.d.ts +46 -3
  20. package/dist/core/engine/lifecycle/resume.js +21 -210
  21. package/dist/core/engine/lifecycle/standalone-claim-acquire.d.ts +68 -2
  22. package/dist/core/engine/lifecycle/standalone-claim-acquire.js +14 -4
  23. package/dist/core/engine/lifecycle/start-commit.d.ts +2 -0
  24. package/dist/core/engine/lifecycle/start-commit.js +16 -8
  25. package/dist/core/engine/lifecycle/start-or-signal-resolution.d.ts +8 -2
  26. package/dist/core/engine/lifecycle/start-or-signal-resolution.js +11 -3
  27. package/dist/core/engine/lifecycle/start-precondition-attribution.d.ts +10 -4
  28. package/dist/core/engine/lifecycle/start-schedule-timing.d.ts +11 -0
  29. package/dist/core/engine/lifecycle/start-schedule-timing.js +21 -0
  30. package/dist/core/engine/lifecycle/start-terminal-conflict-purge.d.ts +90 -11
  31. package/dist/core/engine/lifecycle/start-terminal-conflict-purge.js +24 -5
  32. package/dist/core/engine/lifecycle/start.d.ts +40 -3
  33. package/dist/core/engine/lifecycle/start.js +19 -26
  34. package/dist/core/engine/lifecycle/transition.d.ts +9 -0
  35. package/dist/core/engine/lifecycle/transition.js +20 -25
  36. package/dist/core/engine/lifecycle.d.ts +2 -1
  37. package/dist/core/engine/lifecycle.js +2 -1
  38. package/dist/core/engine/schedule-overlap.js +2 -1
  39. package/dist/core/engine/schedule-run.d.ts +10 -0
  40. package/dist/core/engine/schedule-run.js +1 -1
  41. package/dist/core/engine/schedules.d.ts +11 -2
  42. package/dist/core/engine/termination/complete.js +1 -1
  43. package/dist/core/engine/termination/suspend.js +3 -0
  44. package/dist/core/engine/validation/schedule.d.ts +25 -1
  45. package/dist/core/engine/validation/schedule.js +4 -10
  46. package/dist/core/engine/validation.js +10 -17
  47. package/dist/core/engine/workflow-claim-reclaim-target.js +6 -6
  48. package/dist/core/engine/workflow-claim-registry.d.ts +79 -75
  49. package/dist/core/engine/workflow-claim-registry.js +15 -2
  50. package/dist/core/engine/workflow-generation-fence.d.ts +74 -0
  51. package/dist/core/engine/workflow-generation-fence.js +19 -0
  52. package/dist/core/start-workflow-validation.d.ts +15 -0
  53. package/dist/core/start-workflow-validation.js +11 -1
  54. package/dist/core/workflow-identifiers.d.ts +37 -0
  55. package/dist/core/workflow-identifiers.js +19 -1
  56. package/dist/http.js +2 -2
  57. package/dist/indexeddb.js +1 -1
  58. package/dist/server/fleet-event-feed.js +6 -6
  59. package/dist/server/rest-binding.d.ts +11 -0
  60. package/dist/server/runtime/task-dispatch-envelope.d.ts +32 -5
  61. package/dist/server/runtime/task-dispatch-envelope.js +23 -1
  62. package/dist/server/runtime/task-dispatch.d.ts +3 -1
  63. package/dist/server/runtime/task-dispatch.js +9 -8
  64. package/dist/server/task-ledger-codec.d.ts +19 -0
  65. package/dist/server/task-ledger-codec.js +3 -0
  66. package/dist/server/task-ledger.d.ts +1 -1
  67. package/dist/server/task-ledger.js +1 -0
  68. package/dist/storage/bun-sql.js +132 -99
  69. package/dist/storage/compressed-storage.js +1 -1
  70. package/dist/storage/generation-keys.d.ts +52 -0
  71. package/dist/storage/generation-keys.js +4 -0
  72. package/dist/storage/index.d.ts +76 -65
  73. package/dist/storage/interface.d.ts +24 -232
  74. package/dist/storage/interface.js +1 -1
  75. package/dist/storage/key-prefixes.d.ts +1 -1
  76. package/dist/storage/key-prefixes.js +1 -0
  77. package/dist/storage/lease-keys.d.ts +48 -0
  78. package/dist/storage/lease-keys.js +8 -0
  79. package/dist/storage/lmdb.js +1 -1
  80. package/dist/storage/memory.js +1 -1
  81. package/dist/storage/neon.js +3 -3
  82. package/dist/storage/node-sqlite.js +132 -99
  83. package/dist/storage/postgres.js +3 -3
  84. package/dist/storage/resolve.js +1 -1
  85. package/dist/storage/scoped-storage.js +1 -1
  86. package/dist/storage/signal-keys.d.ts +23 -0
  87. package/dist/storage/signal-keys.js +9 -0
  88. package/dist/storage/testing.js +1 -1
  89. package/dist/storage/turso.js +2 -2
  90. package/dist/storage/workflow-lifecycle-keys.d.ts +128 -0
  91. package/dist/storage/workflow-lifecycle-keys.js +46 -0
  92. package/dist/storage/workflow-record-keys.d.ts +98 -0
  93. package/dist/storage/workflow-record-keys.js +47 -0
  94. package/dist/version.d.ts +1 -1
  95. package/dist/version.js +1 -1
  96. package/dist/web-extension.js +1 -1
  97. package/package.json +1 -1
@@ -38,17 +38,41 @@ export type StartDuplicateIdDecision = {
38
38
  * capability at `Engine.create()`. A store that cannot honour this condition
39
39
  * cannot host an engine that could start a workflow in the first place.
40
40
  *
41
- * KNOWN LIMITATION (PR #959 review). The condition compares a VALUE, so it cannot
42
- * distinguish "this id was never used" from "a run existed here and was purged".
43
- * If a racing winner commits, completes, and is purged or swept by retention
44
- * before this batch commits, `wf:<id>` is absent again, `expectedValue: null`
45
- * matches, and both starts execute. Closing that needs durable per-id generation
46
- * or tombstone state a purge cannot restore — a new persisted mechanism, out of
47
- * scope here. The window is narrow (a winner's whole lifecycle plus a purge inside
48
- * one loser's read-to-commit gap) and the outcome is a duplicate run rather than
49
- * the indefinite hang this fence exists to remove.
41
+ * RESOLVED (PR #959 review; closed by WFT-153). This condition alone compares
42
+ * only a VALUE, so on its own it cannot distinguish "this id was never used"
43
+ * from "a run existed here and was purged" a racing winner that commits,
44
+ * completes, and is purged (or swept by retention) before this batch commits
45
+ * would make `wf:<id>` absent again, matching `expectedValue: null`. That gap
46
+ * is now closed by {@link duplicateIdGenerationCondition} below, an ADDITIONAL
47
+ * condition on a durable per-id generation counter a purge bumps but never
48
+ * resets see its own doc for the mechanism.
50
49
  */
51
50
  duplicateIdCondition: ConditionalBatchCondition;
51
+ /**
52
+ * ADDITIONAL compare-and-swap precondition on the observed `wf-gen:<id>`
53
+ * bytes, closing the residual ABA hole {@link duplicateIdCondition} cannot
54
+ * detect (WFT-153, following WFT-152). `duplicateIdCondition` compares a
55
+ * VALUE, so it cannot tell "this id was never used" from "a run existed
56
+ * here and was purged" — if a racing winner completes and is purged before
57
+ * this batch commits, `wf:<id>` looks absent again and `duplicateIdCondition`
58
+ * alone would match. `wf-gen:<id>` is bumped in the SAME atomic batch that
59
+ * deletes `wf:<id>` on purge (`workflow-generation-fence.ts`) and is never
60
+ * otherwise touched, so a purge landing in the read-to-commit gap changes
61
+ * this key even though `wf:<id>` reads the same — the stale loser's
62
+ * condition on the pre-purge generation bytes fails where the value-only
63
+ * comparison could not detect it. See `storage/generation-keys.ts`.
64
+ */
65
+ duplicateIdGenerationCondition: ConditionalBatchCondition;
66
+ /**
67
+ * The exact `wf-gen:<id>` bytes {@link duplicateIdGenerationCondition} was
68
+ * built from, threaded through to {@link prepareTerminalRunPurge} so a
69
+ * `'start-new'` restart's own displacing purge bumps the generation from
70
+ * this SAME observed value rather than a second, independent read. This is
71
+ * what makes the restart's own CAS trivially self-consistent — it can never
72
+ * fence itself out on its own legitimate restart, because the value it
73
+ * bumps from is exactly the value its own precondition checks.
74
+ */
75
+ observedGenerationBytes: Uint8Array | null;
52
76
  };
53
77
  /**
54
78
  * The decision for a start whose id was GENERATED rather than caller-supplied. A
@@ -59,6 +83,8 @@ export type StartDuplicateIdDecision = {
59
83
  export declare const GENERATED_ID_START_DECISION: {
60
84
  readonly terminalRunToPurge: null;
61
85
  readonly duplicateIdCondition: undefined;
86
+ readonly duplicateIdGenerationCondition: undefined;
87
+ readonly observedGenerationBytes: null;
62
88
  };
63
89
  /**
64
90
  * Decide what a caller-supplied workflow id that already has a persisted record
@@ -86,9 +112,53 @@ export declare const GENERATED_ID_START_DECISION: {
86
112
  * batch. The returned {@link StartDuplicateIdDecision} therefore carries a
87
113
  * `duplicateIdCondition` holding the exact bytes seen here, so that batch can be
88
114
  * conditioned on them — turning that window into a lost compare-and-swap rather
89
- * than a blind overwrite (WFT-152).
115
+ * than a blind overwrite (WFT-152). It also reads `wf-gen:<id>` in the SAME pass
116
+ * and carries `duplicateIdGenerationCondition`/`observedGenerationBytes` (WFT-153),
117
+ * closing the residual ABA window a value-only condition cannot detect — see that
118
+ * field's own doc.
90
119
  */
91
120
  export declare function resolveTerminalConflictForRestart(internals: EngineInternals, workflowId: string, options: StartWorkflowOptions | undefined): Promise<StartDuplicateIdDecision>;
121
+ /**
122
+ * The `'reattach-only'`/`'bulk-retry-only'` fence for `startWorkflow`'s
123
+ * `skipAdmissionIdCheck` parameter (WFT-95 TOCTOU fix). Call immediately
124
+ * after {@link resolveTerminalConflictForRestart} resolves.
125
+ *
126
+ * Both call sites reach `startWorkflow` after a separate, non-atomic read
127
+ * confirmed an already-persisted record they mean to replay or replace — but
128
+ * that confirmation and `resolveTerminalConflictForRestart`'s own atomic read
129
+ * are not the same read:
130
+ *
131
+ * - `dispatchChildWorkflowStart()`'s crash-reattach retry confirms a matching
132
+ * persisted child via `loadWorkflowState()`, then leaves
133
+ * `options.onTerminalConflict` unset — so `resolveTerminalConflictForRestart`
134
+ * either finds the record still there (throws {@link WorkflowAlreadyExistsError},
135
+ * this function is never reached: the expected, unraced reattach) or gone
136
+ * (`terminalRunToPurge: null`, the ordinary fresh-create branch).
137
+ * - `retryFailedWorkflow()`'s checkpoint-absent fallback (`bulk-operations-retry.ts`)
138
+ * confirms a `failed` record via its own `loadWorkflowState()`, then always
139
+ * sets `options.onTerminalConflict: 'start-new'` — so
140
+ * `resolveTerminalConflictForRestart` either finds the SAME terminal record
141
+ * still there (returns its bytes as `terminalRunToPurge`, the expected,
142
+ * unraced purge-and-replace) or finds it gone (`terminalRunToPurge: null`,
143
+ * the same ordinary fresh-create branch).
144
+ *
145
+ * Under `ownership: 'workflow-lease'`, another engine can purge the matched
146
+ * record in the window between either call site's own confirmation read and
147
+ * `resolveTerminalConflictForRestart`'s. Being called with a `'reattach-only'`
148
+ * or `'bulk-retry-only'` `skipAdmissionIdCheck` and a `null` `terminalRunToPurge`
149
+ * therefore means the race happened: there is nothing left to reattach to or
150
+ * replace, so this re-runs strict admission rather than let a bypassed
151
+ * `.`/`..` id fall through into a genuinely fresh create. `coerceStartWorkflowId`
152
+ * throws the same `StartWorkflowValidationError` strict admission would have
153
+ * thrown on the caller's very first (non-retry) attempt — a clean,
154
+ * deterministic rejection instead of a silently created reserved-id run.
155
+ *
156
+ * A no-op for every other `skipAdmissionIdCheck` value: `true` (schedule
157
+ * drain) applies unconditionally and never calls this, and `undefined`
158
+ * (every public start surface) already went through strict admission in
159
+ * `prepareStartWorkflow`.
160
+ */
161
+ export declare function enforceReplayOnlyIdFence(skipAdmissionIdCheck: boolean | 'reattach-only' | 'bulk-retry-only' | undefined, workflowId: string, terminalRunToPurge: WorkflowState | null): void;
92
162
  /**
93
163
  * Prepare a prior terminal run for displacement by a `'start-new'` restart WITHOUT
94
164
  * committing the destructive delete. Returns the storage delete operations (for the
@@ -98,5 +168,14 @@ export declare function resolveTerminalConflictForRestart(internals: EngineInter
98
168
  * entries. `clearPurgedWorkflowInMemoryState` runs `cleanupWaiters` to settle the
99
169
  * old run's pending signal/update/sleep waiters; it only needs
100
170
  * `swallowPromiseRejection`, which `LifecycleCallbacks` already exposes.
171
+ *
172
+ * Also appends the `wf-gen:<id>` bump PUT operation (WFT-153), built from
173
+ * `observedGenerationBytes` — the SAME bytes `resolveTerminalConflictForRestart`
174
+ * already read for `duplicateIdGenerationCondition` — rather than a second,
175
+ * independent read. Reusing that one observed value for both the outer CAS
176
+ * condition and the bump amount is what makes this restart's own commit
177
+ * trivially self-consistent: it can never fence itself out on its own
178
+ * legitimate restart, because the value it bumps from is exactly the value its
179
+ * own precondition checks (see `duplicateIdGenerationCondition`'s doc).
101
180
  */
102
- export declare function prepareTerminalRunPurge(internals: EngineInternals, state: WorkflowState, callbacks: LifecycleCallbacks): Promise<BatchOperation[]>;
181
+ export declare function prepareTerminalRunPurge(internals: EngineInternals, state: WorkflowState, callbacks: LifecycleCallbacks, observedGenerationBytes: Uint8Array | null): Promise<BatchOperation[]>;
@@ -1,4 +1,5 @@
1
1
  import { KEYS, storageHas } from "../../../storage/interface.js";
2
+ import { coerceStartWorkflowId } from "../../start-workflow-validation.js";
2
3
  import {
3
4
  clearPurgedWorkflowInMemoryState,
4
5
  collectWorkflowPurgeDeleteOperations
@@ -6,16 +7,27 @@ import {
6
7
  import { WorkflowAlreadyExistsError, WorkflowTeardownPendingError } from "../errors.js";
7
8
  import { cleanupWaiters } from "../termination/cleanup.js";
8
9
  import { decodeWorkflowState, isTerminalWorkflowStatus } from "../validation.js";
10
+ import { buildWorkflowGenerationBumpOperation } from "../workflow-generation-fence.js";
9
11
  export const GENERATED_ID_START_DECISION = {
10
12
  terminalRunToPurge: null,
11
- duplicateIdCondition: void 0
13
+ duplicateIdCondition: void 0,
14
+ duplicateIdGenerationCondition: void 0,
15
+ observedGenerationBytes: null
12
16
  };
13
17
  export async function resolveTerminalConflictForRestart(internals, workflowId, options) {
14
- const key = KEYS.workflow(workflowId), existingBytes = await internals.storage.get(key);
18
+ const key = KEYS.workflow(workflowId), generationKey = KEYS.workflowGeneration(workflowId), [existingBytes, observedGenerationBytes] = await Promise.all([
19
+ internals.storage.get(key),
20
+ internals.storage.get(generationKey)
21
+ ]), duplicateIdGenerationCondition = {
22
+ key: generationKey,
23
+ expectedValue: observedGenerationBytes
24
+ };
15
25
  if (existingBytes === null)
16
26
  return {
17
27
  terminalRunToPurge: null,
18
- duplicateIdCondition: { key, expectedValue: null }
28
+ duplicateIdCondition: { key, expectedValue: null },
29
+ duplicateIdGenerationCondition,
30
+ observedGenerationBytes
19
31
  };
20
32
  if (options?.onTerminalConflict !== "start-new")
21
33
  throw new WorkflowAlreadyExistsError(workflowId);
@@ -26,13 +38,20 @@ export async function resolveTerminalConflictForRestart(internals, workflowId, o
26
38
  throw new WorkflowTeardownPendingError(workflowId);
27
39
  return {
28
40
  terminalRunToPurge: existingState,
29
- duplicateIdCondition: { key, expectedValue: existingBytes }
41
+ duplicateIdCondition: { key, expectedValue: existingBytes },
42
+ duplicateIdGenerationCondition,
43
+ observedGenerationBytes
30
44
  };
31
45
  }
32
- export async function prepareTerminalRunPurge(internals, state, callbacks) {
46
+ export function enforceReplayOnlyIdFence(skipAdmissionIdCheck, workflowId, terminalRunToPurge) {
47
+ if ((skipAdmissionIdCheck === "reattach-only" || skipAdmissionIdCheck === "bulk-retry-only") && terminalRunToPurge === null)
48
+ coerceStartWorkflowId(workflowId, "options.id");
49
+ }
50
+ export async function prepareTerminalRunPurge(internals, state, callbacks, observedGenerationBytes) {
33
51
  const cleanupWaitersForStart = (id) => cleanupWaiters(internals, id, {
34
52
  swallowPromiseRejection: callbacks.swallowPromiseRejection
35
53
  }), deleteOperations = await collectWorkflowPurgeDeleteOperations(internals, state);
54
+ deleteOperations.push(buildWorkflowGenerationBumpOperation(state.id, observedGenerationBytes));
36
55
  clearPurgedWorkflowInMemoryState(internals, state.id, cleanupWaitersForStart);
37
56
  return deleteOperations;
38
57
  }
@@ -1,5 +1,5 @@
1
1
  import type { BatchOperation } from '../../../storage/interface.ts';
2
- import type { StartOptions, StartWorkflowOptions } from '../../types.ts';
2
+ import type { StartWorkflowOptions } from '../../types.ts';
3
3
  import { type WorkflowHandle } from '../handles.ts';
4
4
  import type { EngineInternals } from '../internals.ts';
5
5
  import { type LifecycleCallbacks } from './shared.ts';
@@ -19,5 +19,42 @@ export declare function startWorkflow(internals: EngineInternals, type: string,
19
19
  * `undefined` (the default, every non-schedule start) is byte-for-byte
20
20
  * the pre-WFT-20 admission path.
21
21
  */
22
- revisionOverride?: string): Promise<WorkflowHandle>;
23
- export declare function resolveScheduledStartAt(internals: EngineInternals, options: StartOptions | undefined, submissionTime: number, callbacks: LifecycleCallbacks): number | undefined;
22
+ revisionOverride?: string,
23
+ /**
24
+ * Internal-only, never part of the public `StartOptions` type (WFT-95).
25
+ * When truthy, `options.id` skips strict fresh-admission validation
26
+ * (`assertValidWorkflowId`'s `.`/`..` rejection) and is instead validated
27
+ * with the decode-compatible `assertDecodableWorkflowId`. This exists
28
+ * ONLY to replay an id that was already durably accepted before strict
29
+ * admission existed — never to let a genuinely fresh caller admit `.`/`..`.
30
+ *
31
+ * Set from exactly three internal call sites, all replaying an
32
+ * already-persisted id rather than admitting a new one:
33
+ * - `drainQueuedScheduleRun()` (via `ScheduledRunStartOptions.skipAdmissionIdCheck`,
34
+ * threaded through `startScheduledRun()`), which restarts a schedule's
35
+ * persisted `queuedRuns[].workflowId` — safe unconditionally (`true`),
36
+ * since a queued run created after this fix was already validated as
37
+ * non-`.`/`..` at schedule-admission time, so relaxing the check here
38
+ * is a no-op for it and only matters for a historical pre-WFT-95 queued run.
39
+ * - `retryFailedWorkflow()`'s checkpoint-absent fallback (`bulk-operations-retry.ts`),
40
+ * which rebuilds an already-persisted, already-validated-at-the-time
41
+ * `workflowId` from its stored input via `onTerminalConflict: 'start-new'`.
42
+ * Passes the literal `'bulk-retry-only'` rather than `true` — see
43
+ * {@link enforceReplayOnlyIdFence}'s doc comment for the TOCTOU race that
44
+ * value fences: the retry's own confirmation read and
45
+ * `resolveTerminalConflictForRestart`'s atomic read are not the same
46
+ * read, so a concurrent purge under `ownership: 'workflow-lease'` can
47
+ * leave nothing to purge-and-replace by the time this runs.
48
+ * - `dispatchChildWorkflowStart()`'s crash-reattach retry, which passes
49
+ * the literal `'reattach-only'` rather than `true` — see
50
+ * {@link enforceReplayOnlyIdFence}'s doc comment for the same TOCTOU
51
+ * race (this one only *speculatively* matched an existing record
52
+ * before this call, unlike the schedule-drain site above).
53
+ *
54
+ * Every other caller (REST, JSON-RPC, direct `engine.start()`,
55
+ * `engine.startOrSignal()`, a fresh `ctx.startChild()`) omits this
56
+ * parameter and gets the strict check, because it is not part of
57
+ * `StartOptions`/`StartWorkflowOptions` and therefore cannot be set from
58
+ * any public surface.
59
+ */
60
+ skipAdmissionIdCheck?: boolean | 'reattach-only' | 'bulk-retry-only'): Promise<WorkflowHandle>;
@@ -1,11 +1,8 @@
1
1
  import { assertPayloadWithinLimit } from "../../payload-size.js";
2
- import { normalizeStorageTimestamp } from "../../scheduler.js";
3
2
  import {
4
- StartWorkflowValidationError,
5
- assertExclusiveStartWorkflowOptions,
6
3
  assertValidOnTerminalConflict,
7
- coerceStartWorkflowId,
8
- coerceStartWorkflowTimestamp
4
+ coerceReplayWorkflowId,
5
+ coerceStartWorkflowId
9
6
  } from "../../start-workflow-validation.js";
10
7
  import {
11
8
  releaseInFlightStart,
@@ -33,6 +30,7 @@ import {
33
30
  resolveCachedStartRevision,
34
31
  resolveStartRevisionUncached
35
32
  } from "./start-revision-resolution.js";
33
+ import { resolveScheduledStartAt } from "./start-schedule-timing.js";
36
34
  import {
37
35
  applyRestartLineage,
38
36
  createInitialCheckpoint,
@@ -40,6 +38,7 @@ import {
40
38
  parseStartOptionDuration
41
39
  } from "./start-state.js";
42
40
  import {
41
+ enforceReplayOnlyIdFence,
43
42
  GENERATED_ID_START_DECISION,
44
43
  prepareTerminalRunPurge,
45
44
  resolveTerminalConflictForRestart
@@ -47,8 +46,8 @@ import {
47
46
  export async function start(internals, type, input, options, callbacks) {
48
47
  return startWorkflow(internals, type, input, options, void 0, callbacks);
49
48
  }
50
- function prepareStartWorkflow(internals, options, callbacks) {
51
- const callerProvidedId = options?.id !== void 0, workflowId = options?.id !== void 0 ? coerceStartWorkflowId(options.id, "options.id") : crypto.randomUUID(), parentHeaders = internals.pendingParentHeaders;
49
+ function prepareStartWorkflow(internals, options, callbacks, skipAdmissionIdCheck) {
50
+ const callerProvidedId = options?.id !== void 0, workflowId = options?.id !== void 0 ? skipAdmissionIdCheck ? coerceReplayWorkflowId(options.id, "options.id") : coerceStartWorkflowId(options.id, "options.id") : crypto.randomUUID(), parentHeaders = internals.pendingParentHeaders;
52
51
  internals.pendingParentHeaders = void 0;
53
52
  const pendingExecutionStateOwnerId = internals.pendingExecutionStateOwnerId, executionStateOwnerId = pendingExecutionStateOwnerId === null ? void 0 : pendingExecutionStateOwnerId ?? workflowId;
54
53
  internals.pendingExecutionStateOwnerId = void 0;
@@ -92,10 +91,10 @@ function resolveAndReserveStartRegistration(internals, type, revisionOverride, c
92
91
  resolvedRevision: revisionOverride
93
92
  }));
94
93
  }
95
- export async function startWorkflow(internals, type, input, options, additionalStartOperations, callbacks, buildIdempotentStartOperations, revisionOverride) {
94
+ export async function startWorkflow(internals, type, input, options, additionalStartOperations, callbacks, buildIdempotentStartOperations, revisionOverride, skipAdmissionIdCheck) {
96
95
  assertServicesSupportedForMode(internals, options);
97
96
  assertValidOnTerminalConflict(options);
98
- const preparation = prepareStartWorkflow(internals, options, callbacks), {
97
+ const preparation = prepareStartWorkflow(internals, options, callbacks, Boolean(skipAdmissionIdCheck)), {
99
98
  workflowId,
100
99
  callerProvidedId,
101
100
  parentHeaders,
@@ -117,9 +116,16 @@ export async function startWorkflow(internals, type, input, options, additionalS
117
116
  resolvedRevision
118
117
  } = await resolveAndReserveStartRegistration(internals, type, revisionOverride, callbacks);
119
118
  inFlightRevision = reservedRevision;
120
- const workflowConcurrency = registration.concurrency, revision = resolveCachedStartRevision(internals, type, resolvedRevision) ?? await resolveStartRevisionUncached(internals, type), { terminalRunToPurge, duplicateIdCondition } = callerProvidedId ? await resolveTerminalConflictForRestart(internals, workflowId, options) : GENERATED_ID_START_DECISION, versionTuple = createWorkflowVersionTuple(internals, registration, callbacks), state = createInitialWorkflowState(internals, workflowId, type, input, versionTuple, revision, options, preparation.normalizedTags, executionStateOwnerId, parentWorkflowId, parentWorkflowExecutionToken, delayedStartTimer, callbacks);
119
+ const workflowConcurrency = registration.concurrency, revision = resolveCachedStartRevision(internals, type, resolvedRevision) ?? await resolveStartRevisionUncached(internals, type), {
120
+ terminalRunToPurge,
121
+ duplicateIdCondition,
122
+ duplicateIdGenerationCondition,
123
+ observedGenerationBytes
124
+ } = callerProvidedId ? await resolveTerminalConflictForRestart(internals, workflowId, options) : GENERATED_ID_START_DECISION;
125
+ enforceReplayOnlyIdFence(skipAdmissionIdCheck, workflowId, terminalRunToPurge);
126
+ const versionTuple = createWorkflowVersionTuple(internals, registration, callbacks), state = createInitialWorkflowState(internals, workflowId, type, input, versionTuple, revision, options, preparation.normalizedTags, executionStateOwnerId, parentWorkflowId, parentWorkflowExecutionToken, delayedStartTimer, callbacks);
121
127
  applyRestartLineage(state, terminalRunToPurge);
122
- const checkpoint = createInitialCheckpoint(internals, workflowId, versionTuple.workflowVersion, options, callbacks), workflowStartHeaders = runWorkflowStartInterceptor(internals, workflowId, type, input, parentHeaders, callbacks), persistedWorkflowStartHeaders = selectPersistedWorkflowStartHeaders(workflowStartHeaders), purgeDeleteOperations = terminalRunToPurge !== null ? await prepareTerminalRunPurge(internals, terminalRunToPurge, callbacks) : void 0;
128
+ const checkpoint = createInitialCheckpoint(internals, workflowId, versionTuple.workflowVersion, options, callbacks), workflowStartHeaders = runWorkflowStartInterceptor(internals, workflowId, type, input, parentHeaders, callbacks), persistedWorkflowStartHeaders = selectPersistedWorkflowStartHeaders(workflowStartHeaders), purgeDeleteOperations = terminalRunToPurge !== null ? await prepareTerminalRunPurge(internals, terminalRunToPurge, callbacks, observedGenerationBytes) : void 0;
123
129
  internals.checkpoints.set(workflowId, checkpoint);
124
130
  setWorkflowStartHeaders(internals, workflowId, workflowStartHeaders, callbacks);
125
131
  internals.workflowVersionTuples.set(workflowId, versionTuple);
@@ -136,7 +142,8 @@ export async function startWorkflow(internals, type, input, options, additionalS
136
142
  buildWorkflowConcurrencyStartOperations: workflowConcurrency === void 0 ? void 0 : () => buildWorkflowConcurrencyStartOperations(internals, type, workflowId, input, workflowConcurrency),
137
143
  callbacks,
138
144
  purgeDeleteOperations,
139
- duplicateIdCondition
145
+ duplicateIdCondition,
146
+ duplicateIdGenerationCondition
140
147
  }, buildIdempotentStartOperations);
141
148
  if (options?.services !== void 0) {
142
149
  internals.workflowServices.set(workflowId, options.services);
@@ -163,17 +170,3 @@ export async function startWorkflow(internals, type, input, options, additionalS
163
170
  rollbackTransientStartState(internals, workflowId);
164
171
  }
165
172
  }
166
- export function resolveScheduledStartAt(internals, options, submissionTime, callbacks) {
167
- assertExclusiveStartWorkflowOptions(options?.startAt, options?.startAfter);
168
- if (options?.startAt !== void 0)
169
- return coerceStartWorkflowTimestamp(options.startAt, "options.startAt");
170
- if (options?.startAfter !== void 0) {
171
- const startAfterMilliseconds = parseStartOptionDuration(internals, options.startAfter, "options.startAfter", callbacks);
172
- try {
173
- return normalizeStorageTimestamp(submissionTime + startAfterMilliseconds, "options.startAfter");
174
- } catch {
175
- throw new StartWorkflowValidationError("options.startAfter must resolve to a finite, non-negative start time");
176
- }
177
- }
178
- return;
179
- }
@@ -1,6 +1,7 @@
1
1
  import type { ForkOptions } from '../../types.ts';
2
2
  import type { WorkflowHandle } from '../handles.ts';
3
3
  import type { EngineInternals } from '../internals.ts';
4
+ import { type FreshResumeClaimTracker } from './resume.ts';
4
5
  import { type LifecycleCallbacks, type RecoverAllOptions } from './shared.ts';
5
6
  export declare function recoverAll(internals: EngineInternals, callbacks: LifecycleCallbacks, options?: RecoverAllOptions): Promise<WorkflowHandle[]>;
6
7
  /** Options for {@link resume}. */
@@ -16,6 +17,14 @@ export type ResumeOptions = {
16
17
  * `engine.resume()` leaves it `false` — nothing was deposed there.
17
18
  */
18
19
  readonly forceReplayFromStorage?: boolean;
20
+ /**
21
+ * Forwarded to `resumeWorkflowFromStorage()`'s matching options — see
22
+ * `ResumeFromStorageOptions` (WFT-134 issue D). Set by
23
+ * `recoverEntryOrIsolateFailure` only; unset for the local-ownership fast
24
+ * path above, which never reaches that function.
25
+ */
26
+ readonly deferClaimReleaseOnRejection?: boolean;
27
+ readonly freshClaimTracker?: FreshResumeClaimTracker;
19
28
  };
20
29
  export declare function resume(internals: EngineInternals, workflowId: string, callbacks: LifecycleCallbacks, onRecoveredWorkflow?: RecoverAllOptions['onRecoveredWorkflow'], options?: ResumeOptions): Promise<WorkflowHandle>;
21
30
  export declare function fork(internals: EngineInternals, sourceWorkflowId: string, options: ForkOptions | undefined, callbacks: LifecycleCallbacks): Promise<WorkflowHandle>;
@@ -1,17 +1,12 @@
1
1
  import { KEYS } from "../../../storage/interface.js";
2
2
  import { deserializeCheckpoint, serializeCheckpoint } from "../../checkpoint.js";
3
- import { RegExpExtensionDecodeError } from "../../codec/extension-codec.js";
4
3
  import { EMPTY_EVENT_HEAD } from "../../event-log.js";
5
4
  import { WorkflowRecoverySkippedEvent } from "../../events.js";
6
- import { VersionMismatchError } from "../../versioning.js";
7
5
  import { forgetCommittedCheckpointBytes } from "../checkpoint-commit-snapshots.js";
8
6
  import { hydrateCheckpointReplayState } from "../checkpoint-replay.js";
9
- import { DynamicWorkflowSourceUnavailableError } from "../dynamic-source-errors.js";
10
7
  import { resolveExecutableRegistrationOrRenamedNotFound } from "../dynamic-source-execution.js";
11
8
  import { WorkflowTypeNotRegisteredForRecoveryError } from "../errors.js";
12
9
  import { commitFencedEngineWrite } from "../fenced-write.js";
13
- import { WorkflowClaimUnavailableError } from "../lease-errors.js";
14
- import { WorkflowRevisionUnavailableError } from "../revision-errors.js";
15
10
  import { normalizeForkStep, selectPersistedWorkflowStartHeaders } from "../state-utilities.js";
16
11
  import { loadWorkflowState } from "../storage-io.js";
17
12
  import { decodeWorkflowState } from "../validation.js";
@@ -23,6 +18,7 @@ import {
23
18
  createForkedWorkflowState
24
19
  } from "./fork-helpers.js";
25
20
  import { derivePreparedExecutionState } from "./persist.js";
21
+ import { isolateRecoveryFailure } from "./recovery-isolation.js";
26
22
  import {
27
23
  buildRecoveryRevisionGroups,
28
24
  classifyRevisionGroups,
@@ -34,6 +30,7 @@ import {
34
30
  loadWorkflowStartHeaders,
35
31
  setWorkflowStartHeaders
36
32
  } from "./shared.js";
33
+ import { releaseFreshlyAcquiredResumeClaim } from "./standalone-claim-acquire.js";
37
34
  function isWorkflowSideRecordKey(key) {
38
35
  return key.includes(":ckpt") || key.includes(":offload") || key.includes(":archive") || key.includes(":timeline:");
39
36
  }
@@ -67,28 +64,19 @@ async function preflightRecoverAll(internals, callbacks) {
67
64
  return result;
68
65
  }
69
66
  async function recoverEntryOrIsolateFailure(internals, workflowId, callbacks, options) {
67
+ const freshClaimTracker = { epoch: null };
70
68
  try {
71
- return await resume(internals, workflowId, callbacks, options?.onRecoveredWorkflow);
69
+ return await resume(internals, workflowId, callbacks, options?.onRecoveredWorkflow, {
70
+ deferClaimReleaseOnRejection: !0,
71
+ freshClaimTracker
72
+ });
72
73
  } catch (error) {
73
- if (error instanceof RegExpExtensionDecodeError) {
74
- await callbacks.failWorkflowForCheckpointDecodeError(workflowId, error);
75
- return null;
76
- }
77
- if (error instanceof VersionMismatchError && options?.versionMismatchPolicy !== "throw") {
78
- await callbacks.failWorkflowForVersionMismatch(workflowId, error);
79
- return null;
80
- }
81
- if (error instanceof WorkflowClaimUnavailableError)
82
- return null;
83
- if (error instanceof WorkflowRevisionUnavailableError) {
84
- await callbacks.failWorkflowForRevisionUnavailable(workflowId, error);
85
- return null;
74
+ try {
75
+ return await isolateRecoveryFailure(workflowId, callbacks, options, error);
76
+ } finally {
77
+ if (freshClaimTracker.epoch !== null)
78
+ await releaseFreshlyAcquiredResumeClaim(internals, workflowId, freshClaimTracker.epoch);
86
79
  }
87
- if (error instanceof DynamicWorkflowSourceUnavailableError) {
88
- await callbacks.failWorkflowForUnavailableDynamicSource(workflowId, error);
89
- return null;
90
- }
91
- throw error;
92
80
  }
93
81
  }
94
82
  export async function recoverAll(internals, callbacks, options) {
@@ -125,7 +113,14 @@ export async function resume(internals, workflowId, callbacks, onRecoveredWorkfl
125
113
  return callbacks.getHandle(workflowId);
126
114
  }
127
115
  }
128
- return resumeWorkflowFromStorage(internals, workflowId, !0, callbacks, onRecoveredWorkflow);
116
+ return resumeWorkflowFromStorage(internals, workflowId, !0, callbacks, onRecoveredWorkflow, {
117
+ ...options?.deferClaimReleaseOnRejection !== void 0 && {
118
+ deferClaimReleaseOnRejection: options.deferClaimReleaseOnRejection
119
+ },
120
+ ...options?.freshClaimTracker !== void 0 && {
121
+ freshClaimTracker: options.freshClaimTracker
122
+ }
123
+ });
129
124
  }
130
125
  export async function fork(internals, sourceWorkflowId, options, callbacks) {
131
126
  const sourceState = await loadWorkflowState(internals, sourceWorkflowId);
@@ -1,6 +1,7 @@
1
1
  export { EMPTY_STORAGE_VALUE, createWorkflowHandle, loadTerminalCleanupTrackedState, loadWorkflowStartHeaders, normalizeStartWorkflowTags, processPendingUpdatesAfterReplay, setWorkflowStartHeaders, type LifecycleCallbacks, type RecoverAllOptions, type RecoveredWorkflowInfo, } from './lifecycle/shared.ts';
2
2
  export { createWorkflowVersionTuple, derivePreparedExecutionState, prepareResumeState, throwVersionMismatch, workflowStateWithVersionTuple, workflowVersionTupleFromState, } from './lifecycle/persist.ts';
3
- export { resolveScheduledStartAt, start, startWorkflow } from './lifecycle/start.ts';
3
+ export { resolveScheduledStartAt } from './lifecycle/start-schedule-timing.ts';
4
+ export { start, startWorkflow } from './lifecycle/start.ts';
4
5
  export { applyRestartLineage, createInitialCheckpoint, createInitialWorkflowState, parseStartOptionDuration, } from './lifecycle/start-state.ts';
5
6
  export { startOrSignal, startWithIdempotency, type StartOrSignalCallbacks, type StartOrSignalResult, } from './lifecycle/start-or-signal.ts';
6
7
  export { buildInitialSearchAttributeOperations, buildStartBatchOperations, validateSearchAttributes, } from './lifecycle/start-batch.ts';
@@ -15,7 +15,8 @@ export {
15
15
  workflowStateWithVersionTuple,
16
16
  workflowVersionTupleFromState
17
17
  } from "./lifecycle/persist.js";
18
- export { resolveScheduledStartAt, start, startWorkflow } from "./lifecycle/start.js";
18
+ export { resolveScheduledStartAt } from "./lifecycle/start-schedule-timing.js";
19
+ export { start, startWorkflow } from "./lifecycle/start.js";
19
20
  export {
20
21
  applyRestartLineage,
21
22
  createInitialCheckpoint,
@@ -44,7 +44,8 @@ export async function drainQueuedScheduleRun(state, callbacks, completedWorkflow
44
44
  workflowId: queuedRun.workflowId,
45
45
  ...queuedRun.occurrence !== void 0 && { occurrence: queuedRun.occurrence },
46
46
  scheduleStateAfterStart: stateAfterStart,
47
- ...completedWorkflowId !== void 0 && { completedWorkflowId }
47
+ ...completedWorkflowId !== void 0 && { completedWorkflowId },
48
+ skipAdmissionIdCheck: !0
48
49
  });
49
50
  return stateAfterStart;
50
51
  }
@@ -9,6 +9,16 @@ export type ScheduledRunStartOptions = {
9
9
  scheduleStateAfterStart?: ScheduleState;
10
10
  /** Prior terminal run whose transient schedule metadata is now settled. */
11
11
  completedWorkflowId?: string;
12
+ /**
13
+ * Internal only (WFT-95). Set by `drainQueuedScheduleRun()` when `workflowId`
14
+ * is a persisted `queuedRuns[].workflowId` being replayed, not a freshly
15
+ * minted one — safe unconditionally, since a post-fix queued run was already
16
+ * validated as non-`.`/`..` at schedule-admission time (relaxing the check
17
+ * here is then a no-op), and it is what lets a legacy pre-WFT-95 queued run
18
+ * whose id is `.`/`..` keep draining instead of pausing the schedule.
19
+ * Threaded straight through to `startWorkflow`'s `skipAdmissionIdCheck`.
20
+ */
21
+ skipAdmissionIdCheck?: boolean;
12
22
  };
13
23
  /**
14
24
  * Launch one scheduled occurrence's workflow run and emit `schedule:fired`.
@@ -50,7 +50,7 @@ export async function startScheduledRun(internals, state, callbacks, options = {
50
50
  internals.workflowServices.set(workflowId, resolution.services);
51
51
  }
52
52
  internals.workflowsNeedingTerminalCleanup.add(workflowId);
53
- await callbacks.startWorkflow(state.workflowType, state.input, { id: workflowId }, scheduleRunOperations, buildPinnedRevisionOverride(state));
53
+ await callbacks.startWorkflow(state.workflowType, state.input, { id: workflowId }, scheduleRunOperations, buildPinnedRevisionOverride(state), options.skipAdmissionIdCheck);
54
54
  internals.engine.dispatchEvent(new ScheduleFiredEvent(state.id, workflowId, internals.options.getNow(), occurrence));
55
55
  if (resolution !== null && resolution.status === "unavailable") {
56
56
  const unavailableError = unavailableServicesError(workflowId, resolution.reason);
@@ -10,10 +10,19 @@ export type RefreshedScheduleState = {
10
10
  currentWorkflowState: WorkflowState | null;
11
11
  };
12
12
  export type ScheduleCallbacks = {
13
- /** `revisionOverride` (WFT-20) is the exact revision a `revisionPolicy: 'pinned'` occurrence must resolve against; `undefined` for an `'active-at-fire'` schedule. */
13
+ /**
14
+ * `revisionOverride` (WFT-20) is the exact revision a `revisionPolicy: 'pinned'`
15
+ * occurrence must resolve against; `undefined` for an `'active-at-fire'` schedule.
16
+ *
17
+ * `skipAdmissionIdCheck` (WFT-95, internal only) is threaded straight through to
18
+ * `startWorkflow`'s parameter of the same name — see its doc comment. Set only when
19
+ * replaying a schedule's already-persisted `queuedRuns[].workflowId` via
20
+ * `drainQueuedScheduleRun()`; every other schedule-run start (a fresh cadence tick,
21
+ * a `cancel-running` replacement) omits it and gets strict id admission.
22
+ */
14
23
  startWorkflow: (type: string, input: unknown, options: {
15
24
  id: string;
16
- }, additionalStartOperations?: BatchOperation[], revisionOverride?: string) => Promise<void>;
25
+ }, additionalStartOperations?: BatchOperation[], revisionOverride?: string, skipAdmissionIdCheck?: boolean) => Promise<void>;
17
26
  loadWorkflowState: (workflowId: string) => Promise<WorkflowState | null | undefined>;
18
27
  cancelWorkflow: (workflowId: string) => Promise<void>;
19
28
  getWorkflowResult: (workflowId: string) => Promise<unknown>;
@@ -177,7 +177,7 @@ async function releaseWorkflowClaimAfterTerminalSettlement(internals, workflowId
177
177
  if (registry.currentEpoch(workflowId) !== capturedEpoch)
178
178
  return;
179
179
  try {
180
- await registry.release(workflowId);
180
+ await registry.release(workflowId, capturedEpoch);
181
181
  } catch {}
182
182
  }
183
183
  export async function completeWorkflow(internals, workflowId, result, callbacks) {
@@ -6,6 +6,7 @@ import { dropQueuedInlineWorkflowStart } from "../inline-launch-queue.js";
6
6
  import { buildWorkflowVisibilityIndexTransition } from "../workflow-indexes.js";
7
7
  import { evictSuspendedWorkflowWaiters } from "./cleanup.js";
8
8
  export async function suspendWorkflow(internals, workflowId, callbacks) {
9
+ const capturedClaimEpochForForget = internals.workflowClaimRegistry?.currentEpoch(workflowId) ?? null;
9
10
  if (!await callbacks.runSerializedWorkflowStateWrite(workflowId, async () => {
10
11
  const state = await callbacks.loadWorkflowState(workflowId);
11
12
  if (!state || state.status !== "running")
@@ -26,6 +27,8 @@ export async function suspendWorkflow(internals, workflowId, callbacks) {
26
27
  return !0;
27
28
  }))
28
29
  return;
30
+ if (internals.workflowClaimRegistry !== null && capturedClaimEpochForForget !== null && internals.workflowClaimRegistry.currentEpoch(workflowId) === capturedClaimEpochForForget)
31
+ internals.workflowClaimRegistry.forgetLocalClaim(workflowId);
29
32
  const event = new WorkflowSuspendedEvent(workflowId);
30
33
  callbacks.dispatchEvent(event);
31
34
  callbacks.forwardEventToHandle(workflowId, event);
@@ -6,8 +6,32 @@ export declare function isValidScheduleTimestamp(value: unknown): value is numbe
6
6
  export declare function isValidScheduleStatus(value: unknown): value is ScheduleStatus;
7
7
  export declare function isValidScheduleOverlapPolicy(value: unknown): value is ScheduleOverlapPolicy;
8
8
  export declare function isValidScheduleRevisionPolicy(value: unknown): value is ScheduleRevisionPolicy;
9
+ /**
10
+ * Whether `value` is a schedule-related workflow id that decoded persisted
11
+ * data may still carry. Deliberately reuses the pre-WFT-95 decode predicate
12
+ * (not the fresh-admission `.`/`..` rejection), because every caller of this
13
+ * function — `decodeScheduleIdentityFields`, `decodeScheduleRunMetadata`,
14
+ * and the persisted `currentWorkflowId`/`queuedRuns[].workflowId` fields in
15
+ * this module — reads already-persisted data. A schedule created before
16
+ * WFT-95 with `id: '.'` or `'..'` must remain decodable and keep firing
17
+ * after upgrade.
18
+ */
9
19
  export declare function isValidScheduleIdentifier(value: unknown): value is string;
10
- export declare function coerceScheduleId(scheduleId: string, fieldName: string): string;
20
+ /**
21
+ * Coerce a caller-supplied `scheduleId` used to look up or control an
22
+ * already-persisted schedule — `getSchedule`, `pauseSchedule`,
23
+ * `resumeSchedule`, `cancelSchedule`, and `updateSchedule` all call this,
24
+ * never schedule creation (that admission path is
25
+ * `normalizeScheduleOptions`'s direct `coerceStartWorkflowId(options.id, …)`
26
+ * call, which keeps the strict `.`/`..` rejection). Deliberately uses the
27
+ * decode-compatible `assertDecodableWorkflowId`, not the admission-only
28
+ * `.`/`..` rejection (WFT-95 review): a schedule created before that
29
+ * rejection landed may already carry `id: '.'` or `'..'`, and callers must
30
+ * still be able to inspect, pause, resume, cancel, or update it instead of
31
+ * having every control operation reject a schedule they can't otherwise
32
+ * reach.
33
+ */
34
+ export declare function coerceScheduleId(scheduleId: unknown, fieldName: string): string;
11
35
  /**
12
36
  * A recurrence specification normalized into the discriminated cadence the
13
37
  * engine persists. Interval periods are resolved to whole milliseconds.
@@ -2,7 +2,7 @@ import { decode } from "../../codec.js";
2
2
  import { isRecord } from "../../debug-output.js";
3
3
  import { parseCronExpression } from "../../schedule.js";
4
4
  import { parseDuration } from "../../scheduler.js";
5
- import { coerceStartWorkflowId } from "../../start-workflow-validation.js";
5
+ import { assertDecodableWorkflowId, isDecodableWorkflowId } from "../../workflow-identifiers.js";
6
6
  import { decodeScheduleCadence } from "./schedule-cadence.js";
7
7
  import { SCHEDULE_OVERLAP_POLICIES, SCHEDULE_REVISION_POLICIES } from "./schedule-options.js";
8
8
  import { decodeScheduleRevisionPolicyFields } from "./schedule-revision.js";
@@ -28,17 +28,11 @@ export function isValidScheduleRevisionPolicy(value) {
28
28
  return typeof value === "string" && SCHEDULE_REVISION_POLICIES.has(value);
29
29
  }
30
30
  export function isValidScheduleIdentifier(value) {
31
- if (typeof value !== "string")
32
- return !1;
33
- try {
34
- coerceStartWorkflowId(value, "schedule id");
35
- return !0;
36
- } catch {
37
- return !1;
38
- }
31
+ return typeof value === "string" && isDecodableWorkflowId(value);
39
32
  }
40
33
  export function coerceScheduleId(scheduleId, fieldName) {
41
- return coerceStartWorkflowId(scheduleId, fieldName);
34
+ assertDecodableWorkflowId(scheduleId, fieldName);
35
+ return scheduleId;
42
36
  }
43
37
  function normalizeIntervalEvery(every) {
44
38
  if (typeof every !== "string" && typeof every !== "number")