@lostgradient/weft 0.2.1 → 0.3.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 (188) hide show
  1. package/README.md +47 -22
  2. package/dist/cli/generated/operation-client.generated.d.ts +28 -1
  3. package/dist/cli/generated/operation-client.generated.js +2 -0
  4. package/dist/cli-main.js +79 -79
  5. package/dist/client/handle-delegation.d.ts +4 -0
  6. package/dist/client/handle-delegation.js +6 -0
  7. package/dist/client/http-client-requests.d.ts +2 -0
  8. package/dist/client/http-client-requests.js +3 -0
  9. package/dist/client/http-client.d.ts +4 -1
  10. package/dist/client/http-client.js +9 -1
  11. package/dist/client/interface.d.ts +57 -2
  12. package/dist/client/local.d.ts +4 -1
  13. package/dist/client/local.js +7 -0
  14. package/dist/client/start-body.d.ts +7 -1
  15. package/dist/client/start-body.js +13 -4
  16. package/dist/core/codec/extension-codec.js +4 -2
  17. package/dist/core/codec/index.d.ts +1 -0
  18. package/dist/core/codec/index.js +1 -0
  19. package/dist/core/codec/serializer-registry.d.ts +122 -0
  20. package/dist/core/codec/serializer-registry.js +51 -0
  21. package/dist/core/context/index.d.ts +9 -0
  22. package/dist/core/context/internals.d.ts +9 -0
  23. package/dist/core/context/internals.js +3 -0
  24. package/dist/core/context/run-operation.d.ts +16 -3
  25. package/dist/core/context/run-operation.js +16 -7
  26. package/dist/core/engine/bulk-operations.js +1 -1
  27. package/dist/core/engine/construction.d.ts +0 -1
  28. package/dist/core/engine/construction.js +10 -1
  29. package/dist/core/engine/disposal.js +12 -0
  30. package/dist/core/engine/engine-create-types.d.ts +0 -14
  31. package/dist/core/engine/engine-internal-types.d.ts +12 -0
  32. package/dist/core/engine/engine-leak-warnings.d.ts +6 -0
  33. package/dist/core/engine/engine-leak-warnings.js +4 -0
  34. package/dist/core/engine/engine-runtime-helpers.d.ts +17 -0
  35. package/dist/core/engine/engine-runtime-helpers.js +26 -5
  36. package/dist/core/engine/errors.d.ts +74 -0
  37. package/dist/core/engine/errors.js +25 -1
  38. package/dist/core/engine/handle-result.js +1 -1
  39. package/dist/core/engine/handles.d.ts +89 -40
  40. package/dist/core/engine/handles.js +25 -27
  41. package/dist/core/engine/index.d.ts +96 -4
  42. package/dist/core/engine/index.js +75 -4
  43. package/dist/core/engine/inline-launch-queue.d.ts +14 -0
  44. package/dist/core/engine/inline-launch-queue.js +32 -7
  45. package/dist/core/engine/internals.d.ts +18 -10
  46. package/dist/core/engine/lifecycle/fork-helpers.js +1 -7
  47. package/dist/core/engine/lifecycle/persist.js +5 -20
  48. package/dist/core/engine/lifecycle/resume.js +25 -4
  49. package/dist/core/engine/lifecycle/start-commit.d.ts +47 -0
  50. package/dist/core/engine/lifecycle/start-commit.js +27 -0
  51. package/dist/core/engine/lifecycle/start-exec.d.ts +30 -2
  52. package/dist/core/engine/lifecycle/start-exec.js +38 -0
  53. package/dist/core/engine/lifecycle/start-or-signal-resolution.d.ts +79 -0
  54. package/dist/core/engine/lifecycle/start-or-signal-resolution.js +60 -0
  55. package/dist/core/engine/lifecycle/start-or-signal.d.ts +45 -0
  56. package/dist/core/engine/lifecycle/start-or-signal.js +141 -0
  57. package/dist/core/engine/lifecycle/start.d.ts +3 -3
  58. package/dist/core/engine/lifecycle/start.js +31 -37
  59. package/dist/core/engine/lifecycle.d.ts +3 -2
  60. package/dist/core/engine/lifecycle.js +9 -2
  61. package/dist/core/engine/listing.js +1 -1
  62. package/dist/core/engine/persisted-data-version.d.ts +5 -9
  63. package/dist/core/engine/persisted-data-version.js +4 -5
  64. package/dist/core/engine/schedule-handle.d.ts +45 -0
  65. package/dist/core/engine/schedule-handle.js +26 -0
  66. package/dist/core/engine/schedules.d.ts +1 -1
  67. package/dist/core/engine/schedules.js +7 -3
  68. package/dist/core/engine/second-instance-detector.d.ts +96 -0
  69. package/dist/core/engine/second-instance-detector.js +108 -0
  70. package/dist/core/engine/signals.d.ts +22 -0
  71. package/dist/core/engine/signals.js +15 -0
  72. package/dist/core/engine/termination/cleanup.d.ts +25 -0
  73. package/dist/core/engine/termination/cleanup.js +19 -1
  74. package/dist/core/engine/termination/complete.js +4 -3
  75. package/dist/core/engine/termination/suspend.d.ts +68 -0
  76. package/dist/core/engine/termination/suspend.js +41 -0
  77. package/dist/core/engine/termination.d.ts +4 -2
  78. package/dist/core/engine/termination.js +2 -0
  79. package/dist/core/engine/validation.js +25 -1
  80. package/dist/core/engine/workflow-feed.d.ts +5 -3
  81. package/dist/core/events/event-map.d.ts +2 -1
  82. package/dist/core/events/workflow-events.d.ts +23 -0
  83. package/dist/core/events/workflow-events.js +9 -0
  84. package/dist/core/list-filter-validation.js +2 -1
  85. package/dist/core/start-workflow-validation.d.ts +22 -0
  86. package/dist/core/start-workflow-validation.js +11 -1
  87. package/dist/core/step-context.d.ts +10 -6
  88. package/dist/core/step-context.js +7 -15
  89. package/dist/core/types/activity.d.ts +6 -3
  90. package/dist/core/types/identity.d.ts +8 -1
  91. package/dist/core/types/launch-metadata.d.ts +33 -0
  92. package/dist/core/types/launch-metadata.js +0 -0
  93. package/dist/core/types/message-handles.d.ts +25 -0
  94. package/dist/core/types/options.d.ts +48 -54
  95. package/dist/core/types/reviews.d.ts +2 -1
  96. package/dist/core/types/services-resolution.d.ts +47 -0
  97. package/dist/core/types/services-resolution.js +0 -0
  98. package/dist/core/types/state.d.ts +11 -11
  99. package/dist/core/types/workflow-builder.d.ts +5 -4
  100. package/dist/core/types/workflow-function.d.ts +17 -0
  101. package/dist/core/types/workflow-snapshot.d.ts +29 -0
  102. package/dist/core/types/workflow-snapshot.js +0 -0
  103. package/dist/core/types.d.ts +3 -0
  104. package/dist/core/types.js +3 -0
  105. package/dist/core/weft-error.d.ts +1 -1
  106. package/dist/core/weft-error.js +3 -1
  107. package/dist/diagnostics/doctor.js +6 -3
  108. package/dist/diagnostics/format.js +2 -2
  109. package/dist/diagnostics/types.d.ts +1 -0
  110. package/dist/diagnostics/version-check.js +6 -4
  111. package/dist/index.d.ts +4 -4
  112. package/dist/index.js +10 -1
  113. package/dist/json-schema.js +1 -1
  114. package/dist/mcp/cli.js +35 -35
  115. package/dist/mcp/list-filter.js +2 -1
  116. package/dist/mcp/session.js +1 -0
  117. package/dist/observability/index.js +2 -2
  118. package/dist/server/handler.js +30 -30
  119. package/dist/server/index.js +33 -33
  120. package/dist/server/interactive-operations.js +1 -0
  121. package/dist/server/operations/resume-workflow.js +2 -2
  122. package/dist/server/operations/start-or-signal-workflow.d.ts +39 -0
  123. package/dist/server/operations/start-or-signal-workflow.js +140 -0
  124. package/dist/server/operations/start-workflow-options.d.ts +32 -0
  125. package/dist/server/operations/start-workflow-options.js +63 -0
  126. package/dist/server/operations/start-workflow.js +7 -69
  127. package/dist/server/operations/suspend-workflow.d.ts +13 -0
  128. package/dist/server/operations/suspend-workflow.js +36 -0
  129. package/dist/server/rest-binding.d.ts +18 -7
  130. package/dist/server/rest-bindings.js +12 -0
  131. package/dist/server/runtime/task-dispatch.js +5 -3
  132. package/dist/server/runtime/task-polling.d.ts +16 -2
  133. package/dist/server/runtime/task-polling.js +20 -5
  134. package/dist/server/runtime/websocket-worker.js +8 -0
  135. package/dist/server/serve-internals.d.ts +8 -0
  136. package/dist/server/serve-internals.js +4 -2
  137. package/dist/server/task-state.d.ts +8 -0
  138. package/dist/service-worker/index.js +28 -28
  139. package/dist/storage/capabilities.d.ts +10 -2
  140. package/dist/storage/capabilities.js +2 -2
  141. package/dist/storage/http.js +2 -2
  142. package/dist/storage/index.d.ts +6 -1
  143. package/dist/storage/indexeddb.js +1 -1
  144. package/dist/storage/interface.d.ts +26 -0
  145. package/dist/storage/interface.js +1 -1
  146. package/dist/storage/key-prefixes.d.ts +1 -1
  147. package/dist/storage/key-prefixes.js +2 -0
  148. package/dist/storage/lmdb.js +1 -1
  149. package/dist/storage/memory.js +1 -1
  150. package/dist/storage/neon-value-mapping.d.ts +47 -0
  151. package/dist/storage/neon-value-mapping.js +11 -0
  152. package/dist/storage/neon.d.ts +108 -0
  153. package/dist/storage/neon.js +10 -0
  154. package/dist/storage/node-sqlite-loader.d.ts +71 -0
  155. package/dist/storage/node-sqlite-loader.js +41 -0
  156. package/dist/storage/node-sqlite.d.ts +1 -19
  157. package/dist/storage/node-sqlite.js +38 -32
  158. package/dist/storage/postgres-key-value-queries.d.ts +79 -0
  159. package/dist/storage/postgres-key-value-queries.js +63 -0
  160. package/dist/storage/resolve.d.ts +2 -165
  161. package/dist/storage/resolve.js +1 -1
  162. package/dist/storage/scoped-storage.js +1 -1
  163. package/dist/storage/storage-configuration.d.ts +209 -0
  164. package/dist/storage/storage-configuration.js +0 -0
  165. package/dist/storage/text-value-store.d.ts +9 -9
  166. package/dist/storage/turso.js +2 -2
  167. package/dist/storage/typed-storage.js +1 -1
  168. package/dist/storage/web-extension.js +1 -1
  169. package/dist/testing/index.js +33 -33
  170. package/dist/version.d.ts +1 -1
  171. package/dist/version.js +1 -1
  172. package/dist/worker/index.js +9 -5
  173. package/dist/worker/long-poll.js +4 -0
  174. package/dist/worker/protocol-messages.d.ts +20 -0
  175. package/dist/worker/protocol-schemas.d.ts +32 -0
  176. package/dist/worker/protocol-schemas.js +8 -4
  177. package/dist/worker/protocol-task-result.d.ts +28 -0
  178. package/dist/worker/protocol-task-result.js +76 -0
  179. package/dist/worker/protocol.d.ts +4 -15
  180. package/dist/worker/protocol.js +1 -1
  181. package/dist/worker/registry/fair-share.d.ts +29 -0
  182. package/dist/worker/registry/fair-share.js +30 -0
  183. package/dist/worker/registry/routing.d.ts +18 -0
  184. package/dist/worker/registry/routing.js +14 -0
  185. package/dist/worker/registry/types.d.ts +7 -0
  186. package/dist/worker/registry.d.ts +16 -1
  187. package/dist/worker/registry.js +24 -36
  188. package/package.json +17 -4
@@ -8,6 +8,7 @@ export const INTERACTIVE_OPERATION_NAMES = [
8
8
  "weft.workflows.cancel",
9
9
  "weft.workflows.timeout",
10
10
  "weft.workflows.resume",
11
+ "weft.workflows.suspend",
11
12
  "weft.workflows.replay",
12
13
  "weft.schedules.create",
13
14
  "weft.schedules.get",
@@ -11,8 +11,8 @@ const resumeWorkflowInput = z.object({
11
11
  });
12
12
  export const resumeWorkflowOperation = createSingleWorkflowControlOperation({
13
13
  name: "weft.workflows.resume",
14
- summary: "Resume a suspended workflow",
15
- description: "Resume a workflow that is suspended (for example, paused awaiting a human review or an operator hold) by `workflowId`, returning the workflow `id`. Faults with NotFound when the workflow is not visible.",
14
+ summary: "Resume a suspended or recovered workflow",
15
+ description: "Re-drive a workflow from its persisted checkpoint by `workflowId`, returning the workflow `id`. Accepts a workflow that was explicitly suspended (`weft.workflows.suspend`) or one left running by a prior process; a suspended workflow is durably flipped back to running as part of resuming. Faults with NotFound when the workflow is not visible, or Conflict when it is in a status that cannot be resumed (terminal or pending).",
16
16
  destructive: !1,
17
17
  tags: ["Workflows"],
18
18
  inputSchema: resumeWorkflowInput,
@@ -0,0 +1,39 @@
1
+ import { z } from 'zod';
2
+ import type { UnknownRestBinding } from '../rest-bindings.ts';
3
+ declare const startOrSignalWorkflowInput: z.ZodObject<{
4
+ type: z.ZodUnknown;
5
+ input: z.ZodOptional<z.ZodUnknown>;
6
+ signalName: z.ZodString;
7
+ signalPayload: z.ZodOptional<z.ZodUnknown>;
8
+ signalId: z.ZodOptional<z.ZodString>;
9
+ id: z.ZodOptional<z.ZodUnknown>;
10
+ executionTimeout: z.ZodOptional<z.ZodUnknown>;
11
+ startAt: z.ZodOptional<z.ZodUnknown>;
12
+ startAfter: z.ZodOptional<z.ZodUnknown>;
13
+ tags: z.ZodOptional<z.ZodUnknown>;
14
+ idempotencyKey: z.ZodOptional<z.ZodUnknown>;
15
+ searchAttributes: z.ZodOptional<z.ZodUnknown>;
16
+ }, z.core.$strip>;
17
+ declare const startOrSignalWorkflowOutput: z.ZodObject<{
18
+ id: z.ZodString;
19
+ }, z.core.$strip>;
20
+ export type StartOrSignalWorkflowInput = z.infer<typeof startOrSignalWorkflowInput>;
21
+ export type StartOrSignalWorkflowOutput = z.infer<typeof startOrSignalWorkflowOutput>;
22
+ export declare const startOrSignalWorkflowOperation: import("../operation-catalog.ts").OperationDefinition<{
23
+ type: unknown;
24
+ signalName: string;
25
+ input?: unknown;
26
+ signalPayload?: unknown;
27
+ signalId?: string | undefined;
28
+ id?: unknown;
29
+ executionTimeout?: unknown;
30
+ startAt?: unknown;
31
+ startAfter?: unknown;
32
+ tags?: unknown;
33
+ idempotencyKey?: unknown;
34
+ searchAttributes?: unknown;
35
+ }, {
36
+ id: string;
37
+ }>;
38
+ export declare const startOrSignalWorkflowRestBinding: UnknownRestBinding;
39
+ export {};
@@ -0,0 +1,140 @@
1
+ import { z } from "zod";
2
+ import {
3
+ IdempotencyKeyPurgedError,
4
+ StartOrSignalConflictError,
5
+ WorkflowNotRegisteredError
6
+ } from "../../core/engine/errors.js";
7
+ import { runtimeWorkflowEngine } from "../../core/runtime-workflow-engine.js";
8
+ import { isSignalIdWithinByteLimit } from "../../core/signal-id.js";
9
+ import { StartWorkflowValidationError } from "../../core/start-workflow-validation.js";
10
+ import { defineOperation } from "../operation-registry.js";
11
+ import { invalidParamsFault, shapeRestFault } from "./operation-helpers.js";
12
+ import { buildSharedStartWorkflowOptions } from "./start-workflow-options.js";
13
+ const startOrSignalWorkflowInput = z.object({
14
+ type: z.unknown().describe("Workflow type name. Runtime validation requires a non-empty string."),
15
+ input: z.unknown().optional(),
16
+ signalName: z.string().min(1).describe("Signal name. Must be a non-empty string."),
17
+ signalPayload: z.unknown().optional(),
18
+ signalId: z.string().min(1).refine(isSignalIdWithinByteLimit, "signalId must be at most 128 bytes").optional(),
19
+ id: z.unknown().optional(),
20
+ executionTimeout: z.unknown().optional(),
21
+ startAt: z.unknown().optional(),
22
+ startAfter: z.unknown().optional(),
23
+ tags: z.unknown().optional(),
24
+ idempotencyKey: z.unknown().optional(),
25
+ searchAttributes: z.unknown().optional()
26
+ }), startOrSignalWorkflowOutput = z.object({
27
+ id: z.string()
28
+ });
29
+ function validateStartOrSignalWorkflowInput(input, lookupSearchAttributeSchema) {
30
+ if (typeof input.type !== "string" || input.type.length === 0)
31
+ throw invalidParamsFault("Missing required field: type");
32
+ let options;
33
+ try {
34
+ options = buildSharedStartWorkflowOptions(input, lookupSearchAttributeSchema(input.type));
35
+ } catch (error) {
36
+ const message = error instanceof Error ? error.message : String(error);
37
+ throw invalidParamsFault(message);
38
+ }
39
+ assertConvergenceTokenProvided(input.signalId, options.idempotencyKey);
40
+ const signal = {
41
+ name: input.signalName,
42
+ payload: input.signalPayload,
43
+ ...input.signalId === void 0 ? {} : { signalId: input.signalId }
44
+ };
45
+ return { type: input.type, signal, options };
46
+ }
47
+ function assertConvergenceTokenProvided(signalId, idempotencyKey) {
48
+ if (signalId === void 0 && idempotencyKey === void 0)
49
+ throw invalidParamsFault("startOrSignal requires either signalId or idempotencyKey to identify the signal to deliver. (Concurrent callers converge on one workflow and one signal only with a shared idempotencyKey, or a shared id plus signalId; a bare signalId starts a fresh run per caller.)");
50
+ if (signalId !== void 0 && idempotencyKey !== void 0)
51
+ throw invalidParamsFault("startOrSignal does not accept both signalId and idempotencyKey: the signal id derives from the idempotency key for convergence. Provide exactly one.");
52
+ }
53
+ function resolveStartOrSignalWorkflowFault(error) {
54
+ const message = error instanceof Error ? error.message : String(error);
55
+ if (error instanceof WorkflowNotRegisteredError)
56
+ throw invalidParamsFault(message);
57
+ if (error instanceof StartOrSignalConflictError || error instanceof IdempotencyKeyPurgedError)
58
+ throw {
59
+ code: "Conflict",
60
+ message,
61
+ data: { reason: message }
62
+ };
63
+ if (error instanceof StartWorkflowValidationError)
64
+ throw invalidParamsFault(message);
65
+ throw {
66
+ code: "EngineFailure",
67
+ message,
68
+ data: {}
69
+ };
70
+ }
71
+ export const startOrSignalWorkflowOperation = defineOperation({
72
+ name: "weft.workflows.startorsignal",
73
+ mcpExposable: !1,
74
+ summary: "Atomically start a workflow or signal it if it already exists",
75
+ description: 'Start a workflow or deliver a signal to it if it already exists (signal-with-start). An absent target is created and delivered the signal in one batch; a non-terminal target (running, pending, or suspended) is signalled; a terminal target faults with Conflict, as does a spent `idempotencyKey` whose run was purged or swept by retention. Requires `type`, `signalName`, and exactly one of `signalId` or `idempotencyKey` (not both). Accepts `input`, `signalPayload`, and the non-idempotency start options from weft.workflows.start (`id`, `executionTimeout`, `startAt`/`startAfter`, `tags`, `searchAttributes`); `idempotencyKey` is governed solely by the "exactly one of" rule above. Returns the workflow `id`. Concurrent callers converge on one workflow and one signal only with a shared `idempotencyKey`, or a shared `id` plus `signalId`; a bare `signalId` (no `id`/`idempotencyKey`) starts a fresh run per caller and does not converge.',
76
+ destructive: !1,
77
+ tags: ["Workflows", "Signals"],
78
+ inputSchema: startOrSignalWorkflowInput,
79
+ outputSchema: startOrSignalWorkflowOutput,
80
+ access: { kind: "public" },
81
+ producibleFaults: ["Conflict"],
82
+ transports: { http: !0, jsonRpcHttp: !0, jsonRpcWebSocket: !0, jsonRpcStdio: !0 },
83
+ unknownKeyPolicy: { http: "strip", jsonRpc: "reject" },
84
+ invoke: async ({ input, engine }) => {
85
+ const typedEngine = runtimeWorkflowEngine(engine), { type, signal, options } = validateStartOrSignalWorkflowInput(input, (validType) => {
86
+ return typedEngine.getWorkflowDefinition(validType)?.searchAttributes;
87
+ });
88
+ try {
89
+ return { id: (await typedEngine.startOrSignal(type, input.input, signal, options)).id };
90
+ } catch (error) {
91
+ resolveStartOrSignalWorkflowFault(error);
92
+ }
93
+ }
94
+ }), startOrSignalWorkflowRestBinding = {
95
+ method: "POST",
96
+ path: "/v1/workflows/start-or-signal",
97
+ pathParamNames: [],
98
+ operationName: "weft.workflows.startorsignal",
99
+ inputSources: {
100
+ type: { kind: "body-field", bodyField: "type" },
101
+ input: { kind: "body-field", bodyField: "input" },
102
+ signalName: { kind: "body-field", bodyField: "signalName" },
103
+ signalPayload: { kind: "body-field", bodyField: "signalPayload" },
104
+ signalId: { kind: "body-field", bodyField: "signalId" },
105
+ id: { kind: "body-field", bodyField: "id" },
106
+ executionTimeout: { kind: "body-field", bodyField: "executionTimeout" },
107
+ startAt: { kind: "body-field", bodyField: "startAt" },
108
+ startAfter: { kind: "body-field", bodyField: "startAfter" },
109
+ tags: { kind: "body-field", bodyField: "tags" },
110
+ idempotencyKey: { kind: "body-field", bodyField: "idempotencyKey" },
111
+ searchAttributes: { kind: "body-field", bodyField: "searchAttributes" }
112
+ },
113
+ extractInput: async (request) => {
114
+ let body;
115
+ try {
116
+ body = await request.json();
117
+ } catch {
118
+ throw invalidParamsFault("Invalid JSON body");
119
+ }
120
+ if (typeof body !== "object" || body === null)
121
+ throw invalidParamsFault("Request body must be a JSON object");
122
+ const record = body;
123
+ return {
124
+ type: record.type,
125
+ input: record.input,
126
+ signalName: record.signalName,
127
+ signalPayload: record.signalPayload,
128
+ signalId: record.signalId,
129
+ id: record.id,
130
+ executionTimeout: record.executionTimeout,
131
+ startAt: record.startAt,
132
+ startAfter: record.startAfter,
133
+ tags: record.tags,
134
+ idempotencyKey: record.idempotencyKey,
135
+ searchAttributes: record.searchAttributes
136
+ };
137
+ },
138
+ success: { kind: "json", status: 201 },
139
+ shapeFault: shapeRestFault
140
+ };
@@ -0,0 +1,32 @@
1
+ import type { SearchAttributeSchema, SearchAttributeValue, StartOptions } from '../../core/types.ts';
2
+ /**
3
+ * The raw, transport-supplied start-option fields shared by `weft.workflows.start`
4
+ * and `weft.workflows.startorsignal`. Each is `unknown` because validation happens
5
+ * in {@link buildSharedStartWorkflowOptions}, not at the schema boundary, so both
6
+ * surfaces hit one cross-transport error path.
7
+ */
8
+ export type SharedStartWorkflowOptionInput = {
9
+ id?: unknown;
10
+ executionTimeout?: unknown;
11
+ startAt?: unknown;
12
+ startAfter?: unknown;
13
+ tags?: unknown;
14
+ idempotencyKey?: unknown;
15
+ searchAttributes?: unknown;
16
+ };
17
+ /**
18
+ * Coerce the shared start-option fields into a validated {@link StartOptions}.
19
+ * Both start operations call this so they cannot drift in how they validate `id`,
20
+ * `executionTimeout`, `startAt`/`startAfter`, `tags`, `idempotencyKey`, and
21
+ * `searchAttributes` (a new field added here covers both surfaces at once). Throws
22
+ * {@link StartWorkflowValidationError} on any malformed field.
23
+ */
24
+ export declare function buildSharedStartWorkflowOptions(input: SharedStartWorkflowOptionInput, searchAttributeSchema: SearchAttributeSchema | undefined): StartOptions;
25
+ /**
26
+ * Coerce a transport-supplied `searchAttributes` object into validated
27
+ * {@link SearchAttributeValue}s. Shared by `weft.workflows.start` and
28
+ * `weft.workflows.startorsignal` so both apply the same null-prototype guard,
29
+ * date-time normalization, schema-presence check, and type validation —
30
+ * preventing the two start surfaces from drifting in how they accept attributes.
31
+ */
32
+ export declare function coerceStartWorkflowSearchAttributes(value: unknown, fieldName: string, schema: SearchAttributeSchema | undefined): Record<string, SearchAttributeValue>;
@@ -0,0 +1,63 @@
1
+ import { validateAttributeType } from "../../core/search-attributes.js";
2
+ import {
3
+ assertExclusiveStartWorkflowOptions,
4
+ coerceStartWorkflowDuration,
5
+ coerceStartWorkflowId,
6
+ coerceStartWorkflowIdempotencyKey,
7
+ coerceStartWorkflowTags,
8
+ coerceStartWorkflowTimestamp,
9
+ StartWorkflowValidationError
10
+ } from "../../core/start-workflow-validation.js";
11
+ export function buildSharedStartWorkflowOptions(input, searchAttributeSchema) {
12
+ const options = {};
13
+ if (input.id !== void 0)
14
+ options.id = coerceStartWorkflowId(input.id, 'Field "id"');
15
+ if (input.executionTimeout !== void 0)
16
+ options.executionTimeout = coerceStartWorkflowDuration(input.executionTimeout, 'Field "executionTimeout"');
17
+ if (input.startAt !== void 0)
18
+ options.startAt = coerceStartWorkflowTimestamp(input.startAt, 'Field "startAt"');
19
+ if (input.startAfter !== void 0)
20
+ options.startAfter = coerceStartWorkflowDuration(input.startAfter, 'Field "startAfter"');
21
+ if (input.tags !== void 0)
22
+ options.tags = coerceStartWorkflowTags(input.tags, 'Field "tags"');
23
+ if (input.idempotencyKey !== void 0)
24
+ options.idempotencyKey = coerceStartWorkflowIdempotencyKey(input.idempotencyKey, 'Field "idempotencyKey"');
25
+ if (input.searchAttributes !== void 0)
26
+ options.searchAttributes = coerceStartWorkflowSearchAttributes(input.searchAttributes, 'Field "searchAttributes"', searchAttributeSchema);
27
+ assertExclusiveStartWorkflowOptions(options.startAt, options.startAfter);
28
+ return options;
29
+ }
30
+ export function coerceStartWorkflowSearchAttributes(value, fieldName, schema) {
31
+ if (typeof value !== "object" || value === null || Array.isArray(value))
32
+ throw new StartWorkflowValidationError(`${fieldName} must be an object`);
33
+ const attributes = Object.create(null);
34
+ for (const [key, attributeValue] of Object.entries(value))
35
+ attributes[key] = coerceStartWorkflowSearchAttributeValue(key, attributeValue, fieldName, schema);
36
+ return attributes;
37
+ }
38
+ function coerceStartWorkflowSearchAttributeValue(key, value, fieldName, schema) {
39
+ if (!isSearchAttributeValue(value))
40
+ throw new StartWorkflowValidationError(`${fieldName}.${key} must be a string, number, boolean, Date, or string array`);
41
+ if (schema === void 0)
42
+ return value;
43
+ const definition = schema[key];
44
+ if (definition === void 0)
45
+ throw new StartWorkflowValidationError(`Unknown search attribute "${key}". Registered attributes: ${Object.keys(schema).join(", ")}`);
46
+ const normalizedValue = definition.type === "string" && definition.format === "date-time" && typeof value === "string" ? coerceDateTimeSearchAttribute(key, value, fieldName) : value;
47
+ try {
48
+ validateAttributeType(key, normalizedValue, definition);
49
+ } catch (error) {
50
+ const message = error instanceof Error ? error.message : String(error);
51
+ throw new StartWorkflowValidationError(message);
52
+ }
53
+ return normalizedValue;
54
+ }
55
+ function coerceDateTimeSearchAttribute(key, value, fieldName) {
56
+ const date = new Date(value);
57
+ if (Number.isNaN(date.getTime()))
58
+ throw new StartWorkflowValidationError(`${fieldName}.${key} must be a valid date-time string`);
59
+ return date;
60
+ }
61
+ function isSearchAttributeValue(value) {
62
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value instanceof Date || Array.isArray(value) && value.every((item) => typeof item === "string");
63
+ }
@@ -1,20 +1,14 @@
1
1
  import { z } from "zod";
2
2
  import {
3
+ IdempotencyKeyPurgedError,
3
4
  WorkflowAlreadyExistsError,
4
5
  WorkflowNotRegisteredError
5
6
  } from "../../core/engine/errors.js";
6
7
  import { runtimeWorkflowEngine } from "../../core/runtime-workflow-engine.js";
7
- import { validateAttributeType } from "../../core/search-attributes.js";
8
- import {
9
- assertExclusiveStartWorkflowOptions,
10
- coerceStartWorkflowDuration,
11
- coerceStartWorkflowId,
12
- coerceStartWorkflowTags,
13
- coerceStartWorkflowTimestamp,
14
- StartWorkflowValidationError
15
- } from "../../core/start-workflow-validation.js";
8
+ import { StartWorkflowValidationError } from "../../core/start-workflow-validation.js";
16
9
  import { defineOperation } from "../operation-registry.js";
17
10
  import { invalidParamsFault, shapeRestFault } from "./operation-helpers.js";
11
+ import { buildSharedStartWorkflowOptions } from "./start-workflow-options.js";
18
12
  const startWorkflowInput = z.object({
19
13
  type: z.unknown().describe("Workflow type name. Runtime validation requires a non-empty string."),
20
14
  input: z.unknown().optional(),
@@ -34,7 +28,7 @@ function validateStartWorkflowInput(input, lookupSearchAttributeSchema) {
34
28
  const type = input.type;
35
29
  let options;
36
30
  try {
37
- options = buildStartWorkflowOptions(input, lookupSearchAttributeSchema(type));
31
+ options = buildSharedStartWorkflowOptions(input, lookupSearchAttributeSchema(type));
38
32
  } catch (error) {
39
33
  const message = error instanceof Error ? error.message : String(error);
40
34
  throw invalidParamsFault(message);
@@ -45,7 +39,7 @@ function resolveStartWorkflowAccess(error) {
45
39
  const message = error instanceof Error ? error.message : String(error);
46
40
  if (error instanceof WorkflowNotRegisteredError)
47
41
  throw invalidParamsFault(message);
48
- if (error instanceof WorkflowAlreadyExistsError)
42
+ if (error instanceof WorkflowAlreadyExistsError || error instanceof IdempotencyKeyPurgedError)
49
43
  throw {
50
44
  code: "Conflict",
51
45
  message,
@@ -63,7 +57,7 @@ export const startWorkflowOperation = defineOperation({
63
57
  name: "weft.workflows.start",
64
58
  mcpExposable: !1,
65
59
  summary: "Start a new workflow",
66
- description: "Start a new workflow execution of a registered type. Requires `type` (the registered workflow type name) and accepts an optional `input` payload plus start options: `id` (client-supplied workflow id), `executionTimeout`, `startAt`/`startAfter` (mutually exclusive scheduling), `tags`, and `searchAttributes`. Returns the workflow `id`. Faults with InvalidParams for an unregistered type or malformed options, and Conflict when a workflow with the same id already exists.",
60
+ description: "Start a new workflow execution of a registered type. Requires `type` (the registered workflow type name) and accepts an optional `input` payload plus start options: `id` (client-supplied workflow id), `executionTimeout`, `startAt`/`startAfter` (mutually exclusive scheduling), `tags`, `idempotencyKey` (at-most-once dedup: a repeated key returns the existing run instead of starting a second), and `searchAttributes`. Returns the workflow `id`. Faults with InvalidParams for an unregistered type or malformed options, and Conflict when a workflow with the same id already exists or the supplied `idempotencyKey` has been spent (its run was purged or swept by retention).",
67
61
  destructive: !1,
68
62
  tags: ["Workflows"],
69
63
  inputSchema: startWorkflowInput,
@@ -82,63 +76,7 @@ export const startWorkflowOperation = defineOperation({
82
76
  resolveStartWorkflowAccess(error);
83
77
  }
84
78
  }
85
- });
86
- function buildStartWorkflowOptions(input, searchAttributeSchema) {
87
- const options = {};
88
- if (input.id !== void 0)
89
- options.id = coerceStartWorkflowId(input.id, 'Field "id"');
90
- if (input.executionTimeout !== void 0)
91
- options.executionTimeout = coerceStartWorkflowDuration(input.executionTimeout, 'Field "executionTimeout"');
92
- if (input.startAt !== void 0)
93
- options.startAt = coerceStartWorkflowTimestamp(input.startAt, 'Field "startAt"');
94
- if (input.startAfter !== void 0)
95
- options.startAfter = coerceStartWorkflowDuration(input.startAfter, 'Field "startAfter"');
96
- if (input.tags !== void 0)
97
- options.tags = coerceStartWorkflowTags(input.tags, 'Field "tags"');
98
- if (input.idempotencyKey !== void 0)
99
- throw new StartWorkflowValidationError("idempotencyKey is not supported over HttpClient because the start workflow HTTP protocol does not implement start idempotency");
100
- if (input.searchAttributes !== void 0)
101
- options.searchAttributes = coerceStartWorkflowSearchAttributes(input.searchAttributes, 'Field "searchAttributes"', searchAttributeSchema);
102
- assertExclusiveStartWorkflowOptions(options.startAt, options.startAfter);
103
- return options;
104
- }
105
- function coerceStartWorkflowSearchAttributes(value, fieldName, schema) {
106
- if (typeof value !== "object" || value === null || Array.isArray(value))
107
- throw new StartWorkflowValidationError(`${fieldName} must be an object`);
108
- const attributes = Object.create(null);
109
- for (const [key, attributeValue] of Object.entries(value)) {
110
- const coercedValue = coerceStartWorkflowSearchAttributeValue(key, attributeValue, fieldName, schema);
111
- attributes[key] = coercedValue;
112
- }
113
- return attributes;
114
- }
115
- function coerceStartWorkflowSearchAttributeValue(key, value, fieldName, schema) {
116
- if (!isSearchAttributeValue(value))
117
- throw new StartWorkflowValidationError(`${fieldName}.${key} must be a string, number, boolean, Date, or string array`);
118
- if (schema === void 0)
119
- return value;
120
- const definition = schema[key];
121
- if (definition === void 0)
122
- throw new StartWorkflowValidationError(`Unknown search attribute "${key}". Registered attributes: ${Object.keys(schema).join(", ")}`);
123
- const normalizedValue = definition.type === "string" && definition.format === "date-time" && typeof value === "string" ? coerceDateTimeSearchAttribute(key, value, fieldName) : value;
124
- try {
125
- validateAttributeType(key, normalizedValue, definition);
126
- } catch (error) {
127
- const message = error instanceof Error ? error.message : String(error);
128
- throw new StartWorkflowValidationError(message);
129
- }
130
- return normalizedValue;
131
- }
132
- function coerceDateTimeSearchAttribute(key, value, fieldName) {
133
- const date = new Date(value);
134
- if (Number.isNaN(date.getTime()))
135
- throw new StartWorkflowValidationError(`${fieldName}.${key} must be a valid date-time string`);
136
- return date;
137
- }
138
- function isSearchAttributeValue(value) {
139
- return typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value instanceof Date || Array.isArray(value) && value.every((item) => typeof item === "string");
140
- }
141
- export const startWorkflowRestBinding = {
79
+ }), startWorkflowRestBinding = {
142
80
  method: "POST",
143
81
  path: "/v1/workflows",
144
82
  pathParamNames: [],
@@ -0,0 +1,13 @@
1
+ import { z } from 'zod';
2
+ import type { UnknownRestBinding } from '../rest-bindings.ts';
3
+ declare const suspendWorkflowInput: z.ZodObject<{
4
+ workflowId: z.ZodString;
5
+ }, z.core.$strip>;
6
+ declare const suspendWorkflowOutput: z.ZodUndefined;
7
+ export type SuspendWorkflowInput = z.infer<typeof suspendWorkflowInput>;
8
+ export type SuspendWorkflowOutput = z.infer<typeof suspendWorkflowOutput>;
9
+ export declare const suspendWorkflowOperation: import("../operation-catalog.ts").OperationDefinition<{
10
+ workflowId: string;
11
+ }, undefined>;
12
+ export declare const suspendWorkflowRestBinding: UnknownRestBinding;
13
+ export {};
@@ -0,0 +1,36 @@
1
+ import { z } from "zod";
2
+ import { WorkflowSuspendNotSupportedError } from "../../core/engine/errors.js";
3
+ import { shapeRestFault } from "./operation-helpers.js";
4
+ import {
5
+ createSingleWorkflowControlOperation,
6
+ extractWorkflowIdFromPath
7
+ } from "./single-workflow-control-operation.js";
8
+ const suspendWorkflowInput = z.object({
9
+ workflowId: z.string().min(1)
10
+ }), suspendWorkflowOutput = z.undefined();
11
+ export const suspendWorkflowOperation = createSingleWorkflowControlOperation({
12
+ name: "weft.workflows.suspend",
13
+ summary: "Suspend a running workflow",
14
+ description: "Request suspension of a running workflow by `id` without terminating it. The workflow transitions to the non-terminal `suspended` status, keeps its durable checkpoint, and is later resumable via `resume`. Unlike cancellation, suspension does not run cancel handlers and does not settle the result. Suspending a workflow that is not running is a no-op. Faults with Unprocessable when the engine runs in worker execution mode (which cannot pause a run without cancelling it).",
15
+ destructive: !1,
16
+ tags: ["Workflows"],
17
+ inputSchema: suspendWorkflowInput,
18
+ outputSchema: suspendWorkflowOutput,
19
+ producibleFaults: ["Unprocessable"],
20
+ invoke: async ({ input, engine }) => {
21
+ await engine.suspend(input.workflowId);
22
+ return;
23
+ },
24
+ mapErrorToFault: ({ error, message }) => error instanceof WorkflowSuspendNotSupportedError ? { code: "Unprocessable", message, data: { reason: message } } : void 0
25
+ }), suspendWorkflowRestBinding = {
26
+ method: "POST",
27
+ path: "/v1/workflows/:id/suspend",
28
+ pathParamNames: ["id"],
29
+ operationName: "weft.workflows.suspend",
30
+ inputSources: {
31
+ workflowId: { kind: "path", pathParam: "id" }
32
+ },
33
+ extractInput: async (_request, pathParams) => extractWorkflowIdFromPath(pathParams),
34
+ success: { kind: "empty", status: 204 },
35
+ shapeFault: shapeRestFault
36
+ };
@@ -111,14 +111,25 @@ export type RestBinding<Input, Output> = {
111
111
  readonly shapeSuccess?: (output: Output, request: Request) => Response;
112
112
  /**
113
113
  * Optional override for fault → HTTP response mapping. When absent,
114
- * the transport adapter falls back to `faultToHttpResponse` (the
115
- * canonical `{ error: { code, message, data? } }` shape).
114
+ * the transport adapter falls back to `faultToHttpResponse`, which emits
115
+ * the REST fault body `{ error: { code, message, data? } }`. This is
116
+ * distinct from the JSON-RPC fault object (`faultToJsonRpcError`): JSON-RPC
117
+ * uses a flat `{ code, message, data }` with a numeric `code` and the
118
+ * symbolic name relocated to `data.weftCode`. REST and JSON-RPC deliberately
119
+ * differ in fault shape, so each transport owns its own projection.
116
120
  *
117
- * During Milestone 1, bindings for REST operations that already exist
118
- * with a different legacy error shape MUST provide this so their
119
- * parity diff test passes byte-for-byte against the legacy handler.
120
- * Milestone 2 drops the per-binding override and all REST endpoints
121
- * move to the canonical fault shape.
121
+ * REST operations provide this to shape faults the way a REST client
122
+ * expects: most use `shapeRestFault`, which masks an `EngineFailure` to a
123
+ * flat `{ error: "Internal server error" }` with status `500` (never
124
+ * leaking internal detail over REST) and maps the remaining fault codes to
125
+ * their HTTP statuses. A few operations supply a bespoke shaper to special-case
126
+ * a particular fault — typically to override its message or to handle one code
127
+ * explicitly — while delegating the rest. The status often matches what the
128
+ * shared map would already produce; the shaper exists for the operation-specific
129
+ * detail (for example, `get-workflow-result` returns the custom message
130
+ * `"Timeout waiting for workflow result"` on a `Timeout`, and `get-stream-chunks`
131
+ * handles `InvalidParams` inline). This per-operation hook is the current
132
+ * contract, not a transitional shim.
122
133
  */
123
134
  readonly shapeFault?: (fault: OperationFault) => Response;
124
135
  };
@@ -139,6 +139,10 @@ import {
139
139
  signalWorkflowOperation,
140
140
  signalWorkflowRestBinding
141
141
  } from "./operations/signal-workflow.js";
142
+ import {
143
+ startOrSignalWorkflowOperation,
144
+ startOrSignalWorkflowRestBinding
145
+ } from "./operations/start-or-signal-workflow.js";
142
146
  import { startWorkflowOperation, startWorkflowRestBinding } from "./operations/start-workflow.js";
143
147
  import {
144
148
  storageBatchOperation,
@@ -162,6 +166,10 @@ import {
162
166
  submitReviewDecisionOperation,
163
167
  submitReviewDecisionRestBinding
164
168
  } from "./operations/submit-review-decision.js";
169
+ import {
170
+ suspendWorkflowOperation,
171
+ suspendWorkflowRestBinding
172
+ } from "./operations/suspend-workflow.js";
165
173
  import {
166
174
  timeoutWorkflowOperation,
167
175
  timeoutWorkflowRestBinding
@@ -191,6 +199,7 @@ import {
191
199
  import { workflowEventsSubscriptionOperation } from "./operations/workflow-events-subscription.js";
192
200
  export const REST_BINDINGS = [
193
201
  startWorkflowRestBinding,
202
+ startOrSignalWorkflowRestBinding,
194
203
  recoverAllRestBinding,
195
204
  listWorkflowsRestBinding,
196
205
  aggregateWorkflowsRestBinding,
@@ -211,6 +220,7 @@ export const REST_BINDINGS = [
211
220
  queryWorkflowRestBinding,
212
221
  queryWorkflowWithInputRestBinding,
213
222
  resumeWorkflowRestBinding,
223
+ suspendWorkflowRestBinding,
214
224
  forkWorkflowRestBinding,
215
225
  timeoutWorkflowRestBinding,
216
226
  updateWorkflowRestBinding,
@@ -314,6 +324,7 @@ export function createLiveOperationRegistry(options) {
314
324
  const resolved = options ?? {};
315
325
  return createOperationRegistry([
316
326
  startWorkflowOperation,
327
+ startOrSignalWorkflowOperation,
317
328
  recoverAllOperation,
318
329
  listWorkflowsOperation,
319
330
  aggregateWorkflowsOperation,
@@ -333,6 +344,7 @@ export function createLiveOperationRegistry(options) {
333
344
  failAsyncActivityOperation,
334
345
  queryWorkflowOperation,
335
346
  resumeWorkflowOperation,
347
+ suspendWorkflowOperation,
336
348
  forkWorkflowOperation,
337
349
  timeoutWorkflowOperation,
338
350
  updateWorkflowOperation,
@@ -59,8 +59,8 @@ async function selectAndReserveWorker(context, options, task, queue, visibilityT
59
59
  const ws = context.workerSockets.get(worker.id);
60
60
  if (!ws)
61
61
  return !1;
62
- const now = Date.now(), existingQueuedRecord = await readQueuedRecord(options.engine.storage, task.operationId);
63
- context.registry.assignTask(worker.id, task.operationId, visibilityTimeout, task.fairShareKey);
62
+ const now = Date.now(), existingQueuedRecord = await readQueuedRecord(options.engine.storage, task.operationId), attemptToken = crypto.randomUUID();
63
+ context.registry.assignTask(worker.id, task.operationId, visibilityTimeout, task.fairShareKey, attemptToken);
64
64
  const deadline = now + visibilityTimeout;
65
65
  context.deadlineTracker.add({ operationId: task.operationId, deadline });
66
66
  const inflightRecord = {
@@ -73,7 +73,8 @@ async function selectAndReserveWorker(context, options, task, queue, visibilityT
73
73
  attempt: task.attempt ?? 1,
74
74
  visibilityTimeout,
75
75
  retryPolicy: task.retryPolicy,
76
- workflowId: task.workflowId
76
+ workflowId: task.workflowId,
77
+ attemptToken
77
78
  }, normalizedInflightRecord = await transitionQueuedToInflight(options.engine.storage, task.operationId, inflightRecord, {
78
79
  queuedRecord: existingQueuedRecord,
79
80
  now
@@ -84,6 +85,7 @@ async function selectAndReserveWorker(context, options, task, queue, visibilityT
84
85
  activityName: task.activityName,
85
86
  input: task.input === void 0 ? null : task.input,
86
87
  attempt: task.attempt ?? 1,
88
+ attemptToken,
87
89
  ...task.headers ? { headers: task.headers } : {}
88
90
  }));
89
91
  recordTaskQueueLatencyMetric(context.metricsCollector, normalizedInflightRecord);
@@ -3,7 +3,21 @@ import { type Principal } from '../principal.ts';
3
3
  import type { PendingTask } from '../task-queue-types.ts';
4
4
  import type { InflightRecord } from '../task-state.ts';
5
5
  import type { ServerContext } from './context.ts';
6
- export declare function createLongPollInflightRecord(queue: string, task: PendingTask): InflightRecord;
7
- export declare function markTaskClaimedByLongPollWorker(context: ServerContext, options: ServeOptions, queue: string, task: PendingTask): Promise<string>;
6
+ /**
7
+ * A freshly created long-poll inflight record always carries an `attemptToken`.
8
+ * The intersection makes that invariant a compile-time fact so the claim can read
9
+ * the token directly without an unreachable empty-string fallback.
10
+ */
11
+ type TokenedInflightRecord = InflightRecord & {
12
+ attemptToken: string;
13
+ };
14
+ export declare function createLongPollInflightRecord(queue: string, task: PendingTask): TokenedInflightRecord;
15
+ /** The worker-facing identity of a long-poll claim: the synthetic worker id and its per-claim token. */
16
+ export interface LongPollClaim {
17
+ workerId: string;
18
+ attemptToken: string;
19
+ }
20
+ export declare function markTaskClaimedByLongPollWorker(context: ServerContext, options: ServeOptions, queue: string, task: PendingTask): Promise<LongPollClaim>;
8
21
  export declare function handleTaskPollRequest(context: ServerContext, options: ServeOptions, request: Request, url: URL, principal?: Principal): Promise<Response | null>;
9
22
  export declare function handleTaskResultRequest(context: ServerContext, options: ServeOptions, request: Request, url: URL, principal?: Principal): Promise<Response | null>;
23
+ export {};