@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
|
@@ -4,9 +4,22 @@ import {
|
|
|
4
4
|
scrubJsonValue,
|
|
5
5
|
init_scrub_json,
|
|
6
6
|
getDbClient2,
|
|
7
|
+
getAgentById2,
|
|
8
|
+
updateAgentStatus2,
|
|
9
|
+
updateAgentMaxTasks2,
|
|
10
|
+
getActiveTaskCount2,
|
|
7
11
|
getTaskById2,
|
|
12
|
+
getChildWorkflowRunId,
|
|
13
|
+
isParentStepAttemptCurrent,
|
|
8
14
|
createForeachChildSteps,
|
|
9
15
|
init_workflow_runs,
|
|
16
|
+
isApiOnlyConfigKey,
|
|
17
|
+
isInternalConfigKey,
|
|
18
|
+
internalConfigKeyError,
|
|
19
|
+
isReservedConfigKey,
|
|
20
|
+
reservedKeyError,
|
|
21
|
+
validateConfigEntry,
|
|
22
|
+
init_swarm_config_guard,
|
|
10
23
|
getOAuthApp,
|
|
11
24
|
getOAuthTokens,
|
|
12
25
|
init_oauth,
|
|
@@ -14,6 +27,7 @@ import {
|
|
|
14
27
|
init_workflow_replay,
|
|
15
28
|
createTaskExtended2,
|
|
16
29
|
getSwarmConfigs2,
|
|
30
|
+
upsertSwarmConfig2,
|
|
17
31
|
getWorkflow2,
|
|
18
32
|
createWorkflowRun2,
|
|
19
33
|
getWorkflowRun2,
|
|
@@ -31,22 +45,36 @@ import {
|
|
|
31
45
|
getStepCountForNode2,
|
|
32
46
|
getLatestStepForNode2,
|
|
33
47
|
getCurrentStepForNode2,
|
|
48
|
+
getApprovalRequestById2,
|
|
34
49
|
claimApprovalCancellationNotification2,
|
|
35
50
|
completeApprovalCancellationNotificationClaim2,
|
|
36
51
|
releaseApprovalCancellationNotificationClaim2,
|
|
52
|
+
updateApprovalRequestNotifications2,
|
|
37
53
|
init_db3
|
|
38
|
-
} from "./cli-
|
|
54
|
+
} from "./cli-kxawjrxg.js";
|
|
55
|
+
import {
|
|
56
|
+
isMultiRuntimeEnabled
|
|
57
|
+
} from "./cli-e8xyjvfc.js";
|
|
39
58
|
import {
|
|
40
59
|
getSlackApp2
|
|
41
|
-
} from "./cli-
|
|
60
|
+
} from "./cli-ce0ak9ms.js";
|
|
61
|
+
import {
|
|
62
|
+
externalChannelToolError2,
|
|
63
|
+
isBlockExternalEnabled2,
|
|
64
|
+
getChannelBlockReason2
|
|
65
|
+
} from "./cli-s53vp709.js";
|
|
42
66
|
import {
|
|
43
67
|
DEFAULT_MODEL,
|
|
44
68
|
resolveCredential
|
|
45
69
|
} from "./cli-qqzmjxkf.js";
|
|
70
|
+
import {
|
|
71
|
+
revokeRuntimeTokensForRuntime,
|
|
72
|
+
init_users
|
|
73
|
+
} from "./cli-e8jyq7d8.js";
|
|
46
74
|
import {
|
|
47
75
|
TERMINAL_TASK_STATUSES,
|
|
48
76
|
init_types
|
|
49
|
-
} from "./cli-
|
|
77
|
+
} from "./cli-mz9afxxf.js";
|
|
50
78
|
import {
|
|
51
79
|
getSlackConfiguration,
|
|
52
80
|
isSlackConfigured,
|
|
@@ -117,7 +145,7 @@ async function notifyAutomationPreflightFailure(preflight) {
|
|
|
117
145
|
return;
|
|
118
146
|
}
|
|
119
147
|
try {
|
|
120
|
-
const { getSlackApp } = await import("./app-
|
|
148
|
+
const { getSlackApp } = await import("./app-ag29n14f.js");
|
|
121
149
|
const app = getSlackApp();
|
|
122
150
|
if (!app) {
|
|
123
151
|
console.warn("[Automation Preflight] Slack not available, cannot send notification");
|
|
@@ -355,6 +383,15 @@ function utcDayBounds(now) {
|
|
|
355
383
|
const start = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
|
356
384
|
return { start: start.toISOString(), end: new Date(start.getTime() + 86400000).toISOString() };
|
|
357
385
|
}
|
|
386
|
+
async function isFirstWorkflowPreflightFailureToday(workflowId, runId, now = new Date) {
|
|
387
|
+
const bounds = utcDayBounds(now);
|
|
388
|
+
const row = await getDbClient2().get(`SELECT 1 AS found FROM workflow_runs
|
|
389
|
+
WHERE workflowId = ? AND id != ? AND status = 'failed'
|
|
390
|
+
AND error LIKE 'needs_setup:%'
|
|
391
|
+
AND startedAt >= ? AND startedAt < ?
|
|
392
|
+
LIMIT 1`, [workflowId, runId, bounds.start, bounds.end]);
|
|
393
|
+
return row == null;
|
|
394
|
+
}
|
|
358
395
|
async function recordSchedulePreflightFailure(scheduleId, failureReason, now = new Date) {
|
|
359
396
|
const message = scrubSecrets(failureReason);
|
|
360
397
|
return await getDbClient2().transaction(async (tx) => {
|
|
@@ -375,7 +412,7 @@ async function recordWorkflowPreflightFailure(input) {
|
|
|
375
412
|
const bounds = utcDayBounds(now);
|
|
376
413
|
return await getDbClient2().transaction(async (tx) => {
|
|
377
414
|
const existing = await tx.get(`SELECT id FROM workflow_runs
|
|
378
|
-
WHERE workflowId = ? AND status = 'failed'
|
|
415
|
+
WHERE workflowId = ? AND status = 'failed' AND parentStepId IS NULL
|
|
379
416
|
AND error LIKE 'needs_setup:%'
|
|
380
417
|
AND startedAt >= ? AND startedAt < ?
|
|
381
418
|
ORDER BY startedAt ASC LIMIT 1`, [input.workflowId, bounds.start, bounds.end]);
|
|
@@ -397,7 +434,10 @@ async function recordWorkflowPreflightFailure(input) {
|
|
|
397
434
|
timestamp,
|
|
398
435
|
timestamp
|
|
399
436
|
]);
|
|
400
|
-
return {
|
|
437
|
+
return {
|
|
438
|
+
runId,
|
|
439
|
+
recorded: await isFirstWorkflowPreflightFailureToday(input.workflowId, runId, now)
|
|
440
|
+
};
|
|
401
441
|
});
|
|
402
442
|
}
|
|
403
443
|
|
|
@@ -450,14 +490,628 @@ class BaseExecutor {
|
|
|
450
490
|
|
|
451
491
|
// src/workflows/executors/human-in-the-loop.ts
|
|
452
492
|
init_zod();
|
|
453
|
-
|
|
493
|
+
|
|
494
|
+
// src/be/approval-config-question.ts
|
|
495
|
+
init_zod();
|
|
496
|
+
init_db3();
|
|
497
|
+
|
|
498
|
+
// src/be/multi-runtime.ts
|
|
499
|
+
init_secret_scrubber();
|
|
500
|
+
init_db3();
|
|
501
|
+
init_users();
|
|
502
|
+
var AGENT_MAX_TASKS_CONFIG_KEY = "AGENT_MAX_TASKS";
|
|
503
|
+
function parseAgentMaxTasksValue(raw) {
|
|
504
|
+
const str = raw.trim();
|
|
505
|
+
if (!/^\d+$/.test(str))
|
|
506
|
+
return null;
|
|
507
|
+
const value = Number(str);
|
|
508
|
+
return value >= 1 && value <= 100 ? value : null;
|
|
509
|
+
}
|
|
510
|
+
function defaultMaxTasksForAgent(agent) {
|
|
511
|
+
return agent.isLead ? 2 : 1;
|
|
512
|
+
}
|
|
513
|
+
async function getAgentMaxTasksConfig(agentId) {
|
|
514
|
+
return (await getSwarmConfigs2({ scope: "agent", scopeId: agentId, key: AGENT_MAX_TASKS_CONFIG_KEY }))[0] ?? null;
|
|
515
|
+
}
|
|
516
|
+
async function reconcileAgentMaxTasksPolicy(agentId) {
|
|
517
|
+
const agent = await getAgentById2(agentId);
|
|
518
|
+
if (!agent)
|
|
519
|
+
return;
|
|
520
|
+
const existing = await getAgentMaxTasksConfig(agentId);
|
|
521
|
+
if (existing) {
|
|
522
|
+
const authoritative = parseAgentMaxTasksValue(existing.value);
|
|
523
|
+
if (authoritative !== null && authoritative !== (agent.maxTasks ?? 1)) {
|
|
524
|
+
await updateAgentMaxTasks2(agentId, authoritative);
|
|
525
|
+
}
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
await upsertSwarmConfig2({
|
|
529
|
+
scope: "agent",
|
|
530
|
+
scopeId: agentId,
|
|
531
|
+
key: AGENT_MAX_TASKS_CONFIG_KEY,
|
|
532
|
+
value: String(agent.maxTasks ?? defaultMaxTasksForAgent(agent)),
|
|
533
|
+
description: "Logical agent max-tasks policy (seeded from agents.maxTasks)"
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
async function resetAgentMaxTasksMirror(agentId) {
|
|
537
|
+
const agent = await getAgentById2(agentId);
|
|
538
|
+
if (!agent)
|
|
539
|
+
return;
|
|
540
|
+
await updateAgentMaxTasks2(agentId, defaultMaxTasksForAgent(agent));
|
|
541
|
+
}
|
|
542
|
+
async function upsertSwarmConfigWithPolicyMirror(data) {
|
|
543
|
+
const scopeId = data.scope === "agent" ? data.scopeId : null;
|
|
544
|
+
if (!scopeId || data.key !== AGENT_MAX_TASKS_CONFIG_KEY) {
|
|
545
|
+
return upsertSwarmConfig2(data);
|
|
546
|
+
}
|
|
547
|
+
return await getDbClient2().transaction(async () => {
|
|
548
|
+
const row = await upsertSwarmConfig2(data);
|
|
549
|
+
const value = parseAgentMaxTasksValue(data.value);
|
|
550
|
+
if (value !== null)
|
|
551
|
+
await updateAgentMaxTasks2(scopeId, value);
|
|
552
|
+
return row;
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
function rowToRuntimeInstance(row) {
|
|
556
|
+
let metadata = null;
|
|
557
|
+
if (row.metadata) {
|
|
558
|
+
try {
|
|
559
|
+
metadata = JSON.parse(row.metadata);
|
|
560
|
+
} catch {
|
|
561
|
+
metadata = null;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return {
|
|
565
|
+
id: row.id,
|
|
566
|
+
agentId: row.agent_id,
|
|
567
|
+
poolId: row.pool_id ?? null,
|
|
568
|
+
controllerId: row.controller_id ?? null,
|
|
569
|
+
externalHandle: row.external_handle ? JSON.parse(row.external_handle) : null,
|
|
570
|
+
provisionAttempts: row.provision_attempts ?? 0,
|
|
571
|
+
provisionError: row.provision_error ?? null,
|
|
572
|
+
provisionRequestedAt: row.provision_requested_at ?? null,
|
|
573
|
+
draining: row.draining === 1,
|
|
574
|
+
idleSince: row.idle_since ?? null,
|
|
575
|
+
leaseExpiresAt: row.lease_expires_at ?? null,
|
|
576
|
+
taskId: row.task_id ?? null,
|
|
577
|
+
status: row.status,
|
|
578
|
+
reportedSlots: row.reported_slots,
|
|
579
|
+
credentialReady: row.credential_ready === null ? null : row.credential_ready === 1,
|
|
580
|
+
metadata,
|
|
581
|
+
lastSeenAt: row.last_seen_at,
|
|
582
|
+
createdAt: row.created_at,
|
|
583
|
+
updatedAt: row.updated_at
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
function runtimeStaleThresholdMinutes() {
|
|
587
|
+
return Number(process.env.RUNTIME_STALE_THRESHOLD_MIN) || 5;
|
|
588
|
+
}
|
|
589
|
+
function runtimeLivenessCutoff() {
|
|
590
|
+
return new Date(Date.now() - runtimeStaleThresholdMinutes() * 60 * 1000).toISOString();
|
|
591
|
+
}
|
|
592
|
+
async function upsertRuntimeInstance(instance) {
|
|
593
|
+
const metadataJson = instance.metadata ? JSON.stringify(instance.metadata) : null;
|
|
594
|
+
const row = await getDbClient2().transaction(async (tx) => {
|
|
595
|
+
const provisioning = await tx.get("SELECT id FROM runtime_instances WHERE id = ? AND status = 'provisioning' AND agent_id = ?", [instance.id, instance.agentId]);
|
|
596
|
+
const row = await tx.get(`INSERT INTO runtime_instances (id, agent_id, status, reported_slots, metadata)
|
|
597
|
+
VALUES (?, ?, 'active', ?, ?)
|
|
598
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
599
|
+
status = 'active',
|
|
600
|
+
reported_slots = excluded.reported_slots,
|
|
601
|
+
metadata = COALESCE(excluded.metadata, runtime_instances.metadata),
|
|
602
|
+
last_seen_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
|
|
603
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
604
|
+
WHERE runtime_instances.agent_id = excluded.agent_id
|
|
605
|
+
RETURNING *`, [instance.id, instance.agentId, instance.reportedSlots, metadataJson]);
|
|
606
|
+
if (row?.pool_id && provisioning) {
|
|
607
|
+
await tx.run("UPDATE agents SET runtime_provision_attempts = 0, runtime_provision_retry_at = NULL WHERE id = ?", [row.agent_id]);
|
|
608
|
+
await tx.run("UPDATE runtime_pools SET consecutive_failures = 0, circuit_open_until = NULL WHERE id = ?", [row.pool_id]);
|
|
609
|
+
}
|
|
610
|
+
return row;
|
|
611
|
+
});
|
|
612
|
+
return row ? rowToRuntimeInstance(row) : null;
|
|
613
|
+
}
|
|
614
|
+
async function getRuntimeInstanceById(id) {
|
|
615
|
+
const row = await getDbClient2().get("SELECT * FROM runtime_instances WHERE id = ?", [id]);
|
|
616
|
+
return row ? rowToRuntimeInstance(row) : null;
|
|
617
|
+
}
|
|
618
|
+
async function insertProvisioningRuntime(instance) {
|
|
619
|
+
const row = await getDbClient2().get(`INSERT INTO runtime_instances (id, agent_id, pool_id, controller_id, status,
|
|
620
|
+
reported_slots, provision_requested_at)
|
|
621
|
+
VALUES (?, ?, ?, ?, 'provisioning', ?, ?) RETURNING *`, [
|
|
622
|
+
instance.id,
|
|
623
|
+
instance.agentId,
|
|
624
|
+
instance.poolId,
|
|
625
|
+
instance.controllerId,
|
|
626
|
+
instance.reportedSlots ?? 1,
|
|
627
|
+
new Date().toISOString()
|
|
628
|
+
]);
|
|
629
|
+
return rowToRuntimeInstance(row);
|
|
630
|
+
}
|
|
631
|
+
async function markRuntimeTerminated(id, reason) {
|
|
632
|
+
await getDbClient2().transaction(async () => {
|
|
633
|
+
const runtime = await getRuntimeInstanceById(id);
|
|
634
|
+
if (!runtime)
|
|
635
|
+
return;
|
|
636
|
+
await markRuntimeInstanceOffline(id, runtime.agentId);
|
|
637
|
+
if (runtime.status === "provisioning") {
|
|
638
|
+
await getDbClient2().run("UPDATE runtime_instances SET provision_error = ? WHERE id = ?", [
|
|
639
|
+
scrubSecrets(reason),
|
|
640
|
+
id
|
|
641
|
+
]);
|
|
642
|
+
}
|
|
643
|
+
await reconcileAgentStatusFromRuntimes(runtime.agentId);
|
|
644
|
+
await revokeRuntimeTokensForRuntime(id);
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
async function touchRuntimeInstance(id, agentId) {
|
|
648
|
+
const result = await getDbClient2().run(`UPDATE runtime_instances
|
|
649
|
+
SET last_seen_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
|
|
650
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
651
|
+
WHERE id = ? AND agent_id = ? AND status = 'active' AND last_seen_at >= ?`, [id, agentId, runtimeLivenessCutoff()]);
|
|
652
|
+
return result.changes > 0;
|
|
653
|
+
}
|
|
654
|
+
async function markRuntimeInstanceOffline(id, agentId) {
|
|
655
|
+
const row = await getDbClient2().get(`UPDATE runtime_instances
|
|
656
|
+
SET status = 'offline', updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
657
|
+
WHERE id = ? AND agent_id = ? RETURNING *`, [id, agentId]);
|
|
658
|
+
return row ? rowToRuntimeInstance(row) : null;
|
|
659
|
+
}
|
|
660
|
+
async function countActiveRuntimeInstancesForAgent(agentId) {
|
|
661
|
+
const result = await getDbClient2().get(`SELECT COUNT(*) as count FROM runtime_instances
|
|
662
|
+
WHERE agent_id = ? AND status = 'active' AND last_seen_at >= ?`, [agentId, runtimeLivenessCutoff()]);
|
|
663
|
+
return result?.count ?? 0;
|
|
664
|
+
}
|
|
665
|
+
async function listRuntimeInstancesForAgent(agentId) {
|
|
666
|
+
const rows = await getDbClient2().query(`SELECT *, (status = 'active' AND last_seen_at >= ?) AS is_live
|
|
667
|
+
FROM runtime_instances
|
|
668
|
+
WHERE agent_id = ?
|
|
669
|
+
ORDER BY created_at ASC, id ASC`, [runtimeLivenessCutoff(), agentId]);
|
|
670
|
+
return rows.map((row) => ({ ...rowToRuntimeInstance(row), isLive: row.is_live === 1 }));
|
|
671
|
+
}
|
|
672
|
+
async function setRuntimeCredentialReady(id, agentId, ready) {
|
|
673
|
+
const result = await getDbClient2().run(`UPDATE runtime_instances
|
|
674
|
+
SET credential_ready = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
675
|
+
WHERE id = ? AND agent_id = ? AND status = 'active' AND last_seen_at >= ?`, [ready ? 1 : 0, id, agentId, runtimeLivenessCutoff()]);
|
|
676
|
+
return result.changes > 0;
|
|
677
|
+
}
|
|
678
|
+
async function hasReadyLiveRuntime(agentId) {
|
|
679
|
+
const row = await getDbClient2().get(`SELECT COUNT(*) c FROM runtime_instances
|
|
680
|
+
WHERE agent_id = ? AND status = 'active' AND last_seen_at >= ?
|
|
681
|
+
AND (credential_ready IS NULL OR credential_ready = 1)`, [agentId, runtimeLivenessCutoff()]);
|
|
682
|
+
return (row?.c ?? 0) > 0;
|
|
683
|
+
}
|
|
684
|
+
async function reconcileAgentStatusFromRuntimes(agentId) {
|
|
685
|
+
await getDbClient2().transaction(async () => {
|
|
686
|
+
const agent = await getAgentById2(agentId);
|
|
687
|
+
if (!agent)
|
|
688
|
+
return;
|
|
689
|
+
if (await countActiveRuntimeInstancesForAgent(agentId) === 0) {
|
|
690
|
+
if (agent.status !== "offline")
|
|
691
|
+
await updateAgentStatus2(agentId, "offline");
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (!await hasReadyLiveRuntime(agentId)) {
|
|
695
|
+
if (agent.status !== "waiting_for_credentials") {
|
|
696
|
+
await updateAgentStatus2(agentId, "waiting_for_credentials");
|
|
697
|
+
}
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
const next = await getActiveTaskCount2(agentId) > 0 ? "busy" : "idle";
|
|
701
|
+
if (agent.status !== next)
|
|
702
|
+
await updateAgentStatus2(agentId, next);
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
async function agentsWithLiveRuntime() {
|
|
706
|
+
const rows = await getDbClient2().query(`SELECT DISTINCT agent_id FROM runtime_instances
|
|
707
|
+
WHERE status = 'active' AND last_seen_at >= ?`, [runtimeLivenessCutoff()]);
|
|
708
|
+
return new Set(rows.map((r) => r.agent_id));
|
|
709
|
+
}
|
|
710
|
+
async function expireStaleRuntimeInstances() {
|
|
711
|
+
if (!isMultiRuntimeEnabled()) {
|
|
712
|
+
return { expired: 0, agentsOffline: 0, pruned: 0 };
|
|
713
|
+
}
|
|
714
|
+
return await getDbClient2().transaction(async (tx) => {
|
|
715
|
+
const cutoff = runtimeLivenessCutoff();
|
|
716
|
+
const stale = await tx.query(`SELECT id, agent_id FROM runtime_instances
|
|
717
|
+
WHERE status = 'active' AND last_seen_at < ? AND pool_id IS NULL`, [cutoff]);
|
|
718
|
+
const pruned = (await tx.run("DELETE FROM runtime_instances WHERE last_seen_at < ? AND status != 'provisioning' AND (pool_id IS NULL OR status = 'offline')", [cutoff])).changes;
|
|
719
|
+
let agentsOffline = 0;
|
|
720
|
+
for (const agentId of new Set(stale.map((r) => r.agent_id))) {
|
|
721
|
+
await reconcileAgentStatusFromRuntimes(agentId);
|
|
722
|
+
if ((await getAgentById2(agentId))?.status === "offline")
|
|
723
|
+
agentsOffline++;
|
|
724
|
+
}
|
|
725
|
+
return { expired: stale.length, agentsOffline, pruned };
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
async function listRuntimeInstancesForPool(poolId) {
|
|
729
|
+
return (await getDbClient2().query("SELECT * FROM runtime_instances WHERE pool_id = ? ORDER BY created_at", [poolId])).map(rowToRuntimeInstance);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// src/be/approval-config-question.ts
|
|
733
|
+
init_swarm_config_guard();
|
|
734
|
+
var ConfigQuestionSettingsSchema = object2({
|
|
735
|
+
key: string2().min(1).max(255).regex(/^\S+$/, "must not contain whitespace").describe("The swarm config key to set, e.g. BITBUCKET_TOKEN"),
|
|
736
|
+
scope: _enum2(["global", "agent", "repo"]).describe("Config scope the value is written to"),
|
|
737
|
+
scopeId: string2().min(1).optional().describe("Agent or repo id for agent/repo scope. From request-human-input, agent scope defaults to the calling agent; pass it explicitly when asking on behalf of another agent."),
|
|
738
|
+
isSecret: boolean().optional().describe("Store encrypted and never show the value (default true). An existing secret row always stays secret."),
|
|
739
|
+
helpUrl: string2().url().refine((url) => /^https?:\/\//i.test(url), "must be an http(s) URL").optional().describe("Link shown next to the field, e.g. where to create the token"),
|
|
740
|
+
options: array2(object2({
|
|
741
|
+
value: string2(),
|
|
742
|
+
label: string2(),
|
|
743
|
+
description: string2().optional()
|
|
744
|
+
})).optional().describe("Allowed values for a non-secret key, rendered as a choice"),
|
|
745
|
+
defaultValue: string2().optional().describe("Prefilled value for a non-secret key")
|
|
746
|
+
});
|
|
747
|
+
var ConfigAnswerSchema = discriminatedUnion("action", [
|
|
748
|
+
object2({ action: literal2("set"), value: string2() }),
|
|
749
|
+
object2({ action: literal2("keep") }),
|
|
750
|
+
object2({ action: literal2("skip") })
|
|
751
|
+
]);
|
|
752
|
+
var ConfigResultSchema = object2({
|
|
753
|
+
key: string2(),
|
|
754
|
+
scope: _enum2(["global", "agent", "repo"]),
|
|
755
|
+
scopeId: string2().nullable(),
|
|
756
|
+
isSecret: boolean(),
|
|
757
|
+
status: _enum2(["set", "kept", "skipped"]),
|
|
758
|
+
updatedAt: string2().nullable(),
|
|
759
|
+
value: string2().optional()
|
|
760
|
+
});
|
|
761
|
+
var ConfigStateSchema = object2({
|
|
762
|
+
exists: boolean(),
|
|
763
|
+
isSecret: boolean(),
|
|
764
|
+
updatedAt: string2().nullable(),
|
|
765
|
+
value: string2().optional().describe("Current value; present only for non-secret rows")
|
|
766
|
+
});
|
|
767
|
+
async function readConfigRow(settings) {
|
|
768
|
+
const scopeId = settings.scope === "global" ? null : settings.scopeId ?? null;
|
|
769
|
+
const row = await getDbClient2().get(`SELECT isSecret, lastUpdatedAt, envPath, description,
|
|
770
|
+
CASE WHEN isSecret = 1 THEN NULL ELSE value END AS plainValue
|
|
771
|
+
FROM swarm_config
|
|
772
|
+
WHERE scope = ? AND ${scopeId === null ? "scopeId IS NULL" : "scopeId = ?"} AND key = ?`, scopeId === null ? [settings.scope, settings.key] : [settings.scope, scopeId, settings.key]);
|
|
773
|
+
if (!row)
|
|
774
|
+
return null;
|
|
775
|
+
const isSecret = row.isSecret === 1 || isApiOnlyConfigKey(settings.key);
|
|
776
|
+
return {
|
|
777
|
+
isSecret,
|
|
778
|
+
updatedAt: row.lastUpdatedAt,
|
|
779
|
+
envPath: row.envPath,
|
|
780
|
+
description: row.description,
|
|
781
|
+
...isSecret || row.plainValue === null ? {} : { value: row.plainValue }
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
async function readConfigState(settings) {
|
|
785
|
+
const row = await readConfigRow(settings);
|
|
786
|
+
if (!row)
|
|
787
|
+
return { exists: false, isSecret: settings.isSecret !== false, updatedAt: null };
|
|
788
|
+
const isSecret = row.isSecret || settings.isSecret !== false;
|
|
789
|
+
return {
|
|
790
|
+
exists: true,
|
|
791
|
+
isSecret,
|
|
792
|
+
updatedAt: row.updatedAt,
|
|
793
|
+
...!isSecret && row.value !== undefined ? { value: row.value } : {}
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
async function normalizeConfigQuestion(question, ctx) {
|
|
797
|
+
const fail = (reason) => ({
|
|
798
|
+
ok: false,
|
|
799
|
+
error: `config question "${question.id}": ${reason}`
|
|
800
|
+
});
|
|
801
|
+
const parsed = ConfigQuestionSettingsSchema.safeParse(question.config);
|
|
802
|
+
if (!parsed.success) {
|
|
803
|
+
const issue = parsed.error.issues[0];
|
|
804
|
+
const path = issue?.path.length ? `config.${issue.path.map(String).join(".")}` : "config";
|
|
805
|
+
return fail(`${path}: ${issue?.message ?? "invalid"}`);
|
|
806
|
+
}
|
|
807
|
+
if (ctx.approvers.policy !== "any") {
|
|
808
|
+
return fail('a config question needs approvers.policy "any" (a config value has one author)');
|
|
809
|
+
}
|
|
810
|
+
const settings = { ...parsed.data };
|
|
811
|
+
if (settings.scope === "agent" && !settings.scopeId && ctx.callerAgentId) {
|
|
812
|
+
settings.scopeId = ctx.callerAgentId;
|
|
813
|
+
}
|
|
814
|
+
if (settings.scope === "global" && settings.scopeId) {
|
|
815
|
+
return fail("global scope must not have scopeId");
|
|
816
|
+
}
|
|
817
|
+
if (settings.scope !== "global" && !settings.scopeId) {
|
|
818
|
+
return fail(`${settings.scope} scope requires scopeId`);
|
|
819
|
+
}
|
|
820
|
+
if (isReservedConfigKey(settings.key))
|
|
821
|
+
return fail(reservedKeyError(settings.key).message);
|
|
822
|
+
if (isInternalConfigKey(settings.key))
|
|
823
|
+
return fail(internalConfigKeyError(settings.key).message);
|
|
824
|
+
const row = await readConfigRow(settings);
|
|
825
|
+
const isSecret = settings.isSecret !== false || row?.isSecret === true || isApiOnlyConfigKey(settings.key);
|
|
826
|
+
if (isSecret && (settings.options || settings.defaultValue !== undefined)) {
|
|
827
|
+
return fail("options and defaultValue are only allowed when isSecret is false (and the key is not already stored as a secret)");
|
|
828
|
+
}
|
|
829
|
+
if (settings.options?.length === 0)
|
|
830
|
+
return fail("options must not be empty");
|
|
831
|
+
if (settings.options && settings.defaultValue !== undefined && !settings.options.some((option) => option.value === settings.defaultValue)) {
|
|
832
|
+
return fail("defaultValue must be one of options");
|
|
833
|
+
}
|
|
834
|
+
const label = question.label?.trim() ? question.label : settings.key;
|
|
835
|
+
return {
|
|
836
|
+
ok: true,
|
|
837
|
+
question: {
|
|
838
|
+
...question,
|
|
839
|
+
label,
|
|
840
|
+
required: question.required ?? true,
|
|
841
|
+
config: { ...settings, isSecret }
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
async function normalizeApprovalQuestions(questions, ctx) {
|
|
846
|
+
const out = [];
|
|
847
|
+
for (const question of questions) {
|
|
848
|
+
if (question.type !== "config") {
|
|
849
|
+
if (question.config !== undefined) {
|
|
850
|
+
return {
|
|
851
|
+
ok: false,
|
|
852
|
+
error: `question "${question.id}": the config block is only valid on type "config"`
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
if (typeof question.label !== "string") {
|
|
856
|
+
return { ok: false, error: `question "${question.id}": label is required` };
|
|
857
|
+
}
|
|
858
|
+
out.push({ ...question, label: question.label });
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
const normalized = await normalizeConfigQuestion(question, ctx);
|
|
862
|
+
if (!normalized.ok)
|
|
863
|
+
return normalized;
|
|
864
|
+
out.push(normalized.question);
|
|
865
|
+
}
|
|
866
|
+
return { ok: true, questions: out };
|
|
867
|
+
}
|
|
868
|
+
function isConfigQuestion(question) {
|
|
869
|
+
return question.type === "config" && typeof question.config === "object" && !!question.config;
|
|
870
|
+
}
|
|
871
|
+
function parseConfigAnswer(question, raw, state) {
|
|
872
|
+
const fail = (reason) => ({
|
|
873
|
+
ok: false,
|
|
874
|
+
error: `config question "${question.id}": ${reason}`
|
|
875
|
+
});
|
|
876
|
+
const required = question.required !== false;
|
|
877
|
+
if (raw === undefined || raw === null) {
|
|
878
|
+
return required ? fail("an answer is required") : { ok: true, answer: { action: "skip" } };
|
|
879
|
+
}
|
|
880
|
+
const parsed = ConfigAnswerSchema.safeParse(raw);
|
|
881
|
+
if (!parsed.success) {
|
|
882
|
+
return fail('answer must be { action: "set", value } , { action: "keep" } or { action: "skip" }');
|
|
883
|
+
}
|
|
884
|
+
const answer = parsed.data;
|
|
885
|
+
if (answer.action === "keep") {
|
|
886
|
+
return state.exists ? { ok: true, answer } : fail("cannot keep a key that is not set");
|
|
887
|
+
}
|
|
888
|
+
if (answer.action === "skip") {
|
|
889
|
+
return required ? fail("a required question cannot be skipped") : { ok: true, answer };
|
|
890
|
+
}
|
|
891
|
+
if (answer.value.trim().length === 0)
|
|
892
|
+
return fail("value must not be empty");
|
|
893
|
+
const { config } = question;
|
|
894
|
+
const isSecret = config.isSecret || state.isSecret;
|
|
895
|
+
if (!isSecret && config.options && !config.options.some((o) => o.value === answer.value)) {
|
|
896
|
+
return fail("value must be one of options");
|
|
897
|
+
}
|
|
898
|
+
const error = validateConfigEntry({
|
|
899
|
+
scope: config.scope,
|
|
900
|
+
scopeId: config.scopeId ?? null,
|
|
901
|
+
key: config.key,
|
|
902
|
+
value: answer.value
|
|
903
|
+
});
|
|
904
|
+
if (error)
|
|
905
|
+
return fail(isSecret ? `value rejected by ${config.key} validation` : error);
|
|
906
|
+
return { ok: true, answer };
|
|
907
|
+
}
|
|
908
|
+
function result(question, status, isSecret, updatedAt, value) {
|
|
909
|
+
const { key, scope, scopeId } = question.config;
|
|
910
|
+
return {
|
|
911
|
+
key,
|
|
912
|
+
scope,
|
|
913
|
+
scopeId: scope === "global" ? null : scopeId ?? null,
|
|
914
|
+
isSecret,
|
|
915
|
+
status,
|
|
916
|
+
updatedAt,
|
|
917
|
+
...!isSecret && value !== undefined ? { value } : {}
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
function skippedConfigResult(question) {
|
|
921
|
+
return result(question, "skipped", question.config.isSecret, null);
|
|
922
|
+
}
|
|
923
|
+
async function keptConfigResult(question) {
|
|
924
|
+
const row = await readConfigRow(question.config);
|
|
925
|
+
if (!row)
|
|
926
|
+
return null;
|
|
927
|
+
const isSecret = question.config.isSecret || row.isSecret;
|
|
928
|
+
return result(question, "kept", isSecret, row.updatedAt, row.value);
|
|
929
|
+
}
|
|
930
|
+
async function applyConfigAnswer(question, value) {
|
|
931
|
+
const { config } = question;
|
|
932
|
+
const existing = await readConfigRow(config);
|
|
933
|
+
const isSecret = config.isSecret || existing?.isSecret === true;
|
|
934
|
+
const written = await upsertSwarmConfigWithPolicyMirror({
|
|
935
|
+
scope: config.scope,
|
|
936
|
+
scopeId: config.scope === "global" ? null : config.scopeId ?? null,
|
|
937
|
+
key: config.key,
|
|
938
|
+
value,
|
|
939
|
+
isSecret,
|
|
940
|
+
envPath: existing?.envPath ?? null,
|
|
941
|
+
description: existing?.description ?? null
|
|
942
|
+
});
|
|
943
|
+
return result(question, "set", isSecret, written.lastUpdatedAt, value);
|
|
944
|
+
}
|
|
945
|
+
function formatConfigResult(value) {
|
|
946
|
+
const parsed = ConfigResultSchema.safeParse(value);
|
|
947
|
+
if (!parsed.success)
|
|
948
|
+
return null;
|
|
949
|
+
const r = parsed.data;
|
|
950
|
+
const where = `${r.scope}${r.scopeId ? `:${r.scopeId}` : ""}`;
|
|
951
|
+
const updated = r.updatedAt ? `, updated ${r.updatedAt}` : "";
|
|
952
|
+
const shown = !r.isSecret && r.value !== undefined ? ` = ${r.value}` : "";
|
|
953
|
+
return `${r.key} (${where}, ${r.isSecret ? "secret" : "plain"}): ${r.status}${updated}${shown}`;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// src/workflows/executors/human-in-the-loop.ts
|
|
454
957
|
init_secret_scrubber();
|
|
455
958
|
|
|
456
959
|
// src/workflows/approval-notifications.ts
|
|
457
960
|
init_db3();
|
|
458
961
|
init_resolver();
|
|
962
|
+
|
|
963
|
+
// src/slack/dm.ts
|
|
964
|
+
function isSlackUserId(id) {
|
|
965
|
+
return /^[UW][A-Z0-9]+$/.test(id);
|
|
966
|
+
}
|
|
967
|
+
async function resolveSlackConversationId(client, channelId) {
|
|
968
|
+
if (!isSlackUserId(channelId))
|
|
969
|
+
return channelId;
|
|
970
|
+
const result = await client.conversations.open({ users: channelId });
|
|
971
|
+
const dmId = result.channel?.id;
|
|
972
|
+
if (!dmId) {
|
|
973
|
+
throw new Error(`conversations.open returned no DM channel for user ${channelId}`);
|
|
974
|
+
}
|
|
975
|
+
return dmId;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// src/workflows/approval-notifications.ts
|
|
459
979
|
init_types();
|
|
980
|
+
init_constants2();
|
|
460
981
|
init_secret_scrubber();
|
|
982
|
+
var SLACK_SECTION_TEXT_LIMIT = 3000;
|
|
983
|
+
function escapeSlackMrkdwn(text) {
|
|
984
|
+
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
985
|
+
}
|
|
986
|
+
function buildSlackQuestionsSummary(questions, timeoutText = "") {
|
|
987
|
+
const header = `*Questions:*
|
|
988
|
+
`;
|
|
989
|
+
const lines = questions.map((q) => `• ${escapeSlackMrkdwn((q.label ?? "").replace(/\s+/g, " ").trim())}${q.type === "config" ? " (set on dashboard)" : ""}`);
|
|
990
|
+
const fits = (shown, omitted) => {
|
|
991
|
+
const tail = omitted > 0 ? `
|
|
992
|
+
_…and ${omitted} more. Answer all of them on the dashboard._` : "";
|
|
993
|
+
const text = `${header}${shown.join(`
|
|
994
|
+
`)}${tail}${timeoutText}`;
|
|
995
|
+
return text.length <= SLACK_SECTION_TEXT_LIMIT ? text : null;
|
|
996
|
+
};
|
|
997
|
+
const full = fits(lines, 0);
|
|
998
|
+
if (full)
|
|
999
|
+
return full;
|
|
1000
|
+
for (let count = lines.length - 1;count >= 0; count--) {
|
|
1001
|
+
const text = fits(lines.slice(0, count), lines.length - count);
|
|
1002
|
+
if (text)
|
|
1003
|
+
return text;
|
|
1004
|
+
}
|
|
1005
|
+
return `${header}_${questions.length} questions. Answer them on the dashboard._`;
|
|
1006
|
+
}
|
|
1007
|
+
function formatTimeout(seconds) {
|
|
1008
|
+
if (seconds < 60)
|
|
1009
|
+
return `${seconds}s`;
|
|
1010
|
+
if (seconds < 3600)
|
|
1011
|
+
return `${Math.round(seconds / 60)}m`;
|
|
1012
|
+
const hours = Math.floor(seconds / 3600);
|
|
1013
|
+
const mins = Math.round(seconds % 3600 / 60);
|
|
1014
|
+
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
|
|
1015
|
+
}
|
|
1016
|
+
async function dispatchApprovalNotifications(input, db = { updateApprovalRequestNotifications: updateApprovalRequestNotifications2, getApprovalRequestById: getApprovalRequestById2 }) {
|
|
1017
|
+
const { requestId, title, questions, timeoutSeconds, notifications } = input;
|
|
1018
|
+
if (!notifications.length)
|
|
1019
|
+
return;
|
|
1020
|
+
const approvalUrl = `${getAppUrl()}/approval-requests/${requestId}`;
|
|
1021
|
+
const updatedChannels = [
|
|
1022
|
+
...notifications
|
|
1023
|
+
];
|
|
1024
|
+
let updated = false;
|
|
1025
|
+
for (let i = 0;i < notifications.length; i++) {
|
|
1026
|
+
const notification = notifications[i];
|
|
1027
|
+
if (notification.channel === "email") {
|
|
1028
|
+
console.warn(`[HITL] Email notifications not yet supported (target: ${notification.target})`);
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
if (notification.channel === "slack") {
|
|
1032
|
+
try {
|
|
1033
|
+
const slackApp = getSlackApp2();
|
|
1034
|
+
if (!slackApp) {
|
|
1035
|
+
console.warn("[HITL] Slack not initialized — cannot send notification");
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
if (isBlockExternalEnabled2()) {
|
|
1039
|
+
const conversationId = await resolveSlackConversationId(slackApp.client, notification.target);
|
|
1040
|
+
const blockReason = await getChannelBlockReason2(slackApp.client, conversationId);
|
|
1041
|
+
if (blockReason) {
|
|
1042
|
+
console.warn(`[HITL] ${externalChannelToolError2(conversationId, blockReason)}`);
|
|
1043
|
+
continue;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
const questionsText = buildSlackQuestionsSummary(questions);
|
|
1047
|
+
const timeoutCaption = timeoutSeconds !== undefined ? [
|
|
1048
|
+
{
|
|
1049
|
+
type: "context",
|
|
1050
|
+
elements: [
|
|
1051
|
+
{
|
|
1052
|
+
type: "mrkdwn",
|
|
1053
|
+
text: `⏱ Timeout: ${formatTimeout(timeoutSeconds)} — times out if not responded`
|
|
1054
|
+
}
|
|
1055
|
+
]
|
|
1056
|
+
}
|
|
1057
|
+
] : [];
|
|
1058
|
+
const blocks = [
|
|
1059
|
+
{
|
|
1060
|
+
type: "section",
|
|
1061
|
+
text: {
|
|
1062
|
+
type: "mrkdwn",
|
|
1063
|
+
text: `\uD83D\uDD14 *Approval Required: ${escapeSlackMrkdwn(title)}*`
|
|
1064
|
+
}
|
|
1065
|
+
},
|
|
1066
|
+
{
|
|
1067
|
+
type: "section",
|
|
1068
|
+
text: {
|
|
1069
|
+
type: "mrkdwn",
|
|
1070
|
+
text: questionsText
|
|
1071
|
+
}
|
|
1072
|
+
},
|
|
1073
|
+
{
|
|
1074
|
+
type: "actions",
|
|
1075
|
+
elements: [
|
|
1076
|
+
{
|
|
1077
|
+
type: "button",
|
|
1078
|
+
text: { type: "plain_text", text: "Review & Respond" },
|
|
1079
|
+
url: approvalUrl,
|
|
1080
|
+
style: "primary"
|
|
1081
|
+
}
|
|
1082
|
+
]
|
|
1083
|
+
},
|
|
1084
|
+
...timeoutCaption
|
|
1085
|
+
];
|
|
1086
|
+
const result = await slackApp.client.chat.postMessage({
|
|
1087
|
+
channel: notification.target,
|
|
1088
|
+
text: `Approval Required: ${escapeSlackMrkdwn(title)} — ${approvalUrl}`,
|
|
1089
|
+
unfurl_links: false,
|
|
1090
|
+
unfurl_media: false,
|
|
1091
|
+
blocks
|
|
1092
|
+
});
|
|
1093
|
+
if (result.ts) {
|
|
1094
|
+
updatedChannels[i] = { ...notification, messageTs: result.ts };
|
|
1095
|
+
updated = true;
|
|
1096
|
+
}
|
|
1097
|
+
console.log(`[HITL] Slack notification sent to ${notification.target} for request ${requestId}`);
|
|
1098
|
+
} catch (err) {
|
|
1099
|
+
console.error(`[HITL] Failed to send Slack notification to ${notification.target}:`, scrubSecrets(err instanceof Error ? err.message : String(err)));
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
if (updated) {
|
|
1104
|
+
try {
|
|
1105
|
+
await db.updateApprovalRequestNotifications(requestId, updatedChannels);
|
|
1106
|
+
const latest = await db.getApprovalRequestById(requestId);
|
|
1107
|
+
if (latest?.status === "cancelled") {
|
|
1108
|
+
await postApprovalCancellationUpdates([latest], latest.resolutionReason ?? "Workflow run cancelled");
|
|
1109
|
+
}
|
|
1110
|
+
} catch (err) {
|
|
1111
|
+
console.error("[HITL] Failed to update notification channels in DB:", scrubSecrets(err instanceof Error ? err.message : String(err)));
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
461
1115
|
async function postApprovalCancellationUpdates(approvals, reason, client = getSlackApp2()?.client) {
|
|
462
1116
|
if (!client)
|
|
463
1117
|
return;
|
|
@@ -530,6 +1184,8 @@ function formatResponses(questions, responses) {
|
|
|
530
1184
|
let answerText;
|
|
531
1185
|
if (answer == null) {
|
|
532
1186
|
answerText = "(no answer)";
|
|
1187
|
+
} else if (q.type === "config") {
|
|
1188
|
+
answerText = formatConfigResult(answer) ?? "(unreadable config result)";
|
|
533
1189
|
} else if (q.type === "approval") {
|
|
534
1190
|
const a = answer;
|
|
535
1191
|
answerText = a.approved ? "Approved" : "Rejected";
|
|
@@ -593,6 +1249,14 @@ var QuestionSchema = discriminatedUnion("type", [
|
|
|
593
1249
|
required: boolean().default(true),
|
|
594
1250
|
description: string2().optional(),
|
|
595
1251
|
defaultValue: boolean().optional()
|
|
1252
|
+
}),
|
|
1253
|
+
object2({
|
|
1254
|
+
id: string2(),
|
|
1255
|
+
type: literal2("config"),
|
|
1256
|
+
label: string2().optional(),
|
|
1257
|
+
required: boolean().default(true),
|
|
1258
|
+
description: string2().optional(),
|
|
1259
|
+
config: ConfigQuestionSettingsSchema
|
|
596
1260
|
})
|
|
597
1261
|
]);
|
|
598
1262
|
var ApproverConfigSchema = object2({
|
|
@@ -610,7 +1274,6 @@ var HITLTimeoutSchema = object2({
|
|
|
610
1274
|
});
|
|
611
1275
|
var DYNAMIC_QUESTIONS_TOKEN_RE = /^\{\{[^}]+\}\}$/;
|
|
612
1276
|
var MAX_HITL_QUESTIONS = 100;
|
|
613
|
-
var SLACK_SECTION_TEXT_LIMIT = 3000;
|
|
614
1277
|
var MAX_REPORTED_ISSUES = 5;
|
|
615
1278
|
var HITLConfigSchema = object2({
|
|
616
1279
|
title: string2(),
|
|
@@ -675,30 +1338,6 @@ function resolveHitlQuestions(value) {
|
|
|
675
1338
|
}
|
|
676
1339
|
return { ok: true, questions };
|
|
677
1340
|
}
|
|
678
|
-
function escapeSlackMrkdwn(text) {
|
|
679
|
-
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
680
|
-
}
|
|
681
|
-
function buildSlackQuestionsSummary(questions, timeoutText = "") {
|
|
682
|
-
const header = `*Questions:*
|
|
683
|
-
`;
|
|
684
|
-
const lines = questions.map((q) => `• ${escapeSlackMrkdwn(q.label.replace(/\s+/g, " ").trim())}`);
|
|
685
|
-
const fits = (shown, omitted) => {
|
|
686
|
-
const tail = omitted > 0 ? `
|
|
687
|
-
_…and ${omitted} more. Answer all of them on the dashboard._` : "";
|
|
688
|
-
const text = `${header}${shown.join(`
|
|
689
|
-
`)}${tail}${timeoutText}`;
|
|
690
|
-
return text.length <= SLACK_SECTION_TEXT_LIMIT ? text : null;
|
|
691
|
-
};
|
|
692
|
-
const full = fits(lines, 0);
|
|
693
|
-
if (full)
|
|
694
|
-
return full;
|
|
695
|
-
for (let count = lines.length - 1;count >= 0; count--) {
|
|
696
|
-
const text = fits(lines.slice(0, count), lines.length - count);
|
|
697
|
-
if (text)
|
|
698
|
-
return text;
|
|
699
|
-
}
|
|
700
|
-
return `${header}_${questions.length} questions. Answer them on the dashboard._`;
|
|
701
|
-
}
|
|
702
1341
|
|
|
703
1342
|
class HumanInTheLoopExecutor extends BaseExecutor {
|
|
704
1343
|
type = "human-in-the-loop";
|
|
@@ -709,7 +1348,17 @@ class HumanInTheLoopExecutor extends BaseExecutor {
|
|
|
709
1348
|
const resolved = resolveHitlQuestions(input.config?.questions);
|
|
710
1349
|
if (!resolved.ok)
|
|
711
1350
|
return { status: "failed", error: resolved.error };
|
|
712
|
-
|
|
1351
|
+
const approvers = input.config?.approvers;
|
|
1352
|
+
const normalized = await normalizeApprovalQuestions(resolved.questions, {
|
|
1353
|
+
approvers: { policy: approvers?.policy }
|
|
1354
|
+
});
|
|
1355
|
+
if (!normalized.ok) {
|
|
1356
|
+
return {
|
|
1357
|
+
status: "failed",
|
|
1358
|
+
error: `human-in-the-loop questions are invalid: ${normalized.error}`
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
return super.run({ ...input, config: { ...input.config, questions: normalized.questions } });
|
|
713
1362
|
}
|
|
714
1363
|
async execute(config, _context, meta) {
|
|
715
1364
|
const { db } = this.deps;
|
|
@@ -765,7 +1414,13 @@ class HumanInTheLoopExecutor extends BaseExecutor {
|
|
|
765
1414
|
};
|
|
766
1415
|
}
|
|
767
1416
|
if (config.notifications?.length) {
|
|
768
|
-
|
|
1417
|
+
dispatchApprovalNotifications({
|
|
1418
|
+
requestId,
|
|
1419
|
+
title: config.title,
|
|
1420
|
+
questions: config.questions,
|
|
1421
|
+
timeoutSeconds: config.timeout?.seconds,
|
|
1422
|
+
notifications: config.notifications
|
|
1423
|
+
}, db).catch((err) => {
|
|
769
1424
|
console.error("[HITL] Unexpected error dispatching notifications:", scrubSecrets(err instanceof Error ? err.message : String(err)));
|
|
770
1425
|
});
|
|
771
1426
|
}
|
|
@@ -776,105 +1431,6 @@ class HumanInTheLoopExecutor extends BaseExecutor {
|
|
|
776
1431
|
correlationId: requestId
|
|
777
1432
|
};
|
|
778
1433
|
}
|
|
779
|
-
async dispatchNotifications(requestId, config, db) {
|
|
780
|
-
if (!config.notifications?.length)
|
|
781
|
-
return;
|
|
782
|
-
const approvalUrl = `${getAppUrl()}/approval-requests/${requestId}`;
|
|
783
|
-
const updatedChannels = [...config.notifications];
|
|
784
|
-
let updated = false;
|
|
785
|
-
for (let i = 0;i < config.notifications.length; i++) {
|
|
786
|
-
const notification = config.notifications[i];
|
|
787
|
-
if (notification.channel === "email") {
|
|
788
|
-
console.warn(`[HITL] Email notifications not yet supported (target: ${notification.target})`);
|
|
789
|
-
continue;
|
|
790
|
-
}
|
|
791
|
-
if (notification.channel === "slack") {
|
|
792
|
-
try {
|
|
793
|
-
const { getSlackApp } = await import("./app-nndcvftq.js");
|
|
794
|
-
const slackApp = getSlackApp();
|
|
795
|
-
if (!slackApp) {
|
|
796
|
-
console.warn("[HITL] Slack not initialized — cannot send notification");
|
|
797
|
-
continue;
|
|
798
|
-
}
|
|
799
|
-
const questions = Array.isArray(config.questions) ? config.questions : [];
|
|
800
|
-
const questionsText = buildSlackQuestionsSummary(questions);
|
|
801
|
-
const timeoutCaption = config.timeout ? [
|
|
802
|
-
{
|
|
803
|
-
type: "context",
|
|
804
|
-
elements: [
|
|
805
|
-
{
|
|
806
|
-
type: "mrkdwn",
|
|
807
|
-
text: `⏱ Timeout: ${formatTimeout(config.timeout.seconds)} — times out if not responded`
|
|
808
|
-
}
|
|
809
|
-
]
|
|
810
|
-
}
|
|
811
|
-
] : [];
|
|
812
|
-
const blocks = [
|
|
813
|
-
{
|
|
814
|
-
type: "section",
|
|
815
|
-
text: {
|
|
816
|
-
type: "mrkdwn",
|
|
817
|
-
text: `\uD83D\uDD14 *Approval Required: ${config.title}*`
|
|
818
|
-
}
|
|
819
|
-
},
|
|
820
|
-
{
|
|
821
|
-
type: "section",
|
|
822
|
-
text: {
|
|
823
|
-
type: "mrkdwn",
|
|
824
|
-
text: questionsText
|
|
825
|
-
}
|
|
826
|
-
},
|
|
827
|
-
{
|
|
828
|
-
type: "actions",
|
|
829
|
-
elements: [
|
|
830
|
-
{
|
|
831
|
-
type: "button",
|
|
832
|
-
text: { type: "plain_text", text: "Review & Respond" },
|
|
833
|
-
url: approvalUrl,
|
|
834
|
-
style: "primary"
|
|
835
|
-
}
|
|
836
|
-
]
|
|
837
|
-
},
|
|
838
|
-
...timeoutCaption
|
|
839
|
-
];
|
|
840
|
-
const result = await slackApp.client.chat.postMessage({
|
|
841
|
-
channel: notification.target,
|
|
842
|
-
text: `Approval Required: ${config.title} — ${approvalUrl}`,
|
|
843
|
-
unfurl_links: false,
|
|
844
|
-
unfurl_media: false,
|
|
845
|
-
blocks
|
|
846
|
-
});
|
|
847
|
-
if (result.ts) {
|
|
848
|
-
updatedChannels[i] = { ...notification, messageTs: result.ts };
|
|
849
|
-
updated = true;
|
|
850
|
-
}
|
|
851
|
-
console.log(`[HITL] Slack notification sent to ${notification.target} for request ${requestId}`);
|
|
852
|
-
} catch (err) {
|
|
853
|
-
console.error(`[HITL] Failed to send Slack notification to ${notification.target}:`, scrubSecrets(err instanceof Error ? err.message : String(err)));
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
if (updated) {
|
|
858
|
-
try {
|
|
859
|
-
await db.updateApprovalRequestNotifications(requestId, updatedChannels);
|
|
860
|
-
const latest = await db.getApprovalRequestById(requestId);
|
|
861
|
-
if (latest?.status === "cancelled") {
|
|
862
|
-
await postApprovalCancellationUpdates([latest], latest.resolutionReason ?? "Workflow run cancelled");
|
|
863
|
-
}
|
|
864
|
-
} catch (err) {
|
|
865
|
-
console.error("[HITL] Failed to update notification channels in DB:", scrubSecrets(err instanceof Error ? err.message : String(err)));
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
function formatTimeout(seconds) {
|
|
871
|
-
if (seconds < 60)
|
|
872
|
-
return `${seconds}s`;
|
|
873
|
-
if (seconds < 3600)
|
|
874
|
-
return `${Math.round(seconds / 60)}m`;
|
|
875
|
-
const hours = Math.floor(seconds / 3600);
|
|
876
|
-
const mins = Math.round(seconds % 3600 / 60);
|
|
877
|
-
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
|
|
878
1434
|
}
|
|
879
1435
|
|
|
880
1436
|
// src/workflows/executors/system-one-extraction.ts
|
|
@@ -2888,8 +3444,18 @@ init_db3();
|
|
|
2888
3444
|
|
|
2889
3445
|
// src/workflows/checkpoint.ts
|
|
2890
3446
|
init_db3();
|
|
2891
|
-
async function
|
|
2892
|
-
await getDbClient2().
|
|
3447
|
+
async function holdsFence(runId, stepId, fence) {
|
|
3448
|
+
const row = await getDbClient2().get(`SELECT 1 AS found
|
|
3449
|
+
FROM workflow_run_steps s
|
|
3450
|
+
JOIN workflow_runs r ON r.id = s.runId
|
|
3451
|
+
WHERE s.id = ? AND s.runId = ? AND s.status = 'running' AND s.retryCount = ?
|
|
3452
|
+
AND r.status IN ('running', 'waiting')`, [stepId, runId, fence.retryCount]);
|
|
3453
|
+
return row != null;
|
|
3454
|
+
}
|
|
3455
|
+
async function checkpointStep(runId, stepId, nodeId, result, ctx, options = {}) {
|
|
3456
|
+
return getDbClient2().transaction(async () => {
|
|
3457
|
+
if (options.fence && !await holdsFence(runId, stepId, options.fence))
|
|
3458
|
+
return false;
|
|
2893
3459
|
await updateWorkflowRunStep2(stepId, {
|
|
2894
3460
|
status: "completed",
|
|
2895
3461
|
output: result.output,
|
|
@@ -2900,42 +3466,48 @@ async function checkpointStep(runId, stepId, nodeId, result, ctx) {
|
|
|
2900
3466
|
await updateWorkflowRun2(runId, {
|
|
2901
3467
|
context: ctx
|
|
2902
3468
|
});
|
|
3469
|
+
return true;
|
|
2903
3470
|
});
|
|
2904
3471
|
}
|
|
2905
3472
|
async function checkpointStepFailure(runId, stepId, error, retryCount, retryPolicy, options) {
|
|
2906
3473
|
const now = new Date().toISOString();
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
3474
|
+
return getDbClient2().transaction(async () => {
|
|
3475
|
+
if (options?.fence && !await holdsFence(runId, stepId, options.fence)) {
|
|
3476
|
+
return { shouldRetry: false, stale: true };
|
|
3477
|
+
}
|
|
3478
|
+
if (retryPolicy && retryCount < retryPolicy.maxRetries) {
|
|
3479
|
+
const delay = calculateBackoff(retryPolicy, retryCount);
|
|
3480
|
+
const nextRetryAt = new Date(Date.now() + delay).toISOString();
|
|
3481
|
+
await updateWorkflowRunStep2(stepId, {
|
|
3482
|
+
status: "failed",
|
|
3483
|
+
error,
|
|
3484
|
+
output: options?.output,
|
|
3485
|
+
retryCount: retryCount + 1,
|
|
3486
|
+
maxRetries: retryPolicy.maxRetries,
|
|
3487
|
+
nextRetryAt
|
|
3488
|
+
});
|
|
3489
|
+
return { shouldRetry: true };
|
|
3490
|
+
}
|
|
2910
3491
|
await updateWorkflowRunStep2(stepId, {
|
|
2911
3492
|
status: "failed",
|
|
2912
3493
|
error,
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
nextRetryAt
|
|
3494
|
+
output: options?.output,
|
|
3495
|
+
finishedAt: now,
|
|
3496
|
+
nextRetryAt: null
|
|
2916
3497
|
});
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
await updateWorkflowRunStep2(stepId, {
|
|
2920
|
-
status: "failed",
|
|
2921
|
-
error,
|
|
2922
|
-
finishedAt: now,
|
|
2923
|
-
nextRetryAt: null
|
|
2924
|
-
});
|
|
2925
|
-
const markRunFailed = options?.markRunFailed ?? true;
|
|
2926
|
-
if (markRunFailed) {
|
|
2927
|
-
await getDbClient2().transaction(async () => {
|
|
3498
|
+
const markRunFailed = options?.markRunFailed ?? true;
|
|
3499
|
+
if (markRunFailed) {
|
|
2928
3500
|
const run = await getWorkflowRun2(runId);
|
|
2929
|
-
if (run?.status
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
}
|
|
2936
|
-
}
|
|
2937
|
-
|
|
2938
|
-
|
|
3501
|
+
if (run?.status === "running" || run?.status === "waiting") {
|
|
3502
|
+
await updateWorkflowRun2(runId, {
|
|
3503
|
+
status: "failed",
|
|
3504
|
+
error: `Step failed: ${error}`,
|
|
3505
|
+
finishedAt: now
|
|
3506
|
+
});
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
return { shouldRetry: false };
|
|
3510
|
+
});
|
|
2939
3511
|
}
|
|
2940
3512
|
async function checkpointStepWaiting(runId, stepId, ctx, options = {}) {
|
|
2941
3513
|
return getDbClient2().transaction(async () => {
|
|
@@ -2982,6 +3554,7 @@ var FOREACH_ITEMS_WAIT_FOR = "foreach.items";
|
|
|
2982
3554
|
|
|
2983
3555
|
// src/workflows/engine.ts
|
|
2984
3556
|
init_db3();
|
|
3557
|
+
init_workflow_runs();
|
|
2985
3558
|
init_workflow_replay();
|
|
2986
3559
|
init_telemetry();
|
|
2987
3560
|
|
|
@@ -3369,7 +3942,7 @@ async function resolveRenderedWorkflowInputs(input) {
|
|
|
3369
3942
|
}
|
|
3370
3943
|
async function startWorkflowExecution2(workflow, triggerData, registry, options = {}) {
|
|
3371
3944
|
const preflight = preflightAutomation(workflowPreflightInput(workflow), await getAutomationSetupStates());
|
|
3372
|
-
if (preflight.state === "needs_setup") {
|
|
3945
|
+
if (preflight.state === "needs_setup" && !options.parentStepId) {
|
|
3373
3946
|
const { runId, recorded } = await recordWorkflowPreflightFailure({
|
|
3374
3947
|
workflowId: workflow.id,
|
|
3375
3948
|
triggerType: options.triggerType ?? "manual",
|
|
@@ -3381,52 +3954,74 @@ async function startWorkflowExecution2(workflow, triggerData, registry, options
|
|
|
3381
3954
|
await notifyAutomationPreflightFailure(preflight);
|
|
3382
3955
|
return runId;
|
|
3383
3956
|
}
|
|
3384
|
-
|
|
3385
|
-
if (
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3957
|
+
let initial;
|
|
3958
|
+
if (preflight.state === "needs_setup") {
|
|
3959
|
+
initial = {
|
|
3960
|
+
status: "failed",
|
|
3961
|
+
error: preflight.failureReason,
|
|
3962
|
+
finishedAt: new Date().toISOString(),
|
|
3963
|
+
preflight: true
|
|
3964
|
+
};
|
|
3965
|
+
} else {
|
|
3966
|
+
workflow = renderAutomationTokens(workflow, workflow.params ?? {});
|
|
3967
|
+
if (workflow.triggerSchema) {
|
|
3968
|
+
const validationErrors = validateJsonSchema(workflow.triggerSchema, triggerData);
|
|
3969
|
+
if (validationErrors.length > 0) {
|
|
3970
|
+
throw new TriggerSchemaError(validationErrors);
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3973
|
+
if (workflow.cooldown && await shouldSkipCooldown(workflow.id, workflow.cooldown)) {
|
|
3974
|
+
initial = { status: "skipped", error: "cooldown", finishedAt: new Date().toISOString() };
|
|
3975
|
+
} else {
|
|
3976
|
+
initial = await buildInitialRunState(workflow, triggerData, registry, options);
|
|
3389
3977
|
}
|
|
3390
3978
|
}
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3979
|
+
const newRunId = crypto.randomUUID();
|
|
3980
|
+
const release = holdWorkflowRun(newRunId);
|
|
3981
|
+
try {
|
|
3982
|
+
const { runId, created, preflightAlert } = await createRunWithInitialState(workflow, triggerData, options, initial, newRunId);
|
|
3983
|
+
if (!created)
|
|
3984
|
+
return runId;
|
|
3985
|
+
if (initial.status === "failed" && initial.preflight) {
|
|
3986
|
+
if (preflightAlert)
|
|
3987
|
+
await notifyAutomationPreflightFailure(preflight);
|
|
3988
|
+
return runId;
|
|
3989
|
+
}
|
|
3990
|
+
if (initial.status === "skipped")
|
|
3991
|
+
return runId;
|
|
3992
|
+
telemetry.workflow("started", {
|
|
3395
3993
|
workflowId: workflow.id,
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
createdBy: options.requestedByUserId,
|
|
3399
|
-
parentStepId: options.parentStepId
|
|
3400
|
-
});
|
|
3401
|
-
await updateWorkflowRun2(runId, {
|
|
3402
|
-
status: "skipped",
|
|
3403
|
-
error: "cooldown",
|
|
3404
|
-
finishedAt: new Date().toISOString()
|
|
3994
|
+
nodeCount: workflow.definition.nodes.length,
|
|
3995
|
+
triggerType: options.triggerType ?? "manual"
|
|
3405
3996
|
});
|
|
3997
|
+
if (initial.status)
|
|
3998
|
+
return runId;
|
|
3999
|
+
const walk = walkGraph(workflow.definition, runId, initial.context, findEntryNodes(workflow.definition), registry, workflow.id, getSecretInputKeys(workflow.input), options);
|
|
4000
|
+
if (options.parentStepId) {
|
|
4001
|
+
walk.catch((err) => console.error(`[workflows] Child workflow run ${runId} failed:`, err));
|
|
4002
|
+
return runId;
|
|
4003
|
+
}
|
|
4004
|
+
await walk;
|
|
3406
4005
|
return runId;
|
|
4006
|
+
} finally {
|
|
4007
|
+
release();
|
|
3407
4008
|
}
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
telemetry.workflow("started", {
|
|
3418
|
-
workflowId: workflow.id,
|
|
3419
|
-
nodeCount: workflow.definition.nodes.length,
|
|
3420
|
-
triggerType: options.triggerType ?? "manual"
|
|
3421
|
-
});
|
|
4009
|
+
}
|
|
4010
|
+
|
|
4011
|
+
class ParentStepNotCurrentError extends Error {
|
|
4012
|
+
constructor(parentStepId, attempt) {
|
|
4013
|
+
super(`Parent step ${parentStepId} (attempt ${attempt}) is no longer running; no child run started`);
|
|
4014
|
+
this.name = "ParentStepNotCurrentError";
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
async function buildInitialRunState(workflow, triggerData, registry, options) {
|
|
3422
4018
|
const notReady = await findWorkflowReadinessProblems(workflow.definition, registry);
|
|
3423
4019
|
if (notReady.length > 0) {
|
|
3424
|
-
|
|
4020
|
+
return {
|
|
3425
4021
|
status: "failed",
|
|
3426
4022
|
error: formatReadinessRunError(notReady),
|
|
3427
4023
|
finishedAt: new Date().toISOString()
|
|
3428
|
-
}
|
|
3429
|
-
return runId;
|
|
4024
|
+
};
|
|
3430
4025
|
}
|
|
3431
4026
|
const ctx = { trigger: triggerData };
|
|
3432
4027
|
if (options.requestedByUserId) {
|
|
@@ -3440,23 +4035,40 @@ async function startWorkflowExecution2(workflow, triggerData, registry, options
|
|
|
3440
4035
|
const resolved = await resolveRenderedWorkflowInputs(workflow.input);
|
|
3441
4036
|
Object.assign(ctx, { input: resolved });
|
|
3442
4037
|
} catch (err) {
|
|
3443
|
-
|
|
4038
|
+
return {
|
|
3444
4039
|
status: "failed",
|
|
3445
4040
|
error: `Input resolution failed: ${err}`,
|
|
3446
4041
|
finishedAt: new Date().toISOString()
|
|
3447
|
-
}
|
|
3448
|
-
return runId;
|
|
4042
|
+
};
|
|
3449
4043
|
}
|
|
3450
4044
|
}
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
4045
|
+
return { context: ctx };
|
|
4046
|
+
}
|
|
4047
|
+
async function createRunWithInitialState(workflow, triggerData, options, initial, runId) {
|
|
4048
|
+
return getDbClient2().transaction(async () => {
|
|
4049
|
+
const { parentStepId } = options;
|
|
4050
|
+
const attempt = options.parentStepAttempt ?? 0;
|
|
4051
|
+
if (parentStepId) {
|
|
4052
|
+
if (!await isParentStepAttemptCurrent(parentStepId, attempt)) {
|
|
4053
|
+
throw new ParentStepNotCurrentError(parentStepId, attempt);
|
|
4054
|
+
}
|
|
4055
|
+
const existingId = await getChildWorkflowRunId(parentStepId, attempt);
|
|
4056
|
+
if (existingId)
|
|
4057
|
+
return { runId: existingId, created: false };
|
|
4058
|
+
}
|
|
4059
|
+
await createWorkflowRun2({
|
|
4060
|
+
id: runId,
|
|
4061
|
+
workflowId: workflow.id,
|
|
4062
|
+
triggerType: options.triggerType ?? "manual",
|
|
4063
|
+
triggerData,
|
|
4064
|
+
createdBy: options.requestedByUserId,
|
|
4065
|
+
parentStepId,
|
|
4066
|
+
parentStepAttempt: parentStepId ? attempt : undefined
|
|
4067
|
+
});
|
|
4068
|
+
await updateWorkflowRun2(runId, initial.status ? { status: initial.status, error: initial.error, finishedAt: initial.finishedAt } : { context: initial.context });
|
|
4069
|
+
const preflightAlert = initial.status === "failed" && initial.preflight ? await isFirstWorkflowPreflightFailureToday(workflow.id, runId) : undefined;
|
|
4070
|
+
return { runId, created: true, preflightAlert };
|
|
4071
|
+
});
|
|
3460
4072
|
}
|
|
3461
4073
|
var activeWalks = new Map;
|
|
3462
4074
|
var executingSteps = new Set;
|
|
@@ -3479,6 +4091,9 @@ async function failRunOnUnreadableReplay(runId, err, stepId) {
|
|
|
3479
4091
|
function isWorkflowRunActive(runId) {
|
|
3480
4092
|
return activeWalks.has(runId);
|
|
3481
4093
|
}
|
|
4094
|
+
function isStepExecuting(stepId) {
|
|
4095
|
+
return executingSteps.has(stepId);
|
|
4096
|
+
}
|
|
3482
4097
|
function holdWorkflowRun(runId) {
|
|
3483
4098
|
activeWalks.set(runId, (activeWalks.get(runId) ?? 0) + 1);
|
|
3484
4099
|
let released = false;
|
|
@@ -3726,12 +4341,27 @@ async function executeStep(def, runId, ctx, node, registry, workflowId, secretKe
|
|
|
3726
4341
|
if (current && (current.status !== "running" || executingSteps.has(current.id))) {
|
|
3727
4342
|
return { current };
|
|
3728
4343
|
}
|
|
4344
|
+
if (current && node.type === "sub-workflow") {
|
|
4345
|
+
claimedStepId = current.id;
|
|
4346
|
+
executingSteps.add(current.id);
|
|
4347
|
+
return {
|
|
4348
|
+
existingStep: current,
|
|
4349
|
+
stepId: current.id,
|
|
4350
|
+
fence: { retryCount: current.retryCount },
|
|
4351
|
+
deduped: false
|
|
4352
|
+
};
|
|
4353
|
+
}
|
|
3729
4354
|
}
|
|
3730
4355
|
const iteration = await getStepCountForNode2(runId, node.id);
|
|
3731
4356
|
const idempotencyKey = `${runId}:${node.id}:${iteration}`;
|
|
3732
4357
|
const existingStep = await getStepByIdempotencyKey2(idempotencyKey);
|
|
3733
4358
|
if (existingStep && (existingStep.status === "completed" || existingStep.status === "waiting")) {
|
|
3734
|
-
return {
|
|
4359
|
+
return {
|
|
4360
|
+
existingStep,
|
|
4361
|
+
stepId: existingStep.id,
|
|
4362
|
+
fence: { retryCount: existingStep.retryCount },
|
|
4363
|
+
deduped: true
|
|
4364
|
+
};
|
|
3735
4365
|
}
|
|
3736
4366
|
const latestForeachStep = node.type === "foreach" ? await getLatestStepForNode2(runId, node.id) : undefined;
|
|
3737
4367
|
const reusableForeachStep = latestForeachStep && !FOREACH_TERMINAL_STEP_STATUSES.has(latestForeachStep.status) ? latestForeachStep : undefined;
|
|
@@ -3748,7 +4378,8 @@ async function executeStep(def, runId, ctx, node, registry, workflowId, secretKe
|
|
|
3748
4378
|
}
|
|
3749
4379
|
claimedStepId = stepId;
|
|
3750
4380
|
executingSteps.add(stepId);
|
|
3751
|
-
|
|
4381
|
+
const fence = { retryCount: reusableForeachStep?.retryCount ?? 0 };
|
|
4382
|
+
return { existingStep, stepId, fence, deduped: false };
|
|
3752
4383
|
};
|
|
3753
4384
|
const dedup = await getDbClient2().transaction(claimStep).catch((err) => {
|
|
3754
4385
|
if (claimedStepId)
|
|
@@ -3772,7 +4403,7 @@ async function executeStep(def, runId, ctx, node, registry, workflowId, secretKe
|
|
|
3772
4403
|
return { outcome: "waiting", successors: [] };
|
|
3773
4404
|
}
|
|
3774
4405
|
try {
|
|
3775
|
-
return await runClaimedStep(def, runId, ctx, node, registry, dedup.existingStep, dedup.stepId, workflowId, options);
|
|
4406
|
+
return await runClaimedStep(def, runId, ctx, node, registry, dedup.existingStep, dedup.stepId, dedup.fence, workflowId, options);
|
|
3776
4407
|
} catch (err) {
|
|
3777
4408
|
if (await failRunOnUnreadableReplay(runId, err, dedup.stepId)) {
|
|
3778
4409
|
return { outcome: "failed", successors: [] };
|
|
@@ -3782,11 +4413,14 @@ async function executeStep(def, runId, ctx, node, registry, workflowId, secretKe
|
|
|
3782
4413
|
executingSteps.delete(dedup.stepId);
|
|
3783
4414
|
}
|
|
3784
4415
|
}
|
|
3785
|
-
async function runClaimedStep(def, runId, ctx, node, registry, existingStep, stepId, workflowId, options = {}) {
|
|
4416
|
+
async function runClaimedStep(def, runId, ctx, node, registry, existingStep, stepId, fence, workflowId, options = {}) {
|
|
4417
|
+
const stale = { outcome: "failed", successors: [] };
|
|
3786
4418
|
const executor = registry.get(node.type);
|
|
3787
4419
|
const systemOneRetryProblems = systemOneRetryViolations(node);
|
|
3788
4420
|
if (systemOneRetryProblems.length > 0) {
|
|
3789
|
-
await checkpointStepFailure(runId, stepId, systemOneRetryProblems.join("; "), 0
|
|
4421
|
+
await checkpointStepFailure(runId, stepId, systemOneRetryProblems.join("; "), 0, undefined, {
|
|
4422
|
+
fence
|
|
4423
|
+
});
|
|
3790
4424
|
return { outcome: "failed", successors: [] };
|
|
3791
4425
|
}
|
|
3792
4426
|
const interpolationCtx = buildNodeInterpolationCtx(node, ctx);
|
|
@@ -3794,7 +4428,11 @@ async function runClaimedStep(def, runId, ctx, node, registry, existingStep, ste
|
|
|
3794
4428
|
const inputErrors = validateJsonSchema(node.inputSchema, interpolationCtx);
|
|
3795
4429
|
if (inputErrors.length > 0) {
|
|
3796
4430
|
const errorMsg = `Input schema validation failed: ${inputErrors.join("; ")}`;
|
|
3797
|
-
await checkpointStepFailure(runId, stepId, errorMsg, 0
|
|
4431
|
+
const { stale: lost } = await checkpointStepFailure(runId, stepId, errorMsg, 0, undefined, {
|
|
4432
|
+
fence
|
|
4433
|
+
});
|
|
4434
|
+
if (lost)
|
|
4435
|
+
return stale;
|
|
3798
4436
|
throw new Error(errorMsg);
|
|
3799
4437
|
}
|
|
3800
4438
|
}
|
|
@@ -3808,11 +4446,11 @@ async function runClaimedStep(def, runId, ctx, node, registry, existingStep, ste
|
|
|
3808
4446
|
const executionCtx = { ...ctx, ...interpolationCtx };
|
|
3809
4447
|
if (scriptBodyUnresolved && scriptBodyUnresolved.length > 0) {
|
|
3810
4448
|
const errorMsg = scriptBodyInterpolationError(node.id, scriptBodyUnresolved);
|
|
3811
|
-
await checkpointStepFailure(runId, stepId, errorMsg, 0);
|
|
4449
|
+
await checkpointStepFailure(runId, stepId, errorMsg, 0, undefined, { fence });
|
|
3812
4450
|
return { outcome: "failed", successors: [] };
|
|
3813
4451
|
}
|
|
3814
4452
|
if (strictUnresolved && strictUnresolved.length > 0) {
|
|
3815
|
-
await checkpointStepFailure(runId, stepId, systemOneUnresolvedError(node.id, strictUnresolved, node.type), 0);
|
|
4453
|
+
await checkpointStepFailure(runId, stepId, systemOneUnresolvedError(node.id, strictUnresolved, node.type), 0, undefined, { fence });
|
|
3816
4454
|
return { outcome: "failed", successors: [] };
|
|
3817
4455
|
}
|
|
3818
4456
|
if (unresolved.length > 0) {
|
|
@@ -3845,19 +4483,20 @@ async function runClaimedStep(def, runId, ctx, node, registry, existingStep, ste
|
|
|
3845
4483
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
3846
4484
|
const retryPolicy = node.retry || executor.retryPolicy;
|
|
3847
4485
|
const currentRetryCount = existingStep?.retryCount || 0;
|
|
3848
|
-
const { shouldRetry } = await checkpointStepFailure(runId, stepId, errorMsg, currentRetryCount, retryPolicy);
|
|
4486
|
+
const { shouldRetry, stale: lost } = await checkpointStepFailure(runId, stepId, errorMsg, currentRetryCount, retryPolicy, { fence });
|
|
4487
|
+
if (lost)
|
|
4488
|
+
return stale;
|
|
3849
4489
|
if (!shouldRetry) {
|
|
3850
4490
|
throw err;
|
|
3851
4491
|
}
|
|
3852
4492
|
return { outcome: "completed", successors: [] };
|
|
3853
4493
|
}
|
|
3854
4494
|
if (result.status === "failed") {
|
|
3855
|
-
const retryPolicy = node.retry || executor.retryPolicy;
|
|
4495
|
+
const retryPolicy = result.retryable === false ? undefined : node.retry || executor.retryPolicy;
|
|
3856
4496
|
const currentRetryCount = existingStep?.retryCount || 0;
|
|
3857
|
-
const { shouldRetry } = await checkpointStepFailure(runId, stepId, result.error || "Executor returned failed status", currentRetryCount, retryPolicy);
|
|
3858
|
-
if (
|
|
3859
|
-
|
|
3860
|
-
}
|
|
4497
|
+
const { shouldRetry, stale: lost } = await checkpointStepFailure(runId, stepId, result.error || "Executor returned failed status", currentRetryCount, retryPolicy, { fence, output: result.output });
|
|
4498
|
+
if (lost)
|
|
4499
|
+
return stale;
|
|
3861
4500
|
if (!shouldRetry) {
|
|
3862
4501
|
throw new Error(result.error || "Step execution failed");
|
|
3863
4502
|
}
|
|
@@ -3878,7 +4517,7 @@ async function runClaimedStep(def, runId, ctx, node, registry, existingStep, ste
|
|
|
3878
4517
|
}
|
|
3879
4518
|
const waiting = await checkpointStepWaiting(runId, stepId, ctx);
|
|
3880
4519
|
if (waiting && result.waitFor === "workflow.child.finished") {
|
|
3881
|
-
const { resumeFromChildRun } = await import("./resume-
|
|
4520
|
+
const { resumeFromChildRun } = await import("./resume-2dvq348g.js");
|
|
3882
4521
|
await resumeFromChildRun(stepId, registry);
|
|
3883
4522
|
}
|
|
3884
4523
|
return { outcome: waiting ? "waiting" : "completed", successors: [] };
|
|
@@ -3887,7 +4526,11 @@ async function runClaimedStep(def, runId, ctx, node, registry, existingStep, ste
|
|
|
3887
4526
|
const outputErrors = validateJsonSchema(node.outputSchema, result.output);
|
|
3888
4527
|
if (outputErrors.length > 0) {
|
|
3889
4528
|
const errorMsg = `Output schema validation failed: ${outputErrors.join("; ")}`;
|
|
3890
|
-
await checkpointStepFailure(runId, stepId, errorMsg, 0
|
|
4529
|
+
const { stale: lost } = await checkpointStepFailure(runId, stepId, errorMsg, 0, undefined, {
|
|
4530
|
+
fence
|
|
4531
|
+
});
|
|
4532
|
+
if (lost)
|
|
4533
|
+
return stale;
|
|
3891
4534
|
throw new Error(errorMsg);
|
|
3892
4535
|
}
|
|
3893
4536
|
}
|
|
@@ -3896,7 +4539,10 @@ async function runClaimedStep(def, runId, ctx, node, registry, existingStep, ste
|
|
|
3896
4539
|
validationResult = await runStepValidation(registry, node, result.output, executionCtx, meta);
|
|
3897
4540
|
if (validationResult.outcome === "halt") {
|
|
3898
4541
|
const errorMsg = "Validation failed (mustPass)";
|
|
3899
|
-
await checkpointStepFailure(runId, stepId, errorMsg, 0, undefined, {
|
|
4542
|
+
await checkpointStepFailure(runId, stepId, errorMsg, 0, undefined, {
|
|
4543
|
+
markRunFailed: false,
|
|
4544
|
+
fence
|
|
4545
|
+
});
|
|
3900
4546
|
return { outcome: "failed", successors: [] };
|
|
3901
4547
|
}
|
|
3902
4548
|
if (validationResult.outcome === "retry") {
|
|
@@ -3907,12 +4553,15 @@ async function runClaimedStep(def, runId, ctx, node, registry, existingStep, ste
|
|
|
3907
4553
|
}
|
|
3908
4554
|
const retryPolicy = node.validation.retry || node.retry;
|
|
3909
4555
|
const currentRetryCount = existingStep?.retryCount || 0;
|
|
3910
|
-
await checkpointStepFailure(runId, stepId, "Validation failed, retrying", currentRetryCount, retryPolicy);
|
|
4556
|
+
const { stale: lost } = await checkpointStepFailure(runId, stepId, "Validation failed, retrying", currentRetryCount, retryPolicy, { fence });
|
|
4557
|
+
if (lost)
|
|
4558
|
+
return stale;
|
|
3911
4559
|
return { outcome: "completed", successors: [] };
|
|
3912
4560
|
}
|
|
3913
4561
|
}
|
|
3914
4562
|
result.nextPort = resolveValidationPort(node.next, result.nextPort, validationResult?.passed);
|
|
3915
|
-
await checkpointStep(runId, stepId, node.id, result, ctx)
|
|
4563
|
+
if (!await checkpointStep(runId, stepId, node.id, result, ctx, { fence }))
|
|
4564
|
+
return stale;
|
|
3916
4565
|
const successors = result.nextPort ? getSuccessors(def, node.id, result.nextPort) : getSuccessors(def, node.id);
|
|
3917
4566
|
return { outcome: "completed", successors };
|
|
3918
4567
|
}
|
|
@@ -4330,7 +4979,7 @@ async function runForeachDrive(runId, parentNodeId, registry) {
|
|
|
4330
4979
|
if (drive.successors.length > 0) {
|
|
4331
4980
|
await walkGraph(workflow.definition, runId, ctx, drive.successors, registry, workflow.id, getSecretInputKeys(workflow.input));
|
|
4332
4981
|
} else {
|
|
4333
|
-
const { finalizeOrWait } = await import("./resume-
|
|
4982
|
+
const { finalizeOrWait } = await import("./resume-2dvq348g.js");
|
|
4334
4983
|
await finalizeOrWait(runId);
|
|
4335
4984
|
}
|
|
4336
4985
|
return drive;
|
|
@@ -4472,7 +5121,7 @@ async function startChild(input) {
|
|
|
4472
5121
|
const parked = await getDbClient2().get("UPDATE workflow_run_steps SET status = 'waiting' WHERE id = ? AND status = 'running' RETURNING id", [child.id]);
|
|
4473
5122
|
if (parked && result.waitFor === "workflow.child.finished") {
|
|
4474
5123
|
ownedChildren.delete(child.id);
|
|
4475
|
-
const { resumeFromChildRun } = await import("./resume-
|
|
5124
|
+
const { resumeFromChildRun } = await import("./resume-2dvq348g.js");
|
|
4476
5125
|
await resumeFromChildRun(child.id, registry);
|
|
4477
5126
|
}
|
|
4478
5127
|
return null;
|
|
@@ -5083,4 +5732,4 @@ function applyDefinitionPatch(def, patch) {
|
|
|
5083
5732
|
return { definition: patchedDef, errors };
|
|
5084
5733
|
}
|
|
5085
5734
|
|
|
5086
|
-
export { notifyAutomationPreflightFailure, automationIntegrationFixUrl, isNeedsSetupFailure, preflightAutomation, renderAutomationTokens, getAutomationSetupStates, schedulePreflightInput, listEnabledAutomationPreflightInputs, recordSchedulePreflightFailure, BaseExecutor, postApprovalCancellationUpdates, createApprovalFollowUpTask, HumanInTheLoopExecutor, resolveWorkflowLlmConfig, SYSTEM_ONE_PROVIDER_IDS, SystemOneExtractionExecutor, SYSTEM_ONE_DECISION_NODE_TYPE, SystemOneDecisionConfigSchema, SystemOneDecisionExecutor, checkpointStep, checkpointStepFailure, checkpointStepWaiting, FAILED_TASK_OUTPUT_PREFIX, FOREACH_ITEMS_WAIT_FOR, loadCompletedStepRouting, resolveInputValue, getSecretInputKeys, validateJsonSchema, findJsonSchemaShapeErrors, getMaxWorkflowStepsPerRun, findWorkflowReadinessProblems, formatReadinessProblems, workflowSaveWarnings, withSaveWarnings, runStepValidation, TriggerSchemaError, startWorkflowExecution2, failRunOnUnreadableReplay, isWorkflowRunActive, holdWorkflowRun, walkGraph, hasRunningStep, findReadyNodes, scriptBodyInterpolationError, ForeachExecutor, rebuildNodeExecutionInput, isForeachChildOwned, parkAndDriveForeach, startForeachDrive, runForeachDrive, FOREACH_TERMINAL_STEP_STATUSES, parseSyntheticNodeId, resolveForeachParent, settleForeachChild, resolveStepNode, stepFailureMode, getNextTargets, generateEdges, resolveValidationPort, getSuccessors, definitionNodeIds, validateDefinition, applyDefinitionPatch };
|
|
5735
|
+
export { notifyAutomationPreflightFailure, automationIntegrationFixUrl, isNeedsSetupFailure, preflightAutomation, renderAutomationTokens, getAutomationSetupStates, schedulePreflightInput, listEnabledAutomationPreflightInputs, recordSchedulePreflightFailure, BaseExecutor, AGENT_MAX_TASKS_CONFIG_KEY, reconcileAgentMaxTasksPolicy, resetAgentMaxTasksMirror, upsertSwarmConfigWithPolicyMirror, runtimeStaleThresholdMinutes, upsertRuntimeInstance, getRuntimeInstanceById, insertProvisioningRuntime, markRuntimeTerminated, touchRuntimeInstance, markRuntimeInstanceOffline, countActiveRuntimeInstancesForAgent, listRuntimeInstancesForAgent, setRuntimeCredentialReady, reconcileAgentStatusFromRuntimes, agentsWithLiveRuntime, expireStaleRuntimeInstances, listRuntimeInstancesForPool, ConfigQuestionSettingsSchema, ConfigStateSchema, readConfigState, normalizeApprovalQuestions, isConfigQuestion, parseConfigAnswer, skippedConfigResult, keptConfigResult, applyConfigAnswer, resolveSlackConversationId, dispatchApprovalNotifications, postApprovalCancellationUpdates, createApprovalFollowUpTask, HumanInTheLoopExecutor, resolveWorkflowLlmConfig, SYSTEM_ONE_PROVIDER_IDS, SystemOneExtractionExecutor, SYSTEM_ONE_DECISION_NODE_TYPE, SystemOneDecisionConfigSchema, SystemOneDecisionExecutor, checkpointStep, checkpointStepFailure, checkpointStepWaiting, FAILED_TASK_OUTPUT_PREFIX, FOREACH_ITEMS_WAIT_FOR, loadCompletedStepRouting, resolveInputValue, getSecretInputKeys, validateJsonSchema, findJsonSchemaShapeErrors, getMaxWorkflowStepsPerRun, findWorkflowReadinessProblems, formatReadinessProblems, workflowSaveWarnings, withSaveWarnings, runStepValidation, TriggerSchemaError, startWorkflowExecution2, ParentStepNotCurrentError, failRunOnUnreadableReplay, isWorkflowRunActive, isStepExecuting, holdWorkflowRun, walkGraph, hasRunningStep, findReadyNodes, scriptBodyInterpolationError, ForeachExecutor, rebuildNodeExecutionInput, isForeachChildOwned, parkAndDriveForeach, startForeachDrive, runForeachDrive, FOREACH_TERMINAL_STEP_STATUSES, parseSyntheticNodeId, resolveForeachParent, settleForeachChild, resolveStepNode, stepFailureMode, getNextTargets, generateEdges, resolveValidationPort, getSuccessors, definitionNodeIds, validateDefinition, applyDefinitionPatch };
|