@lostgradient/weft 0.7.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 (141) hide show
  1. package/README.md +9 -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/activity-retry-state.d.ts +6 -5
  5. package/dist/core/context/activity-retry-state.js +31 -21
  6. package/dist/core/context/durable-activity.d.ts +117 -0
  7. package/dist/core/context/durable-activity.js +79 -0
  8. package/dist/core/context/durable-operations.js +1 -1
  9. package/dist/core/context/index.d.ts +1 -0
  10. package/dist/core/context/index.js +2 -0
  11. package/dist/core/context/operation-request.d.ts +17 -0
  12. package/dist/core/context/parallel-operations.js +1 -0
  13. package/dist/core/context/run-operation-cached-request.d.ts +8 -0
  14. package/dist/core/context/run-operation-cached-request.js +59 -0
  15. package/dist/core/context/run-operation.d.ts +23 -5
  16. package/dist/core/context/run-operation.js +63 -47
  17. package/dist/core/context/speculative-child.js +1 -0
  18. package/dist/core/context/types.d.ts +1 -0
  19. package/dist/core/engine/activity-heartbeat-tracking.d.ts +8 -17
  20. package/dist/core/engine/activity-heartbeat-tracking.js +12 -2
  21. package/dist/core/engine/activity-reconciliation.d.ts +1 -0
  22. package/dist/core/engine/activity-reconciliation.js +7 -2
  23. package/dist/core/engine/anonymous-signal-sequence.js +6 -6
  24. package/dist/core/engine/async-activity-completion.d.ts +5 -7
  25. package/dist/core/engine/async-activity-completion.js +13 -9
  26. package/dist/core/engine/bulk-operations-purge.js +2 -1
  27. package/dist/core/engine/bulk-operations.js +18 -36
  28. package/dist/core/engine/callback-checkpoint-persistence.d.ts +3 -0
  29. package/dist/core/engine/callback-checkpoint-persistence.js +25 -0
  30. package/dist/core/engine/callback-creators-bundles.js +5 -2
  31. package/dist/core/engine/checkpoint-io.d.ts +4 -1
  32. package/dist/core/engine/checkpoint-io.js +19 -10
  33. package/dist/core/engine/completed-review-storage.d.ts +2 -1
  34. package/dist/core/engine/completed-review-storage.js +4 -3
  35. package/dist/core/engine/disposal.js +1 -0
  36. package/dist/core/engine/engine-internal-types.d.ts +1 -0
  37. package/dist/core/engine/index.d.ts +31 -0
  38. package/dist/core/engine/index.js +19 -10
  39. package/dist/core/engine/inline-launch-queue.js +1 -1
  40. package/dist/core/engine/internals.d.ts +25 -2
  41. package/dist/core/engine/lease-manager.js +2 -2
  42. package/dist/core/engine/lifecycle/fork-helpers.js +1 -0
  43. package/dist/core/engine/lifecycle/resume.js +6 -0
  44. package/dist/core/engine/lifecycle/start-exec.d.ts +2 -2
  45. package/dist/core/engine/lifecycle/start-exec.js +6 -4
  46. package/dist/core/engine/lifecycle/start.js +1 -0
  47. package/dist/core/engine/lifecycle/transition.js +6 -0
  48. package/dist/core/engine/memo-durable-activity.d.ts +11 -0
  49. package/dist/core/engine/memo-durable-activity.js +282 -0
  50. package/dist/core/engine/operations-activity.d.ts +6 -2
  51. package/dist/core/engine/operations-activity.js +24 -10
  52. package/dist/core/engine/operations-data.d.ts +4 -1
  53. package/dist/core/engine/operations-data.js +3 -3
  54. package/dist/core/engine/operations-time.d.ts +2 -2
  55. package/dist/core/engine/operations-time.js +40 -6
  56. package/dist/core/engine/reviews.js +7 -4
  57. package/dist/core/engine/schedule-timer.js +2 -0
  58. package/dist/core/engine/schedules.js +3 -0
  59. package/dist/core/engine/storage-io.js +1 -1
  60. package/dist/core/engine/termination/cleanup.js +15 -13
  61. package/dist/core/engine/termination/finalizer-activity.d.ts +1 -1
  62. package/dist/core/engine/termination/finalizer-activity.js +5 -1
  63. package/dist/core/engine/termination/finalizer.js +1 -1
  64. package/dist/core/engine/validation/schedule-options.d.ts +9 -0
  65. package/dist/core/engine/validation/schedule-options.js +59 -0
  66. package/dist/core/engine/validation/schedule.d.ts +3 -8
  67. package/dist/core/engine/validation/schedule.js +40 -56
  68. package/dist/core/engine/validation.js +1 -0
  69. package/dist/core/execution-strategy.d.ts +1 -0
  70. package/dist/core/inline-execution-strategy.context-options.d.ts +1 -0
  71. package/dist/core/inline-execution-strategy.context-options.js +3 -0
  72. package/dist/core/json.js +1 -1
  73. package/dist/core/types/activity.d.ts +11 -0
  74. package/dist/core/types/checkpoint.d.ts +1 -0
  75. package/dist/core/types/schedules.d.ts +10 -0
  76. package/dist/core/types/state.d.ts +6 -0
  77. package/dist/core/types/workflow-context.d.ts +13 -0
  78. package/dist/core/weft-error.d.ts +1 -1
  79. package/dist/core/weft-error.js +2 -0
  80. package/dist/core/worker-execution-strategy.d.ts +1 -0
  81. package/dist/core/worker-inbound-message.d.ts +1 -0
  82. package/dist/core/worker-inbound-message.js +3 -0
  83. package/dist/index.d.ts +2 -1
  84. package/dist/index.js +6 -0
  85. package/dist/json-schema.js +2 -2
  86. package/dist/mcp/cli.js +32 -32
  87. package/dist/observability/index.js +2 -2
  88. package/dist/runtime/portable.d.ts +1 -1
  89. package/dist/server/handler.js +27 -27
  90. package/dist/server/index.d.ts +2 -0
  91. package/dist/server/index.js +32 -32
  92. package/dist/server/operations/create-schedule.d.ts +2 -0
  93. package/dist/server/operations/create-schedule.js +13 -2
  94. package/dist/server/rest-binding.d.ts +1 -1
  95. package/dist/server/runtime/task-dispatch.js +6 -0
  96. package/dist/server/runtime/task-polling.js +9 -1
  97. package/dist/server/runtime/task-reconciliation.js +2 -1
  98. package/dist/server/runtime/websocket-worker.js +7 -2
  99. package/dist/server/serve-internals.d.ts +28 -0
  100. package/dist/server/serve-internals.js +4 -2
  101. package/dist/server/task-queue-types.d.ts +2 -0
  102. package/dist/server/task-state.d.ts +4 -0
  103. package/dist/service-worker/index.d.ts +15 -14
  104. package/dist/service-worker/index.js +28 -28
  105. package/dist/service-worker/scheduler.d.ts +41 -1
  106. package/dist/service-worker/setup.d.ts +18 -1
  107. package/dist/service-worker/setup.js +7 -4
  108. package/dist/storage/auto.d.ts +36 -18
  109. package/dist/storage/auto.js +1 -1
  110. package/dist/storage/compressed-storage.js +1 -1
  111. package/dist/storage/indexeddb.d.ts +6 -1
  112. package/dist/storage/indexeddb.js +1 -1
  113. package/dist/storage/interface.js +1 -1
  114. package/dist/storage/lmdb.js +1 -1
  115. package/dist/storage/memory.js +1 -1
  116. package/dist/storage/neon.js +1 -1
  117. package/dist/storage/resolve.js +1 -1
  118. package/dist/storage/scoped-storage.js +1 -1
  119. package/dist/storage/testing.js +1 -1
  120. package/dist/storage/turso.js +1 -1
  121. package/dist/storage/typed-storage.d.ts +1 -1
  122. package/dist/storage/typed-storage.js +1 -1
  123. package/dist/storage/web-extension.d.ts +1 -1
  124. package/dist/storage/web-extension.js +1 -1
  125. package/dist/testing/index.js +32 -32
  126. package/dist/version.d.ts +1 -1
  127. package/dist/version.js +1 -1
  128. package/dist/worker/execute-with-interceptors.d.ts +9 -3
  129. package/dist/worker/execute-with-interceptors.js +14 -2
  130. package/dist/worker/long-poll.d.ts +2 -1
  131. package/dist/worker/long-poll.js +1 -1
  132. package/dist/worker/protocol-messages.d.ts +2 -0
  133. package/dist/worker/protocol-schemas.d.ts +8 -0
  134. package/dist/worker/protocol-schemas.js +1 -0
  135. package/dist/worker/protocol.js +1 -1
  136. package/dist/worker/remote-activity-context.d.ts +2 -0
  137. package/dist/workers/activity-runner.d.ts +2 -0
  138. package/dist/workers/activity-runner.js +9 -1
  139. package/dist/workers/workflow-runner.d.ts +2 -1
  140. package/dist/workers/workflow-runner.js +1 -0
  141. package/package.json +1 -1
@@ -6,6 +6,9 @@ type PendingTimelineEntryValue = {
6
6
  startedAt: number;
7
7
  entry: WorkflowTimelineEntry;
8
8
  };
9
+ export type PersistCheckpointOptions = {
10
+ timeline?: 'record-operation' | 'preserve-pending';
11
+ };
9
12
  type PersistCheckpointCallbacks = {
10
13
  appendTimelineBatchOperations: (workflowId: string, operation: ContextOperationRequest, step: number, timestamp: number, operations: BatchOperation[]) => PendingTimelineEntryValue;
11
14
  swallowPromiseRejection: (promise: Promise<void>) => void;
@@ -26,7 +29,7 @@ type DevelopmentCheckpointCallbacks = {
26
29
  };
27
30
  export declare function appendTimelineBatchOperations(internals: EngineInternals, workflowId: string, operation: ContextOperationRequest, step: number, timestamp: number, operations: BatchOperation[]): PendingTimelineEntryValue;
28
31
  /** Persist a workflow checkpoint, history entry, timeline record, and event log record. */
29
- export declare function persistCheckpoint(internals: EngineInternals, workflowId: string, operation: ContextOperationRequest, workerCheckpointBytes: ArrayBuffer | undefined, callbacks: PersistCheckpointCallbacks): Promise<void>;
32
+ export declare function persistCheckpoint(internals: EngineInternals, workflowId: string, operation: ContextOperationRequest, workerCheckpointBytes: ArrayBuffer | undefined, callbacks: PersistCheckpointCallbacks, options?: PersistCheckpointOptions): Promise<void>;
30
33
  /** Delete the single checkpoint history entry that overflows the retention limit. */
31
34
  export declare function pruneCheckpointHistory(internals: EngineInternals, workflowId: string, currentStep: number): Promise<void>;
32
35
  /** Validate checkpoint serialization in development mode and dispatch warnings. */
@@ -60,13 +60,13 @@ export function appendTimelineBatchOperations(internals, workflowId, operation,
60
60
  entry
61
61
  };
62
62
  }
63
- export async function persistCheckpoint(internals, workflowId, operation, workerCheckpointBytes, callbacks) {
63
+ export async function persistCheckpoint(internals, workflowId, operation, workerCheckpointBytes, callbacks, options = {}) {
64
64
  if (internals.inlineStrategy?.getContext(workflowId))
65
- await persistInlineCheckpoint(internals, workflowId, operation, callbacks);
65
+ await persistInlineCheckpoint(internals, workflowId, operation, callbacks, options);
66
66
  else if (workerCheckpointBytes && workerCheckpointBytes.byteLength > 0)
67
- await persistWorkerCheckpoint(internals, workflowId, operation, workerCheckpointBytes, callbacks);
67
+ await persistWorkerCheckpoint(internals, workflowId, operation, workerCheckpointBytes, callbacks, options);
68
68
  }
69
- async function persistInlineCheckpoint(internals, workflowId, operation, callbacks) {
69
+ async function persistInlineCheckpoint(internals, workflowId, operation, callbacks, options) {
70
70
  const current = internals.checkpoints.get(workflowId), context = internals.inlineStrategy?.getContext(workflowId);
71
71
  if (!current || !context)
72
72
  return;
@@ -76,13 +76,13 @@ async function persistInlineCheckpoint(internals, workflowId, operation, callbac
76
76
  ...pendingAttributeChanges !== void 0 ? { searchAttributes: pendingAttributeChanges } : {}
77
77
  }), pruned = pruneCheckpointReplayState(advanced, resolvePendingOperationStep(operation, context.stepIndex)), commit = createCheckpointCommit(internals, workflowId, pruned.checkpoint, serializeCheckpoint(attachTransientCheckpointReplayPayload(pruned.checkpoint, pruned.replayPayload)), pruned.replayPayload);
78
78
  appendAttributeOperations(workflowId, commit, previousAttributes, pendingAttributeChanges, hasPendingAttributeChangesValue, callbacks);
79
- await commitCheckpoint(internals, workflowId, operation, commit, callbacks);
79
+ await commitCheckpoint(internals, workflowId, operation, commit, callbacks, options);
80
80
  if (hasPendingAttributeChangesValue)
81
81
  callbacks.dispatchEvent(new AttributesChangedEvent(workflowId, { ...pendingAttributeChanges }));
82
82
  }
83
- async function persistWorkerCheckpoint(internals, workflowId, operation, workerCheckpointBytes, callbacks) {
83
+ async function persistWorkerCheckpoint(internals, workflowId, operation, workerCheckpointBytes, callbacks, options) {
84
84
  const serialized = new Uint8Array(workerCheckpointBytes), checkpoint = deserializeCheckpoint(serialized), workerReplayPayload = readCheckpointReplayPayload(checkpoint), pruned = pruneCheckpointReplayState(checkpoint, resolvePendingOperationStep(operation, checkpoint.step)), replayPayload = mergeCheckpointReplayPayloads(workerReplayPayload, pruned.replayPayload), prunedSerialized = serializeCheckpoint(attachTransientCheckpointReplayPayload(pruned.checkpoint, replayPayload));
85
- await commitCheckpoint(internals, workflowId, operation, createCheckpointCommit(internals, workflowId, pruned.checkpoint, prunedSerialized, replayPayload), callbacks);
85
+ await commitCheckpoint(internals, workflowId, operation, createCheckpointCommit(internals, workflowId, pruned.checkpoint, prunedSerialized, replayPayload), callbacks, options);
86
86
  }
87
87
  function createCheckpointCommit(internals, workflowId, checkpoint, serialized, replayPayload) {
88
88
  const operations = [
@@ -116,9 +116,9 @@ function appendAttributeOperations(workflowId, commit, previousAttributes, pendi
116
116
  });
117
117
  commit.operations.push(...buildIndexOperations(workflowId, previousAttributes, commit.checkpoint.searchAttributes));
118
118
  }
119
- async function commitCheckpoint(internals, workflowId, operation, commit, callbacks) {
119
+ async function commitCheckpoint(internals, workflowId, operation, commit, callbacks, options) {
120
120
  dispatchCheckpointSizeWarning(internals, workflowId, commit, callbacks);
121
- const nextPendingTimelineEntry = callbacks.appendTimelineBatchOperations(workflowId, operation, commit.step, commit.timestamp, commit.operations), { newHead, timestamp } = appendCheckpointEventLog(internals, workflowId, commit), retentionWindow = internals.options.historyPolicy.retentionWindow, compaction = retentionWindow === null ? null : await appendCompactionOperations(internals.storage, workflowId, newHead.sequence, retentionWindow, commit.operations), pendingSideEffects = takePendingAtomicWorkflowCommitSideEffects(internals, workflowId);
121
+ const nextPendingTimelineEntry = options.timeline === "preserve-pending" ? preservePendingTimelineEntry(internals, workflowId, commit.operations) : callbacks.appendTimelineBatchOperations(workflowId, operation, commit.step, commit.timestamp, commit.operations), { newHead, timestamp } = appendCheckpointEventLog(internals, workflowId, commit), retentionWindow = internals.options.historyPolicy.retentionWindow, compaction = retentionWindow === null ? null : await appendCompactionOperations(internals.storage, workflowId, newHead.sequence, retentionWindow, commit.operations), pendingSideEffects = takePendingAtomicWorkflowCommitSideEffects(internals, workflowId);
122
122
  if (pendingSideEffects !== void 0)
123
123
  commit.operations.push(...pendingSideEffects.operations);
124
124
  const storageSupportsConditionalBatch = internals.storage.capabilities().conditionalBatch, sideEffectConditions = checkpointSideEffectConditions(pendingSideEffects, storageSupportsConditionalBatch), conditions = buildCheckpointCommitConditions(workflowId, commit, storageSupportsConditionalBatch, sideEffectConditions);
@@ -129,7 +129,10 @@ async function commitCheckpoint(internals, workflowId, operation, commit, callba
129
129
  clearPendingAtomicWorkflowCommitSideEffects(internals, workflowId);
130
130
  if (commit.expectedSerialized !== void 0)
131
131
  rememberCommittedCheckpointBytes(internals, workflowId, commit.serialized);
132
- internals.pendingTimelineEntries.set(workflowId, nextPendingTimelineEntry);
132
+ if (nextPendingTimelineEntry === void 0)
133
+ internals.pendingTimelineEntries.delete(workflowId);
134
+ else
135
+ internals.pendingTimelineEntries.set(workflowId, nextPendingTimelineEntry);
133
136
  internals.checkpoints.set(workflowId, commit.checkpoint);
134
137
  internals.eventLogHeads.set(workflowId, newHead);
135
138
  if (compaction !== null)
@@ -146,6 +149,12 @@ async function commitCheckpoint(internals, workflowId, operation, commit, callba
146
149
  if (historyEventLimitBreached(internals, newHead.sequence))
147
150
  await callbacks.enforceHistoryCircuitBreaker(workflowId);
148
151
  }
152
+ function preservePendingTimelineEntry(internals, workflowId, operations) {
153
+ const pendingTimelineOperation = buildPendingTimelineOperation(internals, workflowId);
154
+ if (pendingTimelineOperation)
155
+ operations.push(pendingTimelineOperation);
156
+ return internals.pendingTimelineEntries.get(workflowId);
157
+ }
149
158
  function checkpointSideEffectConditions(pendingSideEffects, storageSupportsConditionalBatch) {
150
159
  if (!storageSupportsConditionalBatch)
151
160
  return [];
@@ -1,6 +1,7 @@
1
1
  import type { Storage } from '../../storage/interface.ts';
2
2
  import type { HumanReviewResult, ReviewRequest } from '../review/index.ts';
3
3
  import type { CompletedReviewEntry, ReviewListFilter } from '../types.ts';
4
+ import type { EngineInternals } from './internals.ts';
4
5
  type ReviewListFilterableEntry = {
5
6
  workflowId?: string;
6
7
  reviewType?: string;
@@ -9,6 +10,6 @@ export declare function matchesReviewListFilter(review: ReviewListFilterableEntr
9
10
  export declare function completedReviewStoragePrefix(workflowId?: string): string;
10
11
  export declare function completedReviewStorageKey(workflowId: string, reviewId: string): string;
11
12
  export declare function listCompletedReviewsFromStorage(storage: Storage, filter: ReviewListFilter): Promise<CompletedReviewEntry[]>;
12
- export declare function persistCompletedReviewRecord(storage: Storage, reviewKey: string, reviewData: ReviewRequest, decisionResult: HumanReviewResult): Promise<void>;
13
+ export declare function persistCompletedReviewRecord(internals: EngineInternals, reviewKey: string, reviewData: ReviewRequest, decisionResult: HumanReviewResult): Promise<void>;
13
14
  export declare function deleteCompletedReviewsForWorkflow(storage: Storage, workflowId: string): Promise<void>;
14
15
  export {};
@@ -1,5 +1,6 @@
1
1
  import { encodeStorageKeyComponent } from "../../storage/interface.js";
2
2
  import { encode } from "../codec.js";
3
+ import { commitFencedEngineWrite } from "./fenced-write.js";
3
4
  import { parseCompletedReviewEntry, toCompletedReviewEntry } from "./review-list-entries.js";
4
5
  export function matchesReviewListFilter(review, filter) {
5
6
  if (filter.workflowId !== void 0 && review.workflowId !== filter.workflowId)
@@ -32,16 +33,16 @@ export async function listCompletedReviewsFromStorage(storage, filter) {
32
33
  await appendCompletedReviews(reviews, storage.scan("review-decision:"), filter, (key) => scopedPrefix === null || !key.startsWith(scopedPrefix));
33
34
  return reviews;
34
35
  }
35
- export async function persistCompletedReviewRecord(storage, reviewKey, reviewData, decisionResult) {
36
+ export async function persistCompletedReviewRecord(internals, reviewKey, reviewData, decisionResult) {
36
37
  const completedReview = toCompletedReviewEntry(reviewData, decisionResult);
37
- await storage.batch([
38
+ await commitFencedEngineWrite(internals, [
38
39
  {
39
40
  type: "put",
40
41
  key: completedReviewStorageKey(reviewData.workflowId, reviewData.reviewId),
41
42
  value: encode(completedReview)
42
43
  },
43
44
  { type: "delete", key: reviewKey }
44
- ]);
45
+ ], [], () => Error(`Completed review commit for review "${reviewData.reviewId}" lost its precondition.`));
45
46
  }
46
47
  export async function deleteCompletedReviewsForWorkflow(storage, workflowId) {
47
48
  const completedPrefix = completedReviewStoragePrefix(workflowId), deleteOperations = [];
@@ -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;
@@ -36,6 +36,25 @@ export { assertCompatiblePersistedDataVersion };
36
36
  export declare const ENGINE_PARKED_WORKFLOW_COUNT_FOR_TESTING: unique symbol;
37
37
  export declare const ENGINE_SIGNAL_WAITER_COUNT_FOR_TESTING: unique symbol;
38
38
  export declare const ENGINE_SLEEP_RESOLVER_COUNT_FOR_TESTING: unique symbol;
39
+ /**
40
+ * The `name` of the `process` warning emitted when a lease-owning engine is
41
+ * disposed through the synchronous `[Symbol.dispose]()` path. Sync disposal can
42
+ * only fire the lease release in the background; use `await engine.shutdown()`,
43
+ * `await using`, or `await engine[Symbol.asyncDispose]()` when prompt rolling
44
+ * deploy handoff matters.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * import { ENGINE_LEASE_SYNCHRONOUS_DISPOSE_WARNING_NAME } from '@lostgradient/weft';
49
+ *
50
+ * process.on('warning', (warning) => {
51
+ * if (warning.name === ENGINE_LEASE_SYNCHRONOUS_DISPOSE_WARNING_NAME) {
52
+ * // Alert on shutdown paths that can make lease handoff wait for leaseTtl.
53
+ * }
54
+ * });
55
+ * ```
56
+ */
57
+ export declare const ENGINE_LEASE_SYNCHRONOUS_DISPOSE_WARNING_NAME = "WeftEngineLeaseSynchronousDisposeWarning";
39
58
  export { ENGINE_LEASE_LOST_WARNING_NAME };
40
59
  /**
41
60
  * Durable execution engine.
@@ -408,6 +427,18 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
408
427
  timeout?: number;
409
428
  idempotencyKey?: string;
410
429
  }): Promise<CoordinatedUpdateResult>;
430
+ /**
431
+ * Awaited engine shutdown. Equivalent to
432
+ * `await engine[Symbol.asyncDispose]()` and useful in explicit signal handlers
433
+ * where `await using` syntax cannot own the process lifetime directly.
434
+ *
435
+ * Under `ownership: 'lease'`, this is the explicit prompt-handoff primitive:
436
+ * it drains queued inline starts, tears down in-memory write paths, and awaits
437
+ * lease release before resolving. Synchronous disposal remains immediate and
438
+ * can make the next engine wait for `leaseTtl` if the process exits before its
439
+ * background release completes.
440
+ */
441
+ shutdown(): Promise<void>;
411
442
  /**
412
443
  * Synchronous teardown (`using engine = ...`). Pending inline launches that
413
444
  * have not yet run are **discarded**, not executed. When you need queued
@@ -206,9 +206,19 @@ export {
206
206
  shouldEmitEngineLeakWarningForTesting
207
207
  } from "./engine-leak-warnings.js";
208
208
  export { assertCompatiblePersistedDataVersion };
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");
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);
@@ -769,8 +773,13 @@ export class Engine extends EventTarget {
769
773
  async submitCoordinatedUpdate(workflowId, name, payload, options) {
770
774
  return submitCoordinatedUpdateFromInternals(getInternals(this), workflowId, name, payload, options, this.#createUpdateCallbacks());
771
775
  }
776
+ async shutdown() {
777
+ return this[Symbol.asyncDispose]();
778
+ }
772
779
  [Symbol.dispose]() {
773
780
  const leaseManager = getInternals(this).leaseManager;
781
+ if (leaseManager !== null && leaseManager.currentEpochBytes() !== null)
782
+ process.emitWarning("engine ownership lease disposed synchronously; lease release is fire-and-forget, so exiting before the release completes can make the next instance wait for leaseTtl. Use await engine.shutdown(), await using, or await engine[Symbol.asyncDispose]() for prompt rolling-deploy handoff.", ENGINE_LEASE_SYNCHRONOUS_DISPOSE_WARNING_NAME);
774
783
  disposeEngine(getInternals(this));
775
784
  leaseManager?.release();
776
785
  }
@@ -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);
@@ -31,6 +31,7 @@ import type { Scheduler } from '../scheduler.ts';
31
31
  import type { Checkpoint, StartWorkflowOptions } from '../types.ts';
32
32
  import type { UpdateCoordinator } from '../updates.ts';
33
33
  import type { WorkflowVersionTuple } from '../workflow-version-tuple.ts';
34
+ import type { ActivityHeartbeatKey } from './activity-heartbeat-tracking.ts';
34
35
  import type { PendingTimelineEntry, QueuedInlineWorkflowExecutionStart, RegistrationEntry, ResolvedOptions, TrackedWaiterKeys, WorkflowResultWaiter } from './engine-internal-types.ts';
35
36
  import type { EngineCleanupIntervalDisposalTracker } from './engine-leak-warnings.ts';
36
37
  import type { WorkflowHandle, WorkflowHandleEngine } from './handles.ts';
@@ -89,8 +90,30 @@ export interface EngineInternals {
89
90
  conditionWaiters: Map<string, () => void>;
90
91
  updateWaiters: Map<string, (payload: unknown) => void>;
91
92
  updateWaitersByWorkflow: Map<string, TrackedWaiterKeys>;
92
- 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
+ }>;
93
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>>;
94
117
  interceptors: Interceptor[];
95
118
  composedWorkflowInterceptor: ComposedWorkflowInterceptor | null | undefined;
96
119
  composedActivityInterceptor: ComposedActivityInterceptor | null | undefined;
@@ -148,7 +171,7 @@ export interface EngineInternals {
148
171
  * {@link heartbeatDetails}. Inline-execution only; worker-executed activities run
149
172
  * their function out of process and never observe this.
150
173
  */
151
- lastHeartbeatDetailsByStep: Map<string, Map<number, unknown>>;
174
+ lastHeartbeatDetailsByStep: Map<string, Map<ActivityHeartbeatKey, unknown>>;
152
175
  /**
153
176
  * Per-run, non-serialized `services` value exposed to the workflow body as
154
177
  * `ctx.services`. Set at `engine.start({ services })` and re-provided on
@@ -8,7 +8,7 @@ import {
8
8
  import { EngineLeaseAcquisitionTimeoutError, EngineLeaseCorruptedError } from "./lease-errors.js";
9
9
  const DEFAULT_ACQUIRE_POLL_INTERVAL_MS = 1000;
10
10
  export function createLeaseManager(options) {
11
- const { storage, holderId, getNow, ttlMs, renewIntervalMs, waitTimeoutMs } = options, acquirePollIntervalMs = options.acquirePollIntervalMs ?? DEFAULT_ACQUIRE_POLL_INTERVAL_MS, unconfirmableMarginMs = renewIntervalMs;
11
+ const { storage, holderId, getNow, ttlMs, renewIntervalMs, waitTimeoutMs } = options, acquirePollIntervalMs = options.acquirePollIntervalMs ?? DEFAULT_ACQUIRE_POLL_INTERVAL_MS;
12
12
  let stopped = !1, heldEpoch = null, heldEpochBytes = null, lastHolderBytes = null, renewalInterval = null, leaseLost = !1, inFlightRenewal = null;
13
13
  const delay = options.delay ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
14
14
  function reportLeaseLost(reason) {
@@ -81,7 +81,7 @@ export function createLeaseManager(options) {
81
81
  committed = await storageConditionalBatch(storage, [{ key: KEYS.leaseHolder(), expectedValue: lastHolderBytes }], [{ type: "put", key: KEYS.leaseHolder(), value: holderBytes }]);
82
82
  } catch {
83
83
  const priorExpiry = decodeHolder(lastHolderBytes)?.expiresAt ?? 0;
84
- if (getNow() >= priorExpiry - unconfirmableMarginMs)
84
+ if (getNow() >= priorExpiry)
85
85
  reportLeaseLost("renewal-unconfirmable");
86
86
  return;
87
87
  }
@@ -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,
@@ -0,0 +1,11 @@
1
+ import type { ContextOperationRequest } from '../context.ts';
2
+ import type { EngineInternals } from './internals.ts';
3
+ import { type ActivityOperationCallbacks } from './operations-activity.ts';
4
+ type MemoOperation = Extract<ContextOperationRequest, {
5
+ type: 'memo';
6
+ }>;
7
+ export declare function callMemoFunctionWithDurableActivityScope(internals: EngineInternals, workflowId: string, operation: MemoOperation, callbacks: {
8
+ getActivityOperationCallbacks?: () => ActivityOperationCallbacks;
9
+ persistCheckpoint: (workflowId: string, operation: ContextOperationRequest) => Promise<void>;
10
+ }): Promise<unknown>;
11
+ export {};