@lostgradient/weft 0.8.0 → 0.10.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 (155) hide show
  1. package/README.md +9 -5
  2. package/dist/alerting/alert-manager.d.ts +3 -1
  3. package/dist/alerting/alert-manager.js +5 -2
  4. package/dist/cli/generated/operation-client.generated.d.ts +1 -0
  5. package/dist/cli-main.js +78 -78
  6. package/dist/core/context/durable-operations.js +1 -1
  7. package/dist/core/context/index.d.ts +3 -1
  8. package/dist/core/context/index.js +10 -0
  9. package/dist/core/context/operation-request.d.ts +4 -0
  10. package/dist/core/context/parallel-cache-entry.d.ts +1 -1
  11. package/dist/core/context/parallel-cache-entry.js +12 -8
  12. package/dist/core/context/parallel-operations.d.ts +1 -1
  13. package/dist/core/context/parallel-operations.js +50 -8
  14. package/dist/core/context/run-operation.js +3 -2
  15. package/dist/core/context/speculative-child.js +1 -0
  16. package/dist/core/context/types.d.ts +1 -0
  17. package/dist/core/engine/activity-heartbeat-tracking.d.ts +1 -1
  18. package/dist/core/engine/activity-heartbeat-tracking.js +5 -1
  19. package/dist/core/engine/anonymous-signal-sequence.js +0 -2
  20. package/dist/core/engine/async-activity-completion.d.ts +24 -54
  21. package/dist/core/engine/async-activity-completion.js +25 -90
  22. package/dist/core/engine/async-activity-records.d.ts +106 -0
  23. package/dist/core/engine/async-activity-records.js +124 -0
  24. package/dist/core/engine/bulk-operations-purge.js +2 -1
  25. package/dist/core/engine/bulk-operations.js +10 -29
  26. package/dist/core/engine/callback-creators-bundles.js +1 -1
  27. package/dist/core/engine/callback-creators-core.js +1 -0
  28. package/dist/core/engine/construction.d.ts +1 -3
  29. package/dist/core/engine/construction.js +2 -5
  30. package/dist/core/engine/deferred-consume-envelope.d.ts +10 -1
  31. package/dist/core/engine/deferred-consume-envelope.js +9 -1
  32. package/dist/core/engine/disposal.js +1 -0
  33. package/dist/core/engine/engine-internal-types.d.ts +2 -0
  34. package/dist/core/engine/engine-runtime-helpers.d.ts +8 -0
  35. package/dist/core/engine/engine-runtime-helpers.js +9 -0
  36. package/dist/core/engine/errors.js +1 -1
  37. package/dist/core/engine/index.d.ts +12 -2
  38. package/dist/core/engine/index.js +35 -16
  39. package/dist/core/engine/inline-launch-queue.js +1 -1
  40. package/dist/core/engine/internals.d.ts +25 -3
  41. package/dist/core/engine/lifecycle/fork-helpers.js +1 -0
  42. package/dist/core/engine/lifecycle/recovered-services.d.ts +4 -2
  43. package/dist/core/engine/lifecycle/recovered-services.js +37 -7
  44. package/dist/core/engine/lifecycle/resume.d.ts +2 -2
  45. package/dist/core/engine/lifecycle/resume.js +45 -7
  46. package/dist/core/engine/lifecycle/shared.d.ts +34 -0
  47. package/dist/core/engine/lifecycle/start-exec.d.ts +2 -2
  48. package/dist/core/engine/lifecycle/start-exec.js +6 -4
  49. package/dist/core/engine/lifecycle/start.js +1 -0
  50. package/dist/core/engine/lifecycle/transition.d.ts +1 -1
  51. package/dist/core/engine/lifecycle/transition.js +9 -3
  52. package/dist/core/engine/lifecycle.d.ts +1 -1
  53. package/dist/core/engine/memo-durable-activity.js +8 -10
  54. package/dist/core/engine/operations-activity.d.ts +1 -1
  55. package/dist/core/engine/operations-activity.js +8 -6
  56. package/dist/core/engine/operations-coordination.d.ts +15 -1
  57. package/dist/core/engine/operations-coordination.js +43 -10
  58. package/dist/core/engine/operations-time.d.ts +2 -2
  59. package/dist/core/engine/operations-time.js +40 -6
  60. package/dist/core/engine/ownership-options.d.ts +1 -0
  61. package/dist/core/engine/ownership-options.js +14 -0
  62. package/dist/core/engine/retention.js +4 -0
  63. package/dist/core/engine/schedule-run-metadata.d.ts +3 -0
  64. package/dist/core/engine/schedule-run-metadata.js +29 -0
  65. package/dist/core/engine/schedule-run.js +19 -4
  66. package/dist/core/engine/schedules.js +7 -3
  67. package/dist/core/engine/sub-operation.js +6 -13
  68. package/dist/core/engine/termination/cleanup.js +18 -14
  69. package/dist/core/engine/termination/finalizer-activity.d.ts +1 -1
  70. package/dist/core/engine/termination/finalizer-activity.js +5 -1
  71. package/dist/core/engine/termination/finalizer.js +1 -1
  72. package/dist/core/engine/validation/schedule-options.d.ts +9 -0
  73. package/dist/core/engine/validation/schedule-options.js +59 -0
  74. package/dist/core/engine/validation/schedule.d.ts +3 -8
  75. package/dist/core/engine/validation/schedule.js +40 -56
  76. package/dist/core/engine/validation.js +1 -0
  77. package/dist/core/execution-strategy.d.ts +1 -0
  78. package/dist/core/inline-execution-strategy.context-options.d.ts +1 -0
  79. package/dist/core/inline-execution-strategy.context-options.js +3 -0
  80. package/dist/core/types/activity.d.ts +11 -0
  81. package/dist/core/types/checkpoint.d.ts +1 -0
  82. package/dist/core/types/options.d.ts +12 -0
  83. package/dist/core/types/schedules.d.ts +10 -0
  84. package/dist/core/types/services-resolution.d.ts +8 -3
  85. package/dist/core/types/state.d.ts +6 -0
  86. package/dist/core/types/workflow-builder.d.ts +2 -2
  87. package/dist/core/types/workflow-context.d.ts +40 -0
  88. package/dist/core/worker-execution-strategy.d.ts +1 -0
  89. package/dist/core/worker-inbound-message.d.ts +1 -0
  90. package/dist/core/worker-inbound-message.js +3 -0
  91. package/dist/index.d.ts +2 -2
  92. package/dist/json-schema.js +2 -2
  93. package/dist/mcp/cli.js +32 -32
  94. package/dist/observability/index.js +2 -2
  95. package/dist/runtime/portable.d.ts +1 -1
  96. package/dist/server/authorization.d.ts +3 -3
  97. package/dist/server/fault-to-json-rpc.d.ts +2 -1
  98. package/dist/server/handler.js +28 -28
  99. package/dist/server/index.d.ts +2 -0
  100. package/dist/server/index.js +30 -30
  101. package/dist/server/json-rpc-dispatch.d.ts +2 -2
  102. package/dist/server/json-rpc-parse.d.ts +1 -1
  103. package/dist/server/json-rpc-protocol.d.ts +2 -2
  104. package/dist/server/json-rpc-websocket.d.ts +1 -1
  105. package/dist/server/operation-fault.d.ts +3 -1
  106. package/dist/server/operations/async-activity.js +2 -2
  107. package/dist/server/operations/create-schedule.d.ts +2 -0
  108. package/dist/server/operations/create-schedule.js +13 -2
  109. package/dist/server/principal.d.ts +2 -2
  110. package/dist/server/rest-binding.d.ts +1 -1
  111. package/dist/server/runtime/task-dispatch.js +6 -0
  112. package/dist/server/runtime/task-polling.js +9 -1
  113. package/dist/server/runtime/task-reconciliation.js +2 -1
  114. package/dist/server/stdio-session.d.ts +4 -4
  115. package/dist/server/task-queue-types.d.ts +2 -0
  116. package/dist/server/task-state.d.ts +4 -0
  117. package/dist/service-worker/index.d.ts +15 -14
  118. package/dist/service-worker/index.js +28 -28
  119. package/dist/service-worker/scheduler.d.ts +41 -1
  120. package/dist/storage/auto.d.ts +36 -18
  121. package/dist/storage/auto.js +1 -1
  122. package/dist/storage/compressed-storage.js +1 -1
  123. package/dist/storage/http.js +2 -2
  124. package/dist/storage/index.d.ts +1 -0
  125. package/dist/storage/indexeddb.d.ts +6 -1
  126. package/dist/storage/indexeddb.js +1 -1
  127. package/dist/storage/interface.d.ts +1 -0
  128. package/dist/storage/interface.js +1 -1
  129. package/dist/storage/lmdb.js +1 -1
  130. package/dist/storage/memory.js +1 -1
  131. package/dist/storage/neon.js +1 -1
  132. package/dist/storage/resolve.js +1 -1
  133. package/dist/storage/scoped-storage.js +1 -1
  134. package/dist/storage/testing.js +1 -1
  135. package/dist/storage/turso.js +1 -1
  136. package/dist/storage/typed-storage.js +1 -1
  137. package/dist/storage/web-extension.d.ts +1 -1
  138. package/dist/storage/web-extension.js +1 -1
  139. package/dist/testing/index.js +33 -33
  140. package/dist/version.d.ts +1 -1
  141. package/dist/version.js +1 -1
  142. package/dist/worker/execute-with-interceptors.d.ts +9 -3
  143. package/dist/worker/execute-with-interceptors.js +14 -2
  144. package/dist/worker/long-poll.d.ts +2 -1
  145. package/dist/worker/long-poll.js +1 -1
  146. package/dist/worker/protocol-messages.d.ts +2 -0
  147. package/dist/worker/protocol-schemas.d.ts +8 -0
  148. package/dist/worker/protocol-schemas.js +1 -0
  149. package/dist/worker/protocol.js +1 -1
  150. package/dist/worker/remote-activity-context.d.ts +2 -0
  151. package/dist/workers/activity-runner.d.ts +2 -0
  152. package/dist/workers/activity-runner.js +9 -1
  153. package/dist/workers/workflow-runner.d.ts +2 -1
  154. package/dist/workers/workflow-runner.js +1 -0
  155. package/package.json +2 -2
@@ -11,7 +11,7 @@ export function prepareSleepOperation(internals, duration) {
11
11
  console.log(` \u2192 Creating checkpoint at step ${step}`);
12
12
  console.log(` \u2192 Scheduling timer for ${milliseconds}ms`);
13
13
  }
14
- const operationId = crypto.randomUUID(), callerStack = captureCallerStack(), referenceTime = internals.sleepReferenceTime ?? internals.getNow();
14
+ const operationId = `${internals.context.workflowId}:${step}`, callerStack = captureCallerStack(), referenceTime = internals.sleepReferenceTime ?? internals.getNow();
15
15
  internals.sleepReferenceTime = void 0;
16
16
  return {
17
17
  cached: !1,
@@ -1,6 +1,6 @@
1
1
  import type { ComposedWorkflowInterceptor } from '../interceptor.ts';
2
2
  import type { HumanReviewOptions, HumanReviewResult } from '../review/index.ts';
3
- import type { ActivityCallable, ActivityCallOptions, AwaitChildWorkflowOptions, ChildWorkflowHandle, ChildWorkflowTarget, DetachedChildWorkflowOptions, Duration, QueryDefinition, RunAllResult, SearchAttributeHandle, SearchAttributeValue, SignalDefinition, UpdateDefinition, WorkflowContext, WorkflowLogger, WorkflowMapOptions, WorkflowOperation, WorkflowOperationTupleResult, WorkflowPipeStageDefinition, WorkflowReduceInput, WorkflowReduceOptions, WorkflowRunAllBranch, WorkflowStateNamespace } from '../types.ts';
3
+ import type { ActivityCallable, ActivityCallOptions, AwaitChildWorkflowOptions, ChildWorkflowHandle, ChildWorkflowTarget, DetachedChildWorkflowOptions, Duration, QueryDefinition, RunAllResult, SearchAttributeHandle, SearchAttributeValue, SignalDefinition, UpdateDefinition, WorkflowContext, WorkflowKeyedRaceResult, WorkflowLogger, WorkflowMapOptions, WorkflowOperation, WorkflowOperationTupleResult, WorkflowPipeStageDefinition, WorkflowReduceInput, WorkflowReduceOptions, WorkflowRunAllBranch, WorkflowStateNamespace } from '../types.ts';
4
4
  import type { ContextOperationRequest } from './operation-request.ts';
5
5
  import type { ContextOptions, ErasedSagaStep, OffloadReference, StreamReference, StreamSink } from './types.ts';
6
6
  import * as contextUpdates from './updates.ts';
@@ -23,6 +23,7 @@ export declare function setContextWorkflowInterceptor(context: Context, workflow
23
23
  export declare class Context implements WorkflowContext {
24
24
  #private;
25
25
  readonly workflowId: string;
26
+ readonly workflowExecutionToken: string | undefined;
26
27
  readonly workflowType: string;
27
28
  readonly startedAt: number;
28
29
  readonly signal: AbortSignal;
@@ -81,6 +82,7 @@ export declare class Context implements WorkflowContext {
81
82
  review(options: HumanReviewOptions): Generator<ContextOperationRequest, HumanReviewResult, unknown>;
82
83
  all<const TOperations extends readonly WorkflowOperation<unknown>[]>(operations: TOperations): Generator<ContextOperationRequest, WorkflowOperationTupleResult<TOperations>, unknown>;
83
84
  race<const TOperations extends readonly WorkflowOperation<unknown>[]>(operations: TOperations): Generator<ContextOperationRequest, WorkflowOperationTupleResult<TOperations>[number], unknown>;
85
+ raceKeyed<const TOperations extends Readonly<Record<string, WorkflowOperation<unknown>>>>(operations: TOperations): Generator<ContextOperationRequest, WorkflowKeyedRaceResult<TOperations>, unknown>;
84
86
  memo<T>(key: string, fn: () => T | Promise<T>): Generator<ContextOperationRequest, T, unknown>;
85
87
  offload<T>(key: string, fn: () => Promise<T>): Generator<ContextOperationRequest, OffloadReference, unknown>;
86
88
  stream(key: string, fn: (sink: StreamSink) => AsyncGenerator<unknown, void, unknown>): Generator<ContextOperationRequest, StreamReference, unknown>;
@@ -21,12 +21,14 @@ export function setContextWorkflowInterceptor(context, workflowInterceptor) {
21
21
 
22
22
  export class Context {
23
23
  workflowId;
24
+ workflowExecutionToken;
24
25
  workflowType;
25
26
  startedAt;
26
27
  signal;
27
28
  #log;
28
29
  constructor(options) {
29
30
  this.workflowId = options.workflowId;
31
+ this.workflowExecutionToken = options.workflowExecutionToken;
30
32
  this.workflowType = options.workflowType;
31
33
  this.startedAt = options.startedAt;
32
34
  this.signal = options.abortController.signal;
@@ -154,6 +156,14 @@ export class Context {
154
156
  *race(operations) {
155
157
  return yield* parallelOperations.race(this, getInternals(this), [...operations]);
156
158
  }
159
+ *raceKeyed(operations) {
160
+ if (Object.getOwnPropertySymbols(operations).length > 0)
161
+ throw TypeError("ctx.raceKeyed branch names must be strings or numbers, not symbols");
162
+ const entries = Object.entries(operations);
163
+ if (entries.length === 0)
164
+ throw TypeError("ctx.raceKeyed requires at least one branch");
165
+ return yield* parallelOperations.race(this, getInternals(this), entries.map(([, operation]) => operation), entries.map(([key]) => key));
166
+ }
157
167
  *memo(key, fn) {
158
168
  return yield* parallelOperations.memo(this, getInternals(this), key, fn);
159
169
  }
@@ -30,6 +30,8 @@ export type ContextOperationRequest = {
30
30
  activityName: string;
31
31
  fn?: (input: unknown, context?: unknown) => unknown;
32
32
  input: unknown;
33
+ /** Immutable owner token for this workflow run, used to fence external side effects. */
34
+ workflowExecutionToken?: string;
33
35
  callerStack?: string;
34
36
  options?: Record<string, unknown>;
35
37
  /** Dispatch attempt used for worker payloads, interceptors, and reconciliation verifiers. */
@@ -125,6 +127,8 @@ export type ContextOperationRequest = {
125
127
  type: 'race';
126
128
  operationId: string;
127
129
  operations: ContextOperationRequest[];
130
+ /** Ordered branch names for `ctx.raceKeyed`; absent for positional `ctx.race`. */
131
+ branchNames?: string[];
128
132
  callerStack?: string;
129
133
  } | {
130
134
  type: 'memo';
@@ -57,7 +57,7 @@ export type ParallelOperationCacheEntry = {
57
57
  formatVersion: 2;
58
58
  variant: 'all' | 'race' | 'run-all';
59
59
  branches: ParallelBranchSlot[];
60
- /** Ordered key list for `run-all`; absent for `all` and `race`. */
60
+ /** Ordered key list for `run-all` or `raceKeyed`; absent for `all` and positional `race`. */
61
61
  branchNames?: string[];
62
62
  subOperationCount: number;
63
63
  };
@@ -41,20 +41,24 @@ function hasParallelOperationCacheMarker(value) {
41
41
  return value !== null && typeof value === "object" && value.__weftParallelOperationCache === !0;
42
42
  }
43
43
  function hasValidBranchTopology(variant, branches, subOperationCount, branchNames) {
44
- if (variant === "race") {
45
- if (branches.length !== 1 || subOperationCount < 1)
46
- return !1;
47
- const winner = branches[0];
48
- if (winner == null || typeof winner !== "object")
49
- return !1;
50
- return winner.status === "fulfilled" && branchNames === void 0;
51
- }
44
+ if (variant === "race")
45
+ return hasValidRaceTopology(branches, subOperationCount, branchNames);
52
46
  if (branches.length !== subOperationCount)
53
47
  return !1;
54
48
  if (variant === "run-all")
55
49
  return Array.isArray(branchNames) && branchNames.length === subOperationCount;
56
50
  return branchNames === void 0;
57
51
  }
52
+ function hasValidRaceTopology(branches, subOperationCount, branchNames) {
53
+ if (branches.length !== 1 || subOperationCount < 1)
54
+ return !1;
55
+ const winner = branches[0];
56
+ if (winner == null || typeof winner !== "object")
57
+ return !1;
58
+ if (winner.status !== "fulfilled")
59
+ return !1;
60
+ return branchNames === void 0 || Array.isArray(branchNames) && branchNames.length === subOperationCount;
61
+ }
58
62
  export function isParallelOperationCacheEntry(value) {
59
63
  if (value === null || typeof value !== "object")
60
64
  return !1;
@@ -4,6 +4,6 @@ import type { ContextOperationRequest } from './operation-request.ts';
4
4
  import { assertValidParallelOperationCacheEntry, BranchTopologyChangedError, createParallelOperationCacheEntry, isParallelOperationCacheEntry, type ParallelBranchSlot, type ParallelOperationCacheEntry } from './parallel-cache-entry.ts';
5
5
  export { assertValidParallelOperationCacheEntry, BranchTopologyChangedError, createParallelOperationCacheEntry, isParallelOperationCacheEntry, type ParallelBranchSlot, type ParallelOperationCacheEntry, };
6
6
  export declare function all(context: Context, internals: ContextInternals, operations: Generator<ContextOperationRequest, unknown, unknown>[]): Generator<ContextOperationRequest, unknown[], unknown>;
7
- export declare function race(context: Context, internals: ContextInternals, operations: Generator<ContextOperationRequest, unknown, unknown>[]): Generator<ContextOperationRequest, unknown, unknown>;
7
+ export declare function race(context: Context, internals: ContextInternals, operations: Generator<ContextOperationRequest, unknown, unknown>[], branchNames?: string[]): Generator<ContextOperationRequest, unknown, unknown>;
8
8
  export declare function memo<T>(context: Context, internals: ContextInternals, key: string, fn: () => T | Promise<T>): Generator<ContextOperationRequest, T, unknown>;
9
9
  export declare function runAll<T extends Record<string, readonly [Function] | readonly [Function, unknown]>>(context: Context, internals: ContextInternals, branches: T): Generator<ContextOperationRequest, Record<keyof T, unknown>, unknown>;
@@ -83,7 +83,7 @@ function buildFulfilledAllEntry(values, operationId, subOperationCount) {
83
83
  operationId: `${operationId}:${i}`
84
84
  })), subOperationCount);
85
85
  }
86
- export function* race(context, internals, operations) {
86
+ export function* race(context, internals, operations, branchNames) {
87
87
  const step = internals.stepIndex++;
88
88
  if (internals.accumulatedResults?.has(step)) {
89
89
  const cached = internals.accumulatedResults.get(step);
@@ -91,32 +91,74 @@ export function* race(context, internals, operations) {
91
91
  if (isParallelOperationCacheEntry(cached)) {
92
92
  if (cached.variant !== "race")
93
93
  throw new BranchTopologyChangedError(`ctx.race step ${step} found a cached entry of variant '${cached.variant}'. The same step must use the same parallel primitive across retries.`);
94
- if (operations.length !== cached.subOperationCount)
95
- throw new BranchTopologyChangedError(`ctx.race branch count changed across retry: expected ${cached.subOperationCount}, got ${operations.length}. Branch count must be deterministic.`);
94
+ assertRaceBranchTopology(operations.length, branchNames, cached);
96
95
  const winner = cached.branches[0];
97
- if (winner?.status !== "fulfilled")
98
- throw new BranchTopologyChangedError(`ctx.race step ${step} cached entry has no fulfilled winner slot \u2014 entry is malformed.`);
99
96
  internals.stepIndex += cached.subOperationCount;
100
97
  return winner.value;
101
98
  }
99
+ if (branchNames !== void 0)
100
+ throw new BranchTopologyChangedError(`ctx.raceKeyed step ${step} found a raw cached race value without keyed branch topology. The same step must use raceKeyed across retries.`);
102
101
  return cached;
103
102
  }
104
- const subOperations = primeParallelOperations(operations), operationId = `race:${step}`;
103
+ const operationId = `race:${step}`;
104
+ let subOperations;
105
+ if (branchNames === void 0)
106
+ subOperations = primeParallelOperations(operations);
107
+ else {
108
+ const primed = primeKeyedRaceOperations(operations);
109
+ subOperations = primed.subOperations;
110
+ if (primed.synchronousWinner !== void 0) {
111
+ const result = { key: branchNames[primed.synchronousWinner.index], value: primed.synchronousWinner.value };
112
+ cacheRaceWinner(context, step, result, operationId, operations.length, branchNames);
113
+ return result;
114
+ }
115
+ }
105
116
  stampDeterministicOperationIds(subOperations, operationId);
106
117
  const callerStack = captureCallerStack(), result = yield {
107
118
  type: "race",
108
119
  operationId,
109
120
  operations: subOperations,
121
+ ...branchNames !== void 0 ? { branchNames } : {},
110
122
  callerStack
111
123
  };
124
+ cacheRaceWinner(context, step, result, operationId, subOperations.length, branchNames);
125
+ return result;
126
+ }
127
+ function primeKeyedRaceOperations(operations) {
128
+ const subOperations = [];
129
+ let synchronousWinner;
130
+ for (const [index, operation] of operations.entries()) {
131
+ const primed = operation.next();
132
+ if (primed.done)
133
+ synchronousWinner ??= { index, value: primed.value };
134
+ else
135
+ subOperations.push(primed.value);
136
+ }
137
+ return { subOperations, synchronousWinner };
138
+ }
139
+ function cacheRaceWinner(context, step, result, operationId, subOperationCount, branchNames) {
112
140
  context.accumulatedResults.set(step, {
113
141
  __weftParallelOperationCache: !0,
114
142
  formatVersion: 2,
115
143
  variant: "race",
116
144
  branches: [{ status: "fulfilled", value: result, operationId: `${operationId}:winner` }],
117
- subOperationCount: subOperations.length
145
+ ...branchNames !== void 0 ? { branchNames } : {},
146
+ subOperationCount
118
147
  });
119
- return result;
148
+ }
149
+ function sameBranchNames(left, right) {
150
+ if (left === void 0 || right === void 0)
151
+ return left === right;
152
+ return left.length === right.length && left.every((name, index) => name === right[index]);
153
+ }
154
+ function assertRaceBranchTopology(operationCount, branchNames, cached) {
155
+ if (operationCount !== cached.subOperationCount)
156
+ throw new BranchTopologyChangedError(`ctx.race branch count changed across retry: expected ${cached.subOperationCount}, got ${operationCount}. Branch count must be deterministic.`);
157
+ if (!sameBranchNames(branchNames, cached.branchNames))
158
+ throw new BranchTopologyChangedError(`ctx.race branch names changed across retry: expected ${formatBranchNames(cached.branchNames)}, got ${formatBranchNames(branchNames)}. Branch names and order must be deterministic.`);
159
+ }
160
+ function formatBranchNames(branchNames) {
161
+ return branchNames === void 0 ? "positional branches" : JSON.stringify(branchNames);
120
162
  }
121
163
  function stampDeterministicOperationIds(subOperations, parentOperationId) {
122
164
  for (let i = 0;i < subOperations.length; i++) {
@@ -74,7 +74,7 @@ function getActivityName(activity, explicitName) {
74
74
  function getActivityFunction(activity) {
75
75
  return typeof activity === "function" ? activity : void 0;
76
76
  }
77
- function createFreshRunActivityRequest(internals, step, activityName, activityFunction, input, options, activityStateKey) {
77
+ function createFreshRunActivityRequest(internals, step, activityName, activityFunction, input, options, activityStateKey, workflowExecutionToken) {
78
78
  const queue = options?.queue ?? "default";
79
79
  if (internals.explainMode) {
80
80
  console.log(`[weft] ctx.run(${activityName}, ${JSON.stringify(input)})`);
@@ -87,6 +87,7 @@ function createFreshRunActivityRequest(internals, step, activityName, activityFu
87
87
  activityName,
88
88
  step,
89
89
  ...activityStateKey === void 0 ? {} : { activityStateKey },
90
+ ...workflowExecutionToken === void 0 ? {} : { workflowExecutionToken },
90
91
  ...activityFunction !== void 0 ? { fn: activityFunction } : {},
91
92
  input,
92
93
  callerStack: captureCallerStack(),
@@ -141,7 +142,7 @@ export function createRunActivityRequestAtStep(context, activity, rest, step, co
141
142
  return cachedRequest;
142
143
  const retryPolicy = resolveActivityRetryPolicy(activity, options), scheduleToCloseTimeout = resolveActivityScheduleToCloseTimeout(activity, options);
143
144
  return {
144
- request: createFreshRunActivityRequest(internals, step, activityName, activityFunction, input, resolveDispatchedActivityOptions(activity, options), configuration.activityStateKey),
145
+ request: createFreshRunActivityRequest(internals, step, activityName, activityFunction, input, resolveDispatchedActivityOptions(activity, options), configuration.activityStateKey, context.workflowExecutionToken),
145
146
  step,
146
147
  retryStateKey,
147
148
  ...cacheResultStep === void 0 ? {} : { cacheResultStep },
@@ -24,6 +24,7 @@ function assignOptionalContextOptions(options, internals) {
24
24
  function createSpeculativeChildOptions(parent, internals) {
25
25
  const options = {
26
26
  workflowId: parent.workflowId,
27
+ ...parent.workflowExecutionToken === void 0 ? {} : { workflowExecutionToken: parent.workflowExecutionToken },
27
28
  workflowType: parent.workflowType,
28
29
  startedAt: parent.startedAt,
29
30
  abortController: internals.abortController,
@@ -150,6 +150,7 @@ export interface StreamSink {
150
150
  */
151
151
  export interface ContextOptions {
152
152
  workflowId: string;
153
+ workflowExecutionToken?: string;
153
154
  workflowType: string;
154
155
  startedAt: number;
155
156
  abortController: AbortController;
@@ -52,4 +52,4 @@ export declare function warnIfRetryMissingHeartbeat(internals: EngineInternals,
52
52
  * tracking above; `completeAsync` is supplied by the caller (it owns the
53
53
  * async-completion token machinery).
54
54
  */
55
- export declare function buildActivityContext(internals: EngineInternals, workflowId: string, step: ActivityHeartbeatKey, signal: AbortSignal, completeAsync: () => never): ActivityContext;
55
+ export declare function buildActivityContext(internals: EngineInternals, workflowId: string, workflowExecutionToken: string | undefined, step: ActivityHeartbeatKey, attempt: number, signal: AbortSignal, completeAsync: () => never): ActivityContext;
@@ -35,9 +35,13 @@ export function warnIfRetryMissingHeartbeat(internals, workflowId, step, attempt
35
35
  return;
36
36
  internals.engine.dispatchEvent(new DevelopmentWarningEvent(workflowId, `Activity retry (attempt ${attempt}) at ${activityHeartbeatKeyLabel(step)} has no lastHeartbeatDetails. Either the previous attempt never recorded heartbeat details (it never called heartbeat(), or called it with no details), or the engine process restarted and discarded the in-memory heartbeat (it is not durable). The resumable-batch pattern only resumes across in-process retries; design the activity to restart cleanly when lastHeartbeatDetails is undefined.`, [activityHeartbeatFieldPath(step)]));
37
37
  }
38
- export function buildActivityContext(internals, workflowId, step, signal, completeAsync) {
38
+ export function buildActivityContext(internals, workflowId, workflowExecutionToken, step, attempt, signal, completeAsync) {
39
39
  return {
40
40
  signal,
41
+ ...workflowExecutionToken !== void 0 && {
42
+ workflowExecutionToken,
43
+ activityAttemptToken: `${workflowExecutionToken}:activity:${String(step)}:${String(attempt)}`
44
+ },
41
45
  lastHeartbeatDetails: readLastHeartbeatForStep(internals, workflowId, step),
42
46
  heartbeat: (details) => {
43
47
  internals.heartbeatDetails.set(workflowId, details);
@@ -94,8 +94,6 @@ function extractAnonymousSignalSequence(key) {
94
94
  return Number.isSafeInteger(sequence) && sequence >= 0 ? sequence : null;
95
95
  }
96
96
  function decodeSignalSequence(bytes) {
97
- if (bytes === null)
98
- return 0;
99
97
  const value = decode(bytes);
100
98
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
101
99
  throw Error("Stored anonymous signal sequence must be a non-negative safe integer");
@@ -26,9 +26,33 @@
26
26
  * arrives after token recovery but before replay has adopted the workflow
27
27
  * generator, the engine buffers the completion or failure outcome and drains it
28
28
  * when replay reaches the same deterministic token.
29
+ *
30
+ * Acknowledgement durability: `completeAsyncActivity` / `failAsyncActivity`
31
+ * resolve only after ONE fenced batch has durably (a) deleted the single-use
32
+ * token record and (b) written a resolution record
33
+ * ({@link KEYS.asyncActivityResolution}) carrying the supplied outcome. A crash
34
+ * any time after the acknowledgement therefore cannot lose the outcome:
35
+ * recovery reloads the resolution record, queues it, and redelivers it when
36
+ * replay re-parks on the same deterministic token. The resolution record is
37
+ * deleted through the atomic side-effect buffer, so in the normal case it rides
38
+ * the very checkpoint that records the resumed result; a record whose delete
39
+ * never commits is simply redelivered (idempotent for a deterministic token) or
40
+ * swept by terminal cleanup/purge. If the acknowledgement batch itself fails,
41
+ * the in-memory token claim is restored and the error propagates — the caller
42
+ * learns the completion did NOT stick and can retry the still-live token.
43
+ *
44
+ * One caveat survives a crash: the failure path's raw thrown reason
45
+ * (`originalReason`) is delivered as-is only within the acknowledging process.
46
+ * A redelivery after recovery reconstructs the error from the persisted outcome
47
+ * (message, name, failure category) — the same fidelity the worker resume path
48
+ * has always had.
49
+ *
50
+ * The persisted record shapes, decode guards, key derivations, and the queued
51
+ * resolution buffer live in `async-activity-records.ts`.
29
52
  */
30
53
  import type { OperationOutcome } from '../types.ts';
31
54
  import { WeftError } from '../weft-error.ts';
55
+ import { type PendingAsyncActivity } from './async-activity-records.ts';
32
56
  import type { EngineInternals } from './internals.ts';
33
57
  type AsyncActivityResolutionCallbacks = {
34
58
  feedOperationResult: (workflowId: string, outcome: OperationOutcome, originalReason?: {
@@ -36,12 +60,6 @@ type AsyncActivityResolutionCallbacks = {
36
60
  }) => void;
37
61
  finalizeTimeline: (workflowId: string, status: 'completed' | 'failed', output: unknown) => void;
38
62
  };
39
- /**
40
- * Per-workflow prefix for all async-activity storage keys. Used by cleanup and
41
- * purge paths that need to sweep every async-activity record for a workflow
42
- * without enumerating individual tokens.
43
- */
44
- export declare function asyncActivityWorkflowPrefix(workflowId: string): string;
45
63
  /**
46
64
  * Sentinel thrown by `ActivityContext.completeAsync()` to signal that the
47
65
  * activity is handing off to an out-of-band completion. The engine recognizes
@@ -73,48 +91,6 @@ export declare class AsyncActivityTokenNotFoundError extends WeftError<'AsyncAct
73
91
  readonly token: string;
74
92
  constructor(token: string);
75
93
  }
76
- /**
77
- * In-memory record of an activity that deferred to out-of-band completion and
78
- * is awaiting `completeAsyncActivity` / `failAsyncActivity`.
79
- */
80
- export type PendingAsyncActivity = {
81
- readonly token: string;
82
- readonly workflowId: string;
83
- readonly activityName: string;
84
- readonly operationId: string;
85
- readonly step: number;
86
- readonly attempt: number;
87
- readonly createdAt: number;
88
- };
89
- export type PendingAsyncActivityResolution = {
90
- readonly token: string;
91
- readonly outcome: OperationOutcome;
92
- readonly originalReason?: {
93
- value: unknown;
94
- };
95
- readonly timelineStatus: 'completed' | 'failed';
96
- readonly timelineOutput: unknown;
97
- };
98
- /**
99
- * Derive the durable, deterministic task token for an async activity.
100
- *
101
- * The token is anchored to the workflow id, the activity state key, and the
102
- * dispatch attempt — all of which are stable across replay — so a workflow that
103
- * crashes while parked on an async activity mints the identical token after
104
- * recovery. Plain `ctx.run()` uses the workflow step as the state key.
105
- * `operationId` is deliberately excluded because it is regenerated on every
106
- * yield and would change on replay.
107
- */
108
- export declare function deriveAsyncActivityToken(workflowId: string, step: number | string, attempt: number): string;
109
- /**
110
- * Register a deferred activity: record it in memory and durably, then announce
111
- * the token via {@link ActivityAsyncPendingEvent}. Idempotent on `token`: if the
112
- * token is already registered (e.g. because `recoverPendingAsyncActivities` loaded
113
- * it before the workflow replayed and re-deferred), the durable record is
114
- * refreshed but the event is NOT re-emitted, preventing duplicate side-effects
115
- * (e.g. re-sending a webhook notification) on replay.
116
- */
117
- export declare function registerPendingAsyncActivity(internals: EngineInternals, pending: PendingAsyncActivity): Promise<void>;
118
94
  /**
119
95
  * Park an activity that threw {@link AsyncActivityDeferral}: register the
120
96
  * pending entry durably and return a promise that never settles, so the
@@ -123,12 +99,6 @@ export declare function registerPendingAsyncActivity(internals: EngineInternals,
123
99
  * one-liner.
124
100
  */
125
101
  export declare function parkDeferredAsyncActivity(internals: EngineInternals, deferral: AsyncActivityDeferral, details: Omit<PendingAsyncActivity, 'token' | 'createdAt'>, callbacks: AsyncActivityResolutionCallbacks): Promise<never>;
126
- /**
127
- * Reload pending async-activity records from storage into memory. Called by
128
- * `recoverAll()` so a token minted before a crash is resolvable again — even
129
- * before the recovered workflow has replayed far enough to re-register it.
130
- */
131
- export declare function recoverPendingAsyncActivities(internals: EngineInternals): Promise<void>;
132
102
  /**
133
103
  * Complete a deferred activity out-of-band with `result`, resuming the parked
134
104
  * workflow as though the activity had returned `result` inline.
@@ -1,14 +1,15 @@
1
- import { KEYS, encodeStorageKeyComponent } from "../../storage/interface.js";
2
- import { decode, encode } from "../codec.js";
3
- import { ActivityAsyncPendingEvent } from "../events.js";
1
+ import { KEYS } from "../../storage/interface.js";
4
2
  import { assertPayloadWithinLimit } from "../payload-size.js";
5
3
  import { WeftError } from "../weft-error.js";
4
+ import {
5
+ buildAsyncActivityAcknowledgementOperations,
6
+ queuePendingAsyncActivityResolution,
7
+ registerPendingAsyncActivity,
8
+ shouldBufferPendingAsyncActivityResolution,
9
+ takePendingAsyncActivityResolution
10
+ } from "./async-activity-records.js";
6
11
  import { stageAtomicWorkflowCommitSideEffects } from "./checkpoint-side-effects.js";
7
12
  import { commitFencedEngineWrite } from "./fenced-write.js";
8
- const ASYNC_ACTIVITY_TOKEN_PREFIX = "async-act:v1", ASYNC_ACTIVITY_KEY_PREFIX = "async-act:v1:";
9
- export function asyncActivityWorkflowPrefix(workflowId) {
10
- return `${ASYNC_ACTIVITY_KEY_PREFIX}${encodeStorageKeyComponent(workflowId)}:`;
11
- }
12
13
 
13
14
  export class AsyncActivityDeferral extends Error {
14
15
  token;
@@ -26,43 +27,10 @@ export class AsyncActivityTokenNotFoundError extends WeftError {
26
27
  this.token = token;
27
28
  }
28
29
  }
29
- function isPersistedAsyncActivity(value) {
30
- if (typeof value !== "object" || value === null)
31
- return !1;
32
- const record = value;
33
- return record.version === 1 && typeof record.token === "string" && typeof record.workflowId === "string" && typeof record.activityName === "string" && typeof record.operationId === "string" && typeof record.step === "number" && typeof record.attempt === "number" && typeof record.createdAt === "number";
34
- }
35
- export function deriveAsyncActivityToken(workflowId, step, attempt) {
36
- return `${ASYNC_ACTIVITY_TOKEN_PREFIX}:${workflowId}:${step}:${attempt}`;
37
- }
38
- function buildPersistPendingAsyncActivityOperation(pending) {
39
- const record = {
40
- version: 1,
41
- token: pending.token,
42
- workflowId: pending.workflowId,
43
- activityName: pending.activityName,
44
- operationId: pending.operationId,
45
- step: pending.step,
46
- attempt: pending.attempt,
47
- createdAt: pending.createdAt
48
- };
49
- return {
50
- type: "put",
51
- key: KEYS.asyncActivity(pending.workflowId, pending.token),
52
- value: encode(record)
53
- };
54
- }
55
- export async function registerPendingAsyncActivity(internals, pending) {
56
- const alreadyRegistered = internals.pendingAsyncActivities.has(pending.token);
57
- internals.pendingAsyncActivities.set(pending.token, pending);
58
- await commitFencedEngineWrite(internals, [buildPersistPendingAsyncActivityOperation(pending)], [], () => Error(`Async activity registration for token "${pending.token}" lost its precondition.`));
59
- if (!alreadyRegistered)
60
- internals.engine.dispatchEvent(new ActivityAsyncPendingEvent(pending.token, pending.operationId, pending.workflowId, pending.activityName, pending.attempt));
61
- }
62
30
  export async function parkDeferredAsyncActivity(internals, deferral, details, callbacks) {
63
31
  const queuedResolution = takePendingAsyncActivityResolution(internals, details.workflowId, deferral.token);
64
32
  if (queuedResolution !== void 0) {
65
- deliverPendingAsyncActivityResolution(details.workflowId, queuedResolution, callbacks);
33
+ deliverPendingAsyncActivityResolution(internals, details.workflowId, queuedResolution, callbacks);
66
34
  return new Promise(() => {});
67
35
  }
68
36
  await registerPendingAsyncActivity(internals, {
@@ -72,64 +40,31 @@ export async function parkDeferredAsyncActivity(internals, deferral, details, ca
72
40
  });
73
41
  return new Promise(() => {});
74
42
  }
75
- export async function recoverPendingAsyncActivities(internals) {
76
- for await (const [, bytes] of internals.storage.scan(ASYNC_ACTIVITY_KEY_PREFIX)) {
77
- const decoded = decode(bytes);
78
- if (!isPersistedAsyncActivity(decoded))
79
- continue;
80
- internals.pendingAsyncActivities.set(decoded.token, {
81
- token: decoded.token,
82
- workflowId: decoded.workflowId,
83
- activityName: decoded.activityName,
84
- operationId: decoded.operationId,
85
- step: decoded.step,
86
- attempt: decoded.attempt,
87
- createdAt: decoded.createdAt
88
- });
89
- }
90
- }
91
- async function consumePendingAsyncActivity(internals, token) {
43
+ async function consumePendingAsyncActivity(internals, token, outcome) {
92
44
  const pending = internals.pendingAsyncActivities.get(token);
93
45
  if (!pending)
94
46
  throw new AsyncActivityTokenNotFoundError(token);
95
47
  internals.pendingAsyncActivities.delete(token);
96
- stageAtomicWorkflowCommitSideEffects(internals, pending.workflowId, {
97
- conditions: [],
98
- operations: [{ type: "delete", key: KEYS.asyncActivity(pending.workflowId, token) }]
99
- });
48
+ try {
49
+ await commitFencedEngineWrite(internals, buildAsyncActivityAcknowledgementOperations(pending, outcome), [], () => Error(`Async activity acknowledgement for token "${token}" lost its precondition.`));
50
+ } catch (error) {
51
+ internals.pendingAsyncActivities.set(token, pending);
52
+ throw error;
53
+ }
100
54
  return pending;
101
55
  }
102
- function shouldBufferPendingAsyncActivityResolution(internals, workflowId) {
103
- return internals.inlineStrategy !== null && !internals.inlineStrategy.hasGenerator(workflowId);
104
- }
105
- function queuePendingAsyncActivityResolution(internals, workflowId, resolution) {
106
- internals.pendingAsyncActivityResolutions ??= new Map;
107
- const queued = internals.pendingAsyncActivityResolutions.get(workflowId) ?? [];
108
- queued.push(resolution);
109
- internals.pendingAsyncActivityResolutions.set(workflowId, queued);
110
- }
111
- function takePendingAsyncActivityResolution(internals, workflowId, token) {
112
- internals.pendingAsyncActivityResolutions ??= new Map;
113
- const queued = internals.pendingAsyncActivityResolutions.get(workflowId);
114
- if (queued === void 0)
115
- return;
116
- const index = queued.findIndex((resolution) => resolution.token === token);
117
- if (index === -1)
118
- return;
119
- const resolution = queued[index];
120
- if (resolution === void 0)
121
- return;
122
- queued.splice(index, 1);
123
- if (queued.length === 0)
124
- internals.pendingAsyncActivityResolutions.delete(workflowId);
125
- return resolution;
126
- }
127
- function deliverPendingAsyncActivityResolution(workflowId, resolution, callbacks) {
56
+ function deliverPendingAsyncActivityResolution(internals, workflowId, resolution, callbacks) {
57
+ stageAtomicWorkflowCommitSideEffects(internals, workflowId, {
58
+ conditions: [],
59
+ operations: [
60
+ { type: "delete", key: KEYS.asyncActivityResolution(workflowId, resolution.token) }
61
+ ]
62
+ });
128
63
  callbacks.finalizeTimeline(workflowId, resolution.timelineStatus, resolution.timelineOutput);
129
64
  callbacks.feedOperationResult(workflowId, resolution.outcome, resolution.originalReason);
130
65
  }
131
66
  async function resolvePendingAsyncActivity(internals, token, outcome, callbacks, originalReason) {
132
- const pending = await consumePendingAsyncActivity(internals, token), timelineOutput = outcome.status === "completed" ? outcome.value : outcome.error, resolution = {
67
+ const pending = await consumePendingAsyncActivity(internals, token, outcome), timelineOutput = outcome.status === "completed" ? outcome.value : outcome.error, resolution = {
133
68
  token,
134
69
  outcome,
135
70
  timelineStatus: outcome.status,
@@ -140,7 +75,7 @@ async function resolvePendingAsyncActivity(internals, token, outcome, callbacks,
140
75
  queuePendingAsyncActivityResolution(internals, pending.workflowId, resolution);
141
76
  return;
142
77
  }
143
- deliverPendingAsyncActivityResolution(pending.workflowId, resolution, callbacks);
78
+ deliverPendingAsyncActivityResolution(internals, pending.workflowId, resolution, callbacks);
144
79
  }
145
80
  export async function completeAsyncActivity(internals, token, result, callbacks) {
146
81
  assertPayloadWithinLimit(result, internals.options.payloadSizePolicy.maxBytes, "activity result");