@desplega.ai/agent-swarm 2.5.1 → 2.5.3
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/dist/{acp-adapter-7g6bpwbk.js → acp-adapter-gr2fcyyj.js} +3 -3
- package/dist/{actions-w8xd014k.js → actions-wv766ghf.js} +18 -18
- package/dist/{amp-adapter-z4qvhacj.js → amp-adapter-speb906t.js} +3 -3
- package/dist/{app-nndcvftq.js → app-ag29n14f.js} +3 -3
- package/dist/{assistant-hjmqyscx.js → assistant-3x8bz3eg.js} +19 -19
- package/dist/{boot-reembed-r8rhf9h8.js → boot-reembed-5b3d2eea.js} +1 -1
- package/dist/{boot-reembed-vwcj2y5d.js → boot-reembed-jsmfrpzf.js} +1 -1
- package/dist/{boot-scrub-sweep-fr9q36ne.js → boot-scrub-sweep-s73gerxy.js} +2 -2
- package/dist/{boot-seeds-9jqwnegq.js → boot-seeds-5zmax2wr.js} +4 -4
- package/dist/{claude-adapter-2f8b85df.js → claude-adapter-hswdnm3t.js} +3 -3
- package/dist/{claude-managed-adapter-gt6pa35d.js → claude-managed-adapter-cvbzj1h6.js} +2 -2
- package/dist/{claude-managed-setup-c3hp36ph.js → claude-managed-setup-3k8m3hhc.js} +2 -2
- package/dist/{claude-sdk-session-05kat8rv.js → claude-sdk-session-fkbyk66x.js} +6905 -6855
- package/dist/{cli-y3zyaee5.js → cli-1fc85mvt.js} +5 -5
- package/dist/{cli-v2g93k4v.js → cli-210xd1f4.js} +4 -4
- package/dist/{cli-sn1vbtg2.js → cli-25j01dpj.js} +3 -3
- package/dist/{cli-n6kmqngw.js → cli-35gtk1p6.js} +1 -1
- package/dist/{cli-r9fy0ddc.js → cli-3ngdk5te.js} +1 -1
- package/dist/{cli-z63a269p.js → cli-40jsnrjm.js} +2 -2
- package/dist/{cli-ba8ne1es.js → cli-4rvr7zfx.js} +10 -0
- package/dist/{cli-qfm6my86.js → cli-502eqv93.js} +1 -1
- package/dist/{cli-xd4eqtr2.js → cli-6n8axjv1.js} +1 -1
- package/dist/{cli-hadv0725.js → cli-8s5wx3nz.js} +2 -2
- package/dist/{cli-z2dv25mf.js → cli-99xgw26r.js} +116 -5
- package/dist/{cli-jpxsrxcb.js → cli-9s05422w.js} +1 -1
- package/dist/{cli-kcsd1ez8.js → cli-aecdcr77.js} +343 -325
- package/dist/{cli-w9etnnxk.js → cli-bf8ma3f1.js} +120 -50
- package/dist/{cli-e9btqw4y.js → cli-ce0ak9ms.js} +18 -13
- package/dist/{cli-nj12tn2c.js → cli-e2ar4bp1.js} +71 -20
- package/dist/{cli-gwcf52gz.js → cli-e8jyq7d8.js} +11 -7
- package/dist/{cli-rngde2jv.js → cli-egjggehm.js} +1 -1
- package/dist/{cli-qfjm4gvt.js → cli-ewm3pwbq.js} +2 -2
- package/dist/{cli-9mm5ade4.js → cli-fsdprwrg.js} +1 -1
- package/dist/{cli-3ee5z0q0.js → cli-fzcn3d87.js} +1 -1
- package/dist/{cli-eym3jg7w.js → cli-gpsz7g88.js} +6 -6
- package/dist/{cli-cwyafhw1.js → cli-hcpj2xqz.js} +1 -1
- package/dist/{cli-j1eghqfm.js → cli-hhxjnjzs.js} +1 -1
- package/dist/{cli-pyy4c9ch.js → cli-jjznk3f7.js} +881 -232
- package/dist/{cli-q9pzwxvk.js → cli-kxawjrxg.js} +222 -37
- package/dist/{cli-rh5smt8t.js → cli-mz9afxxf.js} +14 -3
- package/dist/{cli-mz2ygeyq.js → cli-n45mr7zh.js} +2 -2
- package/dist/{cli-fes51ya2.js → cli-p0rzjkya.js} +2 -2
- package/dist/{cli-3qv8naq8.js → cli-p6zs9h4y.js} +827 -107
- package/dist/{cli-mq7kg1b1.js → cli-p9rafdrb.js} +41 -6
- package/dist/{cli-cah5detr.js → cli-qgf8emrn.js} +5 -5
- package/dist/{cli-2wba1ac9.js → cli-qzme5949.js} +26 -26
- package/dist/{cli-znvq0k3t.js → cli-s49hpn37.js} +2 -2
- package/dist/{cli-xc65x52q.js → cli-s53vp709.js} +1 -1
- package/dist/{cli-0jy85zz4.js → cli-svrj71pg.js} +18 -16
- package/dist/{cli-1dp1zj04.js → cli-vwn81jbs.js} +3 -3
- package/dist/{cli-ajqhxtnz.js → cli-w1zwctj2.js} +1 -1
- package/dist/{cli-22rmcjkg.js → cli-y561h5a9.js} +7 -6
- package/dist/{cli-2jg61wab.js → cli-z21whrrg.js} +15 -15
- package/dist/cli.js +19 -18
- package/dist/{codex-adapter-p4ptwar5.js → codex-adapter-n8ka66vf.js} +2 -2
- package/dist/{codex-session-runner-964c7dnk.js → codex-session-runner-fsebts37.js} +1 -1
- package/dist/{commands-mbvhbrcy.js → commands-t04rt2pc.js} +2 -2
- package/dist/{controller-gnq1k1mf.js → controller-rgna04r2.js} +2 -2
- package/dist/{controllers-connect-6dkm71tg.js → controllers-connect-bqm1rtft.js} +20 -20
- package/dist/{cursor-adapter-yq5xgfcg.js → cursor-adapter-swr2k95y.js} +1 -1
- package/dist/{db-88wcymxt.js → db-24ck09xy.js} +1 -1
- package/dist/{db-af9y8nkf.js → db-a4m7haw0.js} +3 -3
- package/dist/{dispatcher-606ydx5v.js → dispatcher-nwf9ss0q.js} +17 -17
- package/dist/{dsh-adapter-tvhc4k2d.js → dsh-adapter-kpdnhjst.js} +2 -2
- package/dist/{external-channel-y8q5z8z2.js → external-channel-j1e2p34p.js} +2 -2
- package/dist/{grok-adapter-39aha52t.js → grok-adapter-qtjz0whk.js} +4 -4
- package/dist/{handlers-rk9dqech.js → handlers-19xr6dnz.js} +20 -20
- package/dist/{heartbeat-jfe9zfjr.js → heartbeat-vtesda5d.js} +20 -20
- package/dist/{hook-b1mez1m8.js → hook-8wqaszda.js} +1 -1
- package/dist/{hook-ffevzed1.js → hook-b2hhj6rd.js} +1 -1
- package/dist/{http-d5b44f1r.js → http-mxcqhhcs.js} +3020 -2463
- package/dist/{index-5en23s5z.js → index-1r8wfpzs.js} +1 -1
- package/dist/{index-cbxkmfc5.js → index-9pn1pfkf.js} +1 -1
- package/dist/{index-mj46rk9k.js → index-kw2mmat7.js} +20 -20
- package/dist/{index-nq8hhv28.js → index-n1vbvta9.js} +19 -10
- package/dist/{index-6srwvd5b.js → index-p4xfz37z.js} +1 -1
- package/dist/{index-d2qeem97.js → index-spwxjgay.js} +17 -17
- package/dist/{index-4q3vj1yr.js → index-srg522m6.js} +17 -17
- package/dist/{keepalive-wsby1btq.js → keepalive-dsd8rtgx.js} +3 -3
- package/dist/{lead-paa141j6.js → lead-zyerk1x2.js} +30 -30
- package/dist/{maintenance-vnshcyv9.js → maintenance-jqmqwnet.js} +1 -1
- package/dist/{oauth-refresh-sweep-d6sqn5np.js → oauth-refresh-sweep-q2c3bnvm.js} +2 -2
- package/dist/{onboard-nr4xhpzy.js → onboard-w4yd7s8t.js} +2 -2
- package/dist/{opencode-adapter-e56086m1.js → opencode-adapter-9tppvxar.js} +2 -2
- package/dist/{otel-impl-ytn9wyv9.js → otel-impl-f36byzbd.js} +1 -1
- package/dist/{pricing-refresh-pva04zee.js → pricing-refresh-xvak5p4v.js} +3 -3
- package/dist/{rbac-roles-7rc1q3g4.js → rbac-roles-682k2mt5.js} +12 -2
- package/dist/rbac-roles-gfn30fb8.js +26 -0
- package/dist/{render-v2-pyfsg9zk.js → render-v2-hgytz8nn.js} +3 -3
- package/dist/{resume-phj8xhqr.js → resume-2dvq348g.js} +7 -5
- package/dist/{session-access-jzx2z9h5.js → session-access-7f21ekq9.js} +4 -4
- package/dist/{setup-rfg0f0sg.js → setup-epffxtbk.js} +1 -1
- package/dist/{work-objects-44z2yjg8.js → work-objects-xz1a5rqk.js} +20 -20
- package/dist/{worker-zsfqzxhm.js → worker-42t8emjt.js} +30 -30
- package/openapi.json +3163 -2114
- package/package.json +7 -6
- package/src/be/approval-config-question.ts +389 -0
- package/src/be/automation-preflight.ts +37 -3
- package/src/be/db/session-tree.ts +158 -13
- package/src/be/db/session-visibility.ts +31 -0
- package/src/be/db/skill-search.ts +87 -0
- package/src/be/db/tasks/read.ts +47 -7
- package/src/be/db/workflow-runs.ts +54 -7
- package/src/be/db.ts +27 -5
- package/src/be/migrations/233_script_run_session_tokens.sql +8 -0
- package/src/be/migrations/235_chat_relay_defer.sql +86 -0
- package/src/be/migrations/237_session_inbox_cleanup.sql +75 -0
- package/src/be/migrations/239_skills_fts.sql +40 -0
- package/src/be/migrations/240_workflow_run_parent_step_attempt.sql +37 -0
- package/src/be/migrations/241_skills_meta_fts.sql +39 -0
- package/src/be/rbac-roles.ts +138 -0
- package/src/be/scripts/run-credential.ts +64 -0
- package/src/be/scripts/run-saved.ts +3 -2
- package/src/be/swarm-config-guard.ts +23 -0
- package/src/be/users.ts +28 -4
- package/src/extensions/catalog.generated.json +4 -4
- package/src/http/all-routes.ts +1 -0
- package/src/http/approval-requests.ts +254 -43
- package/src/http/config.ts +22 -37
- package/src/http/index.ts +2 -0
- package/src/http/schedules.ts +26 -0
- package/src/http/scripts.ts +2 -2
- package/src/http/sessions.ts +118 -2
- package/src/http/user-roles.ts +173 -0
- package/src/http/users.ts +9 -0
- package/src/http/x.ts +2 -2
- package/src/integrations/chat-sdk/commands.ts +14 -1
- package/src/integrations/chat-sdk/inbound.ts +42 -6
- package/src/integrations/chat-sdk/outbound.ts +35 -10
- package/src/integrations/chat-sdk/reactions.ts +107 -0
- package/src/integrations/chat-sdk/relay.ts +75 -7
- package/src/integrations/chat-sdk/thread-buffer.ts +2 -0
- package/src/integrations/reaction-events.ts +20 -0
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/rbac/legacy-policy.ts +3 -0
- package/src/rbac/permissions.ts +8 -0
- package/src/scheduler/schedule-task.ts +44 -3
- package/src/scheduler/scheduler.ts +4 -4
- package/src/scripts-runtime/loader.ts +8 -1
- package/src/slack/ack.ts +2 -1
- package/src/slack/reaction-shortcode.ts +4 -7
- package/src/tasks/worker-follow-up.ts +30 -0
- package/src/tests/approval-config-question.test.ts +702 -0
- package/src/tests/approval-requests-notifications.test.ts +189 -0
- package/src/tests/chat-sdk-commands.test.ts +12 -0
- package/src/tests/chat-sdk-inbound.test.ts +183 -9
- package/src/tests/chat-sdk-outbound-files.test.ts +9 -2
- package/src/tests/chat-sdk-relay.test.ts +361 -10
- package/src/tests/chat-sdk-tools.test.ts +104 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/entrypoint-gitlab-credential-helper.test.ts +273 -0
- package/src/tests/extensions-example-comms-adherence-gate.test.ts +321 -12
- package/src/tests/http-users.test.ts +38 -0
- package/src/tests/list-endpoint-slimming.test.ts +33 -0
- package/src/tests/migration-237-session-inbox-cleanup.test.ts +136 -0
- package/src/tests/migration-240-workflow-run-parent-step-attempt.test.ts +55 -0
- package/src/tests/rbac-engine.test.ts +2 -0
- package/src/tests/rbac-roles.test.ts +10 -0
- package/src/tests/schedule-http-triage-tools.test.ts +44 -0
- package/src/tests/script-api-credential.test.ts +173 -0
- package/src/tests/script-connections.test.ts +10 -0
- package/src/tests/script-run-config-scope.test.ts +120 -0
- package/src/tests/script-run-token.test.ts +75 -0
- package/src/tests/scripts-http.test.ts +19 -0
- package/src/tests/sessions-tree.test.ts +410 -1
- package/src/tests/skill-search-long-content.test.ts +193 -0
- package/src/tests/skill-search.test.ts +173 -22
- package/src/tests/user-roles-http.test.ts +204 -0
- package/src/tests/workflow-hitl-dynamic-questions.test.ts +1 -1
- package/src/tests/workflow-sub-workflow.test.ts +538 -1
- package/src/tools/chat-reply.ts +2 -0
- package/src/tools/request-human-input.ts +27 -5
- package/src/tools/schedules/patch-schedule.ts +18 -0
- package/src/tools/schedules/update-schedule.ts +18 -0
- package/src/types.ts +23 -0
- package/src/workflows/approval-notifications.ts +221 -0
- package/src/workflows/checkpoint.ts +69 -35
- package/src/workflows/engine.ts +234 -87
- package/src/workflows/executors/base.ts +6 -0
- package/src/workflows/executors/human-in-the-loop.ts +36 -173
- package/src/workflows/executors/sub-workflow.ts +45 -16
- package/src/workflows/executors/swarm-script.ts +2 -2
- package/src/workflows/recovery.ts +63 -1
- package/src/workflows/resume.ts +60 -21
- package/src/workflows/retry-poller.ts +25 -4
- package/templates/extensions/comms-adherence-gate/README.md +38 -13
- package/templates/extensions/comms-adherence-gate/hooks.ts +272 -69
- package/templates/extensions/comms-adherence-gate/manifest.json +2 -2
- package/templates/schedules/weekly-harness-upgrade-check/content.md +1 -1
- package/templates/skills/code-quality/content.md +1 -1
- package/templates/skills/turso-interaction/content.md +1 -1
- package/templates/skills/workflow-iterate/content.md +8 -1
- package/dist/rbac-roles-ff1ffmec.js +0 -12
- /package/dist/{cli-stxdrqs2.js → cli-avdvgcay.js} +0 -0
- /package/dist/{pi-mono-adapter-tzps8f38.js → pi-mono-adapter-tqsge1ay.js} +0 -0
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
ConfigQuestionSettingsSchema,
|
|
4
|
+
normalizeApprovalQuestions,
|
|
5
|
+
} from "../../be/approval-config-question";
|
|
2
6
|
import type { ExecutorMeta } from "../../types";
|
|
3
|
-
import { getAppUrl } from "../../utils/constants";
|
|
4
7
|
import { scrubSecrets } from "../../utils/secret-scrubber";
|
|
5
|
-
import {
|
|
8
|
+
import { dispatchApprovalNotifications } from "../approval-notifications";
|
|
6
9
|
import type { ExecutorInput, ExecutorResult } from "./base";
|
|
7
10
|
import { BaseExecutor } from "./base";
|
|
8
11
|
|
|
@@ -57,6 +60,15 @@ const QuestionSchema = z.discriminatedUnion("type", [
|
|
|
57
60
|
description: z.string().optional(),
|
|
58
61
|
defaultValue: z.boolean().optional(),
|
|
59
62
|
}),
|
|
63
|
+
z.object({
|
|
64
|
+
id: z.string(),
|
|
65
|
+
type: z.literal("config"),
|
|
66
|
+
// Defaults to the config key during normalization in `run`.
|
|
67
|
+
label: z.string().optional(),
|
|
68
|
+
required: z.boolean().default(true),
|
|
69
|
+
description: z.string().optional(),
|
|
70
|
+
config: ConfigQuestionSettingsSchema,
|
|
71
|
+
}),
|
|
60
72
|
]);
|
|
61
73
|
|
|
62
74
|
export const ApproverConfigSchema = z.object({
|
|
@@ -88,9 +100,6 @@ const DYNAMIC_QUESTIONS_TOKEN_RE = /^\{\{[^}]+\}\}$/;
|
|
|
88
100
|
*/
|
|
89
101
|
export const MAX_HITL_QUESTIONS = 100;
|
|
90
102
|
|
|
91
|
-
/** Slack Block Kit caps a section block's text at 3000 characters. */
|
|
92
|
-
const SLACK_SECTION_TEXT_LIMIT = 3000;
|
|
93
|
-
|
|
94
103
|
const MAX_REPORTED_ISSUES = 5;
|
|
95
104
|
|
|
96
105
|
const HITLConfigSchema = z.object({
|
|
@@ -197,38 +206,6 @@ export function resolveHitlQuestions(
|
|
|
197
206
|
return { ok: true, questions };
|
|
198
207
|
}
|
|
199
208
|
|
|
200
|
-
function escapeSlackMrkdwn(text: string): string {
|
|
201
|
-
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Render the Slack "Questions" section text within Block Kit's 3000-character
|
|
206
|
-
* section limit. Labels are escaped so upstream text cannot inject mentions or
|
|
207
|
-
* links. When the list does not fit, the tail is replaced by a count; every
|
|
208
|
-
* question stays answerable on the dashboard page the card's button opens.
|
|
209
|
-
*/
|
|
210
|
-
export function buildSlackQuestionsSummary(
|
|
211
|
-
questions: ReadonlyArray<{ label: string }>,
|
|
212
|
-
timeoutText = "",
|
|
213
|
-
): string {
|
|
214
|
-
const header = "*Questions:*\n";
|
|
215
|
-
const lines = questions.map((q) => `• ${escapeSlackMrkdwn(q.label.replace(/\s+/g, " ").trim())}`);
|
|
216
|
-
const fits = (shown: string[], omitted: number) => {
|
|
217
|
-
const tail =
|
|
218
|
-
omitted > 0 ? `\n_…and ${omitted} more. Answer all of them on the dashboard._` : "";
|
|
219
|
-
const text = `${header}${shown.join("\n")}${tail}${timeoutText}`;
|
|
220
|
-
return text.length <= SLACK_SECTION_TEXT_LIMIT ? text : null;
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
const full = fits(lines, 0);
|
|
224
|
-
if (full) return full;
|
|
225
|
-
for (let count = lines.length - 1; count >= 0; count--) {
|
|
226
|
-
const text = fits(lines.slice(0, count), lines.length - count);
|
|
227
|
-
if (text) return text;
|
|
228
|
-
}
|
|
229
|
-
return `${header}_${questions.length} questions. Answer them on the dashboard._`;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
209
|
// ─── Executor ───────────────────────────────────────────────
|
|
233
210
|
|
|
234
211
|
export class HumanInTheLoopExecutor extends BaseExecutor<
|
|
@@ -243,7 +220,18 @@ export class HumanInTheLoopExecutor extends BaseExecutor<
|
|
|
243
220
|
override async run(input: ExecutorInput): Promise<ExecutorResult<HITLOutput>> {
|
|
244
221
|
const resolved = resolveHitlQuestions(input.config?.questions);
|
|
245
222
|
if (!resolved.ok) return { status: "failed", error: resolved.error };
|
|
246
|
-
|
|
223
|
+
// Config questions: the same create-time rules as the tool and REST paths.
|
|
224
|
+
const approvers = input.config?.approvers as { policy?: unknown } | undefined;
|
|
225
|
+
const normalized = await normalizeApprovalQuestions(resolved.questions, {
|
|
226
|
+
approvers: { policy: approvers?.policy },
|
|
227
|
+
});
|
|
228
|
+
if (!normalized.ok) {
|
|
229
|
+
return {
|
|
230
|
+
status: "failed",
|
|
231
|
+
error: `human-in-the-loop questions are invalid: ${normalized.error}`,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
return super.run({ ...input, config: { ...input.config, questions: normalized.questions } });
|
|
247
235
|
}
|
|
248
236
|
|
|
249
237
|
protected async execute(
|
|
@@ -319,7 +307,16 @@ export class HumanInTheLoopExecutor extends BaseExecutor<
|
|
|
319
307
|
|
|
320
308
|
// 3. Dispatch notifications (fire-and-forget — failures must not block the workflow)
|
|
321
309
|
if (config.notifications?.length) {
|
|
322
|
-
|
|
310
|
+
dispatchApprovalNotifications(
|
|
311
|
+
{
|
|
312
|
+
requestId,
|
|
313
|
+
title: config.title,
|
|
314
|
+
questions: config.questions,
|
|
315
|
+
timeoutSeconds: config.timeout?.seconds,
|
|
316
|
+
notifications: config.notifications,
|
|
317
|
+
},
|
|
318
|
+
db,
|
|
319
|
+
).catch((err) => {
|
|
323
320
|
console.error(
|
|
324
321
|
"[HITL] Unexpected error dispatching notifications:",
|
|
325
322
|
scrubSecrets(err instanceof Error ? err.message : String(err)),
|
|
@@ -335,138 +332,4 @@ export class HumanInTheLoopExecutor extends BaseExecutor<
|
|
|
335
332
|
correlationId: requestId,
|
|
336
333
|
} as ExecutorResult<HITLOutput>;
|
|
337
334
|
}
|
|
338
|
-
|
|
339
|
-
/** Dispatch notifications for each configured channel. Updates DB with messageTs on success. */
|
|
340
|
-
private async dispatchNotifications(
|
|
341
|
-
requestId: string,
|
|
342
|
-
config: z.infer<typeof HITLConfigSchema>,
|
|
343
|
-
db: typeof import("../../be/db"),
|
|
344
|
-
): Promise<void> {
|
|
345
|
-
if (!config.notifications?.length) return;
|
|
346
|
-
|
|
347
|
-
const approvalUrl = `${getAppUrl()}/approval-requests/${requestId}`;
|
|
348
|
-
const updatedChannels = [...config.notifications] as Array<
|
|
349
|
-
z.infer<typeof NotificationConfigSchema> & { messageTs?: string }
|
|
350
|
-
>;
|
|
351
|
-
let updated = false;
|
|
352
|
-
|
|
353
|
-
for (let i = 0; i < config.notifications.length; i++) {
|
|
354
|
-
const notification = config.notifications[i]!;
|
|
355
|
-
|
|
356
|
-
if (notification.channel === "email") {
|
|
357
|
-
console.warn(
|
|
358
|
-
`[HITL] Email notifications not yet supported (target: ${notification.target})`,
|
|
359
|
-
);
|
|
360
|
-
continue;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
if (notification.channel === "slack") {
|
|
364
|
-
try {
|
|
365
|
-
const { getSlackApp } = await import("../../slack/app");
|
|
366
|
-
const slackApp = getSlackApp();
|
|
367
|
-
if (!slackApp) {
|
|
368
|
-
console.warn("[HITL] Slack not initialized — cannot send notification");
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
const questions = Array.isArray(config.questions) ? config.questions : [];
|
|
373
|
-
const questionsText = buildSlackQuestionsSummary(questions);
|
|
374
|
-
|
|
375
|
-
// The deadline is a caption under the button, not part of the ask.
|
|
376
|
-
const timeoutCaption = config.timeout
|
|
377
|
-
? [
|
|
378
|
-
{
|
|
379
|
-
type: "context",
|
|
380
|
-
elements: [
|
|
381
|
-
{
|
|
382
|
-
type: "mrkdwn",
|
|
383
|
-
text: `⏱ Timeout: ${formatTimeout(config.timeout.seconds)} — times out if not responded`,
|
|
384
|
-
},
|
|
385
|
-
],
|
|
386
|
-
},
|
|
387
|
-
]
|
|
388
|
-
: [];
|
|
389
|
-
|
|
390
|
-
const blocks = [
|
|
391
|
-
{
|
|
392
|
-
type: "section",
|
|
393
|
-
text: {
|
|
394
|
-
type: "mrkdwn",
|
|
395
|
-
text: `🔔 *Approval Required: ${config.title}*`,
|
|
396
|
-
},
|
|
397
|
-
},
|
|
398
|
-
{
|
|
399
|
-
type: "section",
|
|
400
|
-
text: {
|
|
401
|
-
type: "mrkdwn",
|
|
402
|
-
text: questionsText,
|
|
403
|
-
},
|
|
404
|
-
},
|
|
405
|
-
{
|
|
406
|
-
type: "actions",
|
|
407
|
-
elements: [
|
|
408
|
-
{
|
|
409
|
-
type: "button",
|
|
410
|
-
text: { type: "plain_text", text: "Review & Respond" },
|
|
411
|
-
url: approvalUrl,
|
|
412
|
-
style: "primary",
|
|
413
|
-
},
|
|
414
|
-
],
|
|
415
|
-
},
|
|
416
|
-
...timeoutCaption,
|
|
417
|
-
];
|
|
418
|
-
|
|
419
|
-
const result = await slackApp.client.chat.postMessage({
|
|
420
|
-
channel: notification.target,
|
|
421
|
-
text: `Approval Required: ${config.title} — ${approvalUrl}`,
|
|
422
|
-
unfurl_links: false,
|
|
423
|
-
unfurl_media: false,
|
|
424
|
-
// biome-ignore lint/suspicious/noExplicitAny: Block Kit objects
|
|
425
|
-
blocks: blocks as any,
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
if (result.ts) {
|
|
429
|
-
updatedChannels[i] = { ...notification, messageTs: result.ts };
|
|
430
|
-
updated = true;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
console.log(
|
|
434
|
-
`[HITL] Slack notification sent to ${notification.target} for request ${requestId}`,
|
|
435
|
-
);
|
|
436
|
-
} catch (err) {
|
|
437
|
-
console.error(
|
|
438
|
-
`[HITL] Failed to send Slack notification to ${notification.target}:`,
|
|
439
|
-
scrubSecrets(err instanceof Error ? err.message : String(err)),
|
|
440
|
-
);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// Persist messageTs values back to DB
|
|
446
|
-
if (updated) {
|
|
447
|
-
try {
|
|
448
|
-
await db.updateApprovalRequestNotifications(requestId, updatedChannels);
|
|
449
|
-
const latest = await db.getApprovalRequestById(requestId);
|
|
450
|
-
if (latest?.status === "cancelled") {
|
|
451
|
-
await postApprovalCancellationUpdates(
|
|
452
|
-
[latest],
|
|
453
|
-
latest.resolutionReason ?? "Workflow run cancelled",
|
|
454
|
-
);
|
|
455
|
-
}
|
|
456
|
-
} catch (err) {
|
|
457
|
-
console.error(
|
|
458
|
-
"[HITL] Failed to update notification channels in DB:",
|
|
459
|
-
scrubSecrets(err instanceof Error ? err.message : String(err)),
|
|
460
|
-
);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
function formatTimeout(seconds: number): string {
|
|
467
|
-
if (seconds < 60) return `${seconds}s`;
|
|
468
|
-
if (seconds < 3600) return `${Math.round(seconds / 60)}m`;
|
|
469
|
-
const hours = Math.floor(seconds / 3600);
|
|
470
|
-
const mins = Math.round((seconds % 3600) / 60);
|
|
471
|
-
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
|
|
472
335
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getChildWorkflowRunId, getWorkflowRunLineage } from "../../be/db/workflow-runs";
|
|
3
3
|
import type { ExecutorMeta, WorkflowRun } from "../../types";
|
|
4
|
-
import { startWorkflowExecution } from "../engine";
|
|
4
|
+
import { ParentStepNotCurrentError, startWorkflowExecution } from "../engine";
|
|
5
5
|
import type { ExecutorDependencies, ExecutorResult } from "./base";
|
|
6
6
|
import { BaseExecutor } from "./base";
|
|
7
7
|
import type { ExecutorRegistry } from "./registry";
|
|
@@ -35,15 +35,21 @@ const NON_NODE_CONTEXT_KEYS = new Set(["trigger", "input", "swarm", "workflow"])
|
|
|
35
35
|
/**
|
|
36
36
|
* The parent step's outcome for a child run: its result once the child
|
|
37
37
|
* completed, an error once it failed, was cancelled or was skipped, and null
|
|
38
|
-
* while it is still running or waiting.
|
|
38
|
+
* while it is still running or waiting. Only a failed child is retryable: a
|
|
39
|
+
* cancel or a skip is a decision, and a node retry must not override it. The
|
|
40
|
+
* synchronous (executor) and the parked (`resumeFromChildRun`) paths both
|
|
41
|
+
* apply this, so the same child end gets the same policy whenever it lands.
|
|
39
42
|
*/
|
|
40
43
|
export function childRunOutcome(
|
|
41
44
|
child: WorkflowRun,
|
|
42
|
-
): { output: SubWorkflowOutput } | { error: string } | null {
|
|
45
|
+
): { output: SubWorkflowOutput } | { error: string; retryable: boolean } | null {
|
|
43
46
|
if (child.status === "running" || child.status === "waiting") return null;
|
|
44
47
|
if (child.status !== "completed") {
|
|
45
48
|
const reason = child.error ? `: ${child.error}` : "";
|
|
46
|
-
return {
|
|
49
|
+
return {
|
|
50
|
+
error: `Child workflow run ${child.id} ${child.status}${reason}`,
|
|
51
|
+
retryable: child.status === "failed",
|
|
52
|
+
};
|
|
47
53
|
}
|
|
48
54
|
const outputs: Record<string, unknown> = {};
|
|
49
55
|
for (const [key, value] of Object.entries(child.context ?? {})) {
|
|
@@ -56,8 +62,9 @@ export function childRunOutcome(
|
|
|
56
62
|
|
|
57
63
|
/**
|
|
58
64
|
* Runs another workflow as a child run and waits for it. The child run records
|
|
59
|
-
* this step as its parent, so a re-executed
|
|
60
|
-
* starting a second child
|
|
65
|
+
* this step and its attempt (`retryCount`) as its parent, so a re-executed or
|
|
66
|
+
* recovered step reconnects to it instead of starting a second child, while a
|
|
67
|
+
* retry starts a new one. The resume path is `resumeFromChildRun`.
|
|
61
68
|
*/
|
|
62
69
|
export class SubWorkflowExecutor extends BaseExecutor<
|
|
63
70
|
typeof SubWorkflowConfigSchema,
|
|
@@ -82,9 +89,12 @@ export class SubWorkflowExecutor extends BaseExecutor<
|
|
|
82
89
|
): Promise<ExecutorResult<SubWorkflowOutput>> {
|
|
83
90
|
const { db } = this.deps;
|
|
84
91
|
|
|
85
|
-
|
|
92
|
+
// The retry poller re-executes a failed step under the same id with a
|
|
93
|
+
// higher retryCount; that attempt must not settle on the previous child.
|
|
94
|
+
const attempt = (await db.getWorkflowRunStep(meta.stepId))?.retryCount ?? 0;
|
|
95
|
+
const existingId = await getChildWorkflowRunId(meta.stepId, attempt);
|
|
86
96
|
const existing = existingId ? await db.getWorkflowRun(existingId) : null;
|
|
87
|
-
if (existing) return this.settle(existing);
|
|
97
|
+
if (existing) return this.settle(existing, config.workflowId);
|
|
88
98
|
|
|
89
99
|
// Capped at the max depth, so a long chain fails closed instead of
|
|
90
100
|
// hiding a cycle past the end of the scan.
|
|
@@ -109,17 +119,34 @@ export class SubWorkflowExecutor extends BaseExecutor<
|
|
|
109
119
|
}
|
|
110
120
|
|
|
111
121
|
// Returns once the child run exists; the child walks on its own. A child
|
|
112
|
-
// that already ended (cooldown, readiness, input failure) settles here
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
122
|
+
// that already ended (cooldown, readiness, input failure) settles here,
|
|
123
|
+
// and a concurrent execution of this attempt gets the child it created.
|
|
124
|
+
let childRunId: string;
|
|
125
|
+
try {
|
|
126
|
+
childRunId = await startWorkflowExecution(workflow, config.inputs ?? {}, this.registry, {
|
|
127
|
+
requestedByUserId: meta.requestedByUserId,
|
|
128
|
+
parentStepId: meta.stepId,
|
|
129
|
+
parentStepAttempt: attempt,
|
|
130
|
+
});
|
|
131
|
+
} catch (err) {
|
|
132
|
+
if (err instanceof ParentStepNotCurrentError) return { status: "failed", error: err.message };
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
117
135
|
const child = await db.getWorkflowRun(childRunId);
|
|
118
136
|
if (!child) return { status: "failed", error: `Child workflow run ${childRunId} not found` };
|
|
119
|
-
return this.settle(child);
|
|
137
|
+
return this.settle(child, config.workflowId);
|
|
120
138
|
}
|
|
121
139
|
|
|
122
|
-
private settle(child: WorkflowRun): ExecutorResult<SubWorkflowOutput> {
|
|
140
|
+
private settle(child: WorkflowRun, workflowId: string): ExecutorResult<SubWorkflowOutput> {
|
|
141
|
+
// The attempt already started a child of another workflow (the config
|
|
142
|
+
// changed under a recovered step). That is a conflict, not a reason to
|
|
143
|
+
// start a replacement next to it.
|
|
144
|
+
if (child.workflowId !== workflowId) {
|
|
145
|
+
return {
|
|
146
|
+
status: "failed",
|
|
147
|
+
error: `Child workflow run ${child.id} runs workflow ${child.workflowId}, not ${workflowId}`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
123
150
|
const outcome = childRunOutcome(child);
|
|
124
151
|
if (!outcome) {
|
|
125
152
|
return {
|
|
@@ -129,7 +156,9 @@ export class SubWorkflowExecutor extends BaseExecutor<
|
|
|
129
156
|
correlationId: child.id,
|
|
130
157
|
} as ExecutorResult<SubWorkflowOutput>;
|
|
131
158
|
}
|
|
132
|
-
if ("error" in outcome)
|
|
159
|
+
if ("error" in outcome) {
|
|
160
|
+
return { status: "failed", error: outcome.error, retryable: outcome.retryable };
|
|
161
|
+
}
|
|
133
162
|
return { status: "success", output: outcome.output, nextPort: "success" };
|
|
134
163
|
}
|
|
135
164
|
}
|
|
@@ -10,12 +10,12 @@ import {
|
|
|
10
10
|
restoreScratchScriptLastUsedIfUnchanged,
|
|
11
11
|
touchScratchScriptLastUsed,
|
|
12
12
|
} from "../../be/scripts/db";
|
|
13
|
+
import { runScriptWithRunCredential } from "../../be/scripts/run-credential";
|
|
13
14
|
import {
|
|
14
15
|
DEFAULT_SCRIPT_RESOURCES,
|
|
15
16
|
MAX_SCRIPT_WALL_CLOCK_MS,
|
|
16
17
|
MIN_SCRIPT_WALL_CLOCK_MS,
|
|
17
18
|
} from "../../scripts-runtime/executors/types";
|
|
18
|
-
import { runScript } from "../../scripts-runtime/loader";
|
|
19
19
|
import type { ExecutorMeta } from "../../types";
|
|
20
20
|
import { getMcpBaseUrl } from "../../utils/constants";
|
|
21
21
|
import { findInterpolationTokens } from "../../utils/template";
|
|
@@ -119,7 +119,7 @@ export class SwarmScriptExecutor extends BaseExecutor<
|
|
|
119
119
|
const credentials = await buildScriptCredentialBindingsWithFailures({
|
|
120
120
|
agentId: agentId ?? undefined,
|
|
121
121
|
});
|
|
122
|
-
const output = await
|
|
122
|
+
const output = await runScriptWithRunCredential({
|
|
123
123
|
source: resolved.source,
|
|
124
124
|
args: config.args,
|
|
125
125
|
fsMode: "none",
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
resolveApprovalRequest,
|
|
13
13
|
updateWorkflowRun,
|
|
14
14
|
} from "../be/db";
|
|
15
|
-
import { getSettledChildRunParentSteps } from "../be/db/workflow-runs";
|
|
15
|
+
import { getRunningSubWorkflowSteps, getSettledChildRunParentSteps } from "../be/db/workflow-runs";
|
|
16
16
|
import type { WorkflowRunStep } from "../types";
|
|
17
17
|
import { shapeApprovalResolution } from "./approval-resolution";
|
|
18
18
|
import { loadCompletedStepRouting } from "./completed-step-routing";
|
|
@@ -20,6 +20,7 @@ import { FAILED_TASK_OUTPUT_PREFIX } from "./constants";
|
|
|
20
20
|
import {
|
|
21
21
|
failRunOnUnreadableReplay,
|
|
22
22
|
findReadyNodes,
|
|
23
|
+
isStepExecuting,
|
|
23
24
|
isWorkflowRunActive,
|
|
24
25
|
walkGraph,
|
|
25
26
|
} from "./engine";
|
|
@@ -76,6 +77,9 @@ export async function recoverIncompleteRuns(registry: ExecutorRegistry): Promise
|
|
|
76
77
|
// --- Case 6: foreach fan-outs with queued or orphaned items ---
|
|
77
78
|
recovered += await recoverForeachFanouts(registry);
|
|
78
79
|
|
|
80
|
+
// --- Case 7: sub-workflow steps a crash left running ---
|
|
81
|
+
recovered += await recoverOrphanedSubWorkflowSteps(registry);
|
|
82
|
+
|
|
79
83
|
if (recovered > 0) {
|
|
80
84
|
console.log(`[workflows] Recovered ${recovered} incomplete run(s)`);
|
|
81
85
|
}
|
|
@@ -98,6 +102,13 @@ async function recoverRunningRuns(registry: ExecutorRegistry): Promise<number> {
|
|
|
98
102
|
if (isWorkflowRunActive(runId)) continue;
|
|
99
103
|
const run = await getWorkflowRun(runId);
|
|
100
104
|
if (!run || run.status !== "running") continue;
|
|
105
|
+
// A child run commits with its initial context. One without it was
|
|
106
|
+
// created before that held and lost its trigger and inputs in a crash;
|
|
107
|
+
// walking it from an empty context would run its nodes on blanks.
|
|
108
|
+
if (run.parentStepId && run.context === undefined) {
|
|
109
|
+
await failRunningRun(runId, "Child workflow run lost its initial context in a restart");
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
101
112
|
|
|
102
113
|
const workflow = await getWorkflow(run.workflowId);
|
|
103
114
|
if (!workflow) continue;
|
|
@@ -146,6 +157,57 @@ async function recoverRunningRuns(registry: ExecutorRegistry): Promise<number> {
|
|
|
146
157
|
return recovered;
|
|
147
158
|
}
|
|
148
159
|
|
|
160
|
+
/** Fail a run that is still `running`; a cancel or finish that committed first wins. */
|
|
161
|
+
async function failRunningRun(runId: string, error: string): Promise<void> {
|
|
162
|
+
await getDbClient().transaction(async () => {
|
|
163
|
+
const run = await getWorkflowRun(runId);
|
|
164
|
+
if (run?.status !== "running") return;
|
|
165
|
+
await updateWorkflowRun(runId, {
|
|
166
|
+
status: "failed",
|
|
167
|
+
error,
|
|
168
|
+
finishedAt: new Date().toISOString(),
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Re-enter `sub-workflow` steps a crash left `running`. The running-run case
|
|
175
|
+
* misses two shapes: a run another branch already parked in `waiting`, and a
|
|
176
|
+
* loop iteration of a node that completed before. Walking the node lets
|
|
177
|
+
* executeStep re-claim the same step row, so the executor reconnects to the
|
|
178
|
+
* child that attempt already started. Foreach items are left to the fan-out
|
|
179
|
+
* case: their synthetic node ids are not in the definition.
|
|
180
|
+
*/
|
|
181
|
+
async function recoverOrphanedSubWorkflowSteps(registry: ExecutorRegistry): Promise<number> {
|
|
182
|
+
let recovered = 0;
|
|
183
|
+
for (const row of await getRunningSubWorkflowSteps()) {
|
|
184
|
+
try {
|
|
185
|
+
if (isWorkflowRunActive(row.runId) || isStepExecuting(row.stepId)) continue;
|
|
186
|
+
const run = await getWorkflowRun(row.runId);
|
|
187
|
+
if (!run || (run.status !== "running" && run.status !== "waiting")) continue;
|
|
188
|
+
const workflow = await getWorkflow(run.workflowId);
|
|
189
|
+
const node = workflow?.definition.nodes.find((n) => n.id === row.nodeId);
|
|
190
|
+
if (!workflow || !node) continue;
|
|
191
|
+
// DB reads above yield; a resume may have acquired the run meanwhile.
|
|
192
|
+
if (isWorkflowRunActive(row.runId)) continue;
|
|
193
|
+
await walkGraph(
|
|
194
|
+
workflow.definition,
|
|
195
|
+
row.runId,
|
|
196
|
+
(run.context ?? {}) as Record<string, unknown>,
|
|
197
|
+
[node],
|
|
198
|
+
registry,
|
|
199
|
+
workflow.id,
|
|
200
|
+
getSecretInputKeys(workflow.input),
|
|
201
|
+
);
|
|
202
|
+
recovered++;
|
|
203
|
+
} catch (err) {
|
|
204
|
+
if (await failRunOnUnreadableReplay(row.runId, err)) continue;
|
|
205
|
+
console.error(`[workflows] Failed to recover sub-workflow step ${row.stepId}:`, err);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return recovered;
|
|
209
|
+
}
|
|
210
|
+
|
|
149
211
|
/** Nodes with a failed step the retry poller will pick up (`nextRetryAt` set). */
|
|
150
212
|
function retryPendingNodeIds(steps: WorkflowRunStep[]): Set<string> {
|
|
151
213
|
const nodeIds = new Set<string>();
|
package/src/workflows/resume.ts
CHANGED
|
@@ -18,12 +18,14 @@ import {
|
|
|
18
18
|
updateWorkflowRunStep,
|
|
19
19
|
} from "../be/db";
|
|
20
20
|
import { createForeachAttemptSteps, getChildWorkflowRunId } from "../be/db/workflow-runs";
|
|
21
|
-
import
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
import {
|
|
22
|
+
type RetryPolicy,
|
|
23
|
+
RetryPolicySchema,
|
|
24
|
+
type WaitStateRow,
|
|
25
|
+
type WorkflowDefinition,
|
|
26
|
+
type WorkflowNode,
|
|
27
|
+
type WorkflowRun,
|
|
28
|
+
type WorkflowRunStep,
|
|
27
29
|
} from "../types";
|
|
28
30
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
29
31
|
import {
|
|
@@ -31,6 +33,7 @@ import {
|
|
|
31
33
|
postApprovalCancellationUpdates,
|
|
32
34
|
} from "./approval-notifications";
|
|
33
35
|
import { shapeApprovalResolution } from "./approval-resolution";
|
|
36
|
+
import { checkpointStepFailure } from "./checkpoint";
|
|
34
37
|
import { loadCompletedStepRouting } from "./completed-step-routing";
|
|
35
38
|
import { FAILED_TASK_OUTPUT_PREFIX } from "./constants";
|
|
36
39
|
import { getNextTargets } from "./definition";
|
|
@@ -963,10 +966,11 @@ async function resumeClaimedWait(
|
|
|
963
966
|
}
|
|
964
967
|
|
|
965
968
|
/**
|
|
966
|
-
* Resume a waiting `sub-workflow` step once its child run is
|
|
967
|
-
* completed child completes the step, any other end (or a deleted
|
|
968
|
-
* the step and its run. Returns true when this call claimed the
|
|
969
|
-
* the live event and the recovery sweep both
|
|
969
|
+
* Resume a waiting `sub-workflow` step once its current attempt's child run is
|
|
970
|
+
* terminal: a completed child completes the step, any other end (or a deleted
|
|
971
|
+
* child) fails the step and its run. Returns true when this call claimed the
|
|
972
|
+
* step. Safe to call repeatedly: the live event and the recovery sweep both
|
|
973
|
+
* route here, and only the call that claims the step walks its successors.
|
|
970
974
|
*/
|
|
971
975
|
export async function resumeFromChildRun(
|
|
972
976
|
parentStepId: string,
|
|
@@ -974,13 +978,6 @@ export async function resumeFromChildRun(
|
|
|
974
978
|
): Promise<boolean> {
|
|
975
979
|
const step = await getWorkflowRunStep(parentStepId);
|
|
976
980
|
if (!step || step.status !== "waiting" || step.nodeType !== "sub-workflow") return false;
|
|
977
|
-
const childId = await getChildWorkflowRunId(parentStepId);
|
|
978
|
-
const child = childId ? await getWorkflowRun(childId) : null;
|
|
979
|
-
// The child row exists before the step parks, so a missing one was deleted.
|
|
980
|
-
const outcome = child
|
|
981
|
-
? childRunOutcome(child)
|
|
982
|
-
: { error: `Child workflow run of step ${parentStepId} was deleted` };
|
|
983
|
-
if (!outcome) return false;
|
|
984
981
|
|
|
985
982
|
let claimed = false;
|
|
986
983
|
await failClosedOnUnreadableReplay(step.runId, step.id, async () => {
|
|
@@ -990,10 +987,32 @@ export async function resumeFromChildRun(
|
|
|
990
987
|
if (!workflow) return;
|
|
991
988
|
|
|
992
989
|
const ctx = (run.context ?? {}) as Record<string, unknown>;
|
|
993
|
-
//
|
|
994
|
-
//
|
|
995
|
-
|
|
996
|
-
|
|
990
|
+
// The step's attempt, that attempt's child and the claim share one
|
|
991
|
+
// transaction: a retry that re-parks the step under a newer attempt in
|
|
992
|
+
// between must not be settled by the previous attempt's child.
|
|
993
|
+
const routing = await getDbClient().transaction(async () => {
|
|
994
|
+
const current = await getWorkflowRunStep(step.id);
|
|
995
|
+
if (!current || current.status !== "waiting") return null;
|
|
996
|
+
const childId = await getChildWorkflowRunId(step.id, current.retryCount);
|
|
997
|
+
const child = childId ? await getWorkflowRun(childId) : null;
|
|
998
|
+
// The child row exists before the step parks, so a missing one was deleted.
|
|
999
|
+
const outcome = child
|
|
1000
|
+
? childRunOutcome(child)
|
|
1001
|
+
: { error: `Child workflow run of step ${parentStepId} was deleted`, retryable: false };
|
|
1002
|
+
if (!outcome) return null;
|
|
1003
|
+
// A retryable end (a failed child) is this attempt failing: the node's
|
|
1004
|
+
// retry policy applies exactly as on the executor path. The poller
|
|
1005
|
+
// re-executes the step under the next attempt, which starts a new child.
|
|
1006
|
+
if ("error" in outcome && outcome.retryable) {
|
|
1007
|
+
const policy = childStepRetryPolicy(workflow.definition, step.nodeId);
|
|
1008
|
+
if (policy && current.retryCount < policy.maxRetries) {
|
|
1009
|
+
await checkpointStepFailure(run.id, step.id, outcome.error, current.retryCount, policy);
|
|
1010
|
+
return CHILD_RETRY_SCHEDULED;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
// A foreach item's failure settles the item: under `onNodeFailure:
|
|
1014
|
+
// "continue"` it can close the join, so it routes like a success.
|
|
1015
|
+
return "error" in outcome
|
|
997
1016
|
? await failStepAndRunIfWaiting(
|
|
998
1017
|
workflow.definition,
|
|
999
1018
|
step.id,
|
|
@@ -1012,6 +1031,12 @@ export async function resumeFromChildRun(
|
|
|
1012
1031
|
ctx,
|
|
1013
1032
|
registry,
|
|
1014
1033
|
);
|
|
1034
|
+
});
|
|
1035
|
+
if (!routing) return;
|
|
1036
|
+
if (routing === CHILD_RETRY_SCHEDULED) {
|
|
1037
|
+
claimed = true;
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1015
1040
|
if (!routing.claimed || routing.runFailed) {
|
|
1016
1041
|
claimed = routing.claimed;
|
|
1017
1042
|
return;
|
|
@@ -1036,6 +1061,20 @@ export async function resumeFromChildRun(
|
|
|
1036
1061
|
return claimed;
|
|
1037
1062
|
}
|
|
1038
1063
|
|
|
1064
|
+
const CHILD_RETRY_SCHEDULED = {
|
|
1065
|
+
claimed: true,
|
|
1066
|
+
runFailed: false,
|
|
1067
|
+
successors: [] as WorkflowNode[],
|
|
1068
|
+
} as const;
|
|
1069
|
+
|
|
1070
|
+
/** The `node.retry` of a top-level `sub-workflow` node; foreach items retry through their parent. */
|
|
1071
|
+
function childStepRetryPolicy(def: WorkflowDefinition, nodeId: string): RetryPolicy | null {
|
|
1072
|
+
const node = def.nodes.find((candidate) => candidate.id === nodeId);
|
|
1073
|
+
if (!node?.retry) return null;
|
|
1074
|
+
const parsed = RetryPolicySchema.safeParse(node.retry);
|
|
1075
|
+
return parsed.success ? parsed.data : null;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1039
1078
|
// ─── 64KB firedPayload cap ──────────────────────────────────────────────────
|
|
1040
1079
|
|
|
1041
1080
|
const FIRED_PAYLOAD_BYTE_CAP = 64 * 1024; // 64KB
|