@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
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Durable record layer for out-of-band ("async") activity completion.
3
+ *
4
+ * Two record shapes share the `async-act:v1:` keyspace, discriminated by their
5
+ * decode guards (never by key shape):
6
+ *
7
+ * - **Pending token records** ({@link KEYS.asyncActivity}): an activity that
8
+ * deferred via `ctx.completeAsync()` and is awaiting an external completion.
9
+ * - **Resolution records** ({@link KEYS.asyncActivityResolution}): an
10
+ * acknowledged completion or failure whose resumed-workflow checkpoint has
11
+ * not committed yet. Written atomically with the token-record delete so the
12
+ * acknowledgement is durable before the caller learns it succeeded.
13
+ *
14
+ * The completion orchestration (park, consume, deliver, resume) lives in
15
+ * `async-activity-completion.ts`; this module owns the persisted shapes, the
16
+ * key derivations, and the in-memory resolution queue that recovery drains.
17
+ */
18
+ import { type BatchOperation } from '../../storage/interface.ts';
19
+ import type { OperationOutcome } from '../types.ts';
20
+ import type { EngineInternals } from './internals.ts';
21
+ /**
22
+ * Storage-key prefix for durable async-activity records. Matches the base of
23
+ * {@link KEYS.asyncActivity}; the full key appends `<workflowId>:<token>` (and
24
+ * `:resolution` for resolution records). The trailing colon (absent from the
25
+ * token prefix) scopes the global recovery scan to record keys only.
26
+ */
27
+ export declare const ASYNC_ACTIVITY_KEY_PREFIX = "async-act:v1:";
28
+ /**
29
+ * Per-workflow prefix for all async-activity storage keys — pending token
30
+ * records AND resolution records. Used by cleanup and purge paths that need to
31
+ * sweep every async-activity record for a workflow without enumerating
32
+ * individual tokens.
33
+ */
34
+ export declare function asyncActivityWorkflowPrefix(workflowId: string): string;
35
+ /**
36
+ * In-memory record of an activity that deferred to out-of-band completion and
37
+ * is awaiting `completeAsyncActivity` / `failAsyncActivity`.
38
+ */
39
+ export type PendingAsyncActivity = {
40
+ readonly token: string;
41
+ readonly workflowId: string;
42
+ readonly activityName: string;
43
+ readonly operationId: string;
44
+ readonly step: number;
45
+ readonly attempt: number;
46
+ readonly createdAt: number;
47
+ };
48
+ /**
49
+ * In-memory form of an acknowledged outcome awaiting delivery into the workflow
50
+ * generator. `originalReason` (the raw thrown value on the failure path) exists
51
+ * only within the acknowledging process — it is not persisted, so a resolution
52
+ * reloaded by recovery reconstructs the error from the recorded outcome.
53
+ */
54
+ export type PendingAsyncActivityResolution = {
55
+ readonly token: string;
56
+ readonly outcome: OperationOutcome;
57
+ readonly originalReason?: {
58
+ value: unknown;
59
+ };
60
+ readonly timelineStatus: 'completed' | 'failed';
61
+ readonly timelineOutput: unknown;
62
+ };
63
+ /**
64
+ * Derive the durable, deterministic task token for an async activity.
65
+ *
66
+ * The token is anchored to the workflow id, the activity state key, and the
67
+ * dispatch attempt — all of which are stable across replay — so a workflow that
68
+ * crashes while parked on an async activity mints the identical token after
69
+ * recovery. Plain `ctx.run()` uses the workflow step as the state key.
70
+ * `operationId` is deliberately excluded because it is regenerated on every
71
+ * yield and would change on replay.
72
+ */
73
+ export declare function deriveAsyncActivityToken(workflowId: string, step: number | string, attempt: number): string;
74
+ /**
75
+ * Build the acknowledgement batch for a consumed token: delete the pending
76
+ * token record and persist the resolution record carrying `outcome`, in one
77
+ * batch, so the acknowledgement is durable before the caller learns it
78
+ * succeeded.
79
+ */
80
+ export declare function buildAsyncActivityAcknowledgementOperations(pending: PendingAsyncActivity, outcome: OperationOutcome): BatchOperation[];
81
+ /**
82
+ * Register a deferred activity: record it in memory and durably, then announce
83
+ * the token via {@link ActivityAsyncPendingEvent}. Idempotent on `token`: if the
84
+ * token is already registered (e.g. because `recoverPendingAsyncActivities` loaded
85
+ * it before the workflow replayed and re-deferred), the durable record is
86
+ * refreshed but the event is NOT re-emitted, preventing duplicate side-effects
87
+ * (e.g. re-sending a webhook notification) on replay.
88
+ */
89
+ export declare function registerPendingAsyncActivity(internals: EngineInternals, pending: PendingAsyncActivity): Promise<void>;
90
+ /**
91
+ * Reload async-activity records from storage into memory. Called by
92
+ * `recoverAll()` so a token minted before a crash is resolvable again — even
93
+ * before the recovered workflow has replayed far enough to re-register it —
94
+ * and so an acknowledged-but-not-yet-checkpointed resolution is redelivered
95
+ * when replay re-parks on the same deterministic token.
96
+ */
97
+ export declare function recoverPendingAsyncActivities(internals: EngineInternals): Promise<void>;
98
+ /**
99
+ * True when a resolution cannot be delivered yet because inline replay has not
100
+ * adopted the workflow generator (the post-recovery window).
101
+ */
102
+ export declare function shouldBufferPendingAsyncActivityResolution(internals: EngineInternals, workflowId: string): boolean;
103
+ /** Queue a resolution for delivery when replay reaches its token again. */
104
+ export declare function queuePendingAsyncActivityResolution(internals: EngineInternals, workflowId: string, resolution: PendingAsyncActivityResolution): void;
105
+ /** Take the queued resolution for `token`, if one is waiting. */
106
+ export declare function takePendingAsyncActivityResolution(internals: EngineInternals, workflowId: string, token: string): PendingAsyncActivityResolution | undefined;
@@ -0,0 +1,124 @@
1
+ import { KEYS, encodeStorageKeyComponent } from "../../storage/interface.js";
2
+ import { decode, encode } from "../codec.js";
3
+ import { ActivityAsyncPendingEvent } from "../events.js";
4
+ import { commitFencedEngineWrite } from "./fenced-write.js";
5
+ const ASYNC_ACTIVITY_TOKEN_PREFIX = "async-act:v1";
6
+ export const ASYNC_ACTIVITY_KEY_PREFIX = "async-act:v1:";
7
+ export function asyncActivityWorkflowPrefix(workflowId) {
8
+ return `${ASYNC_ACTIVITY_KEY_PREFIX}${encodeStorageKeyComponent(workflowId)}:`;
9
+ }
10
+ function isPersistedAsyncActivity(value) {
11
+ if (typeof value !== "object" || value === null)
12
+ return !1;
13
+ const record = value;
14
+ 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";
15
+ }
16
+ function isPersistedOperationOutcome(value) {
17
+ if (typeof value !== "object" || value === null)
18
+ return !1;
19
+ const record = value;
20
+ if (record.status === "completed")
21
+ return "value" in record;
22
+ if (record.status === "failed")
23
+ return typeof record.error === "string";
24
+ return !1;
25
+ }
26
+ function isPersistedAsyncActivityResolution(value) {
27
+ if (typeof value !== "object" || value === null)
28
+ return !1;
29
+ const record = value;
30
+ return record.version === 1 && record.kind === "resolution" && typeof record.token === "string" && typeof record.workflowId === "string" && isPersistedOperationOutcome(record.outcome);
31
+ }
32
+ export function deriveAsyncActivityToken(workflowId, step, attempt) {
33
+ return `${ASYNC_ACTIVITY_TOKEN_PREFIX}:${workflowId}:${step}:${attempt}`;
34
+ }
35
+ function buildPersistPendingAsyncActivityOperation(pending) {
36
+ const record = {
37
+ version: 1,
38
+ token: pending.token,
39
+ workflowId: pending.workflowId,
40
+ activityName: pending.activityName,
41
+ operationId: pending.operationId,
42
+ step: pending.step,
43
+ attempt: pending.attempt,
44
+ createdAt: pending.createdAt
45
+ };
46
+ return {
47
+ type: "put",
48
+ key: KEYS.asyncActivity(pending.workflowId, pending.token),
49
+ value: encode(record)
50
+ };
51
+ }
52
+ export function buildAsyncActivityAcknowledgementOperations(pending, outcome) {
53
+ const record = {
54
+ version: 1,
55
+ kind: "resolution",
56
+ token: pending.token,
57
+ workflowId: pending.workflowId,
58
+ outcome
59
+ };
60
+ return [
61
+ { type: "delete", key: KEYS.asyncActivity(pending.workflowId, pending.token) },
62
+ {
63
+ type: "put",
64
+ key: KEYS.asyncActivityResolution(pending.workflowId, pending.token),
65
+ value: encode(record)
66
+ }
67
+ ];
68
+ }
69
+ export async function registerPendingAsyncActivity(internals, pending) {
70
+ const alreadyRegistered = internals.pendingAsyncActivities.has(pending.token);
71
+ internals.pendingAsyncActivities.set(pending.token, pending);
72
+ await commitFencedEngineWrite(internals, [buildPersistPendingAsyncActivityOperation(pending)], [], () => Error(`Async activity registration for token "${pending.token}" lost its precondition.`));
73
+ if (!alreadyRegistered)
74
+ internals.engine.dispatchEvent(new ActivityAsyncPendingEvent(pending.token, pending.operationId, pending.workflowId, pending.activityName, pending.attempt));
75
+ }
76
+ export async function recoverPendingAsyncActivities(internals) {
77
+ for await (const [, bytes] of internals.storage.scan(ASYNC_ACTIVITY_KEY_PREFIX)) {
78
+ const decoded = decode(bytes);
79
+ if (isPersistedAsyncActivity(decoded)) {
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
+ continue;
90
+ }
91
+ if (isPersistedAsyncActivityResolution(decoded))
92
+ queuePendingAsyncActivityResolution(internals, decoded.workflowId, {
93
+ token: decoded.token,
94
+ outcome: decoded.outcome,
95
+ timelineStatus: decoded.outcome.status,
96
+ timelineOutput: decoded.outcome.status === "completed" ? decoded.outcome.value : decoded.outcome.error
97
+ });
98
+ }
99
+ }
100
+ export function shouldBufferPendingAsyncActivityResolution(internals, workflowId) {
101
+ return internals.inlineStrategy !== null && !internals.inlineStrategy.hasGenerator(workflowId);
102
+ }
103
+ export function queuePendingAsyncActivityResolution(internals, workflowId, resolution) {
104
+ internals.pendingAsyncActivityResolutions ??= new Map;
105
+ const queued = internals.pendingAsyncActivityResolutions.get(workflowId) ?? [];
106
+ queued.push(resolution);
107
+ internals.pendingAsyncActivityResolutions.set(workflowId, queued);
108
+ }
109
+ export function takePendingAsyncActivityResolution(internals, workflowId, token) {
110
+ internals.pendingAsyncActivityResolutions ??= new Map;
111
+ const queued = internals.pendingAsyncActivityResolutions.get(workflowId);
112
+ if (queued === void 0)
113
+ return;
114
+ const index = queued.findIndex((resolution) => resolution.token === token);
115
+ if (index === -1)
116
+ return;
117
+ const resolution = queued[index];
118
+ if (resolution === void 0)
119
+ return;
120
+ queued.splice(index, 1);
121
+ if (queued.length === 0)
122
+ internals.pendingAsyncActivityResolutions.delete(workflowId);
123
+ return resolution;
124
+ }
@@ -8,7 +8,7 @@ import {
8
8
  import { decode } from "../codec.js";
9
9
  import { buildIndexOperations } from "../search-attributes.js";
10
10
  import { buildWorkflowTagIndexOperations, normalizeWorkflowTags } from "../workflow-tags.js";
11
- import { asyncActivityWorkflowPrefix } from "./async-activity-completion.js";
11
+ import { asyncActivityWorkflowPrefix } from "./async-activity-records.js";
12
12
  import { forgetCommittedCheckpointBytes } from "./checkpoint-commit-snapshots.js";
13
13
  import { commitFencedEngineWrite } from "./fenced-write.js";
14
14
  import { streamWorkflowStates } from "./listing.js";
@@ -192,6 +192,7 @@ function buildBaseWorkflowDeleteKeys(state) {
192
192
  KEYS.workflowHeaders(state.id),
193
193
  KEYS.terminalCleanupNeeded(state.id),
194
194
  KEYS.workflowConcurrencyHolder(state.id),
195
+ KEYS.scheduleRun(state.id),
195
196
  KEYS.workflowHasServices(state.id),
196
197
  KEYS.finalizerState(state.id),
197
198
  KEYS.teardownOwed(state.id),
@@ -62,20 +62,13 @@ async function runBulkCancellation(internals, filter, options = {}) {
62
62
  const workflowIdsToCancel = preparation.workflowIds, bulkConcurrency = resolveBulkOperationConcurrency(options);
63
63
  let cancelled = 0;
64
64
  const errors = [], cancellationResults = await runBulkWorkflowPool(workflowIdsToCancel, bulkConcurrency, async (workflowId) => {
65
- try {
66
- await internals.engine.cancel(workflowId);
67
- if ((await loadWorkflowState(internals, workflowId))?.status === "cancelled")
68
- return { status: "cancelled" };
69
- return {
70
- status: "failed",
71
- error: { id: workflowId, error: "Workflow no longer cancellable" }
72
- };
73
- } catch (error) {
74
- return {
75
- status: "failed",
76
- error: toBulkOperationError(internals, workflowId, error)
77
- };
78
- }
65
+ await internals.engine.cancel(workflowId);
66
+ if ((await loadWorkflowState(internals, workflowId))?.status === "cancelled")
67
+ return { status: "cancelled" };
68
+ return {
69
+ status: "failed",
70
+ error: { id: workflowId, error: "Workflow no longer cancellable" }
71
+ };
79
72
  });
80
73
  for (const cancellationResult of cancellationResults) {
81
74
  if (cancellationResult.status === "rejected") {
@@ -107,27 +100,15 @@ async function runBulkFailedWorkflowRetry(internals, filter, options = {}) {
107
100
  const bulkConcurrency = resolveBulkOperationConcurrency(options);
108
101
  let retried = 0;
109
102
  const errors = [], retryResults = await runBulkWorkflowPool(preparation.workflowIds, bulkConcurrency, async (workflowId) => {
110
- try {
111
- await retryFailedWorkflow(internals, workflowId);
112
- return { status: "retried" };
113
- } catch (error) {
114
- return {
115
- status: "failed",
116
- error: toBulkOperationError(internals, workflowId, error)
117
- };
118
- }
103
+ await retryFailedWorkflow(internals, workflowId);
104
+ return { status: "retried" };
119
105
  });
120
106
  for (const retryResult of retryResults) {
121
107
  if (retryResult.status === "rejected") {
122
108
  errors.push(toBulkOperationError(internals, retryResult.item, retryResult.reason));
123
109
  continue;
124
110
  }
125
- const { value } = retryResult;
126
- if (value.status === "retried") {
127
- retried += 1;
128
- continue;
129
- }
130
- errors.push(value.error);
111
+ retried += 1;
131
112
  }
132
113
  const result = { retried, failed: errors.length, errors };
133
114
  if (!shouldPersistBulkAudit(options))
@@ -122,7 +122,7 @@ export function createTimeOperationCallbacks(engine) {
122
122
  loadWorkflowState: (workflowId) => loadWorkflowState(getInternals(engine), workflowId),
123
123
  failWorkflow: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine)),
124
124
  runSerializedWorkflowStateWrite: (workflowId, writeOperation) => runSerializedWorkflowStateWrite(getInternals(engine), workflowId, writeOperation),
125
- beginWorkflowExecution: (workflowId, workflowType, input, checkpoint, executionDeadline, executionStateOwnerId, registration) => beginWorkflowExecution(getInternals(engine), workflowId, workflowType, input, checkpoint, executionDeadline, executionStateOwnerId, registration, createLifecycleCallbacks(engine)),
125
+ beginWorkflowExecution: (workflowId, workflowExecutionToken, workflowType, input, checkpoint, executionDeadline, executionStateOwnerId, registration) => beginWorkflowExecution(getInternals(engine), workflowId, workflowExecutionToken, workflowType, input, checkpoint, executionDeadline, executionStateOwnerId, registration, createLifecycleCallbacks(engine)),
126
126
  workflowVersionTupleFromState: (state) => workflowVersionTupleFromState(getInternals(engine), state, createLifecycleCallbacks(engine)),
127
127
  setWorkflowStartHeaders: (workflowId, headers) => setWorkflowStartHeaders(getInternals(engine), workflowId, headers, createLifecycleCallbacks(engine)),
128
128
  loadWorkflowStartHeaders: (workflowId) => loadWorkflowStartHeaders(getInternals(engine), workflowId, createLifecycleCallbacks(engine)),
@@ -76,6 +76,7 @@ export function createLifecycleCallbacks(engine) {
76
76
  swallowPromiseRejection: (promise) => swallowPromiseRejection(promise),
77
77
  enforceHistoryCircuitBreaker: (workflowId) => terminateWorkflow(getInternals(engine), workflowId, "timed-out", createTerminationCallbacks(engine), HISTORY_CIRCUIT_BREAKER_REASON),
78
78
  failWorkflowForUnavailableServices: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine), "system"),
79
+ failWorkflowForRecoveryHook: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine), "system"),
79
80
  failWorkflowForCheckpointDecodeError: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine), "system")
80
81
  };
81
82
  }
@@ -1,8 +1,7 @@
1
- import { AlertManager } from '../../alerting/alert-manager.ts';
2
1
  import type { Storage as WeftStorage } from '../../storage/interface.ts';
3
2
  import { ActivityWorkerDispatcher } from '../../workers/activity-worker-dispatcher.ts';
4
3
  import type { ComposedWorkflowInterceptor, Interceptor } from '../interceptor.ts';
5
- import { type AnyActivityDefinition, type AnyWorkflowDefinition, type EngineOptions, type RegisteredWorkflowDefinition } from '../types.ts';
4
+ import { type AnyActivityDefinition, type AnyWorkflowDefinition, type RegisteredWorkflowDefinition } from '../types.ts';
6
5
  import type { WorkflowLogRecord } from '../types/workflow-log.ts';
7
6
  import type { EngineConstructorOptions, ExecutionStrategyBundle, RegistrationEntry, ResolvedOptions } from './engine-internal-types.ts';
8
7
  export type KnownWorkflowNames<TWorkflows extends object> = Extract<keyof TWorkflows, string>;
@@ -60,5 +59,4 @@ export declare function createExecutionStrategyBundle(parameters: {
60
59
  getLogSink?: () => ((record: WorkflowLogRecord) => void) | undefined;
61
60
  }): ExecutionStrategyBundle;
62
61
  export declare function createActivityWorkerDispatcher(activityExecution: EngineConstructorOptions['activityExecution']): ActivityWorkerDispatcher | null;
63
- export declare function createAlertManagerForEngine(engine: EventTarget, alerts: EngineOptions['alerts'] | undefined, getNow: () => number): AlertManager | null;
64
62
  export {};
@@ -1,4 +1,3 @@
1
- import { AlertManager } from "../../alerting/alert-manager.js";
2
1
  import { CompressedStorage } from "../../storage/compressed-storage.js";
3
2
  import { MemoryStorage } from "../../storage/memory.js";
4
3
  import { ActivityWorkerDispatcher } from "../../workers/activity-worker-dispatcher.js";
@@ -16,7 +15,7 @@ import {
16
15
  DEFAULT_WORKER_TURN_TIMEOUT_MS,
17
16
  MIN_WORKER_PROTOCOL_MESSAGE_BYTES
18
17
  } from "../worker-protocol.js";
19
- import { resolveOwnershipFields } from "./ownership-options.js";
18
+ import { resolveBackgroundTaskMode, resolveOwnershipFields } from "./ownership-options.js";
20
19
  import {
21
20
  normalizeHistoryPolicy,
22
21
  normalizePayloadSizePolicy,
@@ -122,6 +121,7 @@ export function resolveEngineOptions(storage, options, getNow) {
122
121
  getNow,
123
122
  resolveWorkflowServices: options?.resolveWorkflowServices ?? null,
124
123
  onLog: options?.onLog ?? null,
124
+ backgroundTaskMode: resolveBackgroundTaskMode(options),
125
125
  ...resolveBooleanDefaults(options),
126
126
  ...resolveNumericDefaults(options),
127
127
  ...resolveRetentionFields(options),
@@ -235,6 +235,3 @@ export function createActivityWorkerDispatcher(activityExecution) {
235
235
  smol: activityExecution.smol ?? !1
236
236
  }));
237
237
  }
238
- export function createAlertManagerForEngine(engine, alerts, getNow) {
239
- return alerts ? new AlertManager(engine, alerts, getNow) : null;
240
- }
@@ -25,6 +25,7 @@
25
25
  * nested coordinator surfaces up to its parent.
26
26
  */
27
27
  declare const DEFERRED_CONSUME_BRAND: unique symbol;
28
+ declare const KEYED_RACE_RESULT_BRAND: unique symbol;
28
29
  /**
29
30
  * A branch result whose real value is produced by a single deferred consume that
30
31
  * only the winning coordinator performs.
@@ -34,6 +35,12 @@ export type DeferredConsumeEnvelope = {
34
35
  /** Perform the single destructive consume and return the consumed payload. */
35
36
  readonly finalize: () => Promise<unknown>;
36
37
  };
38
+ type KeyedRaceResultEnvelope = {
39
+ readonly [KEYED_RACE_RESULT_BRAND]: true;
40
+ readonly key: string;
41
+ readonly value: unknown;
42
+ };
43
+ export declare function createKeyedRaceResultEnvelope(key: string, value: unknown): KeyedRaceResultEnvelope;
37
44
  /** Wrap a deferred consume into a branded envelope. */
38
45
  export declare function createDeferredConsumeEnvelope(finalize: () => Promise<unknown>): DeferredConsumeEnvelope;
39
46
  /** Detect an envelope by its brand symbol only (never structurally). */
@@ -43,7 +50,9 @@ export declare function isDeferredConsumeEnvelope(value: unknown): value is Defe
43
50
  * cache. A winning `wait-signal` branch surfaces a {@link DeferredConsumeEnvelope}
44
51
  * whose `finalize()` performs the single consume; a nested `ctx.all` branch
45
52
  * surfaces an ARRAY that may hold envelopes at arbitrary positions, so arrays are
46
- * walked and each element finalized. Any other value passes through untouched.
53
+ * walked and each element finalized. A nested `ctx.raceKeyed()` winner is walked
54
+ * through its value while retaining its key. Any other value passes through
55
+ * untouched.
47
56
  *
48
57
  * This runs only on the WINNING path (race winner, or every branch of a settled
49
58
  * `ctx.all`), so finalizing here is exactly the linearization point of "this
@@ -1,4 +1,10 @@
1
- const DEFERRED_CONSUME_BRAND = Symbol("weft.deferredConsume");
1
+ const DEFERRED_CONSUME_BRAND = Symbol("weft.deferredConsume"), KEYED_RACE_RESULT_BRAND = Symbol("weft.keyedRaceResult");
2
+ export function createKeyedRaceResultEnvelope(key, value) {
3
+ return { [KEYED_RACE_RESULT_BRAND]: !0, key, value };
4
+ }
5
+ function isKeyedRaceResultEnvelope(value) {
6
+ return typeof value === "object" && value !== null && value[KEYED_RACE_RESULT_BRAND] === !0;
7
+ }
2
8
  export function createDeferredConsumeEnvelope(finalize) {
3
9
  return { [DEFERRED_CONSUME_BRAND]: !0, finalize };
4
10
  }
@@ -10,5 +16,7 @@ export async function finalizeAndUnwrap(value) {
10
16
  return value.finalize();
11
17
  if (Array.isArray(value))
12
18
  return Promise.all(value.map((element) => finalizeAndUnwrap(element)));
19
+ if (isKeyedRaceResultEnvelope(value))
20
+ return { key: value.key, value: await finalizeAndUnwrap(value.value) };
13
21
  return value;
14
22
  }
@@ -46,6 +46,7 @@ export function disposeEngine(internals) {
46
46
  internals.pendingWebhooks.clear();
47
47
  internals.sleepResolvers.clear();
48
48
  internals.sleepResolversByWorkflow.clear();
49
+ internals.sleepTimersFiredWithoutResolver.clear();
49
50
  internals.checkpoints.clear();
50
51
  internals.pendingExecutionStateOwnerId = void 0;
51
52
  internals.workflowNestingDepths.clear();
@@ -24,6 +24,7 @@ export interface RegistrationEntry {
24
24
  export interface ResolvedOptions {
25
25
  storage: WeftStorage;
26
26
  development: boolean;
27
+ backgroundTaskMode: 'automatic' | 'manual';
27
28
  checkpointHistory: number;
28
29
  checkpointSizeWarningThreshold: number;
29
30
  maxNestingDepth: number;
@@ -84,6 +85,7 @@ export type PendingTimelineEntry = {
84
85
  };
85
86
  export type QueuedInlineWorkflowExecutionStart = {
86
87
  workflowId: string;
88
+ workflowExecutionToken?: string;
87
89
  workflowType: string;
88
90
  input: unknown;
89
91
  checkpoint: Checkpoint;
@@ -4,6 +4,13 @@ import type { Engine } from './index.ts';
4
4
  import { type EngineInternals } from './internals.ts';
5
5
  import type { SecondInstanceDetector } from './second-instance-detector.ts';
6
6
  export declare function isActivityDefinition(value: unknown): value is AnyActivityDefinition;
7
+ type EngineCreateBackgroundTaskOptions = {
8
+ backgroundTasks?: 'automatic' | 'manual' | undefined;
9
+ recover?: boolean | undefined;
10
+ startScheduler?: boolean | undefined;
11
+ };
12
+ export declare function validateEngineCreateBackgroundTaskOptions(options: EngineCreateBackgroundTaskOptions): void;
13
+ export declare function shouldStartEngineScheduler(options: EngineCreateBackgroundTaskOptions, backgroundTaskMode: 'automatic' | 'manual'): boolean;
7
14
  export declare function createQueuedInlineWorkflowStartHandler<TWorkflows extends object, TActivities extends object>(weakEngine: WeakRef<Engine<TWorkflows, TActivities>>, channel: MessageChannel): () => void;
8
15
  /**
9
16
  * Drain pending inline launches for `engine` before teardown. Built with the
@@ -23,3 +30,4 @@ export declare function createCleanupIntervalTick<TWorkflows extends object, TAc
23
30
  */
24
31
  export declare function createSecondInstanceDetectorResolver<TWorkflows extends object, TActivities extends object>(weakEngine: WeakRef<Engine<TWorkflows, TActivities>>): () => SecondInstanceDetector | null;
25
32
  export declare function disposeEngineCleanupInterval(internals: EngineInternals): void;
33
+ export {};
@@ -12,6 +12,15 @@ import { swallowPromiseRejection } from "./strategy-helpers.js";
12
12
  export function isActivityDefinition(value) {
13
13
  return typeof value === "function" && typeof value.name === "string" && "execute" in value && typeof value.execute === "function";
14
14
  }
15
+ export function validateEngineCreateBackgroundTaskOptions(options) {
16
+ if (options.backgroundTasks === "manual" && options.startScheduler === !0)
17
+ throw Error('startScheduler cannot be true when backgroundTasks is "manual"');
18
+ }
19
+ export function shouldStartEngineScheduler(options, backgroundTaskMode) {
20
+ if (backgroundTaskMode === "manual")
21
+ return !1;
22
+ return options.startScheduler ?? options.recover !== !1;
23
+ }
15
24
  function inlineLaunchQueueCallbacksForEngine(engine) {
16
25
  const lifecycleCallbacks = createLifecycleCallbacks(engine);
17
26
  return {
@@ -43,7 +43,7 @@ export class WorkflowTypeNotRegisteredForRecoveryError extends WeftError {
43
43
  const missingWorkflowCount = parameters.missingWorkflows.length, missingTypes = [
44
44
  ...new Set(parameters.missingWorkflows.map((workflow) => workflow.type))
45
45
  ].toSorted(), registeredTypes = [...parameters.registeredTypes].toSorted(), summarizedTypes = summarizeMissingWorkflowTypes(missingTypes);
46
- super("WorkflowTypeNotRegisteredForRecoveryError", `Cannot recover ${missingWorkflowCount} running workflow(s): workflow type(s) not registered: ${summarizedTypes}. Register the missing workflow types before calling \`recoverAll()\`, or pass ` + "`{ acknowledgeUnknownWorkflowTypes: true }` (dangerous \u2014 see migration docs).");
46
+ super("WorkflowTypeNotRegisteredForRecoveryError", `Cannot recover ${missingWorkflowCount} running workflow(s): workflow type(s) not registered: ${summarizedTypes}. Register the missing workflow types before calling \`recoverAll()\`, or pass ` + "`{ acknowledgeUnknownWorkflowTypes: true }` (dangerous \u2014 see " + "https://github.com/stevekinney/weft/blob/main/documentation/guides/recovery-and-deploys.md#acknowledging-drift-acknowledgeunknownworkflowtypes).");
47
47
  this.registeredTypes = registeredTypes;
48
48
  this.missingTypes = missingTypes;
49
49
  this.missingWorkflowSamples = parameters.missingWorkflows.slice(0, MISSING_WORKFLOW_SAMPLE_LIMIT).map((workflow) => ({ ...workflow }));
@@ -21,12 +21,12 @@ import { ScheduleHandle } from './schedule-handle.ts';
21
21
  import { type WorkflowFeedListener, type WorkflowFeedRecord, type WorkflowFeedSelector } from './workflow-feed.ts';
22
22
  export { ActivityReconciliationCapabilityError, ActivityReconciliationConflictError, ActivityReconciliationIndeterminateError, } from './activity-reconciliation.ts';
23
23
  export { AsyncActivityTokenNotFoundError } from './async-activity-completion.ts';
24
- export type { PendingAsyncActivity } from './async-activity-completion.ts';
24
+ export type { PendingAsyncActivity } from './async-activity-records.ts';
25
25
  export type { PendingTimelineEntry, RegistrationEntry, ResolvedOptions, TrackedWaiterKeys, WorkflowResultWaiter, } from './engine-internal-types.ts';
26
26
  export { ActivityResolutionError, BulkDeleteRequiresTerminalWorkflowsError, BulkOperationConfirmationError, EngineCreateNameMismatchError, EngineDisposedError, IdempotencyKeyPurgedError, PersistedDataIncompatibleError, StartOrSignalConflictError, WorkflowAlreadyExistsError, WorkflowConcurrencyLimitExceededError, WorkflowNotFoundError, WorkflowNotRegisteredError, WorkflowSuspendNotSupportedError, WorkflowTeardownPendingError, WorkflowTypeNotRegisteredForRecoveryError, } from './errors.ts';
27
27
  export { HANDLE_RESULT_PROMISE, WorkflowHandle } from './handles.ts';
28
28
  export { EngineLeaseAcquisitionTimeoutError, EngineLeaseCorruptedError, EngineLeaseNotHeldError, } from './lease-errors.ts';
29
- export type { RecoverAllOptions } from './lifecycle.ts';
29
+ export type { RecoverAllOptions, RecoveredWorkflowInfo } from './lifecycle.ts';
30
30
  export { ScheduleHandle } from './schedule-handle.ts';
31
31
  export type { WorkflowFeedListener, WorkflowFeedRecord, WorkflowFeedSelector, } from './workflow-feed.ts';
32
32
  export type { EngineCreateOptions } from './engine-create-types.ts';
@@ -239,6 +239,16 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
239
239
  list<const TAttributeKeys extends readonly AttributeFilterKey[] = readonly AttributeFilterKey[]>(filter?: TypedListFilter<TAttributeKeys>, options?: ListOptions): Promise<PaginatedResult<WorkflowSummary>>;
240
240
  aggregate(filter: ListFilter | undefined, options: AggregateOptions): Promise<AggregateResult>;
241
241
  getRetentionOverview(): RetentionOverview;
242
+ /**
243
+ * Run one host-driven maintenance cycle. This fires due durable timers,
244
+ * deletes expired update responses, applies configured retention, and
245
+ * re-evaluates alert rules without relying on process-local intervals.
246
+ *
247
+ * Use with `backgroundTasks: 'manual'` from a serverless alarm, Cron trigger,
248
+ * or another externally scheduled wake-up. Concurrent calls are safe, but a
249
+ * host should await each cycle before scheduling another.
250
+ */
251
+ runMaintenance(now?: number): Promise<void>;
242
252
  purge(filter?: ListFilter): Promise<PurgeResult>;
243
253
  cancelAll(filter: ListFilter, options: BulkOperationDryRunOptions): Promise<BulkOperationDryRunResult>;
244
254
  cancelAll(filter: ListFilter, options?: BulkOperationCommitOptions): Promise<BulkCancelResult>;