@lostgradient/weft 0.6.0 → 0.8.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 (135) hide show
  1. package/README.md +7 -5
  2. package/dist/cli/generated/operation-client.generated.d.ts +1 -0
  3. package/dist/cli-main.js +63 -63
  4. package/dist/client/event-stream-options.d.ts +58 -0
  5. package/dist/client/event-stream-options.js +0 -0
  6. package/dist/client/event-stream.d.ts +27 -44
  7. package/dist/client/event-stream.js +21 -56
  8. package/dist/client/event-tail-lifecycle.d.ts +21 -0
  9. package/dist/client/event-tail-lifecycle.js +71 -0
  10. package/dist/client/event-tail.d.ts +3 -1
  11. package/dist/client/http-client.d.ts +4 -5
  12. package/dist/client/http-client.js +4 -1
  13. package/dist/client/http-handle.d.ts +4 -5
  14. package/dist/client/http-request.d.ts +7 -0
  15. package/dist/client/index.d.ts +2 -1
  16. package/dist/client/interface.d.ts +23 -7
  17. package/dist/client/local-handles.d.ts +2 -0
  18. package/dist/client/local-handles.js +10 -1
  19. package/dist/client/local.d.ts +3 -3
  20. package/dist/client/open-event-subscription.d.ts +3 -2
  21. package/dist/client/open-event-subscription.js +28 -1
  22. package/dist/client/sse-event-stream.d.ts +27 -0
  23. package/dist/client/sse-event-stream.js +276 -0
  24. package/dist/client/start-body.d.ts +2 -2
  25. package/dist/client/start-body.js +1 -0
  26. package/dist/core/context/activity-retry-state.d.ts +6 -5
  27. package/dist/core/context/activity-retry-state.js +31 -21
  28. package/dist/core/context/durable-activity.d.ts +117 -0
  29. package/dist/core/context/durable-activity.js +79 -0
  30. package/dist/core/context/operation-request.d.ts +15 -0
  31. package/dist/core/context/parallel-operations.js +1 -0
  32. package/dist/core/context/run-operation-cached-request.d.ts +8 -0
  33. package/dist/core/context/run-operation-cached-request.js +59 -0
  34. package/dist/core/context/run-operation.d.ts +23 -5
  35. package/dist/core/context/run-operation.js +62 -47
  36. package/dist/core/engine/activity-heartbeat-tracking.d.ts +8 -17
  37. package/dist/core/engine/activity-heartbeat-tracking.js +7 -1
  38. package/dist/core/engine/activity-reconciliation.d.ts +1 -0
  39. package/dist/core/engine/activity-reconciliation.js +7 -2
  40. package/dist/core/engine/anonymous-signal-sequence.js +6 -4
  41. package/dist/core/engine/async-activity-completion.d.ts +5 -7
  42. package/dist/core/engine/async-activity-completion.js +13 -9
  43. package/dist/core/engine/bulk-operations-purge.js +2 -1
  44. package/dist/core/engine/bulk-operations.js +8 -7
  45. package/dist/core/engine/callback-checkpoint-persistence.d.ts +3 -0
  46. package/dist/core/engine/callback-checkpoint-persistence.js +25 -0
  47. package/dist/core/engine/callback-creators-bundles.js +4 -1
  48. package/dist/core/engine/checkpoint-io.d.ts +4 -1
  49. package/dist/core/engine/checkpoint-io.js +19 -10
  50. package/dist/core/engine/completed-review-storage.d.ts +2 -1
  51. package/dist/core/engine/completed-review-storage.js +4 -3
  52. package/dist/core/engine/construction.js +3 -1
  53. package/dist/core/engine/engine-internal-types.d.ts +2 -0
  54. package/dist/core/engine/errors.d.ts +6 -6
  55. package/dist/core/engine/errors.js +1 -1
  56. package/dist/core/engine/handles.js +1 -1
  57. package/dist/core/engine/index.d.ts +37 -5
  58. package/dist/core/engine/index.js +7 -1
  59. package/dist/core/engine/internals.d.ts +2 -1
  60. package/dist/core/engine/lease-manager.js +2 -2
  61. package/dist/core/engine/lifecycle/recovered-services.js +8 -1
  62. package/dist/core/engine/lifecycle/start-or-signal-create.d.ts +2 -2
  63. package/dist/core/engine/lifecycle/start-or-signal-create.js +17 -15
  64. package/dist/core/engine/lifecycle/start-or-signal-resolution.d.ts +1 -1
  65. package/dist/core/engine/lifecycle/start-or-signal-resolution.js +23 -5
  66. package/dist/core/engine/lifecycle/start-or-signal.d.ts +6 -4
  67. package/dist/core/engine/lifecycle/start-or-signal.js +21 -4
  68. package/dist/core/engine/memo-durable-activity.d.ts +11 -0
  69. package/dist/core/engine/memo-durable-activity.js +282 -0
  70. package/dist/core/engine/operations-activity.d.ts +5 -1
  71. package/dist/core/engine/operations-activity.js +19 -7
  72. package/dist/core/engine/operations-data.d.ts +4 -1
  73. package/dist/core/engine/operations-data.js +3 -3
  74. package/dist/core/engine/reviews.js +7 -4
  75. package/dist/core/engine/schedule-run.js +12 -3
  76. package/dist/core/engine/schedule-timer.js +2 -0
  77. package/dist/core/engine/storage-io.js +2 -2
  78. package/dist/core/engine/termination/complete.js +3 -3
  79. package/dist/core/json.js +1 -1
  80. package/dist/core/scheduler/scheduler-class.js +2 -1
  81. package/dist/core/start-workflow-validation.d.ts +3 -4
  82. package/dist/core/start-workflow-validation.js +1 -1
  83. package/dist/core/timeouts.d.ts +13 -1
  84. package/dist/core/timeouts.js +4 -1
  85. package/dist/core/types/constants.d.ts +16 -0
  86. package/dist/core/types/options.d.ts +60 -12
  87. package/dist/core/types/services-resolution.d.ts +49 -3
  88. package/dist/core/weft-error.d.ts +1 -1
  89. package/dist/core/weft-error.js +3 -0
  90. package/dist/index.d.ts +5 -4
  91. package/dist/index.js +7 -0
  92. package/dist/json-schema.js +1 -1
  93. package/dist/mcp/cli.js +26 -26
  94. package/dist/server/asyncapi-channels.js +82 -16
  95. package/dist/server/handler/auth-context-principal.d.ts +19 -0
  96. package/dist/server/handler/auth-context-principal.js +25 -0
  97. package/dist/server/handler/index.d.ts +2 -1
  98. package/dist/server/handler/index.js +35 -18
  99. package/dist/server/handler/route-dispatch.d.ts +12 -22
  100. package/dist/server/handler/route-dispatch.js +30 -36
  101. package/dist/server/handler/sse-route-dispatch.d.ts +25 -0
  102. package/dist/server/handler/sse-route-dispatch.js +45 -0
  103. package/dist/server/handler.js +28 -28
  104. package/dist/server/index.d.ts +8 -7
  105. package/dist/server/index.js +17 -17
  106. package/dist/server/operations/fleet-events-sse.d.ts +11 -0
  107. package/dist/server/operations/fleet-events-sse.js +164 -0
  108. package/dist/server/operations/sse-stream.d.ts +43 -0
  109. package/dist/server/operations/sse-stream.js +152 -7
  110. package/dist/server/operations/start-or-signal-workflow.d.ts +2 -0
  111. package/dist/server/operations/start-or-signal-workflow.js +25 -14
  112. package/dist/server/operations/start-workflow-options.d.ts +9 -6
  113. package/dist/server/operations/start-workflow-options.js +13 -0
  114. package/dist/server/operations/workflow-events-sse.d.ts +17 -0
  115. package/dist/server/operations/workflow-events-sse.js +200 -0
  116. package/dist/server/rest-bindings.js +12 -0
  117. package/dist/server/runtime/authentication-bridge.js +4 -0
  118. package/dist/server/runtime/cors.d.ts +1 -1
  119. package/dist/server/runtime/cors.js +6 -1
  120. package/dist/server/runtime/websocket-stream.d.ts +4 -0
  121. package/dist/server/runtime/websocket-stream.js +20 -3
  122. package/dist/server/runtime/websocket-worker.js +7 -2
  123. package/dist/server/serve-internals.d.ts +28 -0
  124. package/dist/server/serve-internals.js +4 -2
  125. package/dist/server/workflow-event-feed.d.ts +1 -0
  126. package/dist/server/workflow-event-feed.js +17 -6
  127. package/dist/service-worker/index.js +28 -28
  128. package/dist/service-worker/setup.d.ts +18 -1
  129. package/dist/service-worker/setup.js +7 -4
  130. package/dist/storage/typed-storage.d.ts +1 -1
  131. package/dist/storage/typed-storage.js +1 -1
  132. package/dist/testing/index.js +27 -27
  133. package/dist/version.d.ts +1 -1
  134. package/dist/version.js +1 -1
  135. package/package.json +1 -1
@@ -3,6 +3,8 @@ import { decode, encode } from "../codec.js";
3
3
  import { ActivityAsyncPendingEvent } from "../events.js";
4
4
  import { assertPayloadWithinLimit } from "../payload-size.js";
5
5
  import { WeftError } from "../weft-error.js";
6
+ import { stageAtomicWorkflowCommitSideEffects } from "./checkpoint-side-effects.js";
7
+ import { commitFencedEngineWrite } from "./fenced-write.js";
6
8
  const ASYNC_ACTIVITY_TOKEN_PREFIX = "async-act:v1", ASYNC_ACTIVITY_KEY_PREFIX = "async-act:v1:";
7
9
  export function asyncActivityWorkflowPrefix(workflowId) {
8
10
  return `${ASYNC_ACTIVITY_KEY_PREFIX}${encodeStorageKeyComponent(workflowId)}:`;
@@ -33,7 +35,7 @@ function isPersistedAsyncActivity(value) {
33
35
  export function deriveAsyncActivityToken(workflowId, step, attempt) {
34
36
  return `${ASYNC_ACTIVITY_TOKEN_PREFIX}:${workflowId}:${step}:${attempt}`;
35
37
  }
36
- function persistPendingAsyncActivity(storage, pending) {
38
+ function buildPersistPendingAsyncActivityOperation(pending) {
37
39
  const record = {
38
40
  version: 1,
39
41
  token: pending.token,
@@ -44,12 +46,16 @@ function persistPendingAsyncActivity(storage, pending) {
44
46
  attempt: pending.attempt,
45
47
  createdAt: pending.createdAt
46
48
  };
47
- return storage.put(KEYS.asyncActivity(pending.workflowId, pending.token), encode(record));
49
+ return {
50
+ type: "put",
51
+ key: KEYS.asyncActivity(pending.workflowId, pending.token),
52
+ value: encode(record)
53
+ };
48
54
  }
49
55
  export async function registerPendingAsyncActivity(internals, pending) {
50
56
  const alreadyRegistered = internals.pendingAsyncActivities.has(pending.token);
51
57
  internals.pendingAsyncActivities.set(pending.token, pending);
52
- await persistPendingAsyncActivity(internals.storage, pending);
58
+ await commitFencedEngineWrite(internals, [buildPersistPendingAsyncActivityOperation(pending)], [], () => Error(`Async activity registration for token "${pending.token}" lost its precondition.`));
53
59
  if (!alreadyRegistered)
54
60
  internals.engine.dispatchEvent(new ActivityAsyncPendingEvent(pending.token, pending.operationId, pending.workflowId, pending.activityName, pending.attempt));
55
61
  }
@@ -87,12 +93,10 @@ async function consumePendingAsyncActivity(internals, token) {
87
93
  if (!pending)
88
94
  throw new AsyncActivityTokenNotFoundError(token);
89
95
  internals.pendingAsyncActivities.delete(token);
90
- try {
91
- await internals.storage.delete(KEYS.asyncActivity(pending.workflowId, token));
92
- } catch (error) {
93
- internals.pendingAsyncActivities.set(token, pending);
94
- throw error;
95
- }
96
+ stageAtomicWorkflowCommitSideEffects(internals, pending.workflowId, {
97
+ conditions: [],
98
+ operations: [{ type: "delete", key: KEYS.asyncActivity(pending.workflowId, token) }]
99
+ });
96
100
  return pending;
97
101
  }
98
102
  function shouldBufferPendingAsyncActivityResolution(internals, workflowId) {
@@ -10,6 +10,7 @@ import { buildIndexOperations } from "../search-attributes.js";
10
10
  import { buildWorkflowTagIndexOperations, normalizeWorkflowTags } from "../workflow-tags.js";
11
11
  import { asyncActivityWorkflowPrefix } from "./async-activity-completion.js";
12
12
  import { forgetCommittedCheckpointBytes } from "./checkpoint-commit-snapshots.js";
13
+ import { commitFencedEngineWrite } from "./fenced-write.js";
13
14
  import { streamWorkflowStates } from "./listing.js";
14
15
  import { createTerminalCleanupTimerId } from "./state-utilities.js";
15
16
  import {
@@ -131,7 +132,7 @@ function getWorkflowRetentionDeadline(internals, state) {
131
132
  }
132
133
  export async function purgeWorkflow(internals, state, cleanupWaiters) {
133
134
  const deleteOperations = await collectWorkflowPurgeDeleteOperations(internals, state);
134
- await internals.storage.batch(deleteOperations);
135
+ await commitFencedEngineWrite(internals, deleteOperations, [], () => Error(`Purge commit for workflow "${state.id}" lost its precondition.`));
135
136
  clearPurgedWorkflowInMemoryState(internals, state.id, cleanupWaiters);
136
137
  }
137
138
  export async function collectWorkflowPurgeDeleteOperations(internals, state) {
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  KEYS,
3
3
  requireStorageCapability,
4
- storageConditionalBatch,
5
4
  storageHas
6
5
  } from "../../storage/interface.js";
7
6
  import { assertScopedBulkWorkflowFilter } from "../bulk-workflow-filter.js";
@@ -24,6 +23,10 @@ import {
24
23
  withBulkAuditEvent
25
24
  } from "./bulk-operations-shared.js";
26
25
  import { BulkDeleteRequiresTerminalWorkflowsError } from "./errors.js";
26
+ import {
27
+ assertLeaseHeldForEngineWork,
28
+ commitFencedEngineWriteAllowingPreconditionFailure
29
+ } from "./fenced-write.js";
27
30
  import { BULK_OPERATION_BATCH_SIZE } from "./listing.js";
28
31
  import { createTerminalCleanupTimerId } from "./state-utilities.js";
29
32
  import { loadWorkflowState, runSerializedWorkflowStateWrite } from "./storage-io.js";
@@ -132,6 +135,7 @@ async function runBulkFailedWorkflowRetry(internals, filter, options = {}) {
132
135
  return withBulkAuditEvent(internals, preparation, options, result, retried);
133
136
  }
134
137
  export async function retryFailedAll(internals, filter, options = {}) {
138
+ assertLeaseHeldForEngineWork(internals);
135
139
  return runBulkFailedWorkflowRetry(internals, filter, options);
136
140
  }
137
141
  async function retryFailedWorkflow(internals, workflowId) {
@@ -194,12 +198,9 @@ async function reactivateFailedWorkflowFromCheckpointSerialized(internals, workf
194
198
  throw Error(`Workflow concurrency admission for "${lastConcurrencyStateKey ?? workflowId}" changed too many times while retrying failed workflow "${workflowId}"`);
195
199
  }
196
200
  async function commitFailedWorkflowReactivation(internals, operations, conditions) {
197
- if (conditions.length === 0) {
198
- await internals.storage.batch(operations);
199
- return !0;
200
- }
201
- requireStorageCapability(internals.storage, "conditionalBatch", "retry failed workflow");
202
- return storageConditionalBatch(internals.storage, conditions, operations);
201
+ if (conditions.length > 0)
202
+ requireStorageCapability(internals.storage, "conditionalBatch", "retry failed workflow");
203
+ return commitFencedEngineWriteAllowingPreconditionFailure(internals, operations, conditions);
203
204
  }
204
205
  function buildReactivatedWorkflowState(internals, state) {
205
206
  const reactivatedState = {
@@ -0,0 +1,3 @@
1
+ import type { ContextOperationRequest } from '../context.ts';
2
+ import type { Engine } from './index.ts';
3
+ export declare function persistCheckpointForDataOperation<TWorkflows extends object, TActivities extends object>(engine: Engine<TWorkflows, TActivities>, workflowId: string, operation: ContextOperationRequest): Promise<void>;
@@ -0,0 +1,25 @@
1
+ import { HISTORY_CIRCUIT_BREAKER_REASON } from "../types.js";
2
+ import { validateAttributeValueSizes } from "./attributes-tags.js";
3
+ import { createTerminationCallbacks } from "./callback-creators-core.js";
4
+ import {
5
+ appendTimelineBatchOperations,
6
+ persistCheckpoint,
7
+ pruneCheckpointHistory
8
+ } from "./checkpoint-io.js";
9
+ import { getInternals } from "./internals.js";
10
+ import { swallowPromiseRejection } from "./strategy-helpers.js";
11
+ import { terminateWorkflow } from "./termination.js";
12
+ export function persistCheckpointForDataOperation(engine, workflowId, operation) {
13
+ return persistCheckpoint(getInternals(engine), workflowId, operation, void 0, {
14
+ appendTimelineBatchOperations: (id, checkpointOperation, step, timestamp, operations) => appendTimelineBatchOperations(getInternals(engine), id, checkpointOperation, step, timestamp, operations),
15
+ swallowPromiseRejection: (promise) => {
16
+ swallowPromiseRejection(promise);
17
+ },
18
+ validateAttributeValueSizes,
19
+ pruneCheckpointHistory: (id, step) => pruneCheckpointHistory(getInternals(engine), id, step),
20
+ dispatchEvent: (event) => {
21
+ engine.dispatchEvent(event);
22
+ },
23
+ enforceHistoryCircuitBreaker: (id) => terminateWorkflow(getInternals(engine), id, "timed-out", createTerminationCallbacks(engine), HISTORY_CIRCUIT_BREAKER_REASON)
24
+ }, { timeline: "preserve-pending" });
25
+ }
@@ -3,6 +3,7 @@ import {
3
3
  broadcast as broadcastFromInternals,
4
4
  dispatchPendingUpdateReceived as dispatchPendingUpdateReceivedFromBroadcast
5
5
  } from "./broadcast.js";
6
+ import { persistCheckpointForDataOperation } from "./callback-checkpoint-persistence.js";
6
7
  import {
7
8
  createBroadcastCallbacks,
8
9
  createGuardCallbacks,
@@ -97,7 +98,9 @@ export function createConditionOperationCallbacks(engine) {
97
98
  }
98
99
  export function createDataOperationCallbacks(engine) {
99
100
  return {
100
- runOperationWithResult: (workflowId, operation, execute) => runOperationWithResultForEngine(engine, workflowId, operation, execute)
101
+ runOperationWithResult: (workflowId, operation, execute) => runOperationWithResultForEngine(engine, workflowId, operation, execute),
102
+ persistCheckpoint: (workflowId, operation) => persistCheckpointForDataOperation(engine, workflowId, operation),
103
+ getActivityOperationCallbacks: () => createActivityOperationCallbacks(engine)
101
104
  };
102
105
  }
103
106
  export function createStateOperationCallbacks(engine) {
@@ -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 = [];
@@ -6,6 +6,7 @@ import { WorkerPool } from "../../workers/pool.js";
6
6
  import { optionalInlineDependencies } from "../inline-execution-strategy.context-options.js";
7
7
  import { InlineExecutionStrategy } from "../inline-execution-strategy.js";
8
8
  import {
9
+ DEFAULT_POLL_INTERVAL_MS,
9
10
  DEFAULT_RETENTION_SWEEP_BATCH_SIZE,
10
11
  DEFAULT_RETENTION_SWEEP_INTERVAL_MS
11
12
  } from "../types.js";
@@ -96,7 +97,8 @@ function resolveNumericDefaults(options) {
96
97
  return {
97
98
  checkpointHistory: defaultTo(options?.checkpointHistory, 10),
98
99
  checkpointSizeWarningThreshold: defaultTo(options?.checkpointSizeWarningThreshold, 65536),
99
- maxNestingDepth: defaultTo(options?.maxNestingDepth, 10)
100
+ maxNestingDepth: defaultTo(options?.maxNestingDepth, 10),
101
+ schedulerPollIntervalMs: normalizePositiveSafeIntegerOption(options?.schedulerPollIntervalMs, "options.schedulerPollIntervalMs", DEFAULT_POLL_INTERVAL_MS) ?? DEFAULT_POLL_INTERVAL_MS
100
102
  };
101
103
  }
102
104
  function resolveHistoryFields(options) {
@@ -27,6 +27,8 @@ export interface ResolvedOptions {
27
27
  checkpointHistory: number;
28
28
  checkpointSizeWarningThreshold: number;
29
29
  maxNestingDepth: number;
30
+ /** Durable-timer scheduler poll interval (ms). */
31
+ schedulerPollIntervalMs: number;
30
32
  broadcastEvents: boolean;
31
33
  retention: NormalizedRetentionPolicy | null;
32
34
  retentionSweepIntervalMs: number;
@@ -289,14 +289,14 @@ export declare class ActivityResolutionError extends WeftError<'ActivityResoluti
289
289
  /**
290
290
  * Thrown by {@link Engine.startOrSignal} when the target workflow already exists
291
291
  * and is in a terminal state (completed, failed, cancelled, or timed out). A
292
- * terminal run cannot accept a signal and must not be silently replaced, so
293
- * `startOrSignal` surfaces this conflict instead of starting a fresh run under
294
- * the same id or dropping the signal.
292
+ * terminal run cannot accept a signal. By default `startOrSignal` surfaces this
293
+ * conflict instead of starting a fresh run under the same id or dropping the
294
+ * signal.
295
295
  *
296
296
  * Inspect `workflowId` to identify the conflicting run and `status` to see why
297
- * it was rejected. To deliberately start a new run, choose a different id (or
298
- * idempotency key); to deliver to a fresh run, terminal-state reuse is not a
299
- * supported policy.
297
+ * it was rejected. To deliberately reuse a stable id, call `startOrSignal` with
298
+ * `onTerminalConflict: 'start-new'`, an explicit `id`, and a deterministic
299
+ * `signalId`; otherwise choose a different id or idempotency key.
300
300
  *
301
301
  * @example
302
302
  * ```ts
@@ -118,7 +118,7 @@ export class StartOrSignalConflictError extends WeftError {
118
118
  workflowId;
119
119
  status;
120
120
  constructor(workflowId, status) {
121
- super("StartOrSignalConflictError", `Workflow "${workflowId}" is already in terminal state "${status}" and cannot accept a startOrSignal: a terminal run cannot be signalled and is not replaced. Use a different id or idempotency key to start a new run.`);
121
+ super("StartOrSignalConflictError", `Workflow "${workflowId}" is already in terminal state "${status}" and cannot accept a startOrSignal. Use options.onTerminalConflict: 'start-new' with an explicit id and signalId, or choose a different id or idempotency key to start a new run.`);
122
122
  this.workflowId = workflowId;
123
123
  this.status = status;
124
124
  }
@@ -129,7 +129,7 @@ export class WorkflowHandle extends EventTarget {
129
129
  observer.error?.(event.error);
130
130
  } else if (event instanceof WorkflowTimedOutEvent) {
131
131
  terminalDelivered = !0;
132
- observer.error?.(new WorkflowTimeoutError(event.workflowId, event.timeoutType, event.elapsed));
132
+ observer.error?.(new WorkflowTimeoutError(event.workflowId, event.timeoutType, event.elapsed, event.reason));
133
133
  }
134
134
  };
135
135
  this.addEventListener("workflow:failed", errorHandler, { signal: controller.signal });
@@ -5,7 +5,7 @@ import type { TypedEventTarget, WeftEventMap } from '../events.ts';
5
5
  import type { Interceptor } from '../interceptor.ts';
6
6
  import { type ReviewRequest } from '../review/index.ts';
7
7
  import { Scheduler } from '../scheduler.ts';
8
- import { type AnyActivityDefinition, type AnyWorkflowDefinition, type AttributeFilterKey, type BulkCancelResult, type BulkDeleteResult, type BulkOperationCommitOptions, type BulkOperationDryRunOptions, type BulkOperationDryRunResult, type BulkRetryFailedResult, type BulkSignalAllCommitOptions, type BulkSignalAllDryRunOptions, type BulkSignalResult, type BulkTagResult, type CheckpointState, type CheckpointSummary, type CoordinatedUpdateResult, type DefaultActivityTypes, type DefaultWorkflowRegistry, type ForkOptions, type InferActivityEntries, type InferActivityEntry, type InferWorkflowEntries, type InferWorkflowEntry, type ListFilter, type ListOptions, type PaginatedResult, type PurgeResult, type QueryDefinition, type RegisteredWorkflowDefinition, type RetentionOverview, type ReviewListEntry, type ReviewListFilter, type ScheduleDefinition, type ScheduleFilter, type ScheduleOptions, type ScheduleSpec, type ScheduleSummary, type SearchAttributeValue, type SignalDefinition, type SignalDeliveryOptions, type StartOptions, type StartOrSignalSignal, type StartWorkflowOptions, type SubmitReviewOptions, type TypedListFilter, type UpdateDefinition, type WorkflowEvent, type WorkflowInput, type WorkflowOutput, type WorkflowReplay, type WorkflowServices, type WorkflowServicesUnion, type WorkflowState, type WorkflowSummary, type WorkflowTimelineEntry } from '../types.ts';
8
+ import { type AnyActivityDefinition, type AnyWorkflowDefinition, type AttributeFilterKey, type BulkCancelResult, type BulkDeleteResult, type BulkOperationCommitOptions, type BulkOperationDryRunOptions, type BulkOperationDryRunResult, type BulkRetryFailedResult, type BulkSignalAllCommitOptions, type BulkSignalAllDryRunOptions, type BulkSignalResult, type BulkTagResult, type CheckpointState, type CheckpointSummary, type CoordinatedUpdateResult, type DefaultActivityTypes, type DefaultWorkflowRegistry, type ForkOptions, type InferActivityEntries, type InferActivityEntry, type InferWorkflowEntries, type InferWorkflowEntry, type ListFilter, type ListOptions, type PaginatedResult, type PurgeResult, type QueryDefinition, type RegisteredWorkflowDefinition, type RetentionOverview, type ReviewListEntry, type ReviewListFilter, type ScheduleDefinition, type ScheduleFilter, type ScheduleOptions, type ScheduleSpec, type ScheduleSummary, type SearchAttributeValue, type SignalDefinition, type SignalDeliveryOptions, type StartOrSignalOptions, type StartOrSignalSignal, type StartWorkflowOptions, type SubmitReviewOptions, type TypedListFilter, type UpdateDefinition, type WorkflowEvent, type WorkflowInput, type WorkflowOutput, type WorkflowReplay, type WorkflowServices, type WorkflowServicesUnion, type WorkflowState, type WorkflowSummary, type WorkflowTimelineEntry } from '../types.ts';
9
9
  import type { TimerEntry } from '../types/checkpoint.ts';
10
10
  import type { WorkflowAlreadyRegistered } from '../types/workflow-builder.ts';
11
11
  import { type AggregateOptions, type AggregateResult } from './aggregate.ts';
@@ -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.
@@ -202,8 +221,9 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
202
221
  * first signal commit in one batch and the freshly-launched run consumes the
203
222
  * signal on its first drive. A non-terminal target (running, pending, or
204
223
  * suspended) is signalled through the normal signal path; a terminal target
205
- * throws {@link StartOrSignalConflictError} rather than starting a new run or
206
- * dropping the signal.
224
+ * throws {@link StartOrSignalConflictError} unless
225
+ * `options.onTerminalConflict: 'start-new'` is supplied with an explicit
226
+ * workflow id and deterministic `signal.signalId`.
207
227
  *
208
228
  * Concurrent callers converge on one workflow and one delivered signal. Pass
209
229
  * `options.idempotencyKey` to dedup independent callers (e.g. retried
@@ -213,8 +233,8 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
213
233
  * are `options.id` and `options.idempotencyKey`. Requires a storage backend
214
234
  * with `conditionalBatch`.
215
235
  */
216
- startOrSignal<TName extends KnownWorkflowNames<TWorkflows>>(type: TName, input: WorkflowInput<TWorkflows, TName>, signal: StartOrSignalSignal, options?: StartOptions<WorkflowServices<TWorkflows, TName>>): Promise<StartOrSignalResult<WorkflowOutput<TWorkflows, TName>>>;
217
- startOrSignal<TName extends string>(type: UnknownWorkflowNameWhenDefaultRegistryIsEmpty<TWorkflows, TName>, input: unknown, signal: StartOrSignalSignal, options?: StartOptions): Promise<StartOrSignalResult>;
236
+ startOrSignal<TName extends KnownWorkflowNames<TWorkflows>>(type: TName, input: WorkflowInput<TWorkflows, TName>, signal: StartOrSignalSignal, options?: StartOrSignalOptions<WorkflowServices<TWorkflows, TName>>): Promise<StartOrSignalResult<WorkflowOutput<TWorkflows, TName>>>;
237
+ startOrSignal<TName extends string>(type: UnknownWorkflowNameWhenDefaultRegistryIsEmpty<TWorkflows, TName>, input: unknown, signal: StartOrSignalSignal, options?: StartOrSignalOptions): Promise<StartOrSignalResult>;
218
238
  getHandle(workflowId: string): WorkflowHandle;
219
239
  list<const TAttributeKeys extends readonly AttributeFilterKey[] = readonly AttributeFilterKey[]>(filter?: TypedListFilter<TAttributeKeys>, options?: ListOptions): Promise<PaginatedResult<WorkflowSummary>>;
220
240
  aggregate(filter: ListFilter | undefined, options: AggregateOptions): Promise<AggregateResult>;
@@ -407,6 +427,18 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
407
427
  timeout?: number;
408
428
  idempotencyKey?: string;
409
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>;
410
442
  /**
411
443
  * Synchronous teardown (`using engine = ...`). Pending inline launches that
412
444
  * have not yet run are **discarded**, not executed. When you need queued
@@ -206,7 +206,7 @@ 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
212
 
@@ -289,6 +289,7 @@ export class Engine extends EventTarget {
289
289
  getInternals(this).scheduler = new Scheduler({
290
290
  storage,
291
291
  onTimerFired: (entry) => handleTimerFiredFromInternals(getInternals(this), entry, this.#createTimeOperationCallbacks()),
292
+ pollIntervalMs: resolvedOptions.schedulerPollIntervalMs,
292
293
  getNow,
293
294
  commitTimerCleanup: (operations) => commitFencedEngineWrite(getInternals(this), operations, [], () => Error("Timer cleanup lost its fenced CAS race"))
294
295
  });
@@ -768,8 +769,13 @@ export class Engine extends EventTarget {
768
769
  async submitCoordinatedUpdate(workflowId, name, payload, options) {
769
770
  return submitCoordinatedUpdateFromInternals(getInternals(this), workflowId, name, payload, options, this.#createUpdateCallbacks());
770
771
  }
772
+ async shutdown() {
773
+ return this[Symbol.asyncDispose]();
774
+ }
771
775
  [Symbol.dispose]() {
772
776
  const leaseManager = getInternals(this).leaseManager;
777
+ if (leaseManager !== null && leaseManager.currentEpochBytes() !== null)
778
+ 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);
773
779
  disposeEngine(getInternals(this));
774
780
  leaseManager?.release();
775
781
  }
@@ -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';
@@ -148,7 +149,7 @@ export interface EngineInternals {
148
149
  * {@link heartbeatDetails}. Inline-execution only; worker-executed activities run
149
150
  * their function out of process and never observe this.
150
151
  */
151
- lastHeartbeatDetailsByStep: Map<string, Map<number, unknown>>;
152
+ lastHeartbeatDetailsByStep: Map<string, Map<ActivityHeartbeatKey, unknown>>;
152
153
  /**
153
154
  * Per-run, non-serialized `services` value exposed to the workflow body as
154
155
  * `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
  }
@@ -24,7 +24,8 @@ export async function reprovideRecoveredServices(internals, state, failRun, onCo
24
24
  const resolution = await resolver({
25
25
  workflowId: state.id,
26
26
  workflowType: state.type,
27
- input: state.input
27
+ input: state.input,
28
+ launchOptions: launchOptionsFromWorkflowState(state)
28
29
  });
29
30
  if (resolution.status === "available") {
30
31
  internals.workflowServices.set(state.id, resolution.services);
@@ -47,3 +48,9 @@ export function unavailableServicesError(workflowId, reason) {
47
48
  function missingServicesResolverReason(workflowId) {
48
49
  return `Workflow "${workflowId}" was started with services, but this engine has no resolveWorkflowServices option. Configure EngineOptions.resolveWorkflowServices before recovery so ctx.services can be re-provided.`;
49
50
  }
51
+ function launchOptionsFromWorkflowState(state) {
52
+ return {
53
+ id: state.id,
54
+ ...state.tags !== void 0 && state.tags.length > 0 ? { tags: [...state.tags] } : {}
55
+ };
56
+ }
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * @module core/engine/lifecycle/start-or-signal-create
9
9
  */
10
- import type { StartOptions, StartOrSignalSignal } from '../../types.ts';
10
+ import type { StartOrSignalOptions, StartOrSignalSignal } from '../../types.ts';
11
11
  import { type StartOrSignalOutcome, type WorkflowHandle } from '../handles.ts';
12
12
  import type { EngineInternals } from '../internals.ts';
13
13
  import { type BuildIdempotentStartOperations } from './start-commit.ts';
@@ -55,4 +55,4 @@ export type StartOrSignalResult<TResult = unknown> = {
55
55
  * (scan-then-park), and the caller's payload loses to the pre-buffered one by
56
56
  * the same first-wins dedup.
57
57
  */
58
- export declare function createWithSignalOrFallback(internals: EngineInternals, type: string, input: unknown, signalSpec: StartOrSignalSignal, signalId: string, options: StartOptions | undefined, callbacks: StartOrSignalCallbacks): Promise<StartOrSignalResult>;
58
+ export declare function createWithSignalOrFallback(internals: EngineInternals, type: string, input: unknown, signalSpec: StartOrSignalSignal, signalId: string, options: StartOrSignalOptions | undefined, callbacks: StartOrSignalCallbacks): Promise<StartOrSignalResult>;
@@ -44,23 +44,25 @@ export async function createWithSignalOrFallback(internals, type, input, signalS
44
44
  });
45
45
  if (outcome.kind === "created")
46
46
  return { handle: outcome.handle, outcome: "started" };
47
- if (outcome.kind === "lost-keyed")
48
- return {
49
- handle: await resolveWinnerWithSignal(internals, outcome.id, signalSpec, signalId, callbacks, outcome.idempotencyKey),
50
- outcome: "signalled"
51
- };
52
- if (outcome.kind === "lost-caller-id") {
53
- const resolved = await resolveCallerIdWinnerOrRetry(internals, outcome.id, signalSpec, signalId, callbacks);
54
- if (resolved !== void 0)
55
- return { handle: resolved, outcome: "signalled" };
56
- } else {
57
- const resolved = await plainCreateBufferedSignalOrResolve(internals, type, input, signalSpec, signalId, options, callbacks);
58
- if (resolved !== void 0)
59
- return resolved;
60
- }
47
+ const resolved = await resolveCreateFallbackOutcome(internals, outcome, type, input, signalSpec, signalId, options, callbacks);
48
+ if (resolved !== void 0)
49
+ return resolved;
61
50
  }
62
51
  throw Error(`startOrSignal could not create workflow "${options?.id ?? "<generated>"}" after ${CALLER_ID_CREATE_MAX_ATTEMPTS} attempts: each concurrent same-id winner aborted before its durable commit.`);
63
52
  }
53
+ async function resolveCreateFallbackOutcome(internals, outcome, type, input, signalSpec, signalId, options, callbacks) {
54
+ if (outcome.kind === "lost-keyed")
55
+ return {
56
+ handle: await resolveWinnerWithSignal(internals, outcome.id, signalSpec, signalId, callbacks, outcome.idempotencyKey),
57
+ outcome: "signalled"
58
+ };
59
+ const allowTerminalRestart = options?.onTerminalConflict === "start-new";
60
+ if (outcome.kind === "lost-caller-id") {
61
+ const resolved = await resolveCallerIdWinnerOrRetry(internals, outcome.id, signalSpec, signalId, callbacks, allowTerminalRestart);
62
+ return resolved === void 0 ? void 0 : { handle: resolved, outcome: "signalled" };
63
+ }
64
+ return plainCreateBufferedSignalOrResolve(internals, type, input, signalSpec, signalId, options, callbacks);
65
+ }
64
66
  async function plainCreateBufferedSignalOrResolve(internals, type, input, signalSpec, signalId, options, callbacks) {
65
67
  try {
66
68
  return {
@@ -70,7 +72,7 @@ async function plainCreateBufferedSignalOrResolve(internals, type, input, signal
70
72
  } catch (error) {
71
73
  if (!(error instanceof WorkflowAlreadyExistsError))
72
74
  throw error;
73
- const resolved = await resolveCallerIdWinnerOrRetry(internals, error.workflowId, signalSpec, signalId, callbacks);
75
+ const resolved = await resolveCallerIdWinnerOrRetry(internals, error.workflowId, signalSpec, signalId, callbacks, options?.onTerminalConflict === "start-new");
74
76
  return resolved === void 0 ? void 0 : { handle: resolved, outcome: "signalled" };
75
77
  }
76
78
  }
@@ -47,7 +47,7 @@ export declare function resolveExistingRunOrThrowPurged(internals: EngineInterna
47
47
  * committing (storage failure, oversized payload, throwing start interceptor): no
48
48
  * run exists, so return `undefined` and let the caller retry its own create.
49
49
  */
50
- export declare function resolveCallerIdWinnerOrRetry(internals: EngineInternals, winnerId: string, signalSpec: StartOrSignalSignal, signalId: string, callbacks: StartOrSignalCallbacks): Promise<WorkflowHandle | undefined>;
50
+ export declare function resolveCallerIdWinnerOrRetry(internals: EngineInternals, winnerId: string, signalSpec: StartOrSignalSignal, signalId: string, callbacks: StartOrSignalCallbacks, allowTerminalRestart?: boolean): Promise<WorkflowHandle | undefined>;
51
51
  /**
52
52
  * For a workflow that already exists: signal it if non-terminal, conflict if
53
53
  * terminal. Returns the handle on a successful signal, or `undefined` when the