@codemation/core 0.11.1 → 0.13.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.
- package/CHANGELOG.md +28 -0
- package/dist/{CostCatalogContract-DZgcUBE4.d.cts → CostCatalogContract-Dxq1BTyi.d.cts} +2 -2
- package/dist/{EngineRuntimeRegistration.types-Cggm5GVY.d.cts → EngineRuntimeRegistration.types-CqcTWexS.d.cts} +3 -3
- package/dist/{EngineRuntimeRegistration.types-BQbS9_gs.d.ts → EngineRuntimeRegistration.types-Cr75cSfL.d.ts} +2 -2
- package/dist/InMemoryRunDataFactory-Csy2evr_.d.cts +205 -0
- package/dist/{ItemsInputNormalizer-CwdOhSAK.cjs → ItemsInputNormalizer-57EdA1ad.cjs} +2 -2
- package/dist/{ItemsInputNormalizer-CwdOhSAK.cjs.map → ItemsInputNormalizer-57EdA1ad.cjs.map} +1 -1
- package/dist/{ItemsInputNormalizer-_Mfcd3YU.d.ts → ItemsInputNormalizer-BWtlwdVI.d.ts} +2 -2
- package/dist/{ItemsInputNormalizer-D-MH8MBs.js → ItemsInputNormalizer-BkSvmfAW.js} +2 -2
- package/dist/{ItemsInputNormalizer-D-MH8MBs.js.map → ItemsInputNormalizer-BkSvmfAW.js.map} +1 -1
- package/dist/{ItemsInputNormalizer-C_dpn76M.d.cts → ItemsInputNormalizer-pLrWwUAP.d.cts} +3 -3
- package/dist/{RunIntentService-CEF-sFfI.d.cts → RunIntentService-BitgkKaT.d.cts} +18 -4
- package/dist/{RunIntentService-BVur7x9n.d.ts → RunIntentService-DYpqfu6D.d.ts} +18 -4
- package/dist/{agentMcpTypes-ZiNbNsEi.d.cts → agentMcpTypes-DGIwk6Ue.d.cts} +201 -4
- package/dist/bootstrap/index.cjs +3 -3
- package/dist/bootstrap/index.d.cts +63 -7
- package/dist/bootstrap/index.d.ts +5 -5
- package/dist/bootstrap/index.js +3 -3
- package/dist/{bootstrap-BxuTFTLB.cjs → bootstrap-BEu1fJBM.cjs} +175 -4
- package/dist/bootstrap-BEu1fJBM.cjs.map +1 -0
- package/dist/{bootstrap-D_Yyi0wL.js → bootstrap-CSeInbj1.js} +173 -4
- package/dist/bootstrap-CSeInbj1.js.map +1 -0
- package/dist/browser.cjs +4 -2
- package/dist/browser.d.cts +4 -4
- package/dist/browser.d.ts +3 -3
- package/dist/browser.js +3 -3
- package/dist/contracts.d.cts +5 -5
- package/dist/contracts.d.ts +2 -2
- package/dist/{di-BlEKdoZS.cjs → di-C-2ep8NZ.cjs} +44 -1
- package/dist/di-C-2ep8NZ.cjs.map +1 -0
- package/dist/{di-0Wop7z1y.js → di-D9Mv3kF3.js} +33 -2
- package/dist/di-D9Mv3kF3.js.map +1 -0
- package/dist/{executionPersistenceContracts-BgZMRsTa.d.cts → executionPersistenceContracts-CN9d7AnL.d.cts} +2 -2
- package/dist/{index-62Ba9f7D.d.ts → index-CqZeNGAp.d.ts} +343 -101
- package/dist/{index-zWGtEhrf.d.ts → index-rllWL4r-.d.ts} +459 -5
- package/dist/index.cjs +91 -161
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +458 -97
- package/dist/index.d.ts +5 -5
- package/dist/index.js +74 -159
- package/dist/index.js.map +1 -1
- package/dist/{params-B5SENSzZ.d.cts → params-DRUr0F5v.d.cts} +2 -2
- package/dist/{runtime-cxmUkk0l.js → runtime-6-U2Cou5.js} +690 -18
- package/dist/runtime-6-U2Cou5.js.map +1 -0
- package/dist/{runtime-DBzq5YBi.cjs → runtime-DjYXgOo0.cjs} +749 -17
- package/dist/runtime-DjYXgOo0.cjs.map +1 -0
- package/dist/testing.cjs +3 -3
- package/dist/testing.d.cts +3 -3
- package/dist/testing.d.ts +3 -3
- package/dist/testing.js +3 -3
- package/package.json +1 -1
- package/src/authoring/defineHumanApprovalNode.types.ts +379 -0
- package/src/authoring/index.ts +6 -0
- package/src/binaries/DefaultExecutionBinaryServiceFactory.ts +27 -2
- package/src/binaries/DefaultNodeBinaryAttachmentServiceFactory.ts +14 -0
- package/src/binaries/boundedReadBinary.types.ts +90 -0
- package/src/bootstrap/runtime/EngineRuntimeRegistrar.ts +29 -0
- package/src/contracts/CodemationTelemetryAttributeNames.ts +10 -0
- package/src/contracts/credentialTypes.ts +10 -0
- package/src/contracts/hitlSeamTypes.ts +34 -0
- package/src/contracts/humanTaskStoreTypes.ts +48 -0
- package/src/contracts/inboxChannelTypes.ts +58 -0
- package/src/contracts/index.ts +3 -0
- package/src/contracts/runTypes.ts +61 -3
- package/src/contracts/runtimeTypes.ts +131 -0
- package/src/contracts/workspaceFileTypes.ts +73 -0
- package/src/credentials/CredentialMaterialProvider.types.ts +61 -0
- package/src/credentials/ManagedCredentialMaterialWriteError.ts +14 -0
- package/src/credentials/ManagedMaterialFetchError.ts +16 -0
- package/src/execution/ActivationEnqueueService.ts +16 -0
- package/src/execution/DefaultExecutionContextFactory.ts +11 -0
- package/src/execution/NodeExecutionSnapshotFactory.ts +7 -1
- package/src/execution/NodeExecutor.ts +60 -1
- package/src/execution/NodeExecutorFactory.ts +12 -2
- package/src/execution/NodeSuspensionHandler.ts +220 -0
- package/src/execution/PersistedRunStateTerminalBuilder.ts +5 -2
- package/src/execution/RunStateSemantics.ts +5 -0
- package/src/execution/RunSuspendedError.ts +21 -0
- package/src/index.ts +42 -0
- package/src/orchestration/Engine.ts +12 -2
- package/src/orchestration/EngineWaiters.ts +1 -1
- package/src/orchestration/NodeExecutionRequestHandlerService.ts +25 -2
- package/src/orchestration/RunContinuationService.ts +226 -2
- package/src/orchestration/TestSuiteOrchestrator.ts +5 -4
- package/src/runtime/RunIntentService.ts +3 -0
- package/src/workflow/dsl/ChainCursorResolver.ts +36 -0
- package/dist/InMemoryRunDataFactory-C7YItvHG.d.cts +0 -123
- package/dist/bootstrap-BxuTFTLB.cjs.map +0 -1
- package/dist/bootstrap-D_Yyi0wL.js.map +0 -1
- package/dist/di-0Wop7z1y.js.map +0 -1
- package/dist/di-BlEKdoZS.cjs.map +0 -1
- package/dist/runtime-DBzq5YBi.cjs.map +0 -1
- package/dist/runtime-cxmUkk0l.js.map +0 -1
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
ExecutionContextFactory,
|
|
7
7
|
ExecutionTelemetryFactory,
|
|
8
8
|
NodeExecutionStatePublisher,
|
|
9
|
+
NodeResolver,
|
|
9
10
|
ParentExecutionRef,
|
|
10
11
|
RunDataSnapshot,
|
|
11
12
|
RunId,
|
|
@@ -13,6 +14,7 @@ import type {
|
|
|
13
14
|
WorkflowId,
|
|
14
15
|
} from "../types";
|
|
15
16
|
import { NoOpCostTrackingTelemetryFactory, NoOpExecutionTelemetryFactory } from "../types";
|
|
17
|
+
import type { TypeToken } from "../di";
|
|
16
18
|
|
|
17
19
|
import {
|
|
18
20
|
DefaultExecutionBinaryService,
|
|
@@ -29,6 +31,7 @@ export class DefaultExecutionContextFactory implements ExecutionContextFactory {
|
|
|
29
31
|
private readonly costTrackingFactory: CostTrackingTelemetryFactory = new NoOpCostTrackingTelemetryFactory(),
|
|
30
32
|
private readonly currentDate: () => Date = () => new Date(),
|
|
31
33
|
private readonly collections?: CollectionsContext,
|
|
34
|
+
private readonly nodeResolver?: NodeResolver,
|
|
32
35
|
) {}
|
|
33
36
|
|
|
34
37
|
create(args: {
|
|
@@ -72,6 +75,14 @@ export class DefaultExecutionContextFactory implements ExecutionContextFactory {
|
|
|
72
75
|
getCredential: args.getCredential,
|
|
73
76
|
testContext: args.testContext,
|
|
74
77
|
collections: this.collections,
|
|
78
|
+
resolve: <T>(token: TypeToken<T>): T => {
|
|
79
|
+
if (!this.nodeResolver) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
"ExecutionContext.resolve() is not available: no NodeResolver was provided to DefaultExecutionContextFactory.",
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return this.nodeResolver.resolve(token);
|
|
85
|
+
},
|
|
75
86
|
};
|
|
76
87
|
}
|
|
77
88
|
}
|
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
JsonValue,
|
|
3
3
|
NodeActivationId,
|
|
4
4
|
NodeExecutionSnapshot,
|
|
5
|
+
NodeExecutionStatus,
|
|
5
6
|
NodeId,
|
|
6
7
|
NodeInputsByPort,
|
|
7
8
|
NodeOutputs,
|
|
@@ -70,6 +71,11 @@ export class NodeExecutionSnapshotFactory {
|
|
|
70
71
|
inputsByPort: NodeInputsByPort;
|
|
71
72
|
outputs: NodeOutputs;
|
|
72
73
|
fromPinnedOutput?: boolean;
|
|
74
|
+
/** Override the terminal status for HITL outcomes (defaults to `"completed"`). */
|
|
75
|
+
hitlStatus?: Extract<
|
|
76
|
+
NodeExecutionStatus,
|
|
77
|
+
"hitl-approved" | "hitl-rejected" | "hitl-timeout" | "hitl-auto-accepted" | "hitl-cancelled"
|
|
78
|
+
>;
|
|
73
79
|
}): NodeExecutionSnapshot {
|
|
74
80
|
const fromPinnedOutput = args.fromPinnedOutput ?? false;
|
|
75
81
|
const startedAt = fromPinnedOutput ? (args.previous?.startedAt ?? args.finishedAt) : args.previous?.startedAt;
|
|
@@ -79,7 +85,7 @@ export class NodeExecutionSnapshotFactory {
|
|
|
79
85
|
nodeId: args.nodeId,
|
|
80
86
|
activationId: args.activationId,
|
|
81
87
|
parent: args.parent,
|
|
82
|
-
status: "completed",
|
|
88
|
+
status: args.hitlStatus ?? "completed",
|
|
83
89
|
queuedAt: args.previous?.queuedAt,
|
|
84
90
|
startedAt,
|
|
85
91
|
finishedAt: args.finishedAt,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { isPortsEmission, isUnbrandedPortsEmissionShape } from "../contracts/emitPorts";
|
|
3
3
|
import { CredentialUnboundError } from "../contracts/credentialTypes";
|
|
4
|
+
import { SuspensionRequest } from "../contracts/runtimeTypes";
|
|
4
5
|
|
|
5
6
|
import type {
|
|
6
7
|
Item,
|
|
@@ -8,9 +9,11 @@ import type {
|
|
|
8
9
|
NodeActivationRequest,
|
|
9
10
|
NodeExecutionContext,
|
|
10
11
|
NodeOutputs,
|
|
12
|
+
PersistedRunState,
|
|
11
13
|
RunnableNode,
|
|
12
14
|
RunnableNodeConfig,
|
|
13
15
|
RunnableNodeExecuteArgs,
|
|
16
|
+
RunId,
|
|
14
17
|
TriggerNode,
|
|
15
18
|
WorkflowNodeInstanceFactory,
|
|
16
19
|
} from "../types";
|
|
@@ -20,6 +23,8 @@ import { FanInMergeByOriginMerger } from "./FanInMergeByOriginMerger";
|
|
|
20
23
|
import { ItemExprResolver } from "./ItemExprResolver";
|
|
21
24
|
import { InProcessRetryRunner } from "./InProcessRetryRunner";
|
|
22
25
|
import { NodeOutputNormalizer } from "./NodeOutputNormalizer";
|
|
26
|
+
import { NodeSuspensionHandler } from "./NodeSuspensionHandler";
|
|
27
|
+
import { RunSuspendedError } from "./RunSuspendedError";
|
|
23
28
|
import { RunnableOutputBehaviorResolver } from "./RunnableOutputBehaviorResolver";
|
|
24
29
|
|
|
25
30
|
export class NodeExecutor {
|
|
@@ -33,6 +38,10 @@ export class NodeExecutor {
|
|
|
33
38
|
private readonly retryRunner: InProcessRetryRunner,
|
|
34
39
|
itemExprResolver?: ItemExprResolver,
|
|
35
40
|
outputBehaviorResolver?: RunnableOutputBehaviorResolver,
|
|
41
|
+
/** Required for HITL suspension support. When omitted, `SuspensionRequest` throws upward. */
|
|
42
|
+
private readonly suspensionHandler?: NodeSuspensionHandler,
|
|
43
|
+
/** Required alongside `suspensionHandler`. */
|
|
44
|
+
private readonly loadRunState?: (runId: RunId) => Promise<PersistedRunState | undefined>,
|
|
36
45
|
) {
|
|
37
46
|
this.itemExprResolver = itemExprResolver ?? new ItemExprResolver();
|
|
38
47
|
this.outputBehaviorResolver = outputBehaviorResolver ?? new RunnableOutputBehaviorResolver();
|
|
@@ -173,6 +182,7 @@ export class NodeExecutor {
|
|
|
173
182
|
}) as NodeOutputs;
|
|
174
183
|
}
|
|
175
184
|
const byPort: Partial<Record<string, Item[]>> = {};
|
|
185
|
+
let hasSuspension = false;
|
|
176
186
|
for (let i = 0; i < inputBatch.length; i++) {
|
|
177
187
|
const item = inputBatch[i] as Item;
|
|
178
188
|
this.assertItemJsonNotTopLevelArray(request.nodeId, item);
|
|
@@ -194,7 +204,51 @@ export class NodeExecutor {
|
|
|
194
204
|
items: inputBatch,
|
|
195
205
|
ctx: iterationCtx,
|
|
196
206
|
};
|
|
197
|
-
|
|
207
|
+
let raw: unknown;
|
|
208
|
+
try {
|
|
209
|
+
raw = await Promise.resolve(node.execute(args));
|
|
210
|
+
} catch (e) {
|
|
211
|
+
// Use both instanceof AND name check: under tsx/dev with mixed source/dist resolution,
|
|
212
|
+
// SuspensionRequest may load as two distinct class objects and instanceof fails. The
|
|
213
|
+
// name brand survives the duality because both copies set name="SuspensionRequest".
|
|
214
|
+
const isSuspension =
|
|
215
|
+
e instanceof SuspensionRequest ||
|
|
216
|
+
(e instanceof Error &&
|
|
217
|
+
e.name === "SuspensionRequest" &&
|
|
218
|
+
typeof (e as { request?: unknown }).request === "object");
|
|
219
|
+
if (isSuspension) {
|
|
220
|
+
if (!this.suspensionHandler || !this.loadRunState) {
|
|
221
|
+
// Suspension not supported in this executor configuration — propagate as a regular error.
|
|
222
|
+
throw new Error(
|
|
223
|
+
`Node ${request.nodeId} threw SuspensionRequest but this NodeExecutor has no suspensionHandler configured.`,
|
|
224
|
+
{ cause: e },
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
// Per-item suspension: load current state, persist the suspension entry, and
|
|
228
|
+
// continue processing remaining items. If deliver throws it propagates upward.
|
|
229
|
+
const state = await this.loadRunState(request.runId);
|
|
230
|
+
if (!state) {
|
|
231
|
+
throw new Error(`NodeExecutor: run state not found for runId ${request.runId} during suspension`, {
|
|
232
|
+
cause: e,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
// handleSuspension throws RunSuspendedError after persisting — we re-throw it
|
|
236
|
+
// to exit the loop immediately. Partial byPort outputs are intentionally dropped
|
|
237
|
+
// (TODO: consider stashing outputs of non-suspended items alongside the suspension).
|
|
238
|
+
await this.suspensionHandler.handle({
|
|
239
|
+
runId: request.runId,
|
|
240
|
+
nodeId: request.nodeId,
|
|
241
|
+
activationId: request.activationId,
|
|
242
|
+
itemIndex: i,
|
|
243
|
+
suspensionRequest: e as SuspensionRequest,
|
|
244
|
+
state,
|
|
245
|
+
telemetry: iterationCtx.telemetry,
|
|
246
|
+
});
|
|
247
|
+
hasSuspension = true; // unreachable — handler always throws, but satisfies TS control-flow
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
throw e;
|
|
251
|
+
}
|
|
198
252
|
const normalized = this.outputNormalizer.normalizeExecuteResult({
|
|
199
253
|
baseItem: item,
|
|
200
254
|
raw,
|
|
@@ -209,6 +263,11 @@ export class NodeExecutor {
|
|
|
209
263
|
byPort[port] = list;
|
|
210
264
|
}
|
|
211
265
|
}
|
|
266
|
+
if (hasSuspension) {
|
|
267
|
+
// Unreachable in practice (suspensionHandler always throws RunSuspendedError) but
|
|
268
|
+
// guards against future refactors that might change handler behaviour.
|
|
269
|
+
throw new RunSuspendedError(request.runId, "unknown");
|
|
270
|
+
}
|
|
212
271
|
return byPort as NodeOutputs;
|
|
213
272
|
}
|
|
214
273
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { WorkflowNodeInstanceFactory } from "../types";
|
|
1
|
+
import type { PersistedRunState, RunId, WorkflowNodeInstanceFactory } from "../types";
|
|
2
2
|
|
|
3
3
|
import { InProcessRetryRunner } from "./InProcessRetryRunner";
|
|
4
4
|
import { NodeExecutor } from "./NodeExecutor";
|
|
5
|
+
import { NodeSuspensionHandler } from "./NodeSuspensionHandler";
|
|
5
6
|
import { RunnableOutputBehaviorResolver } from "./RunnableOutputBehaviorResolver";
|
|
6
7
|
|
|
7
8
|
export class NodeExecutorFactory {
|
|
@@ -9,7 +10,16 @@ export class NodeExecutorFactory {
|
|
|
9
10
|
workflowNodeInstanceFactory: WorkflowNodeInstanceFactory,
|
|
10
11
|
retryRunner: InProcessRetryRunner,
|
|
11
12
|
outputBehaviorResolver: RunnableOutputBehaviorResolver,
|
|
13
|
+
suspensionHandler?: NodeSuspensionHandler,
|
|
14
|
+
loadRunState?: (runId: RunId) => Promise<PersistedRunState | undefined>,
|
|
12
15
|
): NodeExecutor {
|
|
13
|
-
return new NodeExecutor(
|
|
16
|
+
return new NodeExecutor(
|
|
17
|
+
workflowNodeInstanceFactory,
|
|
18
|
+
retryRunner,
|
|
19
|
+
undefined,
|
|
20
|
+
outputBehaviorResolver,
|
|
21
|
+
suspensionHandler,
|
|
22
|
+
loadRunState,
|
|
23
|
+
);
|
|
14
24
|
}
|
|
15
25
|
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
import type { HitlResumeTokenSignerSeam, HitlTimeoutJobSchedulerSeam } from "../contracts/hitlSeamTypes";
|
|
5
|
+
import type { HumanTaskRecord, HumanTaskStore } from "../contracts/humanTaskStoreTypes";
|
|
6
|
+
import type {
|
|
7
|
+
HumanTaskHandle,
|
|
8
|
+
NodeActivationId,
|
|
9
|
+
NodeId,
|
|
10
|
+
PersistedRunState,
|
|
11
|
+
PersistedSuspensionEntry,
|
|
12
|
+
RunId,
|
|
13
|
+
SuspensionRequest,
|
|
14
|
+
WorkflowExecutionRepository,
|
|
15
|
+
} from "../types";
|
|
16
|
+
import type { TelemetryScope } from "../contracts/telemetryTypes";
|
|
17
|
+
import { CodemationTelemetryAttributeNames } from "../contracts/CodemationTelemetryAttributeNames";
|
|
18
|
+
|
|
19
|
+
import { RunSuspendedError } from "./RunSuspendedError";
|
|
20
|
+
export { RunSuspendedError };
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Handles per-item `SuspensionRequest` catches in the engine's item execution loop.
|
|
24
|
+
*
|
|
25
|
+
* Responsibilities:
|
|
26
|
+
* 1. Generate a `taskId` (UUID v4).
|
|
27
|
+
* 2. Persist a `HumanTask` row via `HumanTaskStore.create`.
|
|
28
|
+
* 3. Sign a resume URL via `HitlResumeTokenSigner.sign`.
|
|
29
|
+
* 4. Enqueue a delayed BullMQ timeout job via `HitlTimeoutJobScheduler.enqueue`.
|
|
30
|
+
* 5. Build a `HumanTaskHandle` and call `deliver`.
|
|
31
|
+
* 6. Append a `PersistedSuspensionEntry` to the run state and flip status to `"suspended"`.
|
|
32
|
+
* 7. Persist via `WorkflowExecutionRepository.save`.
|
|
33
|
+
* 8. Throw `RunSuspendedError` so the caller can exit cleanly.
|
|
34
|
+
*
|
|
35
|
+
* If `deliver` throws, the error propagates up to `NodeExecutionRequestHandlerService`
|
|
36
|
+
* which routes it through `resumeFromNodeError` → run status becomes `"failed"`.
|
|
37
|
+
*
|
|
38
|
+
* `humanTaskStore`, `tokenSigner`, and `timeoutScheduler` are optional —
|
|
39
|
+
* when not registered (e.g. in unit tests), the handler still suspends the run but
|
|
40
|
+
* skips persistence, token signing, and job scheduling.
|
|
41
|
+
*/
|
|
42
|
+
export class NodeSuspensionHandler {
|
|
43
|
+
constructor(
|
|
44
|
+
private readonly workflowExecutionRepository: WorkflowExecutionRepository,
|
|
45
|
+
private readonly humanTaskStore?: HumanTaskStore,
|
|
46
|
+
private readonly tokenSigner?: HitlResumeTokenSignerSeam,
|
|
47
|
+
private readonly timeoutScheduler?: HitlTimeoutJobSchedulerSeam,
|
|
48
|
+
/** Workspace ID to stamp on HumanTaskRecord in managed mode (T7 security fix). Null in non-managed mode. */
|
|
49
|
+
private readonly workspaceId?: string,
|
|
50
|
+
) {}
|
|
51
|
+
|
|
52
|
+
async handle(args: {
|
|
53
|
+
runId: RunId;
|
|
54
|
+
nodeId: NodeId;
|
|
55
|
+
activationId: NodeActivationId;
|
|
56
|
+
itemIndex: number;
|
|
57
|
+
suspensionRequest: SuspensionRequest;
|
|
58
|
+
state: PersistedRunState;
|
|
59
|
+
/** Telemetry scope of the node's per-item span. Used to emit `hitl.task.*` span events. */
|
|
60
|
+
telemetry?: TelemetryScope;
|
|
61
|
+
}): Promise<never> {
|
|
62
|
+
const taskId = `htask_${globalThis.crypto.randomUUID()}`;
|
|
63
|
+
const { timeout, onTimeout, deliver, decisionSchema, subject, metadata } = args.suspensionRequest.request;
|
|
64
|
+
|
|
65
|
+
const timeoutMs = this.parseDurationMs(timeout);
|
|
66
|
+
const expiresAt = new Date(Date.now() + timeoutMs);
|
|
67
|
+
|
|
68
|
+
const decisionSchemaHash = this.hashSchema(decisionSchema);
|
|
69
|
+
const decisionSchemaJson = this.schemaToJson(decisionSchema);
|
|
70
|
+
|
|
71
|
+
// Build resume token (when signer is available)
|
|
72
|
+
let resumeUrl = "";
|
|
73
|
+
let resumeTokenHash = "";
|
|
74
|
+
if (this.tokenSigner) {
|
|
75
|
+
const token = this.tokenSigner.sign({ taskId, expiresAt, schemaHash: decisionSchemaHash });
|
|
76
|
+
resumeUrl = token; // callers (deliver) receive the raw token; inbox layers wrap into a URL
|
|
77
|
+
resumeTokenHash = this.tokenSigner.hashToken(token);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const handle: HumanTaskHandle = {
|
|
81
|
+
taskId,
|
|
82
|
+
runId: args.runId,
|
|
83
|
+
nodeId: args.nodeId,
|
|
84
|
+
expiresAt,
|
|
85
|
+
resumeUrl,
|
|
86
|
+
...(metadata !== undefined ? { metadata } : {}),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Emit hitl.task.created before calling deliver.
|
|
90
|
+
const channel = (metadata as Record<string, unknown> | undefined)?.["channel"];
|
|
91
|
+
await args.telemetry?.addSpanEvent?.({
|
|
92
|
+
name: "hitl.task.created",
|
|
93
|
+
attributes: {
|
|
94
|
+
[CodemationTelemetryAttributeNames.hitlTaskId]: taskId,
|
|
95
|
+
[CodemationTelemetryAttributeNames.hitlChannel]: typeof channel === "string" ? channel : "unknown",
|
|
96
|
+
[CodemationTelemetryAttributeNames.runId]: args.runId,
|
|
97
|
+
[CodemationTelemetryAttributeNames.nodeId]: args.nodeId,
|
|
98
|
+
expiresAt: expiresAt.toISOString(),
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// D5: deliver throws → emit hitl.task.delivery_failed, then propagate upward;
|
|
103
|
+
// caller routes to resumeFromNodeError → "failed"
|
|
104
|
+
let deliveryRef: Awaited<ReturnType<typeof deliver>>;
|
|
105
|
+
try {
|
|
106
|
+
deliveryRef = await deliver(handle);
|
|
107
|
+
} catch (deliverError) {
|
|
108
|
+
await args.telemetry?.addSpanEvent?.({
|
|
109
|
+
name: "hitl.task.delivery_failed",
|
|
110
|
+
attributes: {
|
|
111
|
+
[CodemationTelemetryAttributeNames.hitlTaskId]: taskId,
|
|
112
|
+
[CodemationTelemetryAttributeNames.hitlChannel]: typeof channel === "string" ? channel : "unknown",
|
|
113
|
+
error: deliverError instanceof Error ? deliverError.message : String(deliverError),
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
throw deliverError;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Persist HumanTask row
|
|
120
|
+
if (this.humanTaskStore) {
|
|
121
|
+
const record: HumanTaskRecord = {
|
|
122
|
+
id: taskId,
|
|
123
|
+
runId: args.runId,
|
|
124
|
+
workflowId: args.state.workflowId,
|
|
125
|
+
// T7: stamp workspaceId in managed mode so HitlCallbackHandler can assert workspace identity.
|
|
126
|
+
// Non-managed mode leaves this undefined (null in DB) — the check in HitlCallbackHandler
|
|
127
|
+
// is guarded by `task.workspaceId !== undefined` and is a no-op when null.
|
|
128
|
+
workspaceId: this.workspaceId ?? undefined,
|
|
129
|
+
nodeId: args.nodeId,
|
|
130
|
+
activationId: args.activationId,
|
|
131
|
+
itemIndex: args.itemIndex,
|
|
132
|
+
status: "pending",
|
|
133
|
+
channel: "local",
|
|
134
|
+
subject,
|
|
135
|
+
metadata: (metadata as Record<string, import("../contracts/workflowTypes").JsonValue>) ?? {},
|
|
136
|
+
decisionSchemaJson,
|
|
137
|
+
decisionSchemaHash,
|
|
138
|
+
onTimeout,
|
|
139
|
+
deliveryRef,
|
|
140
|
+
resumeTokenHash: resumeTokenHash || "no-token",
|
|
141
|
+
expiresAt,
|
|
142
|
+
createdAt: new Date(),
|
|
143
|
+
};
|
|
144
|
+
await this.humanTaskStore.create(record);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Enqueue timeout job
|
|
148
|
+
if (this.timeoutScheduler) {
|
|
149
|
+
await this.timeoutScheduler.enqueueTimeoutJob({ taskId, expiresAt });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const entry: PersistedSuspensionEntry = {
|
|
153
|
+
taskId,
|
|
154
|
+
nodeId: args.nodeId,
|
|
155
|
+
activationId: args.activationId,
|
|
156
|
+
itemIndex: args.itemIndex,
|
|
157
|
+
decisionSchemaHash,
|
|
158
|
+
deliveryRef,
|
|
159
|
+
timeoutAt: expiresAt.toISOString(),
|
|
160
|
+
onTimeout,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const existingSuspensions = args.state.suspension ?? [];
|
|
164
|
+
const updatedState: PersistedRunState = {
|
|
165
|
+
...args.state,
|
|
166
|
+
status: "suspended",
|
|
167
|
+
suspension: [...existingSuspensions, entry],
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
await this.workflowExecutionRepository.save(updatedState);
|
|
171
|
+
|
|
172
|
+
throw new RunSuspendedError(args.runId, taskId);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Parse a duration string into milliseconds.
|
|
177
|
+
* Accepts ISO 8601 durations ("PT24H", "PT30M") and shorthand ("24h", "30m", "1d").
|
|
178
|
+
* Throws for unrecognised formats.
|
|
179
|
+
*/
|
|
180
|
+
private parseDurationMs(duration: string): number {
|
|
181
|
+
// Shorthand: "24h", "30m", "7d", "3600s"
|
|
182
|
+
const shorthand = /^(\d+(?:\.\d+)?)(s|m|h|d)$/i.exec(duration);
|
|
183
|
+
if (shorthand) {
|
|
184
|
+
const value = parseFloat(shorthand[1]!);
|
|
185
|
+
const unit = shorthand[2]!.toLowerCase();
|
|
186
|
+
const multipliers: Record<string, number> = {
|
|
187
|
+
s: 1_000,
|
|
188
|
+
m: 60_000,
|
|
189
|
+
h: 3_600_000,
|
|
190
|
+
d: 86_400_000,
|
|
191
|
+
};
|
|
192
|
+
return value * multipliers[unit]!;
|
|
193
|
+
}
|
|
194
|
+
// ISO 8601 duration subset: PTnHnMnS (days handled via P1D)
|
|
195
|
+
const iso = /^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/.exec(duration);
|
|
196
|
+
if (iso) {
|
|
197
|
+
const days = parseFloat(iso[1] ?? "0");
|
|
198
|
+
const hours = parseFloat(iso[2] ?? "0");
|
|
199
|
+
const minutes = parseFloat(iso[3] ?? "0");
|
|
200
|
+
const seconds = parseFloat(iso[4] ?? "0");
|
|
201
|
+
return (days * 86_400 + hours * 3_600 + minutes * 60 + seconds) * 1_000;
|
|
202
|
+
}
|
|
203
|
+
throw new Error(`NodeSuspensionHandler: unrecognised duration format: "${duration}"`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private hashSchema(schema: unknown): string {
|
|
207
|
+
const json = this.schemaToJson(schema);
|
|
208
|
+
return createHash("sha256").update(json).digest("hex");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private schemaToJson(schema: unknown): string {
|
|
212
|
+
if (schema instanceof z.ZodType) {
|
|
213
|
+
return JSON.stringify(z.toJSONSchema(schema));
|
|
214
|
+
}
|
|
215
|
+
if (typeof (schema as { toJSON?: unknown }).toJSON === "function") {
|
|
216
|
+
return JSON.stringify((schema as { toJSON: () => unknown }).toJSON());
|
|
217
|
+
}
|
|
218
|
+
return JSON.stringify(schema);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EngineRunCounters, PersistedRunState, RunQueueEntry } from "../types";
|
|
1
|
+
import type { EngineRunCounters, PersistedRunState, RunHaltReason, RunQueueEntry } from "../types";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Merges common terminal-run fields onto a loaded {@link PersistedRunState} without repeating object literals.
|
|
@@ -7,7 +7,9 @@ export class PersistedRunStateTerminalBuilder {
|
|
|
7
7
|
mergeTerminal(args: {
|
|
8
8
|
state: PersistedRunState;
|
|
9
9
|
engineCounters: EngineRunCounters;
|
|
10
|
-
status: "completed" | "failed";
|
|
10
|
+
status: "completed" | "failed" | "halted";
|
|
11
|
+
/** Populated when `status === "halted"`. */
|
|
12
|
+
reason?: RunHaltReason;
|
|
11
13
|
queue: RunQueueEntry[];
|
|
12
14
|
outputsByNode: PersistedRunState["outputsByNode"];
|
|
13
15
|
nodeSnapshotsByNodeId: NonNullable<PersistedRunState["nodeSnapshotsByNodeId"]>;
|
|
@@ -18,6 +20,7 @@ export class PersistedRunStateTerminalBuilder {
|
|
|
18
20
|
...args.state,
|
|
19
21
|
engineCounters: args.engineCounters,
|
|
20
22
|
status: args.status,
|
|
23
|
+
reason: args.reason,
|
|
21
24
|
pending: undefined,
|
|
22
25
|
queue: args.queue,
|
|
23
26
|
outputsByNode: args.outputsByNode,
|
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
Items,
|
|
3
3
|
NodeActivationId,
|
|
4
4
|
NodeExecutionSnapshot,
|
|
5
|
+
NodeExecutionStatus,
|
|
5
6
|
NodeId,
|
|
6
7
|
NodeInputsByPort,
|
|
7
8
|
NodeOutputs,
|
|
@@ -139,6 +140,10 @@ export class RunStateSemantics {
|
|
|
139
140
|
finishedAt: string;
|
|
140
141
|
inputsByPort: NodeInputsByPort;
|
|
141
142
|
outputs: NodeOutputs;
|
|
143
|
+
hitlStatus?: Extract<
|
|
144
|
+
NodeExecutionStatus,
|
|
145
|
+
"hitl-approved" | "hitl-rejected" | "hitl-timeout" | "hitl-auto-accepted" | "hitl-cancelled"
|
|
146
|
+
>;
|
|
142
147
|
}): NodeExecutionSnapshot {
|
|
143
148
|
const definition = args.workflow.nodes.find((node) => node.id === args.nodeId);
|
|
144
149
|
if (this.missingRuntimeExecutionMarker.isMarked(definition?.config)) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { RunId } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Internal sentinel thrown by {@link NodeSuspensionHandler} after persisting a suspension
|
|
5
|
+
* entry. `NodeExecutionRequestHandlerService` catches this specifically and returns cleanly —
|
|
6
|
+
* no continuation call, preventing `resumeFromNodeResult` / `resumeFromNodeError` from
|
|
7
|
+
* overwriting the `"suspended"` run status.
|
|
8
|
+
*
|
|
9
|
+
* The `Error` suffix satisfies the ESLint `no-manual-di-new` allowlist. This is NOT a
|
|
10
|
+
* user-facing error — it is an engine-internal control-flow primitive and should NOT be
|
|
11
|
+
* exported from the public barrel.
|
|
12
|
+
*/
|
|
13
|
+
export class RunSuspendedError extends Error {
|
|
14
|
+
constructor(
|
|
15
|
+
readonly runId: RunId,
|
|
16
|
+
readonly taskId: string,
|
|
17
|
+
) {
|
|
18
|
+
super(`RunSuspendedError: run ${runId} suspended on task ${taskId}`);
|
|
19
|
+
this.name = "RunSuspendedError";
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,35 @@
|
|
|
1
1
|
export { SystemClock, type Clock } from "./contracts/Clock";
|
|
2
|
+
export {
|
|
3
|
+
SuspensionRequest,
|
|
4
|
+
type HumanTaskHandle,
|
|
5
|
+
type HumanTaskSubject,
|
|
6
|
+
type HumanTaskActor,
|
|
7
|
+
type HumanTaskId,
|
|
8
|
+
type Duration,
|
|
9
|
+
type ResumeContext,
|
|
10
|
+
} from "./contracts/runtimeTypes";
|
|
11
|
+
export type { PersistedSuspensionEntry, PendingResumeEntry, RunHaltReason } from "./contracts/runTypes";
|
|
12
|
+
export type { HumanTaskRecord, HumanTaskStatus, HumanTaskStore } from "./contracts/humanTaskStoreTypes";
|
|
13
|
+
export { HumanTaskStoreToken } from "./contracts/humanTaskStoreTypes";
|
|
14
|
+
export type { HitlResumeTokenSignerSeam, HitlTimeoutJobSchedulerSeam } from "./contracts/hitlSeamTypes";
|
|
15
|
+
export {
|
|
16
|
+
HitlResumeTokenSignerToken,
|
|
17
|
+
HitlTimeoutJobSchedulerToken,
|
|
18
|
+
HitlWorkspaceIdToken,
|
|
19
|
+
} from "./contracts/hitlSeamTypes";
|
|
20
|
+
export type {
|
|
21
|
+
InboxChannel,
|
|
22
|
+
InboxChannelResolverSeam,
|
|
23
|
+
InboxDeliverArgs,
|
|
24
|
+
InboxDelivery,
|
|
25
|
+
InboxOnDecisionArgs,
|
|
26
|
+
InboxOnTimeoutArgs,
|
|
27
|
+
} from "./contracts/inboxChannelTypes";
|
|
28
|
+
export {
|
|
29
|
+
InboxChannelResolverToken,
|
|
30
|
+
LocalInboxChannelToken,
|
|
31
|
+
ControlPlaneInboxChannelToken,
|
|
32
|
+
} from "./contracts/inboxChannelTypes";
|
|
2
33
|
export * from "./authoring";
|
|
3
34
|
export * from "./ai/AiHost";
|
|
4
35
|
export { AgentConnectionNodeCollector } from "./ai/AgentConnectionNodeCollector";
|
|
@@ -44,3 +75,14 @@ export type {
|
|
|
44
75
|
OAuthMaterial,
|
|
45
76
|
OAuthFlowExecutor,
|
|
46
77
|
} from "./credentials/OAuthFlowExecutor.types";
|
|
78
|
+
export type {
|
|
79
|
+
CredentialMaterialProvider,
|
|
80
|
+
CredentialMaterialRef,
|
|
81
|
+
MaterialBundle,
|
|
82
|
+
CallerContext,
|
|
83
|
+
} from "./credentials/CredentialMaterialProvider.types";
|
|
84
|
+
export { IllegalMaterialSourceError } from "./credentials/CredentialMaterialProvider.types";
|
|
85
|
+
export { ManagedCredentialMaterialWriteError } from "./credentials/ManagedCredentialMaterialWriteError";
|
|
86
|
+
export { ManagedMaterialFetchError } from "./credentials/ManagedMaterialFetchError";
|
|
87
|
+
export type { IWorkspaceFileStorage, WorkspaceFileMetadata } from "./contracts/workspaceFileTypes";
|
|
88
|
+
export { WorkspaceFileNotFoundError, WorkspaceFileStorageToken } from "./contracts/workspaceFileTypes";
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
NodeOutputs,
|
|
12
12
|
ParentExecutionRef,
|
|
13
13
|
PersistedWorkflowTokenRegistryLike,
|
|
14
|
+
ResumeContext,
|
|
14
15
|
RunExecutionOptions,
|
|
15
16
|
RunId,
|
|
16
17
|
RunResult,
|
|
@@ -77,8 +78,9 @@ interface EngineRunContinuationService {
|
|
|
77
78
|
nodeId: NodeId;
|
|
78
79
|
error: Error;
|
|
79
80
|
}): Promise<RunResult>;
|
|
80
|
-
waitForCompletion(runId: RunId): Promise<Extract<RunResult, { status: "completed" | "failed" }>>;
|
|
81
|
+
waitForCompletion(runId: RunId): Promise<Extract<RunResult, { status: "completed" | "failed" | "halted" }>>;
|
|
81
82
|
waitForWebhookResponse(runId: RunId): Promise<WebhookRunResult>;
|
|
83
|
+
resumeRun(args: { runId: RunId; taskId: string; resumeContext: ResumeContext }): Promise<RunResult>;
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
interface EngineNodeExecutionRequestHandler {
|
|
@@ -226,7 +228,7 @@ export class Engine implements NodeActivationContinuation, NodeExecutionRequestH
|
|
|
226
228
|
return await this.deps.runContinuationService.resumeFromStepError(args);
|
|
227
229
|
}
|
|
228
230
|
|
|
229
|
-
async waitForCompletion(runId: RunId): Promise<Extract<RunResult, { status: "completed" | "failed" }>> {
|
|
231
|
+
async waitForCompletion(runId: RunId): Promise<Extract<RunResult, { status: "completed" | "failed" | "halted" }>> {
|
|
230
232
|
return await this.deps.runContinuationService.waitForCompletion(runId);
|
|
231
233
|
}
|
|
232
234
|
|
|
@@ -234,6 +236,14 @@ export class Engine implements NodeActivationContinuation, NodeExecutionRequestH
|
|
|
234
236
|
return await this.deps.runContinuationService.waitForWebhookResponse(runId);
|
|
235
237
|
}
|
|
236
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Re-activate a suspended run item with a human decision (HITL).
|
|
241
|
+
* The HTTP resume endpoint calls this; this method exposes the engine primitive.
|
|
242
|
+
*/
|
|
243
|
+
async resumeRun(args: { runId: RunId; taskId: string; resumeContext: ResumeContext }): Promise<RunResult> {
|
|
244
|
+
return await this.deps.runContinuationService.resumeRun(args);
|
|
245
|
+
}
|
|
246
|
+
|
|
237
247
|
async handleNodeExecutionRequest(request: NodeExecutionRequest): Promise<void> {
|
|
238
248
|
await this.deps.nodeExecutionRequestHandler.handleNodeExecutionRequest(request);
|
|
239
249
|
}
|
|
@@ -21,7 +21,7 @@ export class EngineWaiters {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
resolveRunCompletion(result: RunResult): void {
|
|
24
|
-
if (result.status !== "completed" && result.status !== "failed") return;
|
|
24
|
+
if (result.status !== "completed" && result.status !== "failed" && result.status !== "halted") return;
|
|
25
25
|
const list = this.completionWaiters.get(result.runId);
|
|
26
26
|
if (!list || list.length === 0) return;
|
|
27
27
|
this.completionWaiters.delete(result.runId);
|
|
@@ -4,12 +4,14 @@ import type {
|
|
|
4
4
|
NodeExecutionRequest,
|
|
5
5
|
NodeExecutionRequestHandler,
|
|
6
6
|
PersistedRunState,
|
|
7
|
+
ResumeContext,
|
|
7
8
|
RunDataFactory,
|
|
8
9
|
WorkflowDefinition,
|
|
9
10
|
WorkflowExecutionRepository,
|
|
10
11
|
WorkflowSnapshotResolver,
|
|
11
12
|
} from "../types";
|
|
12
13
|
import type { EngineExecutionLimitsPolicy } from "../policies/executionLimits/EngineExecutionLimitsPolicy";
|
|
14
|
+
import { RunSuspendedError } from "../execution/RunSuspendedError";
|
|
13
15
|
import { NodeActivationRequestComposer } from "../execution/NodeActivationRequestComposer";
|
|
14
16
|
import { NodeRunStateWriterFactory } from "../execution/NodeRunStateWriterFactory";
|
|
15
17
|
import { WorkflowRunExecutionContextFactory } from "../execution/WorkflowRunExecutionContextFactory";
|
|
@@ -84,6 +86,14 @@ export class NodeExecutionRequestHandlerService implements NodeExecutionRequestH
|
|
|
84
86
|
const portKeys = Object.keys(inputsByPort);
|
|
85
87
|
const kind = portKeys.length === 1 && portKeys[0] === "in" ? ("single" as const) : ("multi" as const);
|
|
86
88
|
const batchId = pendingExecution.batchId ?? "batch_1";
|
|
89
|
+
// Splice resumeContext from pendingResume if this activation is a HITL resume.
|
|
90
|
+
const pendingResume = state.pendingResume;
|
|
91
|
+
const resumeContext: ResumeContext | undefined =
|
|
92
|
+
pendingResume?.activationId === request.activationId && pendingResume?.nodeId === request.nodeId
|
|
93
|
+
? (pendingResume.resumeContext as ResumeContext)
|
|
94
|
+
: undefined;
|
|
95
|
+
const baseWithResume = resumeContext != null ? { ...base, resumeContext } : base;
|
|
96
|
+
|
|
87
97
|
const activationRequest =
|
|
88
98
|
kind === "multi"
|
|
89
99
|
? this.nodeActivationRequestComposer.createMultiFromDefinitionWithActivation({
|
|
@@ -92,7 +102,7 @@ export class NodeExecutionRequestHandlerService implements NodeExecutionRequestH
|
|
|
92
102
|
workflowId: request.workflowId,
|
|
93
103
|
parent: resolvedParent,
|
|
94
104
|
executionOptions: request.executionOptions ?? state.executionOptions,
|
|
95
|
-
base,
|
|
105
|
+
base: baseWithResume,
|
|
96
106
|
data,
|
|
97
107
|
definition: {
|
|
98
108
|
id: definition.id,
|
|
@@ -107,7 +117,7 @@ export class NodeExecutionRequestHandlerService implements NodeExecutionRequestH
|
|
|
107
117
|
workflowId: request.workflowId,
|
|
108
118
|
parent: resolvedParent,
|
|
109
119
|
executionOptions: request.executionOptions ?? state.executionOptions,
|
|
110
|
-
base,
|
|
120
|
+
base: baseWithResume,
|
|
111
121
|
data,
|
|
112
122
|
definition: {
|
|
113
123
|
id: definition.id,
|
|
@@ -117,6 +127,14 @@ export class NodeExecutionRequestHandlerService implements NodeExecutionRequestH
|
|
|
117
127
|
input: inputsByPort.in ?? request.input ?? [],
|
|
118
128
|
});
|
|
119
129
|
|
|
130
|
+
// Clear pendingResume from state now that we have consumed it.
|
|
131
|
+
if (resumeContext != null) {
|
|
132
|
+
const clearedState = await this.workflowExecutionRepository.load(request.runId);
|
|
133
|
+
if (clearedState?.pendingResume?.activationId === request.activationId) {
|
|
134
|
+
await this.workflowExecutionRepository.save({ ...clearedState, pendingResume: undefined });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
120
138
|
await this.continuation.markNodeRunning({
|
|
121
139
|
runId: activationRequest.runId,
|
|
122
140
|
activationId: activationRequest.activationId,
|
|
@@ -128,6 +146,11 @@ export class NodeExecutionRequestHandlerService implements NodeExecutionRequestH
|
|
|
128
146
|
try {
|
|
129
147
|
outputs = await this.nodeExecutor.execute(activationRequest);
|
|
130
148
|
} catch (error) {
|
|
149
|
+
if (error instanceof RunSuspendedError) {
|
|
150
|
+
// The node threw SuspensionRequest; NodeSuspensionHandler already persisted the
|
|
151
|
+
// suspension entry and flipped status to "suspended". Nothing more to do here.
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
131
154
|
await this.resumeAfterExecutionError(activationRequest, this.asError(error));
|
|
132
155
|
return;
|
|
133
156
|
}
|