@lostgradient/weft 0.10.0 → 0.12.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 (102) hide show
  1. package/README.md +10 -2
  2. package/dist/cli/generated/operation-catalog.snapshot.json +4671 -0
  3. package/dist/cli/generated/operation-client.generated.d.ts +1 -0
  4. package/dist/cli/index.js +1 -0
  5. package/dist/cli-main.js +227 -401
  6. package/dist/client/http-operations.js +7 -1
  7. package/dist/client/http-request.d.ts +23 -1
  8. package/dist/client/http-request.js +12 -3
  9. package/dist/client/index.d.ts +2 -0
  10. package/dist/client/index.js +7 -0
  11. package/dist/connection.d.ts +8 -0
  12. package/dist/connection.js +41 -17
  13. package/dist/core/checkpoint/serialization.js +2 -2
  14. package/dist/core/engine/callback-creators-core.js +2 -1
  15. package/dist/core/engine/disposal.js +4 -0
  16. package/dist/core/engine/engine-leak-warnings.d.ts +2 -0
  17. package/dist/core/engine/engine-leak-warnings.js +7 -1
  18. package/dist/core/engine/index.d.ts +49 -3
  19. package/dist/core/engine/index.js +19 -3
  20. package/dist/core/engine/inline-parking.js +14 -1
  21. package/dist/core/engine/internals.d.ts +21 -0
  22. package/dist/core/engine/lifecycle/shared.d.ts +25 -0
  23. package/dist/core/engine/lifecycle/transition.js +19 -9
  24. package/dist/core/engine/operations-time.js +8 -28
  25. package/dist/core/engine/sleep-timer-acknowledgements.d.ts +15 -0
  26. package/dist/core/engine/sleep-timer-acknowledgements.js +144 -0
  27. package/dist/core/engine/termination/cleanup.js +3 -0
  28. package/dist/core/types/definition-schema-to-json.d.ts +11 -0
  29. package/dist/core/types/definition-schema-to-json.js +11 -2
  30. package/dist/http.js +2 -0
  31. package/dist/indexeddb.js +1 -0
  32. package/dist/json-schema.js +3 -3
  33. package/dist/mcp/cli.js +63 -61
  34. package/dist/mcp/http.d.ts +7 -2
  35. package/dist/mcp/session.d.ts +3 -3
  36. package/dist/mcp/session.js +3 -2
  37. package/dist/mcp/stdio.d.ts +7 -2
  38. package/dist/observability/index.js +2 -2
  39. package/dist/runtime/portable.d.ts +33 -0
  40. package/dist/runtime/portable.js +17 -6
  41. package/dist/server/engine-event-feed-backend.d.ts +22 -3
  42. package/dist/server/fleet-event-feed.d.ts +65 -0
  43. package/dist/server/handler/index.d.ts +11 -2
  44. package/dist/server/handler/index.js +7 -0
  45. package/dist/server/handler.js +1 -56
  46. package/dist/server/index.d.ts +12 -1
  47. package/dist/server/index.js +87 -64
  48. package/dist/server/operations/submit-review-decision.d.ts +2 -0
  49. package/dist/server/operations/submit-review-decision.js +19 -2
  50. package/dist/server/replay-live-feed-internals.d.ts +19 -0
  51. package/dist/server/replay-live-feed-internals.js +76 -0
  52. package/dist/server/runtime/authentication-bridge.d.ts +2 -2
  53. package/dist/server/serve-internals.js +3 -3
  54. package/dist/server/workflow-event-feed.d.ts +100 -11
  55. package/dist/server/workflow-event-feed.js +1 -140
  56. package/dist/service-worker/index.d.ts +8 -2
  57. package/dist/service-worker/index.js +37 -56
  58. package/dist/service-worker/scheduler.js +1 -0
  59. package/dist/service-worker/setup.d.ts +6 -2
  60. package/dist/storage/auto.js +1 -1
  61. package/dist/storage/bun-sql.js +3 -3
  62. package/dist/storage/cloudflare-durable-object-sql.d.ts +75 -0
  63. package/dist/storage/cloudflare-durable-object-sql.js +0 -0
  64. package/dist/storage/cloudflare-value-codec.d.ts +65 -0
  65. package/dist/storage/cloudflare-value-codec.js +46 -0
  66. package/dist/storage/cloudflare.d.ts +140 -0
  67. package/dist/storage/cloudflare.js +127 -0
  68. package/dist/storage/compressed-storage.js +1 -1
  69. package/dist/storage/http.js +200 -2
  70. package/dist/storage/index.d.ts +1 -0
  71. package/dist/storage/indexeddb.js +311 -1
  72. package/dist/storage/interface.js +1 -1
  73. package/dist/storage/lazy-postgres-pool.d.ts +38 -0
  74. package/dist/storage/lazy-postgres-pool.js +38 -0
  75. package/dist/storage/lmdb.js +1 -1
  76. package/dist/storage/memory.js +1 -1
  77. package/dist/storage/neon-batch.d.ts +8 -8
  78. package/dist/storage/neon.d.ts +26 -79
  79. package/dist/storage/neon.js +4 -4
  80. package/dist/storage/node-sqlite.js +3 -3
  81. package/dist/storage/postgres-key-value-queries.d.ts +7 -1
  82. package/dist/storage/postgres-key-value-queries.js +2 -3
  83. package/dist/storage/postgres-key-value-storage.d.ts +110 -0
  84. package/dist/storage/postgres-key-value-storage.js +204 -0
  85. package/dist/storage/postgres.d.ts +59 -0
  86. package/dist/storage/postgres.js +15 -0
  87. package/dist/storage/resolve.js +1 -1
  88. package/dist/storage/scoped-storage.js +1 -1
  89. package/dist/storage/sql-identifier.d.ts +35 -0
  90. package/dist/storage/sql-identifier.js +5 -0
  91. package/dist/storage/sqlite-key-value-queries.d.ts +11 -6
  92. package/dist/storage/sqlite-key-value-queries.js +3 -3
  93. package/dist/storage/testing.js +1 -1
  94. package/dist/storage/turso.js +2 -2
  95. package/dist/storage/typed-storage.js +138 -2
  96. package/dist/storage/web-extension.js +298 -1
  97. package/dist/testing/index.js +21 -60
  98. package/dist/version.d.ts +1 -1
  99. package/dist/version.js +1 -1
  100. package/dist/web-extension.js +1 -0
  101. package/dist/worker/protocol.js +1 -1
  102. package/package.json +23 -5
@@ -6,6 +6,9 @@ function isJsonRpcFailure(value) {
6
6
  function isJsonRpcSuccess(value) {
7
7
  return typeof value === "object" && value !== null && "result" in value;
8
8
  }
9
+ function isRecord(value) {
10
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11
+ }
9
12
  export function httpClientCatalogTransport(baseUrl, headers) {
10
13
  const endpoint = `${baseUrl}/jsonrpc`;
11
14
  return async (operationName, input) => {
@@ -27,7 +30,10 @@ export function httpClientCatalogTransport(baseUrl, headers) {
27
30
  }
28
31
  if (isJsonRpcFailure(body)) {
29
32
  const { message, data } = body.error, httpStatus = typeof data?.httpStatus === "number" ? data.httpStatus : response.status, faultCode = isFaultCode(data?.weftCode) ? data.weftCode : void 0;
30
- throw new HttpClientError(httpStatus, message, { faultCode });
33
+ throw new HttpClientError(httpStatus, message, {
34
+ faultCode,
35
+ data: isRecord(data) ? data : void 0
36
+ });
31
37
  }
32
38
  if (isJsonRpcSuccess(body))
33
39
  return body.result;
@@ -67,7 +67,11 @@ export declare function resolveHttpClientConnection(options: HttpClientOptions):
67
67
  * the wire fault `code` is surfaced as {@link HttpClientError.faultCode} and a
68
68
  * derived {@link HttpClientError.category} so callers can branch programmatically
69
69
  * instead of string-matching `message`. Both are `undefined` when the body is a
70
- * plain `{ error: string }` or carries no recognized code.
70
+ * plain `{ error: string }` or carries no recognized code. The fault's typed
71
+ * `data` payload (e.g. `InvalidParams`'s `issues`, `NotFound`/`Conflict`'s
72
+ * `resource`/`identifier`) is surfaced verbatim as {@link HttpClientError.data}
73
+ * when the body carries a structured `data` object; `undefined` otherwise —
74
+ * including for a masked `EngineFailure`, whose flat body carries no `data`.
71
75
  *
72
76
  * @example
73
77
  * ```ts
@@ -110,9 +114,27 @@ export declare class HttpClientError extends WeftError<'HttpClientError'> {
110
114
  * carried only the coarse {@link faultCode} (most faults) or no structured body.
111
115
  */
112
116
  readonly weftCode?: WeftErrorCode | undefined;
117
+ /**
118
+ * The fault's wire `data` payload, when the response carried a structured
119
+ * body (`{ error: { data } }` for REST, `error.data` for JSON-RPC). Shape is
120
+ * fault-code-dependent — see {@link FaultCode} and the server's
121
+ * `OperationFault` per-code `data` union for what each code carries (e.g.
122
+ * `InvalidParams.data.issues`, `NotFound.data.resource`). `undefined` for
123
+ * plain-string error bodies, bodies with no `data` field, or a `data` field
124
+ * that is not a JSON object.
125
+ *
126
+ * Over JSON-RPC-over-HTTP this is the raw envelope `error.data` verbatim
127
+ * (see `httpClientCatalogTransport` in `http-operations.ts`), so it also
128
+ * carries the envelope's own `weftCode` (the coarse {@link FaultCode}, not
129
+ * a fine-grained {@link WeftErrorCode}) and `httpStatus` keys alongside the
130
+ * per-code payload — those two are not part of the `OperationFault` data
131
+ * union.
132
+ */
133
+ readonly data?: Readonly<Record<string, unknown>> | undefined;
113
134
  constructor(status: number, message: string, options?: {
114
135
  faultCode?: FaultCode | undefined;
115
136
  weftCode?: WeftErrorCode | undefined;
137
+ data?: Readonly<Record<string, unknown>> | undefined;
116
138
  });
117
139
  }
118
140
  export declare function request<T>(baseUrl: string, path: string, baseHeaders: Record<string, string>, options?: RequestInit): Promise<T>;
@@ -17,12 +17,14 @@ export class HttpClientError extends WeftError {
17
17
  faultCode;
18
18
  category;
19
19
  weftCode;
20
+ data;
20
21
  constructor(status, message, options) {
21
22
  super("HttpClientError", message);
22
23
  this.status = status;
23
24
  this.faultCode = options?.faultCode;
24
25
  this.category = options?.faultCode === void 0 ? void 0 : failureCategoryForFaultCode(options.faultCode);
25
26
  this.weftCode = options?.weftCode;
27
+ this.data = options?.data;
26
28
  }
27
29
  }
28
30
  function buildRequestHeaders(baseHeaders, options) {
@@ -53,11 +55,18 @@ function weftCodeFromData(data) {
53
55
  const candidate = data.weftCode;
54
56
  return isWeftErrorCode(candidate) ? candidate : void 0;
55
57
  }
58
+ function isRecord(value) {
59
+ return typeof value === "object" && value !== null && !Array.isArray(value);
60
+ }
56
61
  async function parseErrorBody(response) {
57
62
  try {
58
63
  const body = await response.json();
59
64
  if (isStructuredErrorBody(body)) {
60
- const { code, message, data } = body.error, weftCode = weftCodeFromData(data), base = weftCode === void 0 ? { message } : { message, weftCode };
65
+ const { code, message, data } = body.error, weftCode = weftCodeFromData(data), base = { message };
66
+ if (weftCode !== void 0)
67
+ base.weftCode = weftCode;
68
+ if (isRecord(data))
69
+ base.data = data;
61
70
  return isFaultCode(code) ? { ...base, faultCode: code } : base;
62
71
  }
63
72
  if (isFlatErrorBody(body) && body.error) {
@@ -74,8 +83,8 @@ export async function request(baseUrl, path, baseHeaders, options) {
74
83
  if (response.status === 404 && (!options?.method || options.method === "GET"))
75
84
  return null;
76
85
  if (!response.ok) {
77
- const { message, faultCode, weftCode } = await parseErrorBody(response);
78
- throw new HttpClientError(response.status, message, { faultCode, weftCode });
86
+ const { message, faultCode, weftCode, data } = await parseErrorBody(response);
87
+ throw new HttpClientError(response.status, message, { faultCode, weftCode, data });
79
88
  }
80
89
  if (response.status === 204)
81
90
  return;
@@ -7,6 +7,8 @@
7
7
  *
8
8
  * @module client/index
9
9
  */
10
+ export { isWeftError, isWeftErrorCode, isWeftErrorLike, isWeftFault, WeftError, } from '../core/weft-error.ts';
11
+ export type { WeftErrorCode } from '../core/weft-error.ts';
10
12
  export type { WorkflowEventStreamOptions, WorkflowEventTransport } from './event-stream-options.ts';
11
13
  export type { WorkflowEventTail } from './event-tail.ts';
12
14
  export { HttpClient } from './http-client.ts';
@@ -1,2 +1,9 @@
1
+ export {
2
+ isWeftError,
3
+ isWeftErrorCode,
4
+ isWeftErrorLike,
5
+ isWeftFault,
6
+ WeftError
7
+ } from "../core/weft-error.js";
1
8
  export { HttpClient } from "./http-client.js";
2
9
  export { HttpClientError } from "./http-request.js";
@@ -14,6 +14,14 @@
14
14
  * A profile token is only applied when neither an explicit `server` option nor
15
15
  * `WEFT_ADDR` redirected the request to a different destination.
16
16
  *
17
+ * This module is imported from `@lostgradient/weft/client` (browser-reachable),
18
+ * so it must stay statically free of `node:*` and Bun-only imports. Environment
19
+ * variables go through {@link readEnvironmentVariable}; `~/.weft/config` and the
20
+ * run lockfile are read through {@link tryLoadNodeBuiltin}, which resolves
21
+ * `node:fs`/`node:fs/promises` via `process.getBuiltinModule` instead of a
22
+ * static import. Both return `undefined` outside Bun/Node, so a browser caller
23
+ * that supplies explicit `server`/`token` never touches either.
24
+ *
17
25
  * @module connection
18
26
  */
19
27
  /**
@@ -1,5 +1,4 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { mkdir, rm } from "node:fs/promises";
1
+ import { isBunRuntime, readEnvironmentVariable, tryLoadNodeBuiltin } from "./runtime/portable.js";
3
2
  export const DEFAULT_WEFT_ADDRESS = "http://localhost:7233";
4
3
 
5
4
  export class ConnectionConfigurationError extends Error {
@@ -9,7 +8,7 @@ export class ConnectionConfigurationError extends Error {
9
8
  }
10
9
  }
11
10
  export function resolveConnection(options = {}) {
12
- const context = resolveConnectionContext(options), server = resolveServerString(context), fallbackProfile = profileForToken(context, server), token = resolveToken(options.token ?? Bun.env.WEFT_TOKEN, fallbackProfile);
11
+ const context = resolveConnectionContext(options), server = resolveServerString(context), fallbackProfile = profileForToken(context, server), token = resolveToken(options.token ?? readEnvironmentVariable("WEFT_TOKEN"), fallbackProfile);
13
12
  return {
14
13
  server: parseServerUrl(server),
15
14
  ...token === void 0 ? {} : { token }
@@ -26,7 +25,7 @@ function parseServerUrl(server) {
26
25
  function profileForToken(context, resolvedServer) {
27
26
  if (context.profile === void 0)
28
27
  return;
29
- if (!(context.options.server !== void 0 || Bun.env.WEFT_ADDR !== void 0))
28
+ if (!(context.options.server !== void 0 || readEnvironmentVariable("WEFT_ADDR") !== void 0))
30
29
  return context.profile;
31
30
  const profileServer = context.profile.server;
32
31
  if (profileServer === void 0)
@@ -44,7 +43,7 @@ function sameDestination(a, b) {
44
43
  return left.origin === right.origin && left.pathname.replace(/\/+$/, "") === right.pathname.replace(/\/+$/, "");
45
44
  }
46
45
  function resolveConnectionContext(options) {
47
- const configuration = readWeftConfiguration(), profileName = options.profile ?? Bun.env.WEFT_PROFILE ?? configuration.defaultProfile, profile = profileName === void 0 ? void 0 : configuration.profiles?.[profileName], runLockfile = options.includeRunLockfile === !1 ? void 0 : readRunLockfile();
46
+ const configuration = readWeftConfiguration(), profileName = options.profile ?? readEnvironmentVariable("WEFT_PROFILE") ?? configuration.defaultProfile, profile = profileName === void 0 ? void 0 : configuration.profiles?.[profileName], runLockfile = options.includeRunLockfile === !1 ? void 0 : readRunLockfile();
48
47
  return {
49
48
  options,
50
49
  ...profile === void 0 ? {} : { profile },
@@ -52,12 +51,26 @@ function resolveConnectionContext(options) {
52
51
  };
53
52
  }
54
53
  function resolveServerString(context) {
55
- return context.options.server ?? Bun.env.WEFT_ADDR ?? context.profile?.server ?? context.runLockfile?.server ?? context.runLockfile?.url ?? DEFAULT_WEFT_ADDRESS;
54
+ return context.options.server ?? readEnvironmentVariable("WEFT_ADDR") ?? context.profile?.server ?? context.runLockfile?.server ?? context.runLockfile?.url ?? DEFAULT_WEFT_ADDRESS;
55
+ }
56
+ function loadFsModule() {
57
+ return tryLoadNodeBuiltin("node:fs");
58
+ }
59
+ function loadFsPromisesModule() {
60
+ return tryLoadNodeBuiltin("node:fs/promises");
56
61
  }
57
62
  export async function writeRunLockfile(server) {
58
- await mkdir(weftHome(), { recursive: !0 });
59
- await Bun.write(runLockfilePath(), `${JSON.stringify({ server }, null, 2)}
60
- `);
63
+ const fsPromises = loadFsPromisesModule();
64
+ if (fsPromises === void 0)
65
+ throw Error("writeRunLockfile requires Bun or Node 22.5+ (process.getBuiltinModule); not available in this runtime.");
66
+ await fsPromises.mkdir(weftHome(), { recursive: !0 });
67
+ const contents = `${JSON.stringify({ server }, null, 2)}
68
+ `;
69
+ if (isBunRuntime()) {
70
+ await Bun.write(runLockfilePath(), contents);
71
+ return;
72
+ }
73
+ await fsPromises.writeFile(runLockfilePath(), contents, "utf8");
61
74
  }
62
75
  export async function removeRunLockfile(server) {
63
76
  const lockfile = readRunLockfile();
@@ -65,15 +78,23 @@ export async function removeRunLockfile(server) {
65
78
  return;
66
79
  if ((lockfile.server ?? lockfile.url) !== server)
67
80
  return;
68
- await rm(runLockfilePath(), { force: !0 });
81
+ const fsPromises = loadFsPromisesModule();
82
+ if (fsPromises === void 0)
83
+ return;
84
+ await fsPromises.rm(runLockfilePath(), { force: !0 });
69
85
  }
70
86
  function readWeftConfiguration() {
87
+ if (!isBunRuntime())
88
+ return {};
89
+ const fs = loadFsModule();
90
+ if (fs === void 0)
91
+ return {};
71
92
  const path = configurationPath();
72
- if (!existsSync(path))
93
+ if (!fs.existsSync(path))
73
94
  return {};
74
95
  let parsed;
75
96
  try {
76
- parsed = Bun.TOML.parse(readFileSync(path, "utf8"));
97
+ parsed = Bun.TOML.parse(fs.readFileSync(path, "utf8"));
77
98
  } catch (error) {
78
99
  const message = error instanceof Error ? error.message : String(error);
79
100
  throw new ConnectionConfigurationError(`Failed to read connection configuration at ${path}: ${message}`);
@@ -81,10 +102,13 @@ function readWeftConfiguration() {
81
102
  return normalizeConfiguration(parsed);
82
103
  }
83
104
  function readRunLockfile() {
105
+ const fs = loadFsModule();
106
+ if (fs === void 0)
107
+ return;
84
108
  const path = runLockfilePath();
85
- if (!existsSync(path))
109
+ if (!fs.existsSync(path))
86
110
  return;
87
- const text = readFileSync(path, "utf8").trim();
111
+ const text = fs.readFileSync(path, "utf8").trim();
88
112
  if (text === "")
89
113
  return;
90
114
  try {
@@ -137,11 +161,11 @@ function normalizeRunLockfile(value) {
137
161
  function resolveToken(token, profile) {
138
162
  const directToken = token ?? profile?.token;
139
163
  if (directToken?.startsWith("env:"))
140
- return Bun.env[directToken.slice(4)];
164
+ return readEnvironmentVariable(directToken.slice(4));
141
165
  if (directToken !== void 0)
142
166
  return directToken;
143
167
  if (profile?.tokenEnv !== void 0)
144
- return Bun.env[profile.tokenEnv];
168
+ return readEnvironmentVariable(profile.tokenEnv);
145
169
  return;
146
170
  }
147
171
  function configurationPath() {
@@ -151,7 +175,7 @@ function runLockfilePath() {
151
175
  return `${weftHome()}/run`;
152
176
  }
153
177
  function weftHome() {
154
- return Bun.env.WEFT_HOME ?? `${Bun.env.HOME ?? "."}/.weft`;
178
+ return readEnvironmentVariable("WEFT_HOME") ?? `${readEnvironmentVariable("HOME") ?? "."}/.weft`;
155
179
  }
156
180
  function stringValue(value) {
157
181
  return typeof value === "string" && value !== "" ? value : void 0;
@@ -29,7 +29,7 @@ export function validateCheckpointShape(value) {
29
29
  validateAccumulatedResultReplayWatermark(record);
30
30
  validateWorkerReplaySignatures(record);
31
31
  validateWorkerReplayFailures(record);
32
- dropLegacyPendingSignals(record);
32
+ dropRetiredPendingSignals(record);
33
33
  assertRecordField(record, "searchAttributes");
34
34
  assertStringField(record, "version");
35
35
  assertNumberField(record, "createdAt");
@@ -148,7 +148,7 @@ function validateAccumulatedResultReplayWatermark(record) {
148
148
  if (typeof watermark !== "number" || !Number.isSafeInteger(watermark) || watermark < 0)
149
149
  throw Error('Invalid checkpoint: invalid "accumulatedResultReplayWatermark" (expected non-negative safe integer)');
150
150
  }
151
- function dropLegacyPendingSignals(record) {
151
+ function dropRetiredPendingSignals(record) {
152
152
  delete record.pendingSignals;
153
153
  }
154
154
  function assertCurrentSchemaVersion(record) {
@@ -77,7 +77,8 @@ export function createLifecycleCallbacks(engine) {
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
79
  failWorkflowForRecoveryHook: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine), "system"),
80
- failWorkflowForCheckpointDecodeError: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine), "system")
80
+ failWorkflowForCheckpointDecodeError: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine), "system"),
81
+ failWorkflowForVersionMismatch: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine), "system")
81
82
  };
82
83
  }
83
84
  export function createTerminationCallbacksWith(engine, handleScheduledWorkflowTerminal) {
@@ -1,6 +1,7 @@
1
1
  import { disposeEngineCleanupInterval } from "./engine-runtime-helpers.js";
2
2
  import { EngineDisposedError } from "./errors.js";
3
3
  import { disposeQueuedInlineWorkflowStarts } from "./inline-launch-queue.js";
4
+ import { rejectAllSleepTimerAcknowledgements } from "./sleep-timer-acknowledgements.js";
4
5
  export function disposeEngine(internals) {
5
6
  internals.disposed = !0;
6
7
  internals.alertManager?.[Symbol.dispose]();
@@ -30,6 +31,7 @@ export function disposeEngine(internals) {
30
31
  internals.handleCache.clear();
31
32
  for (const waiter of internals.resultResolvers.values())
32
33
  waiter.reject(new EngineDisposedError);
34
+ rejectAllSleepTimerAcknowledgements(internals, new EngineDisposedError);
33
35
  internals.resultResolvers.clear();
34
36
  internals.updateWaiters.clear();
35
37
  internals.updateWaitersByWorkflow.clear();
@@ -46,6 +48,8 @@ export function disposeEngine(internals) {
46
48
  internals.pendingWebhooks.clear();
47
49
  internals.sleepResolvers.clear();
48
50
  internals.sleepResolversByWorkflow.clear();
51
+ internals.sleepTimerAcknowledgementWaiters.clear();
52
+ internals.durableInlineOperations.clear();
49
53
  internals.sleepTimersFiredWithoutResolver.clear();
50
54
  internals.checkpoints.clear();
51
55
  internals.pendingExecutionStateOwnerId = void 0;
@@ -15,6 +15,8 @@ export type EngineCleanupIntervalDisposalTracker = {
15
15
  testToken: symbol | undefined;
16
16
  };
17
17
  export declare const engineCleanupIntervalFinalizer: FinalizationRegistry<EngineCleanupIntervalDisposalTracker>;
18
+ /** Test-only hook for the finalizer callback's synchronous warning gate. */
19
+ export declare function finalizeEngineCleanupIntervalTrackerForTesting(tracker: EngineCleanupIntervalDisposalTracker): void;
18
20
  export declare function shouldEmitEngineLeakWarning(): boolean;
19
21
  /** Test-only override for the engine leak-warning environment gate. */
20
22
  export declare function setEngineLeakWarningOverrideForTesting(value: boolean | undefined): void;
@@ -1,6 +1,6 @@
1
1
  let engineLeakWarningOverrideForTesting, engineLeakCollectionCountForTesting = 0, nextEngineLeakWarningTokenForTesting;
2
2
  const engineLeakWarningTokensForTesting = new Set;
3
- export const engineCleanupIntervalFinalizer = new FinalizationRegistry((tracker) => {
3
+ function finalizeEngineCleanupIntervalTracker(tracker) {
4
4
  engineLeakCollectionCountForTesting++;
5
5
  if (tracker.cleanupInterval !== null) {
6
6
  clearInterval(tracker.cleanupInterval);
@@ -15,7 +15,13 @@ export const engineCleanupIntervalFinalizer = new FinalizationRegistry((tracker)
15
15
  engineLeakWarningTokensForTesting.add(tracker.testToken);
16
16
  process.emitWarning("WeftEngineLeakWarning: A Weft Engine was garbage-collected without calling [Symbol.dispose](). Use `using`, `await using`, or call engine[Symbol.dispose]() to clear background timers and release runtime resources.");
17
17
  }
18
+ }
19
+ export const engineCleanupIntervalFinalizer = new FinalizationRegistry((tracker) => {
20
+ finalizeEngineCleanupIntervalTracker(tracker);
18
21
  });
22
+ export function finalizeEngineCleanupIntervalTrackerForTesting(tracker) {
23
+ finalizeEngineCleanupIntervalTracker(tracker);
24
+ }
19
25
  export function shouldEmitEngineLeakWarning() {
20
26
  if (engineLeakWarningOverrideForTesting !== void 0)
21
27
  return engineLeakWarningOverrideForTesting;
@@ -220,9 +220,12 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
220
220
  * (signal-with-start). With an absent target, the workflow record and the
221
221
  * first signal commit in one batch and the freshly-launched run consumes the
222
222
  * signal on its first drive. A non-terminal target (running, pending, or
223
- * suspended) is signalled through the normal signal path; a terminal target
224
- * throws {@link StartOrSignalConflictError} unless
225
- * `options.onTerminalConflict: 'start-new'` is supplied with an explicit
223
+ * suspended) is signalled through a workflow-serialized signal path. If
224
+ * terminal completion wins that handoff, the target is treated as terminal:
225
+ * the default path throws {@link StartOrSignalConflictError}, while
226
+ * `options.onTerminalConflict: 'start-new'` transfers the signal to the
227
+ * replacement run. A terminal target throws {@link StartOrSignalConflictError}
228
+ * unless `options.onTerminalConflict: 'start-new'` is supplied with an explicit
226
229
  * workflow id and deterministic `signal.signalId`.
227
230
  *
228
231
  * Concurrent callers converge on one workflow and one delivered signal. Pass
@@ -368,6 +371,12 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
368
371
  * deploys or explicit operator storage repair.
369
372
  * When set, unknown workflow types are skipped and reported through
370
373
  * {@link WorkflowRecoverySkippedEvent}.
374
+ *
375
+ * A recovered workflow whose persisted version metadata no longer matches
376
+ * its registered `WorkflowDefinition.version` is isolated per
377
+ * {@link RecoverAllOptions.versionMismatchPolicy} (default `'fail-run'`):
378
+ * only that workflow fails, and `recoverAll()` continues recovering its
379
+ * siblings in the same call.
371
380
  */
372
381
  recoverAll(options?: RecoverAllOptions): Promise<WorkflowHandle[]>;
373
382
  /**
@@ -487,3 +496,40 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
487
496
  */
488
497
  fireTimer(entry: TimerEntry): Promise<void>;
489
498
  }
499
+ /**
500
+ * `Engine` with its two chained-builder registration methods removed —
501
+ * `register` and `registerWorkflows`, whose return type is itself
502
+ * `Engine<NarrowedRegistry>` (see `register()`'s JSDoc: registering returns
503
+ * "this same engine with the definition added to its phantom type
504
+ * registry"). That self-reference is what makes `Engine` invariant in its
505
+ * registry generics: a concretely narrowed `Engine<Concrete>` (e.g. from
506
+ * `Engine.create({ workflows })`) is not structurally assignable to the
507
+ * plain default `Engine<DefaultWorkflowRegistry>`, even though every other
508
+ * member — including `start` and `startOrSignal`, whose *parameter* types
509
+ * (not return types) reference `TWorkflows` but never produce another
510
+ * `Engine<T>` — is not part of that recursive comparison.
511
+ *
512
+ * `start` and `startOrSignal` are deliberately KEPT (not omitted): hosted
513
+ * transports genuinely call them at runtime (REST/JSON-RPC workflow starts,
514
+ * MCP tool invocations) via `runtimeWorkflowEngine()`'s registry-erased
515
+ * dynamic-name overload, so a value satisfying this type must still provide
516
+ * them — a duck-typed engine substitute lacking `start` correctly fails to
517
+ * satisfy `RegistryAgnosticEngine` (see this type's `.test-d.ts` coverage).
518
+ *
519
+ * Host-facing options that accept an already-constructed `Engine` without
520
+ * needing the two chained-builder registration methods — `serve({ engine })`,
521
+ * the Service Worker helpers, the MCP session/HTTP/stdio surfaces — use this
522
+ * type instead of the bare default `Engine`, so both `new Engine({ storage })`
523
+ * and `Engine.create({ workflows })` are accepted without a call-site cast.
524
+ * See #708.
525
+ *
526
+ * (`Engine<object, object>` looks like the obvious fix — the widest legal
527
+ * instantiation of the registry generics — but TypeScript's structural check
528
+ * on `register()`'s self-referential return type does not reliably resolve
529
+ * that relationship: it can pass or fail for the identical `Engine<A>` /
530
+ * `Engine<B>` pair depending on unrelated compilation context, such as other
531
+ * files in the same program. Removing the registry-typed members entirely,
532
+ * rather than widening their generic arguments, avoids the recursive
533
+ * comparison altogether.)
534
+ */
535
+ export type RegistryAgnosticEngine = Omit<Engine, 'register' | 'registerWorkflows'>;
@@ -74,7 +74,11 @@ import {
74
74
  shouldStartEngineScheduler,
75
75
  validateEngineCreateBackgroundTaskOptions
76
76
  } from "./engine-runtime-helpers.js";
77
- import { EngineCreateNameMismatchError, EngineDisposedError } from "./errors.js";
77
+ import {
78
+ EngineCreateNameMismatchError,
79
+ EngineDisposedError,
80
+ StartOrSignalConflictError
81
+ } from "./errors.js";
78
82
  import { assertLeaseHeldForEngineWork, commitFencedEngineWrite } from "./fenced-write.js";
79
83
  import { recordFinalizerState } from "./finalizer-state.js";
80
84
  import {
@@ -145,7 +149,11 @@ import {
145
149
  createSecondInstanceDetector
146
150
  } from "./second-instance-detector.js";
147
151
  import { signal as signalWorkflow } from "./signals.js";
148
- import { loadScheduleState, loadWorkflowState } from "./storage-io.js";
152
+ import {
153
+ loadScheduleState,
154
+ loadWorkflowState,
155
+ runSerializedWorkflowStateWrite
156
+ } from "./storage-io.js";
149
157
  import {
150
158
  feedOperationResult,
151
159
  getComposedWorkflowInterceptor,
@@ -163,6 +171,7 @@ import {
163
171
  submitCoordinatedUpdate as submitCoordinatedUpdateFromInternals,
164
172
  update as updateFromInternals
165
173
  } from "./updates.js";
174
+ import { isTerminalWorkflowStatus } from "./validation.js";
166
175
  import { coerceScheduleId } from "./validation/schedule.js";
167
176
  import {
168
177
  replayWorkflowFeed,
@@ -280,6 +289,8 @@ export class Engine extends EventTarget {
280
289
  getInternals(this).updateWaitersByWorkflow = new Map;
281
290
  getInternals(this).sleepResolvers = new Map;
282
291
  getInternals(this).sleepResolversByWorkflow = new Map;
292
+ getInternals(this).sleepTimerAcknowledgementWaiters = new Map;
293
+ getInternals(this).durableInlineOperations = new Map;
283
294
  getInternals(this).sleepTimersFiredWithoutResolver = new Map;
284
295
  getInternals(this).interceptors = resolveEngineInterceptors(options);
285
296
  getInternals(this).composedWorkflowInterceptor = void 0;
@@ -568,7 +579,12 @@ export class Engine extends EventTarget {
568
579
  #createStartOrSignalCallbacks() {
569
580
  return {
570
581
  ...this.#createLifecycleCallbacks(),
571
- signalExistingWorkflow: (workflowId, signalName, payload, signalId) => this.signal(workflowId, signalName, payload, { signalId })
582
+ signalExistingWorkflow: (workflowId, signalName, payload, signalId) => runSerializedWorkflowStateWrite(getInternals(this), workflowId, async () => {
583
+ const state = await loadWorkflowState(getInternals(this), workflowId);
584
+ if (state && isTerminalWorkflowStatus(state.status))
585
+ throw new StartOrSignalConflictError(workflowId, state.status);
586
+ await this.signal(workflowId, signalName, payload, { signalId });
587
+ })
572
588
  };
573
589
  }
574
590
  getHandle(workflowId) {
@@ -2,6 +2,10 @@ import { hasExposedAccessors, hasUpdateHandlers } from "../context/context-prese
2
2
  import {
3
3
  resumeWorkflowFromStorage as resumeWorkflowFromStorageFromLifecycle
4
4
  } from "./lifecycle.js";
5
+ import {
6
+ recordDurableInlineOperation,
7
+ rejectSleepTimerAcknowledgements
8
+ } from "./sleep-timer-acknowledgements.js";
5
9
  import {
6
10
  completeWorkflow as completeWorkflowFromTermination,
7
11
  failWorkflow as failWorkflowFromTermination
@@ -53,7 +57,7 @@ export async function getParkedWorkflowResumeDisposition(internals, workflowId,
53
57
  return "corrupt";
54
58
  return "resumable";
55
59
  }
56
- export async function handleStrategyMessage(internals, message, callbacks) {
60
+ async function dispatchStrategyMessage(internals, message, callbacks) {
57
61
  switch (message.type) {
58
62
  case "completed":
59
63
  await completeWorkflowFromTermination(internals, message.workflowId, message.result, callbacks.createTerminationCallbacks());
@@ -68,6 +72,7 @@ export async function handleStrategyMessage(internals, message, callbacks) {
68
72
  case "checkpoint": {
69
73
  const operation = callbacks.translateOperationRequest(message.operationRequest);
70
74
  await callbacks.persistCheckpoint(message.workflowId, operation, message.checkpoint);
75
+ recordDurableInlineOperation(internals, message.workflowId, operation);
71
76
  callbacks.validateDevelopmentCheckpoint(message.workflowId);
72
77
  if (await callbacks.evaluateConstraints(message.workflowId))
73
78
  break;
@@ -85,3 +90,11 @@ export async function handleStrategyMessage(internals, message, callbacks) {
85
90
  }
86
91
  }
87
92
  }
93
+ export async function handleStrategyMessage(internals, message, callbacks) {
94
+ try {
95
+ await dispatchStrategyMessage(internals, message, callbacks);
96
+ } catch (error) {
97
+ rejectSleepTimerAcknowledgements(internals, message.workflowId, error);
98
+ throw error;
99
+ }
100
+ }
@@ -22,6 +22,7 @@ import type { AlertManager } from '../../alerting/alert-manager.ts';
22
22
  import type { Storage as WeftStorage } from '../../storage/interface.ts';
23
23
  import type { ActivityWorkerDispatcher } from '../../workers/activity-worker-dispatcher.ts';
24
24
  import type { ActivityRegistry } from '../activity-registry.ts';
25
+ import type { ContextOperationRequest } from '../context.ts';
25
26
  import type { EventHeadRecord } from '../event-log.ts';
26
27
  import type { ExecutionStrategy } from '../execution-strategy.ts';
27
28
  import type { InlineExecutionStrategy } from '../inline-execution-strategy.ts';
@@ -39,6 +40,17 @@ import type { WorkflowFeedListener } from './index.ts';
39
40
  import type { LeaseManager } from './lease-manager.ts';
40
41
  import type { ScheduleHandleEngine } from './schedule-handle.ts';
41
42
  import type { SecondInstanceDetector } from './second-instance-detector.ts';
43
+ export type SleepTimerAcknowledgementWaiter = {
44
+ fireAt: number;
45
+ operationId: string;
46
+ reject: (error: Error) => void;
47
+ resolve: () => void;
48
+ };
49
+ export type DurableInlineOperation = {
50
+ operationId: string;
51
+ scheduledFireAt?: number;
52
+ type: ContextOperationRequest['type'];
53
+ };
42
54
  type EngineRuntime = WorkflowHandleEngine & ScheduleHandleEngine & {
43
55
  start(type: string, input: unknown, options?: StartWorkflowOptions): Promise<WorkflowHandle>;
44
56
  };
@@ -102,6 +114,15 @@ export interface EngineInternals {
102
114
  fireAt: number;
103
115
  }>;
104
116
  sleepResolversByWorkflow: Map<string, Set<string>>;
117
+ /**
118
+ * Fired sleep timers awaiting proof that the awakened inline workflow reached
119
+ * its next durable checkpoint or terminal state. External schedulers must not
120
+ * delete a timer before these waiters settle, or a Service Worker eviction can
121
+ * lose the only durable wake-up between resolver settlement and checkpointing.
122
+ */
123
+ sleepTimerAcknowledgementWaiters: Map<string, Set<SleepTimerAcknowledgementWaiter>>;
124
+ /** Most recently persisted inline operation, used to reject stale sleep-timer callbacks. */
125
+ durableInlineOperations: Map<string, DurableInlineOperation>;
105
126
  /**
106
127
  * Per-workflow maps of `operationId` → fired timer `fireAt` for sleep timers
107
128
  * the scheduler tick fired before the resolver was registered. Keyed by
@@ -57,6 +57,22 @@ export type RecoverAllOptions = {
57
57
  * recovery continues with its siblings.
58
58
  */
59
59
  onRecoveredWorkflow?: (info: RecoveredWorkflowInfo) => void | Promise<void>;
60
+ /**
61
+ * Policy for a recovered workflow whose stored {@link WorkflowVersionTuple}
62
+ * (or legacy checkpoint `version`) no longer matches the registered
63
+ * {@link WorkflowDefinition.version}. The mismatch is detected before
64
+ * `resolveWorkflowServices` and `onRecoveredWorkflow` run for that workflow,
65
+ * so a mismatched run never re-provides services or invokes the hook.
66
+ *
67
+ * - `'fail-run'` (default): fail only the mismatched run to a terminal
68
+ * `failed` state with a `system` failure category carrying the
69
+ * {@link VersionMismatchError} message, then continue recovering its
70
+ * siblings. The run never advances user workflow code.
71
+ * - `'throw'`: preserve the pre-#702 behavior — rethrow the
72
+ * {@link VersionMismatchError} out of `recoverAll()` immediately, aborting
73
+ * recovery for every workflow not yet processed in this batch.
74
+ */
75
+ versionMismatchPolicy?: 'fail-run' | 'throw';
60
76
  };
61
77
  export type LifecycleCallbacks = {
62
78
  dispatchEvent: (event: Event) => void;
@@ -97,6 +113,15 @@ export type LifecycleCallbacks = {
97
113
  * other workflows from the same storage backend.
98
114
  */
99
115
  failWorkflowForCheckpointDecodeError: (workflowId: string, error: Error) => Promise<void>;
116
+ /**
117
+ * Force a recovered workflow to a terminal `failed` state because its
118
+ * persisted version metadata no longer matches the registered
119
+ * `WorkflowDefinition.version` (a {@link VersionMismatchError}). Fails just
120
+ * this run with a `system` failure category so `recoverAll()` can continue
121
+ * recovering other workflows under the default `'fail-run'`
122
+ * {@link RecoverAllOptions.versionMismatchPolicy}.
123
+ */
124
+ failWorkflowForVersionMismatch: (workflowId: string, error: Error) => Promise<void>;
100
125
  };
101
126
  /**
102
127
  * Pre-replay history circuit breaker. Called at every restore-from-checkpoint