@desplega.ai/agent-swarm 2.5.2 → 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-csh0zz3b.js → acp-adapter-gr2fcyyj.js} +3 -3
- package/dist/{actions-6xx079d1.js → actions-wv766ghf.js} +18 -18
- package/dist/{amp-adapter-8n6f0hz8.js → amp-adapter-speb906t.js} +3 -3
- package/dist/{app-ft6r6v23.js → app-ag29n14f.js} +3 -3
- package/dist/{assistant-vt2cyfzd.js → assistant-3x8bz3eg.js} +19 -19
- package/dist/{boot-reembed-r3e808ky.js → boot-reembed-5b3d2eea.js} +1 -1
- package/dist/{boot-reembed-tgrfy27t.js → boot-reembed-jsmfrpzf.js} +1 -1
- package/dist/{boot-scrub-sweep-masdwxkm.js → boot-scrub-sweep-s73gerxy.js} +2 -2
- package/dist/{boot-seeds-0n82qc7q.js → boot-seeds-5zmax2wr.js} +4 -4
- package/dist/{claude-adapter-s7j47k06.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-3hzrgrnk.js → claude-managed-setup-3k8m3hhc.js} +2 -2
- package/dist/{claude-sdk-session-0zrdzyx2.js → claude-sdk-session-fkbyk66x.js} +2 -2
- package/dist/{cli-d172svcw.js → cli-1fc85mvt.js} +4 -4
- package/dist/{cli-3e9vs93g.js → cli-210xd1f4.js} +4 -4
- package/dist/{cli-vaxw3bg0.js → cli-25j01dpj.js} +3 -3
- package/dist/{cli-n6kmqngw.js → cli-35gtk1p6.js} +1 -1
- package/dist/{cli-fgn7yg5r.js → cli-3ngdk5te.js} +1 -1
- package/dist/{cli-7ccf7akh.js → cli-40jsnrjm.js} +2 -2
- package/dist/{cli-b1tv22wr.js → cli-502eqv93.js} +1 -1
- package/dist/{cli-ydg6ep51.js → cli-6n8axjv1.js} +1 -1
- package/dist/{cli-f55b6ky0.js → cli-8s5wx3nz.js} +2 -2
- package/dist/{cli-86zpfje5.js → cli-99xgw26r.js} +1 -1
- package/dist/{cli-xw9q173h.js → cli-9s05422w.js} +1 -1
- package/dist/{cli-h5d22pfs.js → cli-aecdcr77.js} +119 -282
- package/dist/{cli-w9etnnxk.js → cli-bf8ma3f1.js} +120 -50
- package/dist/{cli-15xd0j7w.js → cli-ce0ak9ms.js} +11 -11
- package/dist/{cli-y3dzwyy3.js → cli-e2ar4bp1.js} +71 -20
- package/dist/{cli-b6tnjdvx.js → cli-e8jyq7d8.js} +1 -1
- package/dist/{cli-6y43v79h.js → cli-egjggehm.js} +1 -1
- package/dist/{cli-cf63dnxj.js → cli-ewm3pwbq.js} +2 -2
- package/dist/{cli-bpbcpd8b.js → cli-fsdprwrg.js} +1 -1
- package/dist/{cli-qrjv7t18.js → cli-fzcn3d87.js} +1 -1
- package/dist/{cli-14v1sxxy.js → cli-gpsz7g88.js} +6 -6
- package/dist/{cli-wadzapwb.js → cli-hcpj2xqz.js} +1 -1
- package/dist/{cli-jmcjbjkt.js → cli-hhxjnjzs.js} +1 -1
- package/dist/{cli-j3dwd4kz.js → cli-jjznk3f7.js} +718 -108
- package/dist/{cli-csh8fnbg.js → cli-kxawjrxg.js} +147 -28
- package/dist/{cli-mntxtkhn.js → cli-mz9afxxf.js} +2 -1
- package/dist/{cli-0vgy5wfw.js → cli-n45mr7zh.js} +2 -2
- package/dist/{cli-aaqnqpyw.js → cli-p0rzjkya.js} +2 -2
- package/dist/{cli-3qv8naq8.js → cli-p6zs9h4y.js} +827 -107
- package/dist/{cli-e91vshb1.js → cli-p9rafdrb.js} +4 -4
- package/dist/{cli-edtm741g.js → cli-qgf8emrn.js} +5 -5
- package/dist/{cli-ha22h0eg.js → cli-qzme5949.js} +26 -26
- package/dist/{cli-xzge46sj.js → cli-s49hpn37.js} +2 -2
- package/dist/{cli-b63d08rs.js → cli-s53vp709.js} +1 -1
- package/dist/{cli-cyfgxnvb.js → cli-svrj71pg.js} +18 -16
- package/dist/{cli-4qkg9968.js → cli-vwn81jbs.js} +3 -3
- package/dist/{cli-f3pfn4x3.js → cli-w1zwctj2.js} +1 -1
- package/dist/{cli-kcnwn85g.js → cli-y561h5a9.js} +4 -4
- package/dist/{cli-n2918sed.js → cli-z21whrrg.js} +15 -15
- package/dist/cli.js +16 -16
- package/dist/{codex-adapter-brh2tcxr.js → codex-adapter-n8ka66vf.js} +2 -2
- package/dist/{codex-session-runner-0dnj21qs.js → codex-session-runner-fsebts37.js} +1 -1
- package/dist/{commands-1cnvseqx.js → commands-t04rt2pc.js} +2 -2
- package/dist/{controller-kc7s9xhn.js → controller-rgna04r2.js} +2 -2
- package/dist/{controllers-connect-jzx7t4bn.js → controllers-connect-bqm1rtft.js} +20 -20
- package/dist/{cursor-adapter-qpwp5sfg.js → cursor-adapter-swr2k95y.js} +1 -1
- package/dist/{db-v5ytg5pw.js → db-24ck09xy.js} +1 -1
- package/dist/{db-4vq88jkc.js → db-a4m7haw0.js} +3 -3
- package/dist/{dispatcher-trtcrqae.js → dispatcher-nwf9ss0q.js} +17 -17
- package/dist/{dsh-adapter-f20xwn1z.js → dsh-adapter-kpdnhjst.js} +2 -2
- package/dist/{external-channel-n9bymc2n.js → external-channel-j1e2p34p.js} +2 -2
- package/dist/{grok-adapter-4kpmmnr6.js → grok-adapter-qtjz0whk.js} +4 -4
- package/dist/{handlers-ptsraqa8.js → handlers-19xr6dnz.js} +20 -20
- package/dist/{heartbeat-60qtxtjf.js → heartbeat-vtesda5d.js} +20 -20
- package/dist/{hook-rbktcstg.js → hook-8wqaszda.js} +1 -1
- package/dist/{hook-ngtvh729.js → hook-b2hhj6rd.js} +1 -1
- package/dist/{http-z8xy72gp.js → http-mxcqhhcs.js} +2757 -2448
- package/dist/{index-a9nsavhx.js → index-1r8wfpzs.js} +1 -1
- package/dist/{index-cbxkmfc5.js → index-9pn1pfkf.js} +1 -1
- package/dist/{index-s193j5fh.js → index-kw2mmat7.js} +20 -20
- package/dist/{index-06qkmcx6.js → index-n1vbvta9.js} +17 -9
- package/dist/{index-6srwvd5b.js → index-p4xfz37z.js} +1 -1
- package/dist/{index-1cscynkb.js → index-spwxjgay.js} +17 -17
- package/dist/{index-r70q0xjx.js → index-srg522m6.js} +17 -17
- package/dist/{keepalive-a9srcphj.js → keepalive-dsd8rtgx.js} +3 -3
- package/dist/{lead-3devneqy.js → lead-zyerk1x2.js} +30 -30
- package/dist/{maintenance-0er16h2y.js → maintenance-jqmqwnet.js} +1 -1
- package/dist/{oauth-refresh-sweep-b65jcyw2.js → oauth-refresh-sweep-q2c3bnvm.js} +2 -2
- package/dist/{onboard-wqywz5py.js → onboard-w4yd7s8t.js} +2 -2
- package/dist/{opencode-adapter-e56086m1.js → opencode-adapter-9tppvxar.js} +2 -2
- package/dist/{otel-impl-vk32s884.js → otel-impl-f36byzbd.js} +1 -1
- package/dist/{pricing-refresh-t27anv4z.js → pricing-refresh-xvak5p4v.js} +3 -3
- package/dist/{rbac-roles-bmcgsxjn.js → rbac-roles-682k2mt5.js} +1 -1
- package/dist/{rbac-roles-452665y0.js → rbac-roles-gfn30fb8.js} +2 -2
- package/dist/{render-v2-0ttsk2j4.js → render-v2-hgytz8nn.js} +3 -3
- package/dist/{resume-a26kvr41.js → resume-2dvq348g.js} +7 -5
- package/dist/{session-access-dgh9nkwk.js → session-access-7f21ekq9.js} +4 -4
- package/dist/{setup-9vc9qahm.js → setup-epffxtbk.js} +1 -1
- package/dist/{work-objects-vn8nkxwn.js → work-objects-xz1a5rqk.js} +20 -20
- package/dist/{worker-1x1h8vnm.js → worker-42t8emjt.js} +30 -30
- package/openapi.json +3045 -2456
- package/package.json +4 -4
- package/src/be/approval-config-question.ts +389 -0
- package/src/be/automation-preflight.ts +37 -3
- package/src/be/db/session-tree.ts +73 -15
- package/src/be/db/skill-search.ts +87 -0
- package/src/be/db/tasks/read.ts +29 -2
- package/src/be/db/workflow-runs.ts +54 -7
- package/src/be/db.ts +25 -5
- 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/swarm-config-guard.ts +23 -0
- package/src/extensions/catalog.generated.json +4 -4
- package/src/http/approval-requests.ts +209 -38
- package/src/http/config.ts +22 -37
- package/src/http/sessions.ts +27 -2
- package/src/integrations/chat-sdk/inbound.ts +5 -4
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/tests/approval-config-question.test.ts +702 -0
- package/src/tests/chat-sdk-inbound.test.ts +20 -0
- 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/list-endpoint-slimming.test.ts +33 -0
- package/src/tests/migration-240-workflow-run-parent-step-attempt.test.ts +55 -0
- package/src/tests/script-connections.test.ts +6 -0
- package/src/tests/sessions-tree.test.ts +168 -1
- package/src/tests/skill-search-long-content.test.ts +193 -0
- package/src/tests/skill-search.test.ts +173 -22
- package/src/tests/workflow-sub-workflow.test.ts +538 -1
- package/src/tools/request-human-input.ts +27 -5
- package/src/types.ts +7 -0
- package/src/workflows/approval-notifications.ts +10 -3
- 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 +25 -1
- package/src/workflows/executors/sub-workflow.ts +45 -16
- 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/skills/turso-interaction/content.md +1 -1
- package/templates/skills/workflow-iterate/content.md +8 -1
- /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,
|
|
@@ -37,23 +51,30 @@ import {
|
|
|
37
51
|
releaseApprovalCancellationNotificationClaim2,
|
|
38
52
|
updateApprovalRequestNotifications2,
|
|
39
53
|
init_db3
|
|
40
|
-
} from "./cli-
|
|
54
|
+
} from "./cli-kxawjrxg.js";
|
|
55
|
+
import {
|
|
56
|
+
isMultiRuntimeEnabled
|
|
57
|
+
} from "./cli-e8xyjvfc.js";
|
|
41
58
|
import {
|
|
42
59
|
getSlackApp2
|
|
43
|
-
} from "./cli-
|
|
60
|
+
} from "./cli-ce0ak9ms.js";
|
|
44
61
|
import {
|
|
45
62
|
externalChannelToolError2,
|
|
46
63
|
isBlockExternalEnabled2,
|
|
47
64
|
getChannelBlockReason2
|
|
48
|
-
} from "./cli-
|
|
65
|
+
} from "./cli-s53vp709.js";
|
|
49
66
|
import {
|
|
50
67
|
DEFAULT_MODEL,
|
|
51
68
|
resolveCredential
|
|
52
69
|
} from "./cli-qqzmjxkf.js";
|
|
70
|
+
import {
|
|
71
|
+
revokeRuntimeTokensForRuntime,
|
|
72
|
+
init_users
|
|
73
|
+
} from "./cli-e8jyq7d8.js";
|
|
53
74
|
import {
|
|
54
75
|
TERMINAL_TASK_STATUSES,
|
|
55
76
|
init_types
|
|
56
|
-
} from "./cli-
|
|
77
|
+
} from "./cli-mz9afxxf.js";
|
|
57
78
|
import {
|
|
58
79
|
getSlackConfiguration,
|
|
59
80
|
isSlackConfigured,
|
|
@@ -124,7 +145,7 @@ async function notifyAutomationPreflightFailure(preflight) {
|
|
|
124
145
|
return;
|
|
125
146
|
}
|
|
126
147
|
try {
|
|
127
|
-
const { getSlackApp } = await import("./app-
|
|
148
|
+
const { getSlackApp } = await import("./app-ag29n14f.js");
|
|
128
149
|
const app = getSlackApp();
|
|
129
150
|
if (!app) {
|
|
130
151
|
console.warn("[Automation Preflight] Slack not available, cannot send notification");
|
|
@@ -362,6 +383,15 @@ function utcDayBounds(now) {
|
|
|
362
383
|
const start = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
|
363
384
|
return { start: start.toISOString(), end: new Date(start.getTime() + 86400000).toISOString() };
|
|
364
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
|
+
}
|
|
365
395
|
async function recordSchedulePreflightFailure(scheduleId, failureReason, now = new Date) {
|
|
366
396
|
const message = scrubSecrets(failureReason);
|
|
367
397
|
return await getDbClient2().transaction(async (tx) => {
|
|
@@ -382,7 +412,7 @@ async function recordWorkflowPreflightFailure(input) {
|
|
|
382
412
|
const bounds = utcDayBounds(now);
|
|
383
413
|
return await getDbClient2().transaction(async (tx) => {
|
|
384
414
|
const existing = await tx.get(`SELECT id FROM workflow_runs
|
|
385
|
-
WHERE workflowId = ? AND status = 'failed'
|
|
415
|
+
WHERE workflowId = ? AND status = 'failed' AND parentStepId IS NULL
|
|
386
416
|
AND error LIKE 'needs_setup:%'
|
|
387
417
|
AND startedAt >= ? AND startedAt < ?
|
|
388
418
|
ORDER BY startedAt ASC LIMIT 1`, [input.workflowId, bounds.start, bounds.end]);
|
|
@@ -404,7 +434,10 @@ async function recordWorkflowPreflightFailure(input) {
|
|
|
404
434
|
timestamp,
|
|
405
435
|
timestamp
|
|
406
436
|
]);
|
|
407
|
-
return {
|
|
437
|
+
return {
|
|
438
|
+
runId,
|
|
439
|
+
recorded: await isFirstWorkflowPreflightFailureToday(input.workflowId, runId, now)
|
|
440
|
+
};
|
|
408
441
|
});
|
|
409
442
|
}
|
|
410
443
|
|
|
@@ -457,6 +490,470 @@ class BaseExecutor {
|
|
|
457
490
|
|
|
458
491
|
// src/workflows/executors/human-in-the-loop.ts
|
|
459
492
|
init_zod();
|
|
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
|
|
460
957
|
init_secret_scrubber();
|
|
461
958
|
|
|
462
959
|
// src/workflows/approval-notifications.ts
|
|
@@ -489,7 +986,7 @@ function escapeSlackMrkdwn(text) {
|
|
|
489
986
|
function buildSlackQuestionsSummary(questions, timeoutText = "") {
|
|
490
987
|
const header = `*Questions:*
|
|
491
988
|
`;
|
|
492
|
-
const lines = questions.map((q) => `• ${escapeSlackMrkdwn(q.label.replace(/\s+/g, " ").trim())}`);
|
|
989
|
+
const lines = questions.map((q) => `• ${escapeSlackMrkdwn((q.label ?? "").replace(/\s+/g, " ").trim())}${q.type === "config" ? " (set on dashboard)" : ""}`);
|
|
493
990
|
const fits = (shown, omitted) => {
|
|
494
991
|
const tail = omitted > 0 ? `
|
|
495
992
|
_…and ${omitted} more. Answer all of them on the dashboard._` : "";
|
|
@@ -687,6 +1184,8 @@ function formatResponses(questions, responses) {
|
|
|
687
1184
|
let answerText;
|
|
688
1185
|
if (answer == null) {
|
|
689
1186
|
answerText = "(no answer)";
|
|
1187
|
+
} else if (q.type === "config") {
|
|
1188
|
+
answerText = formatConfigResult(answer) ?? "(unreadable config result)";
|
|
690
1189
|
} else if (q.type === "approval") {
|
|
691
1190
|
const a = answer;
|
|
692
1191
|
answerText = a.approved ? "Approved" : "Rejected";
|
|
@@ -750,6 +1249,14 @@ var QuestionSchema = discriminatedUnion("type", [
|
|
|
750
1249
|
required: boolean().default(true),
|
|
751
1250
|
description: string2().optional(),
|
|
752
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
|
|
753
1260
|
})
|
|
754
1261
|
]);
|
|
755
1262
|
var ApproverConfigSchema = object2({
|
|
@@ -841,7 +1348,17 @@ class HumanInTheLoopExecutor extends BaseExecutor {
|
|
|
841
1348
|
const resolved = resolveHitlQuestions(input.config?.questions);
|
|
842
1349
|
if (!resolved.ok)
|
|
843
1350
|
return { status: "failed", error: resolved.error };
|
|
844
|
-
|
|
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 } });
|
|
845
1362
|
}
|
|
846
1363
|
async execute(config, _context, meta) {
|
|
847
1364
|
const { db } = this.deps;
|
|
@@ -2927,8 +3444,18 @@ init_db3();
|
|
|
2927
3444
|
|
|
2928
3445
|
// src/workflows/checkpoint.ts
|
|
2929
3446
|
init_db3();
|
|
2930
|
-
async function
|
|
2931
|
-
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;
|
|
2932
3459
|
await updateWorkflowRunStep2(stepId, {
|
|
2933
3460
|
status: "completed",
|
|
2934
3461
|
output: result.output,
|
|
@@ -2939,42 +3466,48 @@ async function checkpointStep(runId, stepId, nodeId, result, ctx) {
|
|
|
2939
3466
|
await updateWorkflowRun2(runId, {
|
|
2940
3467
|
context: ctx
|
|
2941
3468
|
});
|
|
3469
|
+
return true;
|
|
2942
3470
|
});
|
|
2943
3471
|
}
|
|
2944
3472
|
async function checkpointStepFailure(runId, stepId, error, retryCount, retryPolicy, options) {
|
|
2945
3473
|
const now = new Date().toISOString();
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
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
|
+
}
|
|
2949
3491
|
await updateWorkflowRunStep2(stepId, {
|
|
2950
3492
|
status: "failed",
|
|
2951
3493
|
error,
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
nextRetryAt
|
|
3494
|
+
output: options?.output,
|
|
3495
|
+
finishedAt: now,
|
|
3496
|
+
nextRetryAt: null
|
|
2955
3497
|
});
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
await updateWorkflowRunStep2(stepId, {
|
|
2959
|
-
status: "failed",
|
|
2960
|
-
error,
|
|
2961
|
-
finishedAt: now,
|
|
2962
|
-
nextRetryAt: null
|
|
2963
|
-
});
|
|
2964
|
-
const markRunFailed = options?.markRunFailed ?? true;
|
|
2965
|
-
if (markRunFailed) {
|
|
2966
|
-
await getDbClient2().transaction(async () => {
|
|
3498
|
+
const markRunFailed = options?.markRunFailed ?? true;
|
|
3499
|
+
if (markRunFailed) {
|
|
2967
3500
|
const run = await getWorkflowRun2(runId);
|
|
2968
|
-
if (run?.status
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
}
|
|
2975
|
-
}
|
|
2976
|
-
|
|
2977
|
-
|
|
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
|
+
});
|
|
2978
3511
|
}
|
|
2979
3512
|
async function checkpointStepWaiting(runId, stepId, ctx, options = {}) {
|
|
2980
3513
|
return getDbClient2().transaction(async () => {
|
|
@@ -3021,6 +3554,7 @@ var FOREACH_ITEMS_WAIT_FOR = "foreach.items";
|
|
|
3021
3554
|
|
|
3022
3555
|
// src/workflows/engine.ts
|
|
3023
3556
|
init_db3();
|
|
3557
|
+
init_workflow_runs();
|
|
3024
3558
|
init_workflow_replay();
|
|
3025
3559
|
init_telemetry();
|
|
3026
3560
|
|
|
@@ -3408,7 +3942,7 @@ async function resolveRenderedWorkflowInputs(input) {
|
|
|
3408
3942
|
}
|
|
3409
3943
|
async function startWorkflowExecution2(workflow, triggerData, registry, options = {}) {
|
|
3410
3944
|
const preflight = preflightAutomation(workflowPreflightInput(workflow), await getAutomationSetupStates());
|
|
3411
|
-
if (preflight.state === "needs_setup") {
|
|
3945
|
+
if (preflight.state === "needs_setup" && !options.parentStepId) {
|
|
3412
3946
|
const { runId, recorded } = await recordWorkflowPreflightFailure({
|
|
3413
3947
|
workflowId: workflow.id,
|
|
3414
3948
|
triggerType: options.triggerType ?? "manual",
|
|
@@ -3420,52 +3954,74 @@ async function startWorkflowExecution2(workflow, triggerData, registry, options
|
|
|
3420
3954
|
await notifyAutomationPreflightFailure(preflight);
|
|
3421
3955
|
return runId;
|
|
3422
3956
|
}
|
|
3423
|
-
|
|
3424
|
-
if (
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
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);
|
|
3428
3977
|
}
|
|
3429
3978
|
}
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
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", {
|
|
3434
3993
|
workflowId: workflow.id,
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
createdBy: options.requestedByUserId,
|
|
3438
|
-
parentStepId: options.parentStepId
|
|
3439
|
-
});
|
|
3440
|
-
await updateWorkflowRun2(runId, {
|
|
3441
|
-
status: "skipped",
|
|
3442
|
-
error: "cooldown",
|
|
3443
|
-
finishedAt: new Date().toISOString()
|
|
3994
|
+
nodeCount: workflow.definition.nodes.length,
|
|
3995
|
+
triggerType: options.triggerType ?? "manual"
|
|
3444
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;
|
|
3445
4005
|
return runId;
|
|
4006
|
+
} finally {
|
|
4007
|
+
release();
|
|
3446
4008
|
}
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
telemetry.workflow("started", {
|
|
3457
|
-
workflowId: workflow.id,
|
|
3458
|
-
nodeCount: workflow.definition.nodes.length,
|
|
3459
|
-
triggerType: options.triggerType ?? "manual"
|
|
3460
|
-
});
|
|
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) {
|
|
3461
4018
|
const notReady = await findWorkflowReadinessProblems(workflow.definition, registry);
|
|
3462
4019
|
if (notReady.length > 0) {
|
|
3463
|
-
|
|
4020
|
+
return {
|
|
3464
4021
|
status: "failed",
|
|
3465
4022
|
error: formatReadinessRunError(notReady),
|
|
3466
4023
|
finishedAt: new Date().toISOString()
|
|
3467
|
-
}
|
|
3468
|
-
return runId;
|
|
4024
|
+
};
|
|
3469
4025
|
}
|
|
3470
4026
|
const ctx = { trigger: triggerData };
|
|
3471
4027
|
if (options.requestedByUserId) {
|
|
@@ -3479,23 +4035,40 @@ async function startWorkflowExecution2(workflow, triggerData, registry, options
|
|
|
3479
4035
|
const resolved = await resolveRenderedWorkflowInputs(workflow.input);
|
|
3480
4036
|
Object.assign(ctx, { input: resolved });
|
|
3481
4037
|
} catch (err) {
|
|
3482
|
-
|
|
4038
|
+
return {
|
|
3483
4039
|
status: "failed",
|
|
3484
4040
|
error: `Input resolution failed: ${err}`,
|
|
3485
4041
|
finishedAt: new Date().toISOString()
|
|
3486
|
-
}
|
|
3487
|
-
return runId;
|
|
4042
|
+
};
|
|
3488
4043
|
}
|
|
3489
4044
|
}
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
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
|
+
});
|
|
3499
4072
|
}
|
|
3500
4073
|
var activeWalks = new Map;
|
|
3501
4074
|
var executingSteps = new Set;
|
|
@@ -3518,6 +4091,9 @@ async function failRunOnUnreadableReplay(runId, err, stepId) {
|
|
|
3518
4091
|
function isWorkflowRunActive(runId) {
|
|
3519
4092
|
return activeWalks.has(runId);
|
|
3520
4093
|
}
|
|
4094
|
+
function isStepExecuting(stepId) {
|
|
4095
|
+
return executingSteps.has(stepId);
|
|
4096
|
+
}
|
|
3521
4097
|
function holdWorkflowRun(runId) {
|
|
3522
4098
|
activeWalks.set(runId, (activeWalks.get(runId) ?? 0) + 1);
|
|
3523
4099
|
let released = false;
|
|
@@ -3765,12 +4341,27 @@ async function executeStep(def, runId, ctx, node, registry, workflowId, secretKe
|
|
|
3765
4341
|
if (current && (current.status !== "running" || executingSteps.has(current.id))) {
|
|
3766
4342
|
return { current };
|
|
3767
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
|
+
}
|
|
3768
4354
|
}
|
|
3769
4355
|
const iteration = await getStepCountForNode2(runId, node.id);
|
|
3770
4356
|
const idempotencyKey = `${runId}:${node.id}:${iteration}`;
|
|
3771
4357
|
const existingStep = await getStepByIdempotencyKey2(idempotencyKey);
|
|
3772
4358
|
if (existingStep && (existingStep.status === "completed" || existingStep.status === "waiting")) {
|
|
3773
|
-
return {
|
|
4359
|
+
return {
|
|
4360
|
+
existingStep,
|
|
4361
|
+
stepId: existingStep.id,
|
|
4362
|
+
fence: { retryCount: existingStep.retryCount },
|
|
4363
|
+
deduped: true
|
|
4364
|
+
};
|
|
3774
4365
|
}
|
|
3775
4366
|
const latestForeachStep = node.type === "foreach" ? await getLatestStepForNode2(runId, node.id) : undefined;
|
|
3776
4367
|
const reusableForeachStep = latestForeachStep && !FOREACH_TERMINAL_STEP_STATUSES.has(latestForeachStep.status) ? latestForeachStep : undefined;
|
|
@@ -3787,7 +4378,8 @@ async function executeStep(def, runId, ctx, node, registry, workflowId, secretKe
|
|
|
3787
4378
|
}
|
|
3788
4379
|
claimedStepId = stepId;
|
|
3789
4380
|
executingSteps.add(stepId);
|
|
3790
|
-
|
|
4381
|
+
const fence = { retryCount: reusableForeachStep?.retryCount ?? 0 };
|
|
4382
|
+
return { existingStep, stepId, fence, deduped: false };
|
|
3791
4383
|
};
|
|
3792
4384
|
const dedup = await getDbClient2().transaction(claimStep).catch((err) => {
|
|
3793
4385
|
if (claimedStepId)
|
|
@@ -3811,7 +4403,7 @@ async function executeStep(def, runId, ctx, node, registry, workflowId, secretKe
|
|
|
3811
4403
|
return { outcome: "waiting", successors: [] };
|
|
3812
4404
|
}
|
|
3813
4405
|
try {
|
|
3814
|
-
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);
|
|
3815
4407
|
} catch (err) {
|
|
3816
4408
|
if (await failRunOnUnreadableReplay(runId, err, dedup.stepId)) {
|
|
3817
4409
|
return { outcome: "failed", successors: [] };
|
|
@@ -3821,11 +4413,14 @@ async function executeStep(def, runId, ctx, node, registry, workflowId, secretKe
|
|
|
3821
4413
|
executingSteps.delete(dedup.stepId);
|
|
3822
4414
|
}
|
|
3823
4415
|
}
|
|
3824
|
-
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: [] };
|
|
3825
4418
|
const executor = registry.get(node.type);
|
|
3826
4419
|
const systemOneRetryProblems = systemOneRetryViolations(node);
|
|
3827
4420
|
if (systemOneRetryProblems.length > 0) {
|
|
3828
|
-
await checkpointStepFailure(runId, stepId, systemOneRetryProblems.join("; "), 0
|
|
4421
|
+
await checkpointStepFailure(runId, stepId, systemOneRetryProblems.join("; "), 0, undefined, {
|
|
4422
|
+
fence
|
|
4423
|
+
});
|
|
3829
4424
|
return { outcome: "failed", successors: [] };
|
|
3830
4425
|
}
|
|
3831
4426
|
const interpolationCtx = buildNodeInterpolationCtx(node, ctx);
|
|
@@ -3833,7 +4428,11 @@ async function runClaimedStep(def, runId, ctx, node, registry, existingStep, ste
|
|
|
3833
4428
|
const inputErrors = validateJsonSchema(node.inputSchema, interpolationCtx);
|
|
3834
4429
|
if (inputErrors.length > 0) {
|
|
3835
4430
|
const errorMsg = `Input schema validation failed: ${inputErrors.join("; ")}`;
|
|
3836
|
-
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;
|
|
3837
4436
|
throw new Error(errorMsg);
|
|
3838
4437
|
}
|
|
3839
4438
|
}
|
|
@@ -3847,11 +4446,11 @@ async function runClaimedStep(def, runId, ctx, node, registry, existingStep, ste
|
|
|
3847
4446
|
const executionCtx = { ...ctx, ...interpolationCtx };
|
|
3848
4447
|
if (scriptBodyUnresolved && scriptBodyUnresolved.length > 0) {
|
|
3849
4448
|
const errorMsg = scriptBodyInterpolationError(node.id, scriptBodyUnresolved);
|
|
3850
|
-
await checkpointStepFailure(runId, stepId, errorMsg, 0);
|
|
4449
|
+
await checkpointStepFailure(runId, stepId, errorMsg, 0, undefined, { fence });
|
|
3851
4450
|
return { outcome: "failed", successors: [] };
|
|
3852
4451
|
}
|
|
3853
4452
|
if (strictUnresolved && strictUnresolved.length > 0) {
|
|
3854
|
-
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 });
|
|
3855
4454
|
return { outcome: "failed", successors: [] };
|
|
3856
4455
|
}
|
|
3857
4456
|
if (unresolved.length > 0) {
|
|
@@ -3884,19 +4483,20 @@ async function runClaimedStep(def, runId, ctx, node, registry, existingStep, ste
|
|
|
3884
4483
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
3885
4484
|
const retryPolicy = node.retry || executor.retryPolicy;
|
|
3886
4485
|
const currentRetryCount = existingStep?.retryCount || 0;
|
|
3887
|
-
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;
|
|
3888
4489
|
if (!shouldRetry) {
|
|
3889
4490
|
throw err;
|
|
3890
4491
|
}
|
|
3891
4492
|
return { outcome: "completed", successors: [] };
|
|
3892
4493
|
}
|
|
3893
4494
|
if (result.status === "failed") {
|
|
3894
|
-
const retryPolicy = node.retry || executor.retryPolicy;
|
|
4495
|
+
const retryPolicy = result.retryable === false ? undefined : node.retry || executor.retryPolicy;
|
|
3895
4496
|
const currentRetryCount = existingStep?.retryCount || 0;
|
|
3896
|
-
const { shouldRetry } = await checkpointStepFailure(runId, stepId, result.error || "Executor returned failed status", currentRetryCount, retryPolicy);
|
|
3897
|
-
if (
|
|
3898
|
-
|
|
3899
|
-
}
|
|
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;
|
|
3900
4500
|
if (!shouldRetry) {
|
|
3901
4501
|
throw new Error(result.error || "Step execution failed");
|
|
3902
4502
|
}
|
|
@@ -3917,7 +4517,7 @@ async function runClaimedStep(def, runId, ctx, node, registry, existingStep, ste
|
|
|
3917
4517
|
}
|
|
3918
4518
|
const waiting = await checkpointStepWaiting(runId, stepId, ctx);
|
|
3919
4519
|
if (waiting && result.waitFor === "workflow.child.finished") {
|
|
3920
|
-
const { resumeFromChildRun } = await import("./resume-
|
|
4520
|
+
const { resumeFromChildRun } = await import("./resume-2dvq348g.js");
|
|
3921
4521
|
await resumeFromChildRun(stepId, registry);
|
|
3922
4522
|
}
|
|
3923
4523
|
return { outcome: waiting ? "waiting" : "completed", successors: [] };
|
|
@@ -3926,7 +4526,11 @@ async function runClaimedStep(def, runId, ctx, node, registry, existingStep, ste
|
|
|
3926
4526
|
const outputErrors = validateJsonSchema(node.outputSchema, result.output);
|
|
3927
4527
|
if (outputErrors.length > 0) {
|
|
3928
4528
|
const errorMsg = `Output schema validation failed: ${outputErrors.join("; ")}`;
|
|
3929
|
-
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;
|
|
3930
4534
|
throw new Error(errorMsg);
|
|
3931
4535
|
}
|
|
3932
4536
|
}
|
|
@@ -3935,7 +4539,10 @@ async function runClaimedStep(def, runId, ctx, node, registry, existingStep, ste
|
|
|
3935
4539
|
validationResult = await runStepValidation(registry, node, result.output, executionCtx, meta);
|
|
3936
4540
|
if (validationResult.outcome === "halt") {
|
|
3937
4541
|
const errorMsg = "Validation failed (mustPass)";
|
|
3938
|
-
await checkpointStepFailure(runId, stepId, errorMsg, 0, undefined, {
|
|
4542
|
+
await checkpointStepFailure(runId, stepId, errorMsg, 0, undefined, {
|
|
4543
|
+
markRunFailed: false,
|
|
4544
|
+
fence
|
|
4545
|
+
});
|
|
3939
4546
|
return { outcome: "failed", successors: [] };
|
|
3940
4547
|
}
|
|
3941
4548
|
if (validationResult.outcome === "retry") {
|
|
@@ -3946,12 +4553,15 @@ async function runClaimedStep(def, runId, ctx, node, registry, existingStep, ste
|
|
|
3946
4553
|
}
|
|
3947
4554
|
const retryPolicy = node.validation.retry || node.retry;
|
|
3948
4555
|
const currentRetryCount = existingStep?.retryCount || 0;
|
|
3949
|
-
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;
|
|
3950
4559
|
return { outcome: "completed", successors: [] };
|
|
3951
4560
|
}
|
|
3952
4561
|
}
|
|
3953
4562
|
result.nextPort = resolveValidationPort(node.next, result.nextPort, validationResult?.passed);
|
|
3954
|
-
await checkpointStep(runId, stepId, node.id, result, ctx)
|
|
4563
|
+
if (!await checkpointStep(runId, stepId, node.id, result, ctx, { fence }))
|
|
4564
|
+
return stale;
|
|
3955
4565
|
const successors = result.nextPort ? getSuccessors(def, node.id, result.nextPort) : getSuccessors(def, node.id);
|
|
3956
4566
|
return { outcome: "completed", successors };
|
|
3957
4567
|
}
|
|
@@ -4369,7 +4979,7 @@ async function runForeachDrive(runId, parentNodeId, registry) {
|
|
|
4369
4979
|
if (drive.successors.length > 0) {
|
|
4370
4980
|
await walkGraph(workflow.definition, runId, ctx, drive.successors, registry, workflow.id, getSecretInputKeys(workflow.input));
|
|
4371
4981
|
} else {
|
|
4372
|
-
const { finalizeOrWait } = await import("./resume-
|
|
4982
|
+
const { finalizeOrWait } = await import("./resume-2dvq348g.js");
|
|
4373
4983
|
await finalizeOrWait(runId);
|
|
4374
4984
|
}
|
|
4375
4985
|
return drive;
|
|
@@ -4511,7 +5121,7 @@ async function startChild(input) {
|
|
|
4511
5121
|
const parked = await getDbClient2().get("UPDATE workflow_run_steps SET status = 'waiting' WHERE id = ? AND status = 'running' RETURNING id", [child.id]);
|
|
4512
5122
|
if (parked && result.waitFor === "workflow.child.finished") {
|
|
4513
5123
|
ownedChildren.delete(child.id);
|
|
4514
|
-
const { resumeFromChildRun } = await import("./resume-
|
|
5124
|
+
const { resumeFromChildRun } = await import("./resume-2dvq348g.js");
|
|
4515
5125
|
await resumeFromChildRun(child.id, registry);
|
|
4516
5126
|
}
|
|
4517
5127
|
return null;
|
|
@@ -5122,4 +5732,4 @@ function applyDefinitionPatch(def, patch) {
|
|
|
5122
5732
|
return { definition: patchedDef, errors };
|
|
5123
5733
|
}
|
|
5124
5734
|
|
|
5125
|
-
export { notifyAutomationPreflightFailure, automationIntegrationFixUrl, isNeedsSetupFailure, preflightAutomation, renderAutomationTokens, getAutomationSetupStates, schedulePreflightInput, listEnabledAutomationPreflightInputs, recordSchedulePreflightFailure, BaseExecutor, 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, 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 };
|