@lostgradient/weft 0.2.0 → 0.2.1

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 (61) hide show
  1. package/dist/cli-main.js +77 -77
  2. package/dist/core/context/index.d.ts +1 -0
  3. package/dist/core/context/index.js +3 -0
  4. package/dist/core/context/internals.d.ts +1 -0
  5. package/dist/core/context/internals.js +2 -1
  6. package/dist/core/context/speculative-child.js +2 -0
  7. package/dist/core/context/types.d.ts +6 -0
  8. package/dist/core/engine/bulk-operations-purge.js +1 -0
  9. package/dist/core/engine/callback-creators-bundles.js +2 -1
  10. package/dist/core/engine/callback-creators-core.js +2 -1
  11. package/dist/core/engine/construction.d.ts +1 -0
  12. package/dist/core/engine/construction.js +5 -2
  13. package/dist/core/engine/engine-internal-types.d.ts +5 -0
  14. package/dist/core/engine/index.d.ts +26 -0
  15. package/dist/core/engine/index.js +7 -1
  16. package/dist/core/engine/internals.d.ts +8 -0
  17. package/dist/core/engine/lifecycle/recovered-services.d.ts +45 -0
  18. package/dist/core/engine/lifecycle/recovered-services.js +34 -0
  19. package/dist/core/engine/lifecycle/resume.js +8 -1
  20. package/dist/core/engine/lifecycle/shared.d.ts +8 -0
  21. package/dist/core/engine/lifecycle/start-batch.js +23 -12
  22. package/dist/core/engine/lifecycle/start.js +11 -0
  23. package/dist/core/engine/operations-data.d.ts +16 -0
  24. package/dist/core/engine/operations-data.js +6 -0
  25. package/dist/core/engine/operations-time.d.ts +3 -2
  26. package/dist/core/engine/operations-time.js +6 -1
  27. package/dist/core/engine/termination/cleanup.js +2 -0
  28. package/dist/core/inline-execution-strategy.d.ts +5 -0
  29. package/dist/core/inline-execution-strategy.js +2 -1
  30. package/dist/core/types/options.d.ts +89 -0
  31. package/dist/core/types/workflow-context.d.ts +25 -0
  32. package/dist/core/weft-error.d.ts +45 -13
  33. package/dist/core/weft-error.js +9 -0
  34. package/dist/index.d.ts +7 -2
  35. package/dist/index.js +1 -1
  36. package/dist/json-schema.js +3 -3
  37. package/dist/mcp/cli.js +17 -17
  38. package/dist/server/handler.js +12 -12
  39. package/dist/server/index.js +24 -24
  40. package/dist/service-worker/index.js +12 -12
  41. package/dist/storage/http.js +1 -1
  42. package/dist/storage/index.d.ts +2 -1
  43. package/dist/storage/indexeddb.js +1 -1
  44. package/dist/storage/interface.d.ts +14 -0
  45. package/dist/storage/interface.js +1 -1
  46. package/dist/storage/key-prefixes.d.ts +1 -1
  47. package/dist/storage/key-prefixes.js +1 -0
  48. package/dist/storage/lmdb.js +1 -1
  49. package/dist/storage/memory.js +1 -1
  50. package/dist/storage/resolve.js +1 -1
  51. package/dist/storage/scoped-storage.js +1 -1
  52. package/dist/storage/text-value-store.d.ts +4 -1
  53. package/dist/storage/turso.js +1 -1
  54. package/dist/storage/typed-storage.js +1 -1
  55. package/dist/storage/web-extension.js +1 -1
  56. package/dist/testing/event-loop.d.ts +36 -2
  57. package/dist/testing/index.d.ts +31 -1
  58. package/dist/testing/index.js +17 -17
  59. package/dist/version.d.ts +1 -1
  60. package/dist/version.js +1 -1
  61. package/package.json +1 -1
@@ -27,6 +27,7 @@ export declare class Context implements WorkflowContext {
27
27
  readonly signal: AbortSignal;
28
28
  constructor(options: ContextOptions);
29
29
  get executionTimeRemaining(): number;
30
+ get services(): unknown;
30
31
  get stepIndex(): number;
31
32
  get nestingDepth(): number;
32
33
  get accumulatedResults(): Map<number, unknown>;
@@ -37,6 +37,9 @@ export class Context {
37
37
  return 1 / 0;
38
38
  return Math.max(0, internals.deadline - internals.getNow());
39
39
  }
40
+ get services() {
41
+ return getInternals(this).services;
42
+ }
40
43
  get stepIndex() {
41
44
  return getInternals(this).stepIndex;
42
45
  }
@@ -25,6 +25,7 @@ export interface ContextInternals {
25
25
  executionStateOwnerId: string;
26
26
  resolveWorkflowType: ((target: string | Function) => string) | undefined;
27
27
  registerCancelHandler: ((handler: () => Promise<void> | void) => () => void) | undefined;
28
+ services: unknown;
28
29
  }
29
30
  export declare function initializeInternals(context: Context, options: ContextOptions, initialSessionState: Record<string, unknown> | undefined): void;
30
31
  export declare function getInternals(context: Context): ContextInternals;
@@ -24,7 +24,8 @@ export function initializeInternals(context, options, initialSessionState) {
24
24
  nestingDepth: options.nestingDepth ?? 0,
25
25
  executionStateOwnerId: options.executionStateOwnerId ?? options.workflowId,
26
26
  resolveWorkflowType: options.resolveWorkflowType,
27
- registerCancelHandler: options.registerCancelHandler
27
+ registerCancelHandler: options.registerCancelHandler,
28
+ services: options.services
28
29
  };
29
30
  INTERNALS.set(context, internals);
30
31
  }
@@ -16,6 +16,8 @@ function assignOptionalContextOptions(options, internals) {
16
16
  options.sleepReferenceTime = internals.sleepReferenceTime;
17
17
  if (internals.resolveWorkflowType !== void 0)
18
18
  options.resolveWorkflowType = internals.resolveWorkflowType;
19
+ if (internals.services !== void 0)
20
+ options.services = internals.services;
19
21
  }
20
22
  function createSpeculativeChildOptions(parent, internals) {
21
23
  const options = {
@@ -178,4 +178,10 @@ export interface ContextOptions {
178
178
  * it is not persisted or restored after engine restart.
179
179
  */
180
180
  registerCancelHandler?: (handler: () => Promise<void> | void) => () => void;
181
+ /**
182
+ * Host-supplied, per-run capabilities exposed as `ctx.services`. Never
183
+ * checkpointed; held only for this run and re-provided on recovery via the
184
+ * engine's `resolveWorkflowServices` resolver.
185
+ */
186
+ services?: unknown;
181
187
  }
@@ -178,6 +178,7 @@ function buildBaseWorkflowDeleteKeys(state) {
178
178
  KEYS.checkpoint(state.id),
179
179
  KEYS.workflowHeaders(state.id),
180
180
  KEYS.terminalCleanupNeeded(state.id),
181
+ KEYS.workflowHasServices(state.id),
181
182
  KEYS.attribute(state.id),
182
183
  KEYS.terminalWorkflow(state.updatedAt, state.id)
183
184
  ]);
@@ -102,7 +102,8 @@ export function createTimeOperationCallbacks(engine) {
102
102
  parseStartOptionDuration: (value, fieldName) => parseStartOptionDuration(getInternals(engine), value, fieldName, createLifecycleCallbacks(engine)),
103
103
  runDeferredTerminalCleanup: (workflowId, timerId) => runDeferredTerminalCleanup(getInternals(engine), workflowId, timerId, createTerminationCallbacks(engine)),
104
104
  handleScheduleTimer: (entry) => handleScheduleTimerForEngine(engine, entry),
105
- timeout: (workflowId) => engine.timeout(workflowId)
105
+ timeout: (workflowId) => engine.timeout(workflowId),
106
+ handleCleanupError: (source, error, workflowId) => createTerminationCallbacks(engine).handleCleanupError(source, error, workflowId)
106
107
  };
107
108
  }
108
109
  export function createUpdateCallbacks(engine) {
@@ -74,7 +74,8 @@ export function createLifecycleCallbacks(engine) {
74
74
  hasLocalCheckpointOwnership: (workflowId, workflowStatus) => hasLocalCheckpointOwnership(getInternals(engine), workflowId, workflowStatus),
75
75
  handleCleanupError: (source, error, workflowId) => createTerminationCallbacks(engine).handleCleanupError(source, error, workflowId),
76
76
  swallowPromiseRejection: (promise) => swallowPromiseRejection(promise),
77
- enforceHistoryCircuitBreaker: (workflowId) => terminateWorkflow(getInternals(engine), workflowId, "timed-out", createTerminationCallbacks(engine), HISTORY_CIRCUIT_BREAKER_REASON)
77
+ enforceHistoryCircuitBreaker: (workflowId) => terminateWorkflow(getInternals(engine), workflowId, "timed-out", createTerminationCallbacks(engine), HISTORY_CIRCUIT_BREAKER_REASON),
78
+ failWorkflowForUnavailableServices: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine), "system")
78
79
  };
79
80
  }
80
81
  export function createTerminationCallbacksWith(engine, handleScheduledWorkflowTerminal) {
@@ -54,6 +54,7 @@ export declare function createExecutionStrategyBundle(parameters: {
54
54
  getComposedWorkflowInterceptor?: () => ComposedWorkflowInterceptor | null;
55
55
  resolveWorkflowType: (target: string | Function) => string;
56
56
  registerCancelHandler?: (workflowId: string, handler: () => Promise<void> | void) => () => void;
57
+ getWorkflowServices?: (workflowId: string) => unknown;
57
58
  }): ExecutionStrategyBundle;
58
59
  export declare function createActivityWorkerDispatcher(activityExecution: EngineConstructorOptions['activityExecution']): ActivityWorkerDispatcher | null;
59
60
  export declare function createAlertManagerForEngine(engine: EventTarget, alerts: EngineOptions['alerts'] | undefined, getNow: () => number): AlertManager | null;
@@ -107,6 +107,7 @@ export function resolveEngineOptions(storage, options, getNow) {
107
107
  return {
108
108
  storage,
109
109
  getNow,
110
+ resolveWorkflowServices: options?.resolveWorkflowServices ?? null,
110
111
  ...resolveBooleanDefaults(options),
111
112
  ...resolveNumericDefaults(options),
112
113
  ...resolveRetentionFields(options),
@@ -169,7 +170,8 @@ export function createExecutionStrategyBundle(parameters) {
169
170
  getRegistration,
170
171
  getComposedWorkflowInterceptor,
171
172
  resolveWorkflowType,
172
- registerCancelHandler
173
+ registerCancelHandler,
174
+ getWorkflowServices
173
175
  } = parameters, workerExecutionConfiguration = normalizeWorkerExecutionConfiguration(options);
174
176
  if (workerExecutionConfiguration.mode === "worker") {
175
177
  const pool = new WorkerPool({
@@ -195,7 +197,8 @@ export function createExecutionStrategyBundle(parameters) {
195
197
  maxNestingDepth,
196
198
  development,
197
199
  resolveWorkflowType,
198
- ...registerCancelHandler !== void 0 && { registerCancelHandler }
200
+ ...registerCancelHandler !== void 0 && { registerCancelHandler },
201
+ ...getWorkflowServices !== void 0 && { getWorkflowServices }
199
202
  });
200
203
  return { strategy: inlineStrategy, inlineStrategy };
201
204
  }
@@ -30,6 +30,11 @@ export interface ResolvedOptions {
30
30
  archiveAdapter: ArchiveAdapter | null;
31
31
  payloadSizePolicy: NormalizedPayloadSizePolicy;
32
32
  getNow: () => number;
33
+ /**
34
+ * Re-provides the non-serialized per-run `services` value on recovery; `null`
35
+ * when the engine was created without `resolveWorkflowServices`.
36
+ */
37
+ resolveWorkflowServices: EngineOptions['resolveWorkflowServices'] | null;
33
38
  }
34
39
  export interface WorkflowResultWaiter {
35
40
  promise: Promise<unknown>;
@@ -230,6 +230,32 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
230
230
  getStreamChunks(workflowId: string, key: string, options?: {
231
231
  after?: number;
232
232
  }): Promise<StoredStreamChunk[]>;
233
+ /**
234
+ * Read a value a workflow offloaded with `ctx.offload(key, ...)` back out of
235
+ * storage by `workflowId` + `key`.
236
+ *
237
+ * This is the external, post-completion reader for offloaded artifacts — the
238
+ * missing sibling of {@link getStreamChunks} and {@link getEvents}. Offloaded
239
+ * values survive normal completion (`completeWorkflow`/`failWorkflow` preserve
240
+ * them) so a consumer can read a finished workflow's offloaded output after
241
+ * `handle.result()` resolves. They are swept only when a workflow is
242
+ * terminated, cancelled, or times out.
243
+ *
244
+ * @returns The decoded offload value, or `null` when no value is stored under
245
+ * that key (key was never written, workflow ID unknown, or artifact swept).
246
+ *
247
+ * @example
248
+ * ```ts
249
+ * import { Engine } from '@lostgradient/weft';
250
+ *
251
+ * async function readReport(engine: Engine, workflowId: string): Promise<unknown> {
252
+ * // `null` when the workflow offloaded nothing under this key, or after a
253
+ * // terminated workflow swept its output artifacts.
254
+ * return engine.getOffload(workflowId, 'report');
255
+ * }
256
+ * ```
257
+ */
258
+ getOffload(workflowId: string, key: string): Promise<unknown>;
233
259
  fork(sourceWorkflowId: string, options?: ForkOptions): Promise<WorkflowHandle>;
234
260
  resume(workflowId: string): Promise<WorkflowHandle>;
235
261
  /**
@@ -90,6 +90,7 @@ import {
90
90
  removeTags as removeWorkflowTags,
91
91
  setAttributes as setWorkflowAttributes
92
92
  } from "./listing.js";
93
+ import { getOffloadFromInternals } from "./operations-data.js";
93
94
  import { getStreamChunksFromInternals } from "./operations-stream.js";
94
95
  import {
95
96
  handleTimerFired as handleTimerFiredFromInternals
@@ -213,7 +214,8 @@ export class Engine extends EventTarget {
213
214
  getRegistration: getInternals(this).registrations.get.bind(getInternals(this).registrations),
214
215
  getComposedWorkflowInterceptor: () => getComposedWorkflowInterceptor(getInternals(this)),
215
216
  resolveWorkflowType: this.#resolveWorkflowTypeTarget.bind(this),
216
- registerCancelHandler: (workflowId, handler) => registerCancelHandler(getInternals(this), workflowId, handler)
217
+ registerCancelHandler: (workflowId, handler) => registerCancelHandler(getInternals(this), workflowId, handler),
218
+ getWorkflowServices: (workflowId) => getInternals(this).workflowServices.get(workflowId)
217
219
  });
218
220
  getInternals(this).storage = storage;
219
221
  getInternals(this).abortController = new AbortController;
@@ -261,6 +263,7 @@ export class Engine extends EventTarget {
261
263
  if (queuedInlineWorkflowStartChannel !== null)
262
264
  queuedInlineWorkflowStartChannel.port1.onmessage = createQueuedInlineWorkflowStartHandler(weakEngine, queuedInlineWorkflowStartChannel);
263
265
  getInternals(this).heartbeatDetails = new Map;
266
+ getInternals(this).workflowServices = new Map;
264
267
  getInternals(this).pendingAsyncActivities = new Map;
265
268
  getInternals(this).pendingStarts = new Set;
266
269
  getInternals(this).pendingScheduleCreations = new Set;
@@ -496,6 +499,9 @@ export class Engine extends EventTarget {
496
499
  async getStreamChunks(workflowId, key, options) {
497
500
  return getStreamChunksFromInternals(getInternals(this), workflowId, key, options);
498
501
  }
502
+ async getOffload(workflowId, key) {
503
+ return getOffloadFromInternals(getInternals(this), workflowId, key);
504
+ }
499
505
  async fork(sourceWorkflowId, options) {
500
506
  return forkFromLifecycle(getInternals(this), sourceWorkflowId, options, this.#createLifecycleCallbacks());
501
507
  }
@@ -108,6 +108,14 @@ export interface EngineInternals {
108
108
  workflowHeaders: Map<string, Map<string, string>>;
109
109
  workflowStateWriteChains: Map<string, Promise<void>>;
110
110
  heartbeatDetails: Map<string, unknown>;
111
+ /**
112
+ * Per-run, non-serialized `services` value exposed to the workflow body as
113
+ * `ctx.services`. Set at `engine.start({ services })` and re-provided on
114
+ * recovery by `resolveWorkflowServices`. Never checkpointed — held only in
115
+ * engine memory, keyed by workflowId, and cleared on terminal cleanup (the
116
+ * same lifecycle as {@link heartbeatDetails}).
117
+ */
118
+ workflowServices: Map<string, unknown>;
111
119
  /**
112
120
  * Activities that called `ctx.completeAsync()` and are awaiting out-of-band
113
121
  * completion via `engine.completeAsyncActivity` / `failAsyncActivity`, keyed
@@ -0,0 +1,45 @@
1
+ import type { WorkflowState } from '../../types.ts';
2
+ import type { EngineInternals } from '../internals.ts';
3
+ /**
4
+ * Re-provide a recovered inline workflow's non-serialized `services` before its
5
+ * generator is driven forward, and decide whether execution should proceed.
6
+ *
7
+ * Both recovery entry points use this: `resumeWorkflowFromStorage` (for a run
8
+ * left `running`) and the delayed-start timer handler (for a `startAfter`/
9
+ * `startAt` run that crashed `pending` before its timer fired). On a fresh
10
+ * process the in-memory `workflowServices` map is empty, so without this a
11
+ * recovered run that originally had services would silently execute with
12
+ * `ctx.services === undefined`.
13
+ *
14
+ * The resolver is only consulted for runs launched WITH services, detected by
15
+ * the durable `KEYS.workflowHasServices` marker. A run that never had services
16
+ * has no marker and proceeds without touching the resolver — so a fail-closed
17
+ * resolver does not fail healthy no-services runs.
18
+ *
19
+ * Returns `false` to proceed (services available, none expected, or no resolver
20
+ * is configured). Returns `true` to STOP — the run was terminally
21
+ * failed (services unavailable), or the terminal commit faulted and the run was
22
+ * left for a later boot to retry. Either way the generator must not advance:
23
+ * driving it without services would crash the body and that throw would escape
24
+ * into the recovery loop, aborting sibling runs.
25
+ *
26
+ * Worker mode skips this (services are inline-only, rejected at `engine.start()`).
27
+ * A resolver throw is treated as `unavailable` with the error as the reason,
28
+ * for the same sibling-isolation reason.
29
+ *
30
+ * @param failRun - Terminally fails just this run with `reason`. Supplied by the
31
+ * caller because the two entry points reach the termination machinery through
32
+ * different callback bundles. It receives the canonical terminal error built
33
+ * by {@link unavailableServicesError}, so both recovery paths fail the run
34
+ * with an identical message and (via `failWorkflow`'s default) the `system`
35
+ * failure category.
36
+ * @param onCommitError - Records a fail-warn when `failRun` itself throws, so the
37
+ * swallowed terminal-commit fault is still observable.
38
+ */
39
+ export declare function reprovideRecoveredServices(internals: EngineInternals, state: WorkflowState, failRun: (workflowId: string, error: Error) => Promise<void>, onCommitError: (source: string, error: unknown, workflowId: string) => void): Promise<boolean>;
40
+ /**
41
+ * The canonical terminal error for a recovered run whose services could not be
42
+ * re-provided. Shared by both recovery paths so they fail with an identical
43
+ * message (the failure category is `system`, the default for `failWorkflow`).
44
+ */
45
+ export declare function unavailableServicesError(workflowId: string, reason: string): Error;
@@ -0,0 +1,34 @@
1
+ import { KEYS } from "../../../storage/interface.js";
2
+ export async function reprovideRecoveredServices(internals, state, failRun, onCommitError) {
3
+ const resolver = internals.options.resolveWorkflowServices;
4
+ if (internals.inlineStrategy === null || !resolver)
5
+ return !1;
6
+ if (internals.workflowServices.has(state.id))
7
+ return !1;
8
+ if (await internals.storage.get(KEYS.workflowHasServices(state.id)) === null)
9
+ return !1;
10
+ let reason;
11
+ try {
12
+ const resolution = await resolver({
13
+ workflowId: state.id,
14
+ workflowType: state.type,
15
+ input: state.input
16
+ });
17
+ if (resolution.status === "available") {
18
+ internals.workflowServices.set(state.id, resolution.services);
19
+ return !1;
20
+ }
21
+ reason = resolution.reason;
22
+ } catch (error) {
23
+ reason = error instanceof Error ? error.message : String(error);
24
+ }
25
+ try {
26
+ await failRun(state.id, unavailableServicesError(state.id, reason));
27
+ } catch (error) {
28
+ onCommitError("reprovideRecoveredServices", error, state.id);
29
+ }
30
+ return !0;
31
+ }
32
+ export function unavailableServicesError(workflowId, reason) {
33
+ return Error(`Recovered workflow "${workflowId}" services unavailable: ${reason}`);
34
+ }
@@ -10,12 +10,16 @@ import { loadWorkflowState } from "../storage-io.js";
10
10
  import { getComposedWorkflowInterceptor } from "../strategy-helpers.js";
11
11
  import { decodeWorkflowState } from "../validation.js";
12
12
  import { prepareResumeState } from "./persist.js";
13
+ import { reprovideRecoveredServices } from "./recovered-services.js";
13
14
  import {
14
15
  enforceHistoryPolicyBeforeReplay,
15
16
  loadTerminalCleanupTrackedState,
16
17
  loadWorkflowStartHeaders,
17
18
  setWorkflowStartHeaders
18
19
  } from "./shared.js";
20
+ async function prepareRecoveredServicesOrFail(internals, state, callbacks) {
21
+ return reprovideRecoveredServices(internals, state, callbacks.failWorkflowForUnavailableServices, callbacks.handleCleanupError);
22
+ }
19
23
  function assertResumeNotTerminating(internals, workflowId) {
20
24
  if (internals.terminalizingWorkflows.has(workflowId))
21
25
  throw Error(`Cannot resume workflow "${workflowId}": termination is in progress`);
@@ -57,7 +61,8 @@ function relaunchInlineWorkflowAfterResume(internals, latestState, args) {
57
61
  sleepReferenceTime: resumeCheckpoint.createdAt,
58
62
  ...latestState.executionDeadline !== void 0 && {
59
63
  deadline: latestState.executionDeadline
60
- }
64
+ },
65
+ services: internals.workflowServices.get(workflowId)
61
66
  });
62
67
  setContextWorkflowInterceptor(context, getComposedWorkflowInterceptor(internals));
63
68
  if (internals.options.development)
@@ -117,6 +122,8 @@ export async function resumeWorkflowFromStorage(internals, workflowId, dispatchR
117
122
  return callbacks.getHandle(workflowId);
118
123
  const workflowStartHeaders = await loadWorkflowStartHeaders(internals, workflowId, callbacks);
119
124
  await loadTerminalCleanupTrackedState(internals, workflowId, callbacks);
125
+ if (await prepareRecoveredServicesOrFail(internals, state, callbacks))
126
+ return callbacks.getHandle(workflowId);
120
127
  const handle = callbacks.getHandle(workflowId);
121
128
  await callbacks.runSerializedWorkflowStateWrite(workflowId, () => performSerializedResume(internals, {
122
129
  workflowId,
@@ -48,6 +48,14 @@ export type LifecycleCallbacks = {
48
48
  * history is terminated without being replayed.
49
49
  */
50
50
  enforceHistoryCircuitBreaker: (workflowId: string) => Promise<void>;
51
+ /**
52
+ * Force a recovered workflow to a terminal `failed` state because its
53
+ * non-serialized `services` could not be re-provided (the engine's
54
+ * `resolveWorkflowServices` reported `unavailable`). Fails just this run with
55
+ * a `system` failure category; the engine and other recovered runs continue.
56
+ * `error` is the canonical {@link unavailableServicesError}.
57
+ */
58
+ failWorkflowForUnavailableServices: (workflowId: string, error: Error) => Promise<void>;
51
59
  };
52
60
  /**
53
61
  * Pre-replay history circuit breaker. Called at every restore-from-checkpoint
@@ -19,18 +19,7 @@ export function buildStartBatchOperations(_internals, workflowId, state, checkpo
19
19
  ...visibilityIndexOperations,
20
20
  ...buildWorkflowTagIndexOperations(workflowId, void 0, state.tags),
21
21
  ...buildInitialSearchAttributeOperations(_internals, workflowId, registration, options?.searchAttributes, callbacks),
22
- ...workflowStartHeaders && workflowStartHeaders.size > 0 ? [
23
- {
24
- type: "put",
25
- key: KEYS.workflowHeaders(workflowId),
26
- value: encodeWorkflowStartHeaders(workflowStartHeaders)
27
- },
28
- {
29
- type: "put",
30
- key: KEYS.terminalCleanupNeeded(workflowId),
31
- value: EMPTY_STORAGE_VALUE
32
- }
33
- ] : [],
22
+ ...buildPerRunScratchOperations(workflowId, options, workflowStartHeaders),
34
23
  ...additionalOperations ?? []
35
24
  ];
36
25
  if (executionDeadline !== void 0)
@@ -44,6 +33,28 @@ export function buildStartBatchOperations(_internals, workflowId, state, checkpo
44
33
  operations.push(...buildTimerBatchOperations(delayedStartTimer));
45
34
  return operations;
46
35
  }
36
+ function buildPerRunScratchOperations(workflowId, options, workflowStartHeaders) {
37
+ const hasHeaders = workflowStartHeaders !== void 0 && workflowStartHeaders.size > 0, hasServices = options?.services !== void 0, operations = [];
38
+ if (hasHeaders)
39
+ operations.push({
40
+ type: "put",
41
+ key: KEYS.workflowHeaders(workflowId),
42
+ value: encodeWorkflowStartHeaders(workflowStartHeaders)
43
+ });
44
+ if (hasServices)
45
+ operations.push({
46
+ type: "put",
47
+ key: KEYS.workflowHasServices(workflowId),
48
+ value: EMPTY_STORAGE_VALUE
49
+ });
50
+ if (hasHeaders || hasServices)
51
+ operations.push({
52
+ type: "put",
53
+ key: KEYS.terminalCleanupNeeded(workflowId),
54
+ value: EMPTY_STORAGE_VALUE
55
+ });
56
+ return operations;
57
+ }
47
58
  export function buildInitialSearchAttributeOperations(_internals, workflowId, registration, searchAttributes, callbacks) {
48
59
  if (!searchAttributes || Object.keys(searchAttributes).length === 0)
49
60
  return [];
@@ -51,11 +51,18 @@ function rollbackTransientStartState(internals, workflowId) {
51
51
  internals.checkpoints.delete(workflowId);
52
52
  internals.workflowHeaders.delete(workflowId);
53
53
  internals.workflowVersionTuples.delete(workflowId);
54
+ internals.workflowServices.delete(workflowId);
55
+ internals.workflowsNeedingTerminalCleanup.delete(workflowId);
56
+ }
57
+ function assertServicesSupportedForMode(internals, options) {
58
+ if (options?.services !== void 0 && internals.inlineStrategy === null)
59
+ throw Error('options.services is only supported in inline execution mode; it cannot be serialized to a Worker. Remove services or use workflowExecutionMode: "inline".');
54
60
  }
55
61
  export async function startWorkflow(internals, type, input, options, additionalStartOperations, callbacks) {
56
62
  const registration = internals.registrations.get(type);
57
63
  if (!registration)
58
64
  throw new WorkflowNotRegisteredError(type);
65
+ assertServicesSupportedForMode(internals, options);
59
66
  const preparation = prepareStartWorkflow(internals, options, callbacks), { workflowId, callerProvidedId, parentHeaders, executionStateOwnerId, delayedStartTimer } = preparation;
60
67
  if (internals.pendingStarts.has(workflowId))
61
68
  throw new WorkflowAlreadyExistsError(workflowId);
@@ -73,6 +80,10 @@ export async function startWorkflow(internals, type, input, options, additionalS
73
80
  internals.workflowVersionTuples.set(workflowId, versionTuple);
74
81
  const startOperations = buildStartBatchOperations(internals, workflowId, state, checkpoint, registration, options, state.executionDeadline, delayedStartTimer, persistedWorkflowStartHeaders, additionalStartOperations, callbacks);
75
82
  await persistStartBatch(internals, startOperations);
83
+ if (options?.services !== void 0) {
84
+ internals.workflowServices.set(workflowId, options.services);
85
+ internals.workflowsNeedingTerminalCleanup.add(workflowId);
86
+ }
76
87
  const handle = createWorkflowHandle(internals, workflowId, callbacks);
77
88
  if (!delayedStartTimer)
78
89
  beginWorkflowExecution(internals, workflowId, type, input, checkpoint, state.executionDeadline, state.executionStateOwnerId ?? workflowId, registration, callbacks);
@@ -19,5 +19,21 @@ export type DataOperationCallbacks = {
19
19
  export declare function processMemoOperation(_internals: EngineInternals, workflowId: string, operation: MemoOperation, callbacks: DataOperationCallbacks): Promise<void>;
20
20
  export declare function processOffloadOperation(internals: EngineInternals, workflowId: string, operation: OffloadOperation, callbacks: DataOperationCallbacks): Promise<void>;
21
21
  export declare function processLoadOperation(internals: EngineInternals, workflowId: string, operation: LoadOperation, callbacks: DataOperationCallbacks): Promise<void>;
22
+ /**
23
+ * Read an offloaded value back out of storage by `workflowId` + `key`, decoding
24
+ * it with the same codec {@link processOffloadOperation} wrote it with.
25
+ *
26
+ * This is the post-completion sibling of the in-workflow `ctx.load()` read
27
+ * (see {@link processLoadOperation}): `ctx.load()` is restricted to the running
28
+ * workflow's own offloads and throws on a miss, whereas this external reader
29
+ * lets a consumer read a *terminal* workflow's offloaded output after
30
+ * `handle.result()` resolves — the artifact survives normal completion
31
+ * (`completeWorkflow`/`failWorkflow` preserve `offload:` keys) and is swept only
32
+ * when the workflow is terminated/cancelled.
33
+ *
34
+ * @returns The decoded offload value, or `null` when no value is stored under
35
+ * that key (either the key was never written, or the artifact was swept).
36
+ */
37
+ export declare function getOffloadFromInternals(internals: EngineInternals, workflowId: string, key: string): Promise<unknown>;
22
38
  export declare function processArchiveOperation(internals: EngineInternals, workflowId: string, operation: ArchiveOperation, callbacks: DataOperationCallbacks): Promise<void>;
23
39
  export {};
@@ -30,6 +30,12 @@ export async function processLoadOperation(internals, workflowId, operation, cal
30
30
  return decode(raw);
31
31
  });
32
32
  }
33
+ export async function getOffloadFromInternals(internals, workflowId, key) {
34
+ const raw = await internals.storage.get(KEYS.offload(workflowId, key));
35
+ if (raw === null)
36
+ return null;
37
+ return decode(raw);
38
+ }
33
39
  export async function processArchiveOperation(internals, workflowId, operation, callbacks) {
34
40
  return callbacks.runOperationWithResult(workflowId, operation, async () => {
35
41
  await internals.storage.put(KEYS.archive(workflowId, operation.key), encode(operation.data));
@@ -19,10 +19,11 @@ export type TimeOperationCallbacks = {
19
19
  runDeferredTerminalCleanup: (workflowId: string, timerId: string) => Promise<void>;
20
20
  handleScheduleTimer: (entry: TimerEntry) => Promise<void>;
21
21
  timeout: (workflowId: string) => Promise<void>;
22
+ handleCleanupError: (source: string, error: unknown, workflowId: string) => void;
22
23
  };
23
24
  export declare function createDelayedStartTimerEntry(_internals: EngineInternals, workflowId: string, scheduledStartAt: number, options: StartOptions | undefined, callbacks: Pick<TimeOperationCallbacks, 'parseStartOptionDuration'>): TimerEntry;
24
25
  export declare function processSleepOperation(internals: EngineInternals, workflowId: string, operation: SleepOperation, callbacks: Pick<TimeOperationCallbacks, 'completeOperation' | 'loadWorkflowState'>): Promise<void>;
25
26
  export declare function registerSleepResolver(internals: EngineInternals, workflowId: string, operationId: string, resolve: () => void): void;
26
- export declare function startDelayedWorkflow(internals: EngineInternals, entry: TimerEntry, callbacks: Pick<TimeOperationCallbacks, 'beginWorkflowExecution' | 'failWorkflow' | 'loadWorkflowStartHeaders' | 'loadWorkflowState' | 'runSerializedWorkflowStateWrite' | 'setWorkflowStartHeaders' | 'workflowVersionTupleFromState'>): Promise<void>;
27
- export declare function handleTimerFired(internals: EngineInternals, entry: TimerEntry, callbacks: Pick<TimeOperationCallbacks, 'failWorkflow' | 'loadWorkflowStartHeaders' | 'loadWorkflowState' | 'runDeferredTerminalCleanup' | 'runSerializedWorkflowStateWrite' | 'handleScheduleTimer' | 'setWorkflowStartHeaders' | 'timeout' | 'beginWorkflowExecution' | 'workflowVersionTupleFromState'>): Promise<void>;
27
+ export declare function startDelayedWorkflow(internals: EngineInternals, entry: TimerEntry, callbacks: Pick<TimeOperationCallbacks, 'beginWorkflowExecution' | 'failWorkflow' | 'handleCleanupError' | 'loadWorkflowStartHeaders' | 'loadWorkflowState' | 'runSerializedWorkflowStateWrite' | 'setWorkflowStartHeaders' | 'workflowVersionTupleFromState'>): Promise<void>;
28
+ export declare function handleTimerFired(internals: EngineInternals, entry: TimerEntry, callbacks: Pick<TimeOperationCallbacks, 'failWorkflow' | 'handleCleanupError' | 'loadWorkflowStartHeaders' | 'loadWorkflowState' | 'runDeferredTerminalCleanup' | 'runSerializedWorkflowStateWrite' | 'handleScheduleTimer' | 'setWorkflowStartHeaders' | 'timeout' | 'beginWorkflowExecution' | 'workflowVersionTupleFromState'>): Promise<void>;
28
29
  export {};
@@ -1,7 +1,8 @@
1
- import { KEYS } from "../../storage/interface.js";
1
+ import { KEYS, storageHas } from "../../storage/interface.js";
2
2
  import { deserializeCheckpoint } from "../checkpoint.js";
3
3
  import { encode } from "../codec.js";
4
4
  import { buildTimerBatchOperations, normalizeStorageTimestamp } from "../scheduler.js";
5
+ import { reprovideRecoveredServices } from "./lifecycle/recovered-services.js";
5
6
  import { buildWorkflowVisibilityIndexTransition } from "./workflow-indexes.js";
6
7
  export function createDelayedStartTimerEntry(_internals, workflowId, scheduledStartAt, options, callbacks) {
7
8
  return {
@@ -85,6 +86,10 @@ export async function startDelayedWorkflow(internals, entry, callbacks) {
85
86
  });
86
87
  if (!runningState)
87
88
  return;
89
+ if (await reprovideRecoveredServices(internals, runningState, (workflowId, error) => callbacks.failWorkflow(workflowId, error), callbacks.handleCleanupError))
90
+ return;
91
+ if (await storageHas(internals.storage, KEYS.terminalCleanupNeeded(entry.workflowId)))
92
+ internals.workflowsNeedingTerminalCleanup.add(entry.workflowId);
88
93
  internals.checkpoints.set(entry.workflowId, checkpoint);
89
94
  internals.workflowVersionTuples.set(entry.workflowId, callbacks.workflowVersionTupleFromState(runningState));
90
95
  callbacks.setWorkflowStartHeaders(entry.workflowId, await callbacks.loadWorkflowStartHeaders(entry.workflowId));
@@ -71,6 +71,7 @@ export function cleanupWaiters(internals, workflowId, callbacks) {
71
71
  cleanupReviewEscalations(internals, workflowId, callbacks);
72
72
  internals.workflowNestingDepths.delete(workflowId);
73
73
  internals.workflowHeaders.delete(workflowId);
74
+ internals.workflowServices.delete(workflowId);
74
75
  internals.workflowTypeByWorkflowId.delete(workflowId);
75
76
  }
76
77
  export async function cleanupWorkflowStorage(internals, workflowId, includeOutputArtifacts) {
@@ -86,6 +87,7 @@ export async function cleanupWorkflowStorage(internals, workflowId, includeOutpu
86
87
  prefixes.push(`offload:${encodedWorkflowId}:`, `blob:${encodedWorkflowId}:`);
87
88
  await internals.storage.delete(KEYS.workflowHeaders(workflowId));
88
89
  await internals.storage.delete(KEYS.signalSequence(workflowId));
90
+ await internals.storage.delete(KEYS.workflowHasServices(workflowId));
89
91
  if (internals.storage.deletePrefix) {
90
92
  for (const prefix of prefixes)
91
93
  await internals.storage.deletePrefix(prefix);
@@ -25,6 +25,11 @@ export interface InlineExecutionDependencies {
25
25
  development?: boolean;
26
26
  getComposedWorkflowInterceptor?: () => ComposedWorkflowInterceptor | null;
27
27
  registerCancelHandler?: (workflowId: string, handler: () => Promise<void> | void) => () => void;
28
+ /**
29
+ * Look up the non-serialized per-run `services` value for a workflow, exposed
30
+ * to the body as `ctx.services`. Returns `undefined` when none was supplied.
31
+ */
32
+ getWorkflowServices?: (workflowId: string) => unknown;
28
33
  }
29
34
  type InlineStartWorkflowParameters = {
30
35
  workflowId: string;
@@ -25,7 +25,8 @@ function createInlineContextOptions(dependencies, registration, parameters, work
25
25
  ...parameters.deadline !== void 0 && { deadline: parameters.deadline },
26
26
  ...registerCancelHandler !== void 0 && {
27
27
  registerCancelHandler: (handler) => registerCancelHandler(parameters.workflowId, handler)
28
- }
28
+ },
29
+ services: dependencies.getWorkflowServices?.(parameters.workflowId)
29
30
  };
30
31
  }
31
32