@lostgradient/weft 0.8.0 → 0.9.0

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 (103) hide show
  1. package/README.md +7 -3
  2. package/dist/cli/generated/operation-client.generated.d.ts +1 -0
  3. package/dist/cli-main.js +78 -78
  4. package/dist/core/context/durable-operations.js +1 -1
  5. package/dist/core/context/index.d.ts +1 -0
  6. package/dist/core/context/index.js +2 -0
  7. package/dist/core/context/operation-request.d.ts +2 -0
  8. package/dist/core/context/run-operation.js +3 -2
  9. package/dist/core/context/speculative-child.js +1 -0
  10. package/dist/core/context/types.d.ts +1 -0
  11. package/dist/core/engine/activity-heartbeat-tracking.d.ts +1 -1
  12. package/dist/core/engine/activity-heartbeat-tracking.js +5 -1
  13. package/dist/core/engine/anonymous-signal-sequence.js +0 -2
  14. package/dist/core/engine/bulk-operations.js +10 -29
  15. package/dist/core/engine/callback-creators-bundles.js +1 -1
  16. package/dist/core/engine/disposal.js +1 -0
  17. package/dist/core/engine/engine-internal-types.d.ts +1 -0
  18. package/dist/core/engine/index.js +13 -9
  19. package/dist/core/engine/inline-launch-queue.js +1 -1
  20. package/dist/core/engine/internals.d.ts +23 -1
  21. package/dist/core/engine/lifecycle/fork-helpers.js +1 -0
  22. package/dist/core/engine/lifecycle/resume.js +6 -0
  23. package/dist/core/engine/lifecycle/start-exec.d.ts +2 -2
  24. package/dist/core/engine/lifecycle/start-exec.js +6 -4
  25. package/dist/core/engine/lifecycle/start.js +1 -0
  26. package/dist/core/engine/lifecycle/transition.js +6 -0
  27. package/dist/core/engine/memo-durable-activity.js +1 -1
  28. package/dist/core/engine/operations-activity.d.ts +1 -1
  29. package/dist/core/engine/operations-activity.js +7 -5
  30. package/dist/core/engine/operations-time.d.ts +2 -2
  31. package/dist/core/engine/operations-time.js +40 -6
  32. package/dist/core/engine/schedules.js +3 -0
  33. package/dist/core/engine/termination/cleanup.js +15 -13
  34. package/dist/core/engine/termination/finalizer-activity.d.ts +1 -1
  35. package/dist/core/engine/termination/finalizer-activity.js +5 -1
  36. package/dist/core/engine/termination/finalizer.js +1 -1
  37. package/dist/core/engine/validation/schedule-options.d.ts +9 -0
  38. package/dist/core/engine/validation/schedule-options.js +59 -0
  39. package/dist/core/engine/validation/schedule.d.ts +3 -8
  40. package/dist/core/engine/validation/schedule.js +40 -56
  41. package/dist/core/engine/validation.js +1 -0
  42. package/dist/core/execution-strategy.d.ts +1 -0
  43. package/dist/core/inline-execution-strategy.context-options.d.ts +1 -0
  44. package/dist/core/inline-execution-strategy.context-options.js +3 -0
  45. package/dist/core/types/activity.d.ts +11 -0
  46. package/dist/core/types/checkpoint.d.ts +1 -0
  47. package/dist/core/types/schedules.d.ts +10 -0
  48. package/dist/core/types/state.d.ts +6 -0
  49. package/dist/core/types/workflow-context.d.ts +13 -0
  50. package/dist/core/worker-execution-strategy.d.ts +1 -0
  51. package/dist/core/worker-inbound-message.d.ts +1 -0
  52. package/dist/core/worker-inbound-message.js +3 -0
  53. package/dist/json-schema.js +2 -2
  54. package/dist/mcp/cli.js +32 -32
  55. package/dist/observability/index.js +2 -2
  56. package/dist/runtime/portable.d.ts +1 -1
  57. package/dist/server/handler.js +27 -27
  58. package/dist/server/index.d.ts +2 -0
  59. package/dist/server/index.js +30 -30
  60. package/dist/server/operations/create-schedule.d.ts +2 -0
  61. package/dist/server/operations/create-schedule.js +13 -2
  62. package/dist/server/rest-binding.d.ts +1 -1
  63. package/dist/server/runtime/task-dispatch.js +6 -0
  64. package/dist/server/runtime/task-polling.js +9 -1
  65. package/dist/server/runtime/task-reconciliation.js +2 -1
  66. package/dist/server/task-queue-types.d.ts +2 -0
  67. package/dist/server/task-state.d.ts +4 -0
  68. package/dist/service-worker/index.d.ts +15 -14
  69. package/dist/service-worker/index.js +28 -28
  70. package/dist/service-worker/scheduler.d.ts +41 -1
  71. package/dist/storage/auto.d.ts +36 -18
  72. package/dist/storage/auto.js +1 -1
  73. package/dist/storage/compressed-storage.js +1 -1
  74. package/dist/storage/indexeddb.d.ts +6 -1
  75. package/dist/storage/indexeddb.js +1 -1
  76. package/dist/storage/interface.js +1 -1
  77. package/dist/storage/lmdb.js +1 -1
  78. package/dist/storage/memory.js +1 -1
  79. package/dist/storage/neon.js +1 -1
  80. package/dist/storage/resolve.js +1 -1
  81. package/dist/storage/scoped-storage.js +1 -1
  82. package/dist/storage/testing.js +1 -1
  83. package/dist/storage/turso.js +1 -1
  84. package/dist/storage/typed-storage.js +1 -1
  85. package/dist/storage/web-extension.d.ts +1 -1
  86. package/dist/storage/web-extension.js +1 -1
  87. package/dist/testing/index.js +33 -33
  88. package/dist/version.d.ts +1 -1
  89. package/dist/version.js +1 -1
  90. package/dist/worker/execute-with-interceptors.d.ts +9 -3
  91. package/dist/worker/execute-with-interceptors.js +14 -2
  92. package/dist/worker/long-poll.d.ts +2 -1
  93. package/dist/worker/long-poll.js +1 -1
  94. package/dist/worker/protocol-messages.d.ts +2 -0
  95. package/dist/worker/protocol-schemas.d.ts +8 -0
  96. package/dist/worker/protocol-schemas.js +1 -0
  97. package/dist/worker/protocol.js +1 -1
  98. package/dist/worker/remote-activity-context.d.ts +2 -0
  99. package/dist/workers/activity-runner.d.ts +2 -0
  100. package/dist/workers/activity-runner.js +9 -1
  101. package/dist/workers/workflow-runner.d.ts +2 -1
  102. package/dist/workers/workflow-runner.js +1 -0
  103. package/package.json +1 -1
@@ -11,7 +11,7 @@ export function prepareSleepOperation(internals, duration) {
11
11
  console.log(` \u2192 Creating checkpoint at step ${step}`);
12
12
  console.log(` \u2192 Scheduling timer for ${milliseconds}ms`);
13
13
  }
14
- const operationId = crypto.randomUUID(), callerStack = captureCallerStack(), referenceTime = internals.sleepReferenceTime ?? internals.getNow();
14
+ const operationId = `${internals.context.workflowId}:${step}`, callerStack = captureCallerStack(), referenceTime = internals.sleepReferenceTime ?? internals.getNow();
15
15
  internals.sleepReferenceTime = void 0;
16
16
  return {
17
17
  cached: !1,
@@ -23,6 +23,7 @@ export declare function setContextWorkflowInterceptor(context: Context, workflow
23
23
  export declare class Context implements WorkflowContext {
24
24
  #private;
25
25
  readonly workflowId: string;
26
+ readonly workflowExecutionToken: string | undefined;
26
27
  readonly workflowType: string;
27
28
  readonly startedAt: number;
28
29
  readonly signal: AbortSignal;
@@ -21,12 +21,14 @@ export function setContextWorkflowInterceptor(context, workflowInterceptor) {
21
21
 
22
22
  export class Context {
23
23
  workflowId;
24
+ workflowExecutionToken;
24
25
  workflowType;
25
26
  startedAt;
26
27
  signal;
27
28
  #log;
28
29
  constructor(options) {
29
30
  this.workflowId = options.workflowId;
31
+ this.workflowExecutionToken = options.workflowExecutionToken;
30
32
  this.workflowType = options.workflowType;
31
33
  this.startedAt = options.startedAt;
32
34
  this.signal = options.abortController.signal;
@@ -30,6 +30,8 @@ export type ContextOperationRequest = {
30
30
  activityName: string;
31
31
  fn?: (input: unknown, context?: unknown) => unknown;
32
32
  input: unknown;
33
+ /** Immutable owner token for this workflow run, used to fence external side effects. */
34
+ workflowExecutionToken?: string;
33
35
  callerStack?: string;
34
36
  options?: Record<string, unknown>;
35
37
  /** Dispatch attempt used for worker payloads, interceptors, and reconciliation verifiers. */
@@ -74,7 +74,7 @@ function getActivityName(activity, explicitName) {
74
74
  function getActivityFunction(activity) {
75
75
  return typeof activity === "function" ? activity : void 0;
76
76
  }
77
- function createFreshRunActivityRequest(internals, step, activityName, activityFunction, input, options, activityStateKey) {
77
+ function createFreshRunActivityRequest(internals, step, activityName, activityFunction, input, options, activityStateKey, workflowExecutionToken) {
78
78
  const queue = options?.queue ?? "default";
79
79
  if (internals.explainMode) {
80
80
  console.log(`[weft] ctx.run(${activityName}, ${JSON.stringify(input)})`);
@@ -87,6 +87,7 @@ function createFreshRunActivityRequest(internals, step, activityName, activityFu
87
87
  activityName,
88
88
  step,
89
89
  ...activityStateKey === void 0 ? {} : { activityStateKey },
90
+ ...workflowExecutionToken === void 0 ? {} : { workflowExecutionToken },
90
91
  ...activityFunction !== void 0 ? { fn: activityFunction } : {},
91
92
  input,
92
93
  callerStack: captureCallerStack(),
@@ -141,7 +142,7 @@ export function createRunActivityRequestAtStep(context, activity, rest, step, co
141
142
  return cachedRequest;
142
143
  const retryPolicy = resolveActivityRetryPolicy(activity, options), scheduleToCloseTimeout = resolveActivityScheduleToCloseTimeout(activity, options);
143
144
  return {
144
- request: createFreshRunActivityRequest(internals, step, activityName, activityFunction, input, resolveDispatchedActivityOptions(activity, options), configuration.activityStateKey),
145
+ request: createFreshRunActivityRequest(internals, step, activityName, activityFunction, input, resolveDispatchedActivityOptions(activity, options), configuration.activityStateKey, context.workflowExecutionToken),
145
146
  step,
146
147
  retryStateKey,
147
148
  ...cacheResultStep === void 0 ? {} : { cacheResultStep },
@@ -24,6 +24,7 @@ function assignOptionalContextOptions(options, internals) {
24
24
  function createSpeculativeChildOptions(parent, internals) {
25
25
  const options = {
26
26
  workflowId: parent.workflowId,
27
+ ...parent.workflowExecutionToken === void 0 ? {} : { workflowExecutionToken: parent.workflowExecutionToken },
27
28
  workflowType: parent.workflowType,
28
29
  startedAt: parent.startedAt,
29
30
  abortController: internals.abortController,
@@ -150,6 +150,7 @@ export interface StreamSink {
150
150
  */
151
151
  export interface ContextOptions {
152
152
  workflowId: string;
153
+ workflowExecutionToken?: string;
153
154
  workflowType: string;
154
155
  startedAt: number;
155
156
  abortController: AbortController;
@@ -52,4 +52,4 @@ export declare function warnIfRetryMissingHeartbeat(internals: EngineInternals,
52
52
  * tracking above; `completeAsync` is supplied by the caller (it owns the
53
53
  * async-completion token machinery).
54
54
  */
55
- export declare function buildActivityContext(internals: EngineInternals, workflowId: string, step: ActivityHeartbeatKey, signal: AbortSignal, completeAsync: () => never): ActivityContext;
55
+ export declare function buildActivityContext(internals: EngineInternals, workflowId: string, workflowExecutionToken: string | undefined, step: ActivityHeartbeatKey, attempt: number, signal: AbortSignal, completeAsync: () => never): ActivityContext;
@@ -35,9 +35,13 @@ export function warnIfRetryMissingHeartbeat(internals, workflowId, step, attempt
35
35
  return;
36
36
  internals.engine.dispatchEvent(new DevelopmentWarningEvent(workflowId, `Activity retry (attempt ${attempt}) at ${activityHeartbeatKeyLabel(step)} has no lastHeartbeatDetails. Either the previous attempt never recorded heartbeat details (it never called heartbeat(), or called it with no details), or the engine process restarted and discarded the in-memory heartbeat (it is not durable). The resumable-batch pattern only resumes across in-process retries; design the activity to restart cleanly when lastHeartbeatDetails is undefined.`, [activityHeartbeatFieldPath(step)]));
37
37
  }
38
- export function buildActivityContext(internals, workflowId, step, signal, completeAsync) {
38
+ export function buildActivityContext(internals, workflowId, workflowExecutionToken, step, attempt, signal, completeAsync) {
39
39
  return {
40
40
  signal,
41
+ ...workflowExecutionToken !== void 0 && {
42
+ workflowExecutionToken,
43
+ activityAttemptToken: `${workflowExecutionToken}:activity:${String(step)}:${String(attempt)}`
44
+ },
41
45
  lastHeartbeatDetails: readLastHeartbeatForStep(internals, workflowId, step),
42
46
  heartbeat: (details) => {
43
47
  internals.heartbeatDetails.set(workflowId, details);
@@ -94,8 +94,6 @@ function extractAnonymousSignalSequence(key) {
94
94
  return Number.isSafeInteger(sequence) && sequence >= 0 ? sequence : null;
95
95
  }
96
96
  function decodeSignalSequence(bytes) {
97
- if (bytes === null)
98
- return 0;
99
97
  const value = decode(bytes);
100
98
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
101
99
  throw Error("Stored anonymous signal sequence must be a non-negative safe integer");
@@ -62,20 +62,13 @@ async function runBulkCancellation(internals, filter, options = {}) {
62
62
  const workflowIdsToCancel = preparation.workflowIds, bulkConcurrency = resolveBulkOperationConcurrency(options);
63
63
  let cancelled = 0;
64
64
  const errors = [], cancellationResults = await runBulkWorkflowPool(workflowIdsToCancel, bulkConcurrency, async (workflowId) => {
65
- try {
66
- await internals.engine.cancel(workflowId);
67
- if ((await loadWorkflowState(internals, workflowId))?.status === "cancelled")
68
- return { status: "cancelled" };
69
- return {
70
- status: "failed",
71
- error: { id: workflowId, error: "Workflow no longer cancellable" }
72
- };
73
- } catch (error) {
74
- return {
75
- status: "failed",
76
- error: toBulkOperationError(internals, workflowId, error)
77
- };
78
- }
65
+ await internals.engine.cancel(workflowId);
66
+ if ((await loadWorkflowState(internals, workflowId))?.status === "cancelled")
67
+ return { status: "cancelled" };
68
+ return {
69
+ status: "failed",
70
+ error: { id: workflowId, error: "Workflow no longer cancellable" }
71
+ };
79
72
  });
80
73
  for (const cancellationResult of cancellationResults) {
81
74
  if (cancellationResult.status === "rejected") {
@@ -107,27 +100,15 @@ async function runBulkFailedWorkflowRetry(internals, filter, options = {}) {
107
100
  const bulkConcurrency = resolveBulkOperationConcurrency(options);
108
101
  let retried = 0;
109
102
  const errors = [], retryResults = await runBulkWorkflowPool(preparation.workflowIds, bulkConcurrency, async (workflowId) => {
110
- try {
111
- await retryFailedWorkflow(internals, workflowId);
112
- return { status: "retried" };
113
- } catch (error) {
114
- return {
115
- status: "failed",
116
- error: toBulkOperationError(internals, workflowId, error)
117
- };
118
- }
103
+ await retryFailedWorkflow(internals, workflowId);
104
+ return { status: "retried" };
119
105
  });
120
106
  for (const retryResult of retryResults) {
121
107
  if (retryResult.status === "rejected") {
122
108
  errors.push(toBulkOperationError(internals, retryResult.item, retryResult.reason));
123
109
  continue;
124
110
  }
125
- const { value } = retryResult;
126
- if (value.status === "retried") {
127
- retried += 1;
128
- continue;
129
- }
130
- errors.push(value.error);
111
+ retried += 1;
131
112
  }
132
113
  const result = { retried, failed: errors.length, errors };
133
114
  if (!shouldPersistBulkAudit(options))
@@ -122,7 +122,7 @@ export function createTimeOperationCallbacks(engine) {
122
122
  loadWorkflowState: (workflowId) => loadWorkflowState(getInternals(engine), workflowId),
123
123
  failWorkflow: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine)),
124
124
  runSerializedWorkflowStateWrite: (workflowId, writeOperation) => runSerializedWorkflowStateWrite(getInternals(engine), workflowId, writeOperation),
125
- beginWorkflowExecution: (workflowId, workflowType, input, checkpoint, executionDeadline, executionStateOwnerId, registration) => beginWorkflowExecution(getInternals(engine), workflowId, workflowType, input, checkpoint, executionDeadline, executionStateOwnerId, registration, createLifecycleCallbacks(engine)),
125
+ beginWorkflowExecution: (workflowId, workflowExecutionToken, workflowType, input, checkpoint, executionDeadline, executionStateOwnerId, registration) => beginWorkflowExecution(getInternals(engine), workflowId, workflowExecutionToken, workflowType, input, checkpoint, executionDeadline, executionStateOwnerId, registration, createLifecycleCallbacks(engine)),
126
126
  workflowVersionTupleFromState: (state) => workflowVersionTupleFromState(getInternals(engine), state, createLifecycleCallbacks(engine)),
127
127
  setWorkflowStartHeaders: (workflowId, headers) => setWorkflowStartHeaders(getInternals(engine), workflowId, headers, createLifecycleCallbacks(engine)),
128
128
  loadWorkflowStartHeaders: (workflowId) => loadWorkflowStartHeaders(getInternals(engine), workflowId, createLifecycleCallbacks(engine)),
@@ -46,6 +46,7 @@ export function disposeEngine(internals) {
46
46
  internals.pendingWebhooks.clear();
47
47
  internals.sleepResolvers.clear();
48
48
  internals.sleepResolversByWorkflow.clear();
49
+ internals.sleepTimersFiredWithoutResolver.clear();
49
50
  internals.checkpoints.clear();
50
51
  internals.pendingExecutionStateOwnerId = void 0;
51
52
  internals.workflowNestingDepths.clear();
@@ -84,6 +84,7 @@ export type PendingTimelineEntry = {
84
84
  };
85
85
  export type QueuedInlineWorkflowExecutionStart = {
86
86
  workflowId: string;
87
+ workflowExecutionToken?: string;
87
88
  workflowType: string;
88
89
  input: unknown;
89
90
  checkpoint: Checkpoint;
@@ -209,6 +209,16 @@ export { assertCompatiblePersistedDataVersion };
209
209
  export const ENGINE_PARKED_WORKFLOW_COUNT_FOR_TESTING = Symbol("engineParkedWorkflowCountForTesting"), ENGINE_SIGNAL_WAITER_COUNT_FOR_TESTING = Symbol("engineSignalWaiterCountForTesting"), ENGINE_SLEEP_RESOLVER_COUNT_FOR_TESTING = Symbol("engineSleepResolverCountForTesting"), ENGINE_LEASE_SYNCHRONOUS_DISPOSE_WARNING_NAME = "WeftEngineLeaseSynchronousDisposeWarning";
210
210
 
211
211
  export { ENGINE_LEASE_LOST_WARNING_NAME };
212
+ function scheduleDefinitionFromInternals(internals, definition) {
213
+ const workflowType = typeof definition.workflow === "string" ? definition.workflow : definition.workflow.name, definitionSpec = definition.every !== void 0 ? { every: definition.every } : { cron: definition.cron ?? "" };
214
+ return scheduleFromInternals(internals, workflowType, definition.input, definitionSpec, {
215
+ ...definition.id !== void 0 && { id: definition.id },
216
+ ...definition.description !== void 0 && { description: definition.description },
217
+ ...definition.overlapPolicy !== void 0 && { overlap: definition.overlapPolicy },
218
+ ...definition.backfill !== void 0 && { backfill: definition.backfill },
219
+ ...definition.jitter !== void 0 && { jitter: definition.jitter }
220
+ });
221
+ }
212
222
 
213
223
  export class Engine extends EventTarget {
214
224
  static async create(options) {
@@ -267,6 +277,7 @@ export class Engine extends EventTarget {
267
277
  getInternals(this).updateWaitersByWorkflow = new Map;
268
278
  getInternals(this).sleepResolvers = new Map;
269
279
  getInternals(this).sleepResolversByWorkflow = new Map;
280
+ getInternals(this).sleepTimersFiredWithoutResolver = new Map;
270
281
  getInternals(this).interceptors = resolveEngineInterceptors(options);
271
282
  getInternals(this).composedWorkflowInterceptor = void 0;
272
283
  getInternals(this).composedActivityInterceptor = void 0;
@@ -599,15 +610,8 @@ export class Engine extends EventTarget {
599
610
  return untagAllWorkflows(getInternals(this), filter, tags, options);
600
611
  }
601
612
  async schedule(typeOrDefinition, input, spec, options) {
602
- if (typeof typeOrDefinition === "object") {
603
- const definition = typeOrDefinition, workflowType = typeof definition.workflow === "string" ? definition.workflow : definition.workflow.name, definitionSpec = definition.every !== void 0 ? { every: definition.every } : { cron: definition.cron ?? "" };
604
- return scheduleFromInternals(getInternals(this), workflowType, definition.input, definitionSpec, {
605
- ...definition.id !== void 0 && { id: definition.id },
606
- ...definition.overlapPolicy !== void 0 && { overlap: definition.overlapPolicy },
607
- ...definition.backfill !== void 0 && { backfill: definition.backfill },
608
- ...definition.jitter !== void 0 && { jitter: definition.jitter }
609
- });
610
- }
613
+ if (typeof typeOrDefinition === "object")
614
+ return scheduleDefinitionFromInternals(getInternals(this), typeOrDefinition);
611
615
  if (spec === void 0)
612
616
  throw Error("A cron string or schedule spec must be provided when scheduling by workflow type.");
613
617
  return scheduleFromInternals(getInternals(this), typeOrDefinition, input, spec, options);
@@ -58,7 +58,7 @@ async function startQueuedInlineWorkflowExecution(internals, start, callbacks) {
58
58
  return;
59
59
  internals.queuedInlineWorkflowStartIds.delete(start.workflowId);
60
60
  internals.engine.dispatchEvent(new WorkflowStartedEvent(start.workflowId, start.workflowType, start.input));
61
- startWorkflowExecution(internals, start.workflowId, start.workflowType, start.input, start.checkpoint, start.nestingDepth, start.executionDeadline, start.executionStateOwnerId);
61
+ startWorkflowExecution(internals, start.workflowId, state.workflowExecutionToken, start.workflowType, start.input, start.checkpoint, start.nestingDepth, start.executionDeadline, start.executionStateOwnerId);
62
62
  await callbacks.processPendingUpdatesAfterInlineAdvance(start.workflowId);
63
63
  } finally {
64
64
  internals.queuedInlineWorkflowStartIds.delete(start.workflowId);
@@ -90,8 +90,30 @@ export interface EngineInternals {
90
90
  conditionWaiters: Map<string, () => void>;
91
91
  updateWaiters: Map<string, (payload: unknown) => void>;
92
92
  updateWaitersByWorkflow: Map<string, TrackedWaiterKeys>;
93
- sleepResolvers: Map<string, () => void>;
93
+ /**
94
+ * In-process sleep resolvers keyed by `${workflowId}:${operationId}`. Each
95
+ * carries the run's expected `fireAt` so a fired timer only settles the sleep
96
+ * whose deadline it represents — a stale timer from a terminated run that
97
+ * reused the same deterministic operationId (its durable timer outlives
98
+ * terminal cleanup) is ignored rather than resolving a replacement run early.
99
+ */
100
+ sleepResolvers: Map<string, {
101
+ resolve: () => void;
102
+ fireAt: number;
103
+ }>;
94
104
  sleepResolversByWorkflow: Map<string, Set<string>>;
105
+ /**
106
+ * Per-workflow maps of `operationId` → fired timer `fireAt` for sleep timers
107
+ * the scheduler tick fired before the resolver was registered. Keyed by
108
+ * `workflowId` so per-workflow cleanup (`cleanupSleepResolvers`,
109
+ * `evictSleepResolversWithoutResolving`) can call `.delete(workflowId)` and
110
+ * sweep all markers in O(1) — including orphaned ones where the timer fired
111
+ * while the workflow was suspended (no `sleepResolversByWorkflow` entry) or
112
+ * during recovery of an already-elapsed sleep that takes the early-return
113
+ * path. The `fireAt` lets `processSleepOperation` ignore a marker left by a
114
+ * stale earlier-run timer. Cleared entirely at engine disposal.
115
+ */
116
+ sleepTimersFiredWithoutResolver: Map<string, Map<string, number>>;
95
117
  interceptors: Interceptor[];
96
118
  composedWorkflowInterceptor: ComposedWorkflowInterceptor | null | undefined;
97
119
  composedActivityInterceptor: ComposedActivityInterceptor | null | undefined;
@@ -22,6 +22,7 @@ export function createForkedWorkflowState(_internals, workflowId, sourceState, v
22
22
  type: sourceState.type,
23
23
  status: "running",
24
24
  input: sourceState.input,
25
+ workflowExecutionToken: crypto.randomUUID(),
25
26
  versionTuple,
26
27
  executionStateOwnerId: workflowId,
27
28
  createdAt: forkedAt,
@@ -51,6 +51,9 @@ async function relaunchInlineWorkflowAfterResume(internals, latestState, args) {
51
51
  await rehydrateChildCancellationHandlers(internals, workflowId, callbacks);
52
52
  const context = new Context({
53
53
  workflowId,
54
+ ...latestState.workflowExecutionToken !== void 0 && {
55
+ workflowExecutionToken: latestState.workflowExecutionToken
56
+ },
54
57
  workflowType: latestState.type,
55
58
  startedAt: getWorkflowExecutionStartedAt(latestState),
56
59
  abortController: workflowAbort,
@@ -85,6 +88,9 @@ async function relaunchWorkerWorkflowAfterResume(internals, latestState, args) {
85
88
  const serialized = serializeCheckpoint(resumeCheckpoint);
86
89
  internals.strategy.startWorkflow({
87
90
  workflowId,
91
+ ...latestState.workflowExecutionToken !== void 0 && {
92
+ workflowExecutionToken: latestState.workflowExecutionToken
93
+ },
88
94
  workflowType: latestState.type,
89
95
  input: latestState.input,
90
96
  checkpoint: serialized,
@@ -2,8 +2,8 @@ import type { Checkpoint, StartOptions, WorkflowState } from '../../types.ts';
2
2
  import type { EngineInternals } from '../internals.ts';
3
3
  import { type LifecycleCallbacks, type RegistrationEntry } from './shared.ts';
4
4
  export declare function runWorkflowStartInterceptor(_internals: EngineInternals, workflowId: string, workflowType: string, input: unknown, parentHeaders: Map<string, string> | undefined, callbacks: LifecycleCallbacks): Map<string, string> | undefined;
5
- export declare function startWorkflowExecution(internals: EngineInternals, workflowId: string, workflowType: string, input: unknown, checkpoint: Checkpoint, nestingDepth: number, executionDeadline: number | undefined, executionStateOwnerId: string, _callbacks?: LifecycleCallbacks): void;
6
- export declare function beginWorkflowExecution(internals: EngineInternals, workflowId: string, workflowType: string, input: unknown, checkpoint: Checkpoint, executionDeadline: number | undefined, executionStateOwnerId: string, _registration: RegistrationEntry, callbacks: LifecycleCallbacks, onStarted?: () => void): void;
5
+ export declare function startWorkflowExecution(internals: EngineInternals, workflowId: string, workflowExecutionToken: string | undefined, workflowType: string, input: unknown, checkpoint: Checkpoint, nestingDepth: number, executionDeadline: number | undefined, executionStateOwnerId: string, _callbacks?: LifecycleCallbacks): void;
6
+ export declare function beginWorkflowExecution(internals: EngineInternals, workflowId: string, workflowExecutionToken: string | undefined, workflowType: string, input: unknown, checkpoint: Checkpoint, executionDeadline: number | undefined, executionStateOwnerId: string, _registration: RegistrationEntry, callbacks: LifecycleCallbacks, onStarted?: () => void): void;
7
7
  /**
8
8
  * `defer: false` is an inline-only liveness gate: it awaits the moment the
9
9
  * generator is first driven. A worker-mode start queues to the Worker transport
@@ -20,12 +20,13 @@ export function runWorkflowStartInterceptor(_internals, workflowId, workflowType
20
20
  });
21
21
  return capturedHeaders;
22
22
  }
23
- export function startWorkflowExecution(internals, workflowId, workflowType, input, checkpoint, nestingDepth, executionDeadline, executionStateOwnerId, _callbacks) {
23
+ export function startWorkflowExecution(internals, workflowId, workflowExecutionToken, workflowType, input, checkpoint, nestingDepth, executionDeadline, executionStateOwnerId, _callbacks) {
24
24
  if (nestingDepth !== 0)
25
25
  internals.workflowNestingDepths.set(workflowId, nestingDepth);
26
26
  internals.workflowTypeByWorkflowId.set(workflowId, workflowType);
27
27
  internals.strategy.startWorkflow({
28
28
  workflowId,
29
+ ...workflowExecutionToken !== void 0 && { workflowExecutionToken },
29
30
  workflowType,
30
31
  input,
31
32
  checkpoint: serializeCheckpoint(checkpoint),
@@ -39,12 +40,13 @@ export function startWorkflowExecution(internals, workflowId, workflowType, inpu
39
40
  }
40
41
  });
41
42
  }
42
- export function beginWorkflowExecution(internals, workflowId, workflowType, input, checkpoint, executionDeadline, executionStateOwnerId, _registration, callbacks, onStarted) {
43
+ export function beginWorkflowExecution(internals, workflowId, workflowExecutionToken, workflowType, input, checkpoint, executionDeadline, executionStateOwnerId, _registration, callbacks, onStarted) {
43
44
  const nestingDepth = internals.pendingNestingDepth ?? 0;
44
45
  internals.pendingNestingDepth = void 0;
45
46
  if (internals.inlineStrategy !== null) {
46
47
  callbacks.queueInlineWorkflowExecutionStart({
47
48
  workflowId,
49
+ ...workflowExecutionToken !== void 0 && { workflowExecutionToken },
48
50
  workflowType,
49
51
  input,
50
52
  checkpoint,
@@ -56,7 +58,7 @@ export function beginWorkflowExecution(internals, workflowId, workflowType, inpu
56
58
  return;
57
59
  }
58
60
  callbacks.dispatchEvent(new WorkflowStartedEvent(workflowId, workflowType, input));
59
- startWorkflowExecution(internals, workflowId, workflowType, input, checkpoint, nestingDepth, executionDeadline, executionStateOwnerId, callbacks);
61
+ startWorkflowExecution(internals, workflowId, workflowExecutionToken, workflowType, input, checkpoint, nestingDepth, executionDeadline, executionStateOwnerId, callbacks);
60
62
  onStarted?.();
61
63
  }
62
64
  export function assertDeferSupported(internals, options, isDelayedStart) {
@@ -71,7 +73,7 @@ export async function beginExecutionAwaitingLiveness(internals, params, workflow
71
73
  if (params.isDelayed)
72
74
  return;
73
75
  const liveness = params.options?.defer === !1 ? Promise.withResolvers() : void 0;
74
- beginWorkflowExecution(internals, workflowId, params.type, params.input, params.checkpoint, params.state.executionDeadline, params.state.executionStateOwnerId ?? workflowId, params.registration, callbacks, liveness ? () => liveness.resolve() : void 0);
76
+ beginWorkflowExecution(internals, workflowId, params.state.workflowExecutionToken, params.type, params.input, params.checkpoint, params.state.executionDeadline, params.state.executionStateOwnerId ?? workflowId, params.registration, callbacks, liveness ? () => liveness.resolve() : void 0);
75
77
  if (liveness)
76
78
  await liveness.promise;
77
79
  }
@@ -145,6 +145,7 @@ function buildInitialIdentitySlice(workflowId, type, input, versionTuple, execut
145
145
  status: delayedStartTimer ? "pending" : "running",
146
146
  input,
147
147
  versionTuple,
148
+ workflowExecutionToken: crypto.randomUUID(),
148
149
  ...executionStateOwnerId !== void 0 && { executionStateOwnerId },
149
150
  createdAt: now,
150
151
  ...!delayedStartTimer && { startedAt: now },
@@ -144,6 +144,9 @@ function launchInlineWorkflowFromCheckpoint(internals, workflowId, state, checkp
144
144
  resetCancelHandlers(internals, workflowId);
145
145
  const context = new Context({
146
146
  workflowId,
147
+ ...state.workflowExecutionToken !== void 0 && {
148
+ workflowExecutionToken: state.workflowExecutionToken
149
+ },
147
150
  workflowType: state.type,
148
151
  startedAt: getWorkflowExecutionStartedAt(state),
149
152
  abortController: workflowAbort,
@@ -172,6 +175,9 @@ function launchWorkerWorkflowFromCheckpoint(internals, workflowId, state, checkp
172
175
  const serialized = serializeCheckpoint(checkpoint);
173
176
  internals.strategy.startWorkflow({
174
177
  workflowId,
178
+ ...state.workflowExecutionToken !== void 0 && {
179
+ workflowExecutionToken: state.workflowExecutionToken
180
+ },
175
181
  workflowType: state.type,
176
182
  input: state.input,
177
183
  checkpoint: serialized,
@@ -149,7 +149,7 @@ class MemoDurableActivityScope {
149
149
  finishTimerWrite();
150
150
  }
151
151
  this.#throwIfClosed();
152
- registerSleepResolver(this.#internals, this.#workflowId, operation.operationId, resolve);
152
+ registerSleepResolver(this.#internals, this.#workflowId, operation.operationId, resolve, operation.scheduledFireAt);
153
153
  await promise;
154
154
  }
155
155
  #activityOperationId(ordinal, attempt) {
@@ -25,7 +25,7 @@ export type ActivityOperationCallbacks = {
25
25
  };
26
26
  export declare function buildActivityVerification(_internals: EngineInternals, activityName: string, verify: ActivityFunctionWithMetadata['verify'], context: ReturnType<typeof buildActivityVerificationContext>, result: unknown): Promise<void>;
27
27
  export declare function buildActivityCompensation(internals: EngineInternals, workflowId: string, operation: ActivityOperation, result: unknown): (() => Promise<void>) | undefined;
28
- export declare function invokeWorkerActivity(internals: EngineInternals, operationId: string, activityName: string, input: unknown, attempt: number): Promise<unknown>;
28
+ export declare function invokeWorkerActivity(internals: EngineInternals, operationId: string, activityName: string, input: unknown, attempt: number, workflowExecutionToken: string | undefined, activityAttemptToken: string | undefined): Promise<unknown>;
29
29
  export declare function invokeInlineActivity(internals: EngineInternals, workflowId: string, operation: ActivityOperation, activityContext: ActivityContext, _activityName: string, input: unknown): unknown;
30
30
  /**
31
31
  * Execute an activity function, dispatching to a Web Worker pool when
@@ -40,7 +40,7 @@ export function buildActivityCompensation(internals, workflowId, operation, resu
40
40
  await activity.compensate?.(operation.input, result);
41
41
  };
42
42
  }
43
- export async function invokeWorkerActivity(internals, operationId, activityName, input, attempt) {
43
+ export async function invokeWorkerActivity(internals, operationId, activityName, input, attempt, workflowExecutionToken, activityAttemptToken) {
44
44
  const dispatcher = internals.activityWorkerDispatcher;
45
45
  if (!dispatcher)
46
46
  throw Error(`No activity worker dispatcher available for "${activityName}"`);
@@ -48,7 +48,9 @@ export async function invokeWorkerActivity(internals, operationId, activityName,
48
48
  operationId,
49
49
  activityName,
50
50
  input,
51
- attempt
51
+ attempt,
52
+ ...workflowExecutionToken !== void 0 && { workflowExecutionToken },
53
+ ...activityAttemptToken !== void 0 && { activityAttemptToken }
52
54
  });
53
55
  if (result.status === "failed") {
54
56
  const error = Error(result.error);
@@ -74,11 +76,11 @@ function getActivityStateKey(operation) {
74
76
  return operation.activityStateKey ?? operation.step ?? 0;
75
77
  }
76
78
  export async function executeActivity(internals, workflowId, operation, callbacks, attempt = getActivityAttempt(operation), coordinatorSignal) {
77
- const activityInput = operation.input, activityStateKey = getActivityStateKey(operation);
79
+ const activityInput = operation.input, activityStateKey = getActivityStateKey(operation), workflowExecutionToken = operation.workflowExecutionToken;
78
80
  warnIfRetryMissingHeartbeat(internals, workflowId, activityStateKey, attempt);
79
- const asyncToken = deriveAsyncActivityToken(workflowId, activityStateKey, attempt), { perAttemptTimeoutMs, attemptAbortController, activitySignal } = resolvePerAttemptTimeout(internals, workflowId, operation, coordinatorSignal), activityContext = buildActivityContext(internals, workflowId, activityStateKey, activitySignal, () => {
81
+ const asyncToken = deriveAsyncActivityToken(workflowId, activityStateKey, attempt), { perAttemptTimeoutMs, attemptAbortController, activitySignal } = resolvePerAttemptTimeout(internals, workflowId, operation, coordinatorSignal), activityContext = buildActivityContext(internals, workflowId, workflowExecutionToken, activityStateKey, attempt, activitySignal, () => {
80
82
  throw new AsyncActivityDeferral(asyncToken);
81
- }), invokeActivity = internals.activityWorkerDispatcher ? (activityName, input) => invokeWorkerActivity(internals, operation.operationId, activityName, input, attempt) : (activityName, input) => withPerAttemptTimeout(invokeInlineActivity(internals, workflowId, operation, activityContext, activityName, input), perAttemptTimeoutMs, activityName, attempt, attemptAbortController), composedActivity = callbacks.getComposedActivityInterceptor(), executeWithActivityInterceptors = async (activityName, input, headers) => {
83
+ }), activityAttemptToken = activityContext.activityAttemptToken, invokeActivity = internals.activityWorkerDispatcher ? (activityName, input) => invokeWorkerActivity(internals, operation.operationId, activityName, input, attempt, workflowExecutionToken, activityAttemptToken) : (activityName, input) => withPerAttemptTimeout(invokeInlineActivity(internals, workflowId, operation, activityContext, activityName, input), perAttemptTimeoutMs, activityName, attempt, attemptAbortController), composedActivity = callbacks.getComposedActivityInterceptor(), executeWithActivityInterceptors = async (activityName, input, headers) => {
82
84
  if (!composedActivity)
83
85
  return invokeActivity(activityName, input);
84
86
  const activityInterception = {
@@ -12,7 +12,7 @@ export type TimeOperationCallbacks = {
12
12
  loadWorkflowState: (workflowId: string) => Promise<WorkflowState | null>;
13
13
  failWorkflow: (workflowId: string, error: Error) => Promise<void>;
14
14
  runSerializedWorkflowStateWrite: <Result>(workflowId: string, writeOperation: () => Promise<Result>) => Promise<Result>;
15
- beginWorkflowExecution: (workflowId: string, workflowType: string, input: unknown, checkpoint: Checkpoint, executionDeadline: number | undefined, executionStateOwnerId: string, registration: RegistrationEntry) => void;
15
+ beginWorkflowExecution: (workflowId: string, workflowExecutionToken: string | undefined, workflowType: string, input: unknown, checkpoint: Checkpoint, executionDeadline: number | undefined, executionStateOwnerId: string, registration: RegistrationEntry) => void;
16
16
  workflowVersionTupleFromState: (state: WorkflowState) => WorkflowVersionTuple;
17
17
  setWorkflowStartHeaders: (workflowId: string, headers: Map<string, string> | undefined) => void;
18
18
  loadWorkflowStartHeaders: (workflowId: string) => Promise<Map<string, string> | undefined>;
@@ -25,7 +25,7 @@ export type TimeOperationCallbacks = {
25
25
  };
26
26
  export declare function createDelayedStartTimerEntry(_internals: EngineInternals, workflowId: string, scheduledStartAt: number, options: StartOptions | undefined, callbacks: Pick<TimeOperationCallbacks, 'parseStartOptionDuration'>): TimerEntry;
27
27
  export declare function processSleepOperation(internals: EngineInternals, workflowId: string, operation: SleepOperation, callbacks: Pick<TimeOperationCallbacks, 'completeOperation' | 'loadWorkflowState'>): Promise<void>;
28
- export declare function registerSleepResolver(internals: EngineInternals, workflowId: string, operationId: string, resolve: () => void): void;
28
+ export declare function registerSleepResolver(internals: EngineInternals, workflowId: string, operationId: string, resolve: () => void, scheduledFireAt: number): void;
29
29
  export declare function startDelayedWorkflow(internals: EngineInternals, entry: TimerEntry, callbacks: Pick<TimeOperationCallbacks, 'beginWorkflowExecution' | 'dispatchEvent' | 'failWorkflow' | 'handleCleanupError' | 'loadWorkflowStartHeaders' | 'loadWorkflowState' | 'runSerializedWorkflowStateWrite' | 'setWorkflowStartHeaders' | 'workflowVersionTupleFromState'>): Promise<void>;
30
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
31
  export {};
@@ -16,6 +16,18 @@ export function createDelayedStartTimerEntry(_internals, workflowId, scheduledSt
16
16
  }
17
17
  };
18
18
  }
19
+ function sleepTimerFiredEarly(internals, workflowId, operation) {
20
+ const workflowMarkers = internals.sleepTimersFiredWithoutResolver.get(workflowId);
21
+ if (!workflowMarkers)
22
+ return !1;
23
+ const markedFireAt = workflowMarkers.get(operation.operationId);
24
+ if (markedFireAt === void 0)
25
+ return !1;
26
+ workflowMarkers.delete(operation.operationId);
27
+ if (workflowMarkers.size === 0)
28
+ internals.sleepTimersFiredWithoutResolver.delete(workflowId);
29
+ return markedFireAt >= operation.scheduledFireAt;
30
+ }
19
31
  export async function processSleepOperation(internals, workflowId, operation, callbacks) {
20
32
  if (operation.scheduledFireAt <= internals.options.getNow()) {
21
33
  callbacks.completeOperation(workflowId, void 0);
@@ -28,13 +40,24 @@ export async function processSleepOperation(internals, workflowId, operation, ca
28
40
  fireAt: operation.scheduledFireAt,
29
41
  kind: "sleep"
30
42
  });
31
- registerSleepResolver(internals, workflowId, operation.operationId, resolve);
43
+ registerSleepResolver(internals, workflowId, operation.operationId, resolve, operation.scheduledFireAt);
44
+ const resolverKey = `${workflowId}:${operation.operationId}`;
45
+ if (sleepTimerFiredEarly(internals, workflowId, operation) && internals.sleepResolvers.has(resolverKey))
46
+ resolveSleepTimer(internals, {
47
+ id: `sleep:${operation.operationId}`,
48
+ workflowId,
49
+ fireAt: operation.scheduledFireAt,
50
+ kind: "sleep"
51
+ });
32
52
  await promise;
33
53
  if ((await callbacks.loadWorkflowState(workflowId))?.status === "running")
34
54
  callbacks.completeOperation(workflowId, void 0);
35
55
  }
36
- export function registerSleepResolver(internals, workflowId, operationId, resolve) {
37
- internals.sleepResolvers.set(`${workflowId}:${operationId}`, resolve);
56
+ export function registerSleepResolver(internals, workflowId, operationId, resolve, scheduledFireAt) {
57
+ internals.sleepResolvers.set(`${workflowId}:${operationId}`, {
58
+ resolve,
59
+ fireAt: scheduledFireAt
60
+ });
38
61
  let workflowOperations = internals.sleepResolversByWorkflow.get(workflowId);
39
62
  if (!workflowOperations) {
40
63
  workflowOperations = new Set;
@@ -94,7 +117,7 @@ export async function startDelayedWorkflow(internals, entry, callbacks) {
94
117
  internals.checkpoints.set(entry.workflowId, checkpoint);
95
118
  internals.workflowVersionTuples.set(entry.workflowId, callbacks.workflowVersionTupleFromState(runningState));
96
119
  callbacks.setWorkflowStartHeaders(entry.workflowId, await callbacks.loadWorkflowStartHeaders(entry.workflowId));
97
- callbacks.beginWorkflowExecution(entry.workflowId, runningState.type, runningState.input, checkpoint, executionDeadline, runningState.executionStateOwnerId ?? entry.workflowId, registration);
120
+ callbacks.beginWorkflowExecution(entry.workflowId, runningState.workflowExecutionToken, runningState.type, runningState.input, checkpoint, executionDeadline, runningState.executionStateOwnerId ?? entry.workflowId, registration);
98
121
  }
99
122
  async function loadDelayedWorkflowCheckpoint(internals, entry, callbacks) {
100
123
  const checkpointBytes = await internals.storage.get(KEYS.checkpoint(entry.workflowId));
@@ -168,11 +191,22 @@ async function handleReviewTimer(internals, entry, callbacks) {
168
191
  }
169
192
  function resolveSleepTimer(internals, entry) {
170
193
  const operationId = entry.id.replace("sleep:", ""), resolverKey = `${entry.workflowId}:${operationId}`, resolver = internals.sleepResolvers.get(resolverKey);
171
- if (!resolver)
194
+ if (!resolver) {
195
+ let workflowMarkers = internals.sleepTimersFiredWithoutResolver.get(entry.workflowId);
196
+ if (!workflowMarkers) {
197
+ workflowMarkers = new Map;
198
+ internals.sleepTimersFiredWithoutResolver.set(entry.workflowId, workflowMarkers);
199
+ }
200
+ const existing = workflowMarkers.get(operationId);
201
+ if (existing === void 0 || entry.fireAt > existing)
202
+ workflowMarkers.set(operationId, entry.fireAt);
203
+ return;
204
+ }
205
+ if (entry.fireAt < resolver.fireAt)
172
206
  return;
173
207
  internals.sleepResolvers.delete(resolverKey);
174
208
  untrackSleepResolver(internals, entry.workflowId, operationId);
175
- resolver();
209
+ resolver.resolve();
176
210
  }
177
211
  function untrackSleepResolver(internals, workflowId, operationId) {
178
212
  const workflowOperations = internals.sleepResolversByWorkflow.get(workflowId);
@@ -35,6 +35,9 @@ export async function schedule(internals, type, input, spec, options) {
35
35
  id: scheduleId,
36
36
  workflowType: type,
37
37
  input,
38
+ ...normalizedOptions.description !== void 0 && {
39
+ description: normalizedOptions.description
40
+ },
38
41
  ...cadenceFields,
39
42
  status: "active",
40
43
  overlap: normalizedOptions.overlap,