@desplega.ai/agent-swarm 2.5.2 → 2.5.4
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-ek38cwsv.js} +5 -5
- package/dist/{actions-6xx079d1.js → actions-92mepq03.js} +19 -19
- package/dist/{amp-adapter-8n6f0hz8.js → amp-adapter-9gaxg015.js} +4 -4
- package/dist/{app-ft6r6v23.js → app-ctn8gsgx.js} +3 -3
- package/dist/{assistant-vt2cyfzd.js → assistant-zyr5kz47.js} +21 -21
- package/dist/{boot-reembed-tgrfy27t.js → boot-reembed-dwzfttdr.js} +1 -1
- package/dist/{boot-reembed-r3e808ky.js → boot-reembed-w75maga2.js} +1 -1
- package/dist/{boot-scrub-sweep-masdwxkm.js → boot-scrub-sweep-e9kq43kf.js} +3 -3
- package/dist/{boot-seeds-0n82qc7q.js → boot-seeds-x28v3fhb.js} +4 -4
- package/dist/{claude-adapter-s7j47k06.js → claude-adapter-f0afqnne.js} +5 -5
- package/dist/{claude-managed-adapter-gt6pa35d.js → claude-managed-adapter-x81a7r65.js} +3 -3
- package/dist/{claude-managed-setup-3hzrgrnk.js → claude-managed-setup-bqzwjd9x.js} +2 -2
- package/dist/{claude-sdk-session-0zrdzyx2.js → claude-sdk-session-7b7whkaq.js} +9307 -9254
- package/dist/{cli-a5ebc95n.js → cli-0shx9j93.js} +16 -13
- package/dist/{cli-4qkg9968.js → cli-19202kew.js} +4 -4
- package/dist/{cli-cyfgxnvb.js → cli-1r5ad7j5.js} +19 -17
- package/dist/{cli-jmcjbjkt.js → cli-25n3mv5d.js} +9 -7
- package/dist/cli-2655k16q.js +140 -0
- package/dist/{cli-wadzapwb.js → cli-277c31dw.js} +1 -1
- package/dist/{cli-n6kmqngw.js → cli-4b2z6n0m.js} +29 -7
- package/dist/{cli-7ccf7akh.js → cli-5bnsc0e1.js} +9 -7
- package/dist/{cli-qrjv7t18.js → cli-621ce7kw.js} +1 -1
- package/dist/{cli-xzge46sj.js → cli-64mh9mf3.js} +20 -6
- package/dist/{cli-b6tnjdvx.js → cli-6a1xg1p7.js} +6 -2
- package/dist/{cli-ydg6ep51.js → cli-6n8axjv1.js} +1 -1
- package/dist/{cli-e91vshb1.js → cli-7p4jxgtt.js} +5 -5
- package/dist/{cli-f3pfn4x3.js → cli-9j129brq.js} +9 -3
- package/dist/{cli-xw9q173h.js → cli-9s05422w.js} +1 -1
- package/dist/{cli-15xd0j7w.js → cli-abs8ndtr.js} +12 -12
- package/dist/{cli-h5d22pfs.js → cli-asq5ydm4.js} +219 -309
- package/dist/{cli-w9etnnxk.js → cli-bf8ma3f1.js} +120 -50
- package/dist/{cli-0vgy5wfw.js → cli-bvpgr9ny.js} +2 -2
- package/dist/{cli-3e9vs93g.js → cli-c61mbdds.js} +4 -4
- package/dist/{cli-e5bxnjps.js → cli-c952c7ac.js} +5 -5
- package/dist/{cli-p859hcc8.js → cli-dew6x255.js} +1 -1
- package/dist/{cli-tx00c0h6.js → cli-dqygeeh5.js} +17 -1
- package/dist/{cli-gpv60278.js → cli-e42vss7b.js} +2 -2
- package/dist/{cli-b63d08rs.js → cli-eftzyh9y.js} +1 -1
- package/dist/{cli-fgn7yg5r.js → cli-ethgy7ha.js} +1 -1
- package/dist/{cli-86zpfje5.js → cli-ff1tm9n9.js} +1 -1
- package/dist/{cli-cf63dnxj.js → cli-g9vd9ece.js} +37 -12
- package/dist/{cli-j3dwd4kz.js → cli-gbfxp48k.js} +770 -126
- package/dist/{cli-n2918sed.js → cli-gn5af0x5.js} +21 -21
- package/dist/{cli-d172svcw.js → cli-gwhjmh8g.js} +4 -4
- package/dist/{cli-stxdrqs2.js → cli-hh5we352.js} +24 -5
- package/dist/{cli-0ebey9hg.js → cli-j6rktc91.js} +1 -1
- package/dist/{cli-csh8fnbg.js → cli-jb30dzvk.js} +473 -58
- package/dist/{cli-vaxw3bg0.js → cli-jbnn4vxb.js} +3 -3
- package/dist/{cli-ha22h0eg.js → cli-k4tjsdx9.js} +45 -40
- package/dist/{cli-f55b6ky0.js → cli-m4ecdxvz.js} +40 -13
- package/dist/{cli-mntxtkhn.js → cli-mz9afxxf.js} +2 -1
- package/dist/{cli-ta8bd7th.js → cli-pzatvw94.js} +1 -1
- package/dist/{cli-3qv8naq8.js → cli-rr7zx19j.js} +1591 -353
- package/dist/{cli-b1tv22wr.js → cli-s682zgqz.js} +1 -1
- package/dist/{cli-bpbcpd8b.js → cli-sbwsxvtd.js} +1 -1
- package/dist/{cli-0xk505e8.js → cli-sxgtf5fw.js} +1 -1
- package/dist/{cli-6y43v79h.js → cli-tsma7p8v.js} +1 -1
- package/dist/{cli-edtm741g.js → cli-wghjkr0e.js} +5 -5
- package/dist/{cli-aaqnqpyw.js → cli-wx2nj19m.js} +30 -11
- package/dist/{cli-y3dzwyy3.js → cli-x0f2v69c.js} +180 -60
- package/dist/{cli-kcnwn85g.js → cli-xtrz6bzs.js} +7 -7
- package/dist/{cli-14v1sxxy.js → cli-xzfq4at3.js} +7 -7
- package/dist/cli.js +20 -20
- package/dist/{codex-adapter-brh2tcxr.js → codex-adapter-wch12q99.js} +3 -3
- package/dist/{codex-hook-e2he7aca.js → codex-hook-brpnycn5.js} +1 -1
- package/dist/{codex-session-runner-0dnj21qs.js → codex-session-runner-41pfcr95.js} +2 -2
- package/dist/{commands-1cnvseqx.js → commands-9849dkj6.js} +2 -2
- package/dist/{controller-kc7s9xhn.js → controller-ja0sd95p.js} +29 -3
- package/dist/{controllers-connect-jzx7t4bn.js → controllers-connect-xp05dj6x.js} +20 -20
- package/dist/{cursor-adapter-qpwp5sfg.js → cursor-adapter-623qxqew.js} +1 -2
- package/dist/{db-4vq88jkc.js → db-47vj40dy.js} +3 -3
- package/dist/{db-v5ytg5pw.js → db-w759qmx0.js} +2 -2
- package/dist/{dispatcher-trtcrqae.js → dispatcher-7b8e4z2j.js} +17 -17
- package/dist/{docker-nfy2gc8t.js → docker-s84hjs91.js} +2 -2
- package/dist/{dsh-adapter-f20xwn1z.js → dsh-adapter-2ezw3xea.js} +2 -3
- package/dist/{external-channel-n9bymc2n.js → external-channel-e2s028da.js} +2 -2
- package/dist/{grok-adapter-4kpmmnr6.js → grok-adapter-ys7gaqee.js} +6 -6
- package/dist/{handlers-ptsraqa8.js → handlers-wy1dr6ja.js} +20 -20
- package/dist/{heartbeat-60qtxtjf.js → heartbeat-w3mntghj.js} +20 -20
- package/dist/{hook-rbktcstg.js → hook-3jjywnpb.js} +9 -3
- package/dist/{hook-ngtvh729.js → hook-g7kvv6x9.js} +3 -2
- package/dist/{http-z8xy72gp.js → http-tkp7d4kb.js} +2984 -2483
- package/dist/{index-r70q0xjx.js → index-1ef1myq2.js} +17 -17
- package/dist/{index-a9nsavhx.js → index-3n8m9ea2.js} +1 -1
- package/dist/{index-cbxkmfc5.js → index-9pn1pfkf.js} +1 -1
- package/dist/{index-s193j5fh.js → index-nmm79qgr.js} +20 -20
- package/dist/{index-6srwvd5b.js → index-p4xfz37z.js} +1 -1
- package/dist/{index-06qkmcx6.js → index-q7m26kvs.js} +33 -23
- package/dist/{index-1cscynkb.js → index-xdaahthb.js} +17 -17
- package/dist/{keepalive-a9srcphj.js → keepalive-v4ta8wgg.js} +3 -3
- package/dist/{lead-3devneqy.js → lead-sawnyqxk.js} +35 -35
- package/dist/{maintenance-0er16h2y.js → maintenance-84n1ex21.js} +1 -1
- package/dist/{oauth-refresh-sweep-b65jcyw2.js → oauth-refresh-sweep-vjnnandw.js} +2 -2
- package/dist/{onboard-wqywz5py.js → onboard-sfmpe4np.js} +2 -2
- package/dist/{opencode-adapter-e56086m1.js → opencode-adapter-jmpst4q4.js} +4 -4
- package/dist/{otel-impl-vk32s884.js → otel-impl-v6cc0zve.js} +1 -1
- package/dist/{pi-mono-adapter-tzps8f38.js → pi-mono-adapter-fk8fkcf6.js} +33 -6
- package/dist/{pricing-refresh-t27anv4z.js → pricing-refresh-fsk1e1zc.js} +3 -3
- package/dist/{rbac-roles-452665y0.js → rbac-roles-prmkdnkw.js} +2 -2
- package/dist/{rbac-roles-bmcgsxjn.js → rbac-roles-zxmxqa39.js} +1 -1
- package/dist/{render-v2-0ttsk2j4.js → render-v2-pjwxvnkw.js} +3 -3
- package/dist/{resume-a26kvr41.js → resume-frdtgfa0.js} +9 -5
- package/dist/{session-access-dgh9nkwk.js → session-access-fpez4y5q.js} +4 -4
- package/dist/{setup-9vc9qahm.js → setup-mq6m3m8q.js} +1 -1
- package/dist/{work-objects-vn8nkxwn.js → work-objects-kfahhjsq.js} +20 -20
- package/dist/{worker-1x1h8vnm.js → worker-v5e5rxj3.js} +35 -35
- package/openapi.json +3078 -2424
- package/package.json +7 -7
- package/plugin/commands/create-pr.md +2 -2
- package/plugin/pi-skills/create-pr/SKILL.md +2 -2
- package/src/be/approval-config-question.ts +389 -0
- package/src/be/automation-preflight.ts +37 -3
- package/src/be/db/approvals.ts +48 -0
- 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/tasks/write.ts +15 -5
- package/src/be/db/workflow-idempotency.ts +183 -0
- package/src/be/db/workflow-runs.ts +366 -8
- package/src/be/db.ts +92 -22
- 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/migrations/242_workflow_idempotency_keys.sql +40 -0
- package/src/be/sealed-json.ts +1 -0
- package/src/be/seed-scripts/catalog/compound-insights.inline.ts +2 -2
- package/src/be/seed-scripts/catalog/compound-insights.ts +2 -2
- package/src/be/seed-skills/bundled-files.generated.json +1 -1
- package/src/be/sql-connections.ts +7 -1
- package/src/be/swarm-config-guard.ts +25 -0
- package/src/be/users.ts +9 -0
- package/src/commands/controller.ts +33 -0
- package/src/commands/runner.ts +6 -2
- package/src/extensions/apply-task-create.ts +57 -11
- package/src/extensions/catalog.generated.json +4 -4
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +25 -0
- package/src/extensions/dispatcher.ts +4 -0
- package/src/extensions/post-bridge.ts +8 -0
- package/src/hooks/context-bound-note.ts +196 -0
- package/src/hooks/hook.ts +12 -0
- package/src/http/approval-requests.ts +209 -38
- package/src/http/config.ts +70 -53
- package/src/http/idempotency-key.ts +117 -0
- package/src/http/sessions.ts +27 -2
- package/src/http/workflows.ts +129 -24
- package/src/integrations/chat-sdk/inbound.ts +5 -4
- package/src/prompts/context-bound-note.ts +48 -0
- package/src/prompts/session-templates.ts +25 -1
- package/src/providers/amp-adapter.ts +30 -8
- package/src/providers/claude-adapter.ts +29 -4
- package/src/providers/claude-managed-adapter.ts +26 -0
- package/src/providers/claude-sdk-session.ts +10 -0
- package/src/providers/claude-session-events.ts +13 -0
- package/src/providers/codex-adapter.ts +12 -0
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/opencode-adapter.ts +18 -0
- package/src/providers/pi-mono-adapter.ts +35 -0
- package/src/runtime-providers/docker.ts +6 -2
- package/src/scripts-runtime/types/swarm-extension.d.ts +35 -0
- package/src/tests/amp-adapter.test.ts +97 -0
- package/src/tests/approval-config-question.test.ts +702 -0
- package/src/tests/approval-requests.test.ts +10 -7
- package/src/tests/approval-sweeps.test.ts +12 -6
- package/src/tests/base-prompt.test.ts +1 -1
- package/src/tests/chat-sdk-inbound.test.ts +20 -0
- package/src/tests/claude-hook-output.test.ts +79 -1
- package/src/tests/claude-managed-adapter.test.ts +275 -0
- package/src/tests/codex-adapter.test.ts +163 -0
- package/src/tests/context-bound-note.test.ts +269 -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/extensions-pre-task.test.ts +273 -0
- package/src/tests/fetch-resolved-env.test.ts +17 -0
- package/src/tests/fixtures/extensions/record-blocks/hooks.ts +10 -0
- package/src/tests/fixtures/extensions/record-blocks/manifest.json +7 -0
- package/src/tests/fixtures/fake-amp.ts +22 -8
- package/src/tests/fixtures/held-template-render.ts +42 -0
- package/src/tests/fixtures/workflow-idempotency-trigger.child.ts +73 -0
- 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/openapi-route-headers.test.ts +2 -0
- package/src/tests/opencode-adapter.test.ts +134 -0
- package/src/tests/pi-mono-adapter.test.ts +105 -0
- package/src/tests/prompt-template-session.test.ts +4 -3
- package/src/tests/runner-push-delivery.test.ts +25 -0
- package/src/tests/runtime-controller-liveness.test.ts +133 -0
- package/src/tests/runtime-providers-docker.test.ts +2 -0
- package/src/tests/script-connections.test.ts +9 -0
- package/src/tests/script-run-config-scope.test.ts +91 -3
- package/src/tests/session-logs.test.ts +70 -0
- package/src/tests/sessions-tree.test.ts +205 -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-cascade-cancel.test.ts +1169 -0
- package/src/tests/workflow-foreach-supersede-retry.test.ts +488 -0
- package/src/tests/workflow-idempotency.test.ts +915 -0
- package/src/tests/workflow-sub-workflow.test.ts +538 -1
- package/src/tools/request-human-input.ts +27 -5
- package/src/tools/workflows/cancel-workflow-run.ts +1 -1
- package/src/types.ts +7 -0
- package/src/utils/context-window.ts +8 -6
- package/src/workflows/approval-cancel.ts +15 -13
- package/src/workflows/approval-notifications.ts +15 -4
- package/src/workflows/checkpoint.ts +72 -35
- package/src/workflows/engine.ts +262 -93
- package/src/workflows/executors/base.ts +6 -0
- package/src/workflows/executors/foreach.ts +2 -2
- package/src/workflows/executors/human-in-the-loop.ts +25 -1
- package/src/workflows/executors/sub-workflow.ts +45 -16
- package/src/workflows/foreach-driver.ts +10 -3
- package/src/workflows/foreach-join.ts +29 -6
- package/src/workflows/recovery.ts +68 -1
- package/src/workflows/resume.ts +228 -77
- package/src/workflows/retry-poller.ts +74 -7
- package/src/workflows/task-step-routing.ts +29 -7
- package/templates/ai-toolbox.manifest.json +8 -8
- package/templates/community/open-prompt-showreel/PLAYBOOK.md +5 -6
- package/templates/community/open-prompt-showreel/lead-prompt.md +1 -1
- package/templates/community/open-prompt-showreel/skills/open-prompt-showreel/SKILL.md +4 -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/agent-browser/SKILL.md +4 -4
- package/templates/skills/agent-browser/content.md +4 -4
- package/templates/skills/code-quality/config.json +1 -1
- package/templates/skills/code-quality/content.md +2 -2
- package/templates/skills/delegate-work/content.md +29 -25
- package/templates/skills/delegate-work/files/scripts/codex-exec.sh +5 -5
- package/templates/skills/qa/content.md +2 -2
- package/templates/skills/researching/content.md +2 -0
- package/templates/skills/turso-interaction/content.md +1 -1
- package/templates/skills/work-on-task/SKILL.md +1 -1
- package/templates/skills/work-on-task/content.md +1 -1
- package/templates/skills/workflow-iterate/content.md +8 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { TERMINAL_TASK_STATUSES, type WorkflowRunStatus } from "../../types";
|
|
2
2
|
import { scrubSecrets } from "../../utils/secret-scrubber";
|
|
3
3
|
import { scrubJsonValue } from "../scrub-json";
|
|
4
4
|
import { getDbClient } from "./runtime";
|
|
5
5
|
|
|
6
6
|
// Parent/child links between workflow runs (migration 203): a `sub-workflow`
|
|
7
|
-
// step starts a child run that records the step in `parentStepId
|
|
7
|
+
// step starts a child run that records the step in `parentStepId`, and the
|
|
8
|
+
// step's `retryCount` at that time in `parentStepAttempt` (migration 240).
|
|
8
9
|
|
|
9
10
|
const TERMINAL_STATUSES = new Set<WorkflowRunStatus>([
|
|
10
11
|
"completed",
|
|
@@ -13,15 +14,371 @@ const TERMINAL_STATUSES = new Set<WorkflowRunStatus>([
|
|
|
13
14
|
"skipped",
|
|
14
15
|
]);
|
|
15
16
|
|
|
16
|
-
/**
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Deepest run tree the lineage guard and the cascade cancel walk. Twice the
|
|
19
|
+
* sub-workflow nesting cap (MAX_SUB_WORKFLOW_DEPTH, 16), so any tree the
|
|
20
|
+
* engine admits fits; a deeper chain is corrupt and fails closed.
|
|
21
|
+
*/
|
|
22
|
+
export const WORKFLOW_RUN_TREE_MAX_DEPTH = 32;
|
|
23
|
+
|
|
24
|
+
/** Thrown when a run's stored parent links cannot be walked: a cycle, or a chain past the depth bound. */
|
|
25
|
+
export class WorkflowRunTreeError extends Error {
|
|
26
|
+
constructor(message: string) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "WorkflowRunTreeError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Thrown when work is created for a workflow step that may no longer run: the
|
|
34
|
+
* step is not `running`, its run is not live, or the run's lineage is blocked
|
|
35
|
+
* (see `getRunLineageBlock`).
|
|
36
|
+
*/
|
|
37
|
+
export class WorkflowStepNotExecutableError extends Error {
|
|
38
|
+
constructor(stepId: string, reason: string) {
|
|
39
|
+
super(`Workflow step ${stepId} may not start new work: ${reason}`);
|
|
40
|
+
this.name = "WorkflowStepNotExecutableError";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Why the run and its ancestors forbid new engine work, or null when they
|
|
46
|
+
* allow it. Blocked when the run is missing, the run or an ancestor is
|
|
47
|
+
* `cancelled`, a stored parent link points at a missing step or run, the
|
|
48
|
+
* links form a cycle, or the chain is deeper than WORKFLOW_RUN_TREE_MAX_DEPTH.
|
|
49
|
+
* A completed or failed ancestor does not block: a child of an earlier
|
|
50
|
+
* attempt may outlive its parent's run. The run's own live status is left to
|
|
51
|
+
* the caller, since the retry claim also revives a `failed` run.
|
|
52
|
+
*
|
|
53
|
+
* Call it inside the transaction that writes the new work, so a cascade
|
|
54
|
+
* cancel either commits first and is seen here or sees the new row itself.
|
|
55
|
+
*/
|
|
56
|
+
export async function getRunLineageBlock(runId: string): Promise<string | null> {
|
|
57
|
+
const rows = await getDbClient().query<{
|
|
58
|
+
id: string;
|
|
59
|
+
status: WorkflowRunStatus;
|
|
60
|
+
parentStepId: string | null;
|
|
61
|
+
depth: number;
|
|
62
|
+
}>(
|
|
63
|
+
`WITH RECURSIVE lineage(id, status, parentStepId, depth) AS (
|
|
64
|
+
SELECT id, status, parentStepId, 0 FROM workflow_runs WHERE id = ?
|
|
65
|
+
UNION ALL
|
|
66
|
+
SELECT r.id, r.status, r.parentStepId, l.depth + 1
|
|
67
|
+
FROM lineage l
|
|
68
|
+
JOIN workflow_run_steps s ON s.id = l.parentStepId
|
|
69
|
+
JOIN workflow_runs r ON r.id = s.runId
|
|
70
|
+
WHERE l.depth < ?
|
|
71
|
+
)
|
|
72
|
+
SELECT id, status, parentStepId, depth FROM lineage ORDER BY depth`,
|
|
73
|
+
[runId, WORKFLOW_RUN_TREE_MAX_DEPTH],
|
|
74
|
+
);
|
|
75
|
+
if (rows.length === 0) return `Workflow run ${runId} is missing`;
|
|
76
|
+
const seen = new Set<string>();
|
|
77
|
+
for (const row of rows) {
|
|
78
|
+
if (seen.has(row.id)) return `Workflow run ${runId} has a cycle in its parent links`;
|
|
79
|
+
seen.add(row.id);
|
|
80
|
+
if (row.status === "cancelled") {
|
|
81
|
+
return row.id === runId
|
|
82
|
+
? `Workflow run ${runId} is cancelled`
|
|
83
|
+
: `Ancestor workflow run ${row.id} is cancelled`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const top = rows[rows.length - 1]!;
|
|
87
|
+
if (top.parentStepId == null) return null;
|
|
88
|
+
if (top.depth >= WORKFLOW_RUN_TREE_MAX_DEPTH) {
|
|
89
|
+
return `Workflow run ${runId} is nested deeper than ${WORKFLOW_RUN_TREE_MAX_DEPTH} levels`;
|
|
90
|
+
}
|
|
91
|
+
return `Parent step ${top.parentStepId} of workflow run ${top.id} is missing`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The run and every run below it, root first: children of every attempt of
|
|
96
|
+
* every step, through terminal runs too, since a run that ended can still have
|
|
97
|
+
* a live child of an earlier attempt. The tree comes from stored links only
|
|
98
|
+
* (child.parentStepId → step → run). There is no size limit: the engine admits
|
|
99
|
+
* any number of children per run, and a tree it admitted must stay
|
|
100
|
+
* cancellable. Throws WorkflowRunTreeError on a cycle or past `maxDepth`, so
|
|
101
|
+
* the caller's transaction rolls back whole.
|
|
102
|
+
*/
|
|
103
|
+
export async function getWorkflowRunTreeIds(
|
|
104
|
+
rootId: string,
|
|
105
|
+
bounds: { maxDepth?: number } = {},
|
|
106
|
+
): Promise<string[]> {
|
|
107
|
+
const maxDepth = bounds.maxDepth ?? WORKFLOW_RUN_TREE_MAX_DEPTH;
|
|
108
|
+
const ids = [rootId];
|
|
109
|
+
const seen = new Set(ids);
|
|
110
|
+
// One level per pass, so a cycle stops at its first repeated run instead of
|
|
111
|
+
// unrolling down to the depth bound.
|
|
112
|
+
let level = [rootId];
|
|
113
|
+
for (let depth = 1; level.length > 0; depth++) {
|
|
114
|
+
const next: string[] = [];
|
|
115
|
+
for (let i = 0; i < level.length; i += TREE_QUERY_CHUNK) {
|
|
116
|
+
const chunk = level.slice(i, i + TREE_QUERY_CHUNK);
|
|
117
|
+
const rows = await getDbClient().query<{ id: string }>(
|
|
118
|
+
`SELECT c.id
|
|
119
|
+
FROM workflow_run_steps s
|
|
120
|
+
JOIN workflow_runs c ON c.parentStepId = s.id
|
|
121
|
+
WHERE s.runId IN (${chunk.map(() => "?").join(", ")})`,
|
|
122
|
+
chunk,
|
|
123
|
+
);
|
|
124
|
+
for (const { id } of rows) {
|
|
125
|
+
// Each run has one parent link, so reaching a run twice means a cycle.
|
|
126
|
+
if (seen.has(id)) {
|
|
127
|
+
throw new WorkflowRunTreeError(`Workflow run tree of ${rootId} has a cycle at run ${id}`);
|
|
128
|
+
}
|
|
129
|
+
seen.add(id);
|
|
130
|
+
next.push(id);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (next.length > 0 && depth > maxDepth) {
|
|
134
|
+
throw new WorkflowRunTreeError(
|
|
135
|
+
`Workflow run tree of ${rootId} is nested deeper than ${maxDepth} levels`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
ids.push(...next);
|
|
139
|
+
level = next;
|
|
140
|
+
}
|
|
141
|
+
return ids;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Parent runs per children query, well under SQLite's bound-parameter limit. */
|
|
145
|
+
const TREE_QUERY_CHUNK = 500;
|
|
146
|
+
|
|
147
|
+
/** A run's status alone, without opening its sealed context. Null when the run is missing. */
|
|
148
|
+
export async function getWorkflowRunStatus(runId: string): Promise<WorkflowRunStatus | null> {
|
|
149
|
+
const row = await getDbClient().get<{ status: WorkflowRunStatus }>(
|
|
150
|
+
"SELECT status FROM workflow_runs WHERE id = ?",
|
|
151
|
+
[runId],
|
|
152
|
+
);
|
|
153
|
+
return row?.status ?? null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Run `fn` over `ids` in chunks small enough for one statement's bound parameters. */
|
|
157
|
+
async function forEachChunk(
|
|
158
|
+
ids: string[],
|
|
159
|
+
fn: (chunk: string[], placeholders: string) => Promise<void>,
|
|
160
|
+
): Promise<void> {
|
|
161
|
+
for (let i = 0; i < ids.length; i += TREE_QUERY_CHUNK) {
|
|
162
|
+
const chunk = ids.slice(i, i + TREE_QUERY_CHUNK);
|
|
163
|
+
await fn(chunk, chunk.map(() => "?").join(", "));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Ids of the non-terminal tasks of the given runs, including tasks of earlier
|
|
169
|
+
* attempts: a retry detaches a task from its step, but the run link stays.
|
|
170
|
+
*/
|
|
171
|
+
export async function getOpenWorkflowTreeTaskIds(runIds: string[]): Promise<string[]> {
|
|
172
|
+
const ids: string[] = [];
|
|
173
|
+
const terminal = TERMINAL_TASK_STATUSES.map(() => "?").join(", ");
|
|
174
|
+
await forEachChunk(runIds, async (chunk, placeholders) => {
|
|
175
|
+
const rows = await getDbClient().query<{ id: string }>(
|
|
176
|
+
`SELECT id FROM agent_tasks
|
|
177
|
+
WHERE workflowRunId IN (${placeholders}) AND status NOT IN (${terminal})`,
|
|
178
|
+
[...chunk, ...TERMINAL_TASK_STATUSES],
|
|
179
|
+
);
|
|
180
|
+
for (const row of rows) ids.push(row.id);
|
|
181
|
+
});
|
|
182
|
+
return ids;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Ids of tasks that a non-terminal task lists in `dependsOn`: the only
|
|
187
|
+
* dependents a cancel can fail. One scan over open tasks, so a cancel of many
|
|
188
|
+
* tasks looks for dependents once instead of once per task.
|
|
189
|
+
*/
|
|
190
|
+
export async function getTaskIdsWithDependents(): Promise<Set<string>> {
|
|
191
|
+
const rows = await getDbClient().query<{ id: string }>(
|
|
192
|
+
`SELECT DISTINCT dep.value AS id
|
|
193
|
+
FROM agent_tasks t, json_each(t.dependsOn) AS dep
|
|
194
|
+
WHERE t.status NOT IN (${TERMINAL_TASK_STATUSES.map(() => "?").join(", ")})
|
|
195
|
+
AND t.dependsOn IS NOT NULL AND t.dependsOn != '[]'`,
|
|
196
|
+
[...TERMINAL_TASK_STATUSES],
|
|
197
|
+
);
|
|
198
|
+
return new Set(rows.map((row) => row.id));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* True when any of the given runs still has work a cancel would close: a live
|
|
203
|
+
* run, a pending, running or waiting step, a queued retry, an open task or a
|
|
204
|
+
* pending approval. Reads only, with a LIMIT 1 probe per chunk.
|
|
205
|
+
*/
|
|
206
|
+
export async function workflowRunTreeHasOpenWork(runIds: string[]): Promise<boolean> {
|
|
207
|
+
const terminal = TERMINAL_TASK_STATUSES.map(() => "?").join(", ");
|
|
208
|
+
const probes: Array<{ sql: (ids: string) => string; params: string[] }> = [
|
|
209
|
+
{
|
|
210
|
+
sql: (ids) =>
|
|
211
|
+
`SELECT 1 FROM workflow_runs WHERE id IN (${ids}) AND status IN ('running', 'waiting') LIMIT 1`,
|
|
212
|
+
params: [],
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
sql: (ids) => `SELECT 1 FROM workflow_run_steps
|
|
216
|
+
WHERE runId IN (${ids})
|
|
217
|
+
AND (status IN ('pending', 'running', 'waiting')
|
|
218
|
+
OR (status = 'failed' AND nextRetryAt IS NOT NULL))
|
|
219
|
+
LIMIT 1`,
|
|
220
|
+
params: [],
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
sql: (ids) =>
|
|
224
|
+
`SELECT 1 FROM agent_tasks WHERE workflowRunId IN (${ids}) AND status NOT IN (${terminal}) LIMIT 1`,
|
|
225
|
+
params: [...TERMINAL_TASK_STATUSES],
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
sql: (ids) =>
|
|
229
|
+
`SELECT 1 FROM approval_requests WHERE workflowRunId IN (${ids}) AND status = 'pending' LIMIT 1`,
|
|
230
|
+
params: [],
|
|
231
|
+
},
|
|
232
|
+
];
|
|
233
|
+
let open = false;
|
|
234
|
+
await forEachChunk(runIds, async (chunk, placeholders) => {
|
|
235
|
+
for (const probe of probes) {
|
|
236
|
+
if (open) return;
|
|
237
|
+
if (await getDbClient().get(probe.sql(placeholders), [...chunk, ...probe.params])) {
|
|
238
|
+
open = true;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
return open;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Cancel the pending, running and waiting steps of the given runs. The
|
|
247
|
+
* migration-140 trigger closes each cancelled step's pending approval.
|
|
248
|
+
*/
|
|
249
|
+
export async function cancelOpenWorkflowTreeSteps(
|
|
250
|
+
runIds: string[],
|
|
251
|
+
reason: string,
|
|
252
|
+
finishedAt: string,
|
|
253
|
+
): Promise<void> {
|
|
254
|
+
await forEachChunk(runIds, async (chunk, placeholders) => {
|
|
255
|
+
await getDbClient().run(
|
|
256
|
+
`UPDATE workflow_run_steps
|
|
257
|
+
SET status = 'cancelled', error = ?, finishedAt = ?
|
|
258
|
+
WHERE runId IN (${placeholders}) AND status IN ('pending', 'running', 'waiting')`,
|
|
259
|
+
[scrubSecrets(reason), finishedAt, ...chunk],
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Drop the pending retries of the failed steps of the given runs. The step
|
|
266
|
+
* keeps its status, error, output and retryCount; only the queued next
|
|
267
|
+
* attempt goes.
|
|
268
|
+
*/
|
|
269
|
+
export async function disarmWorkflowTreeRetries(
|
|
270
|
+
runIds: string[],
|
|
271
|
+
finishedAt: string,
|
|
272
|
+
): Promise<void> {
|
|
273
|
+
await forEachChunk(runIds, async (chunk, placeholders) => {
|
|
274
|
+
await getDbClient().run(
|
|
275
|
+
`UPDATE workflow_run_steps
|
|
276
|
+
SET nextRetryAt = NULL, finishedAt = COALESCE(finishedAt, ?)
|
|
277
|
+
WHERE runId IN (${placeholders}) AND status = 'failed' AND nextRetryAt IS NOT NULL`,
|
|
278
|
+
[finishedAt, ...chunk],
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Mark the given runs that are still running or waiting `cancelled`, without
|
|
285
|
+
* the per-run `workflow.child.finished` wake-up: it is for runs whose parent
|
|
286
|
+
* step the same cancel closes. A terminal run keeps its status and reason.
|
|
287
|
+
*/
|
|
288
|
+
export async function cancelLiveWorkflowTreeRuns(
|
|
289
|
+
runIds: string[],
|
|
290
|
+
reason: string,
|
|
291
|
+
finishedAt: string,
|
|
292
|
+
): Promise<void> {
|
|
293
|
+
await forEachChunk(runIds, async (chunk, placeholders) => {
|
|
294
|
+
await getDbClient().run(
|
|
295
|
+
`UPDATE workflow_runs
|
|
296
|
+
SET status = 'cancelled', error = ?, finishedAt = ?, lastUpdatedAt = ?
|
|
297
|
+
WHERE id IN (${placeholders}) AND status IN ('running', 'waiting')`,
|
|
298
|
+
[scrubSecrets(reason), finishedAt, finishedAt, ...chunk],
|
|
299
|
+
);
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** The id of the child run a `sub-workflow` step started on `attempt`, if any. */
|
|
304
|
+
export async function getChildWorkflowRunId(
|
|
305
|
+
parentStepId: string,
|
|
306
|
+
attempt: number,
|
|
307
|
+
): Promise<string | null> {
|
|
18
308
|
const row = await getDbClient().get<{ id: string }>(
|
|
19
|
-
"SELECT id FROM workflow_runs WHERE parentStepId = ?",
|
|
20
|
-
[parentStepId],
|
|
309
|
+
"SELECT id FROM workflow_runs WHERE parentStepId = ? AND parentStepAttempt = ?",
|
|
310
|
+
[parentStepId, attempt],
|
|
21
311
|
);
|
|
22
312
|
return row?.id ?? null;
|
|
23
313
|
}
|
|
24
314
|
|
|
315
|
+
/**
|
|
316
|
+
* True while `attempt` of the step is executing in a live run: the step is
|
|
317
|
+
* `running` with that `retryCount` and no ancestor run is cancelled. A child
|
|
318
|
+
* run is created only then, so a cancelled run or tree, a step another attempt
|
|
319
|
+
* moved on, or a step already parked never gets a new child.
|
|
320
|
+
*/
|
|
321
|
+
export async function isParentStepAttemptCurrent(
|
|
322
|
+
parentStepId: string,
|
|
323
|
+
attempt: number,
|
|
324
|
+
): Promise<boolean> {
|
|
325
|
+
const row = await getDbClient().get<{ runId: string }>(
|
|
326
|
+
`SELECT r.id AS runId
|
|
327
|
+
FROM workflow_run_steps s
|
|
328
|
+
JOIN workflow_runs r ON r.id = s.runId
|
|
329
|
+
WHERE s.id = ? AND s.status = 'running' AND s.retryCount = ?
|
|
330
|
+
AND r.status IN ('running', 'waiting')`,
|
|
331
|
+
[parentStepId, attempt],
|
|
332
|
+
);
|
|
333
|
+
return row != null && (await getRunLineageBlock(row.runId)) == null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Why `stepId` may not start new work (a task, an approval, a child run), or
|
|
338
|
+
* null when it may: the step is `running` in a live run whose lineage is not
|
|
339
|
+
* blocked. Call it inside the transaction that inserts the work.
|
|
340
|
+
*/
|
|
341
|
+
export async function getStepExecutionBlock(stepId: string): Promise<string | null> {
|
|
342
|
+
const row = await getDbClient().get<{
|
|
343
|
+
stepStatus: string;
|
|
344
|
+
runId: string;
|
|
345
|
+
runStatus: WorkflowRunStatus;
|
|
346
|
+
}>(
|
|
347
|
+
`SELECT s.status AS stepStatus, r.id AS runId, r.status AS runStatus
|
|
348
|
+
FROM workflow_run_steps s
|
|
349
|
+
JOIN workflow_runs r ON r.id = s.runId
|
|
350
|
+
WHERE s.id = ?`,
|
|
351
|
+
[stepId],
|
|
352
|
+
);
|
|
353
|
+
if (!row) return "the step or its run is missing";
|
|
354
|
+
if (row.runStatus !== "running" && row.runStatus !== "waiting") {
|
|
355
|
+
return `its run is ${row.runStatus}`;
|
|
356
|
+
}
|
|
357
|
+
if (row.stepStatus !== "running") return `the step is ${row.stepStatus}`;
|
|
358
|
+
return getRunLineageBlock(row.runId);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* `sub-workflow` steps left `running` in a live run: each is its node's latest
|
|
363
|
+
* row, so a row a newer one superseded is not listed. The caller skips the
|
|
364
|
+
* ones this process is still executing.
|
|
365
|
+
*/
|
|
366
|
+
export async function getRunningSubWorkflowSteps(): Promise<
|
|
367
|
+
Array<{ stepId: string; runId: string; nodeId: string }>
|
|
368
|
+
> {
|
|
369
|
+
return getDbClient().query<{ stepId: string; runId: string; nodeId: string }>(
|
|
370
|
+
`SELECT s.id AS stepId, s.runId AS runId, s.nodeId AS nodeId
|
|
371
|
+
FROM workflow_run_steps s
|
|
372
|
+
JOIN workflow_runs r ON r.id = s.runId
|
|
373
|
+
WHERE s.status = 'running' AND s.nodeType = 'sub-workflow'
|
|
374
|
+
AND r.status IN ('running', 'waiting')
|
|
375
|
+
AND s.rowid = (
|
|
376
|
+
SELECT MAX(rowid) FROM workflow_run_steps
|
|
377
|
+
WHERE runId = s.runId AND nodeId = s.nodeId
|
|
378
|
+
)`,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
25
382
|
/** Where a child run came from: the `sub-workflow` step that started it and that step's run. */
|
|
26
383
|
export type WorkflowRunParent = {
|
|
27
384
|
runId: string;
|
|
@@ -65,12 +422,13 @@ export async function getWorkflowRunLineage(runId: string, maxDepth: number): Pr
|
|
|
65
422
|
return rows.map((row) => row.workflowId);
|
|
66
423
|
}
|
|
67
424
|
|
|
68
|
-
/** Waiting `sub-workflow` steps whose child run is terminal or deleted. */
|
|
425
|
+
/** Waiting `sub-workflow` steps whose current attempt's child run is terminal or deleted. */
|
|
69
426
|
export async function getSettledChildRunParentSteps(): Promise<string[]> {
|
|
70
427
|
const rows = await getDbClient().query<{ id: string }>(
|
|
71
428
|
`SELECT s.id
|
|
72
429
|
FROM workflow_run_steps s
|
|
73
|
-
LEFT JOIN workflow_runs c
|
|
430
|
+
LEFT JOIN workflow_runs c
|
|
431
|
+
ON c.parentStepId = s.id AND c.parentStepAttempt = s.retryCount
|
|
74
432
|
WHERE s.status = 'waiting' AND s.nodeType = 'sub-workflow'
|
|
75
433
|
AND (c.id IS NULL OR c.status IN ('completed', 'failed', 'cancelled', 'skipped'))`,
|
|
76
434
|
);
|
package/src/be/db.ts
CHANGED
|
@@ -208,6 +208,7 @@ import {
|
|
|
208
208
|
sessionPageFields,
|
|
209
209
|
sessionSummaryQuery,
|
|
210
210
|
} from "./db/session-tree";
|
|
211
|
+
import { searchSkillRowsRanked } from "./db/skill-search";
|
|
211
212
|
import { classifyTaskHumanFree, reclassifyTaskHumanFree } from "./db/tasks/human-free";
|
|
212
213
|
import {
|
|
213
214
|
type AgentTaskRow,
|
|
@@ -221,7 +222,14 @@ import {
|
|
|
221
222
|
rowToAgentTaskSummary,
|
|
222
223
|
} from "./db/tasks/read";
|
|
223
224
|
import { configureTaskWriteDependencies, failTask } from "./db/tasks/write";
|
|
224
|
-
import {
|
|
225
|
+
import { tombstoneWorkflowIdempotencyKeys } from "./db/workflow-idempotency";
|
|
226
|
+
import {
|
|
227
|
+
emitChildRunFinished,
|
|
228
|
+
getRunLineageBlock,
|
|
229
|
+
getStepExecutionBlock,
|
|
230
|
+
WorkflowStepNotExecutableError,
|
|
231
|
+
wakeParentsOfDeletedRuns,
|
|
232
|
+
} from "./db/workflow-runs";
|
|
225
233
|
import { scrubJsonValue } from "./scrub-json";
|
|
226
234
|
import { openSealedJson, sealedJsonForDisplay, sealJson } from "./sealed-json";
|
|
227
235
|
import { configSecretName, registerStoredSecret } from "./secret-registry";
|
|
@@ -2854,6 +2862,14 @@ export async function createTaskExtended(
|
|
|
2854
2862
|
const claim = await getDbClient().transaction<
|
|
2855
2863
|
{ existing: ExistingTrackerContextWork } | { row: AgentTaskRow }
|
|
2856
2864
|
>(async () => {
|
|
2865
|
+
// A workflow step's task commits only while the step may still start
|
|
2866
|
+
// work. Checked in the INSERT's transaction: a cascade cancel of the
|
|
2867
|
+
// step's run tree either commits first and is seen here, or sees and
|
|
2868
|
+
// cancels this task.
|
|
2869
|
+
if (options?.workflowRunStepId) {
|
|
2870
|
+
const blocked = await getStepExecutionBlock(options.workflowRunStepId);
|
|
2871
|
+
if (blocked) throw new WorkflowStepNotExecutableError(options.workflowRunStepId, blocked);
|
|
2872
|
+
}
|
|
2857
2873
|
const existingTrackerWork = options?.bypassTrackerContextDedup
|
|
2858
2874
|
? null
|
|
2859
2875
|
: await findExistingLinearTrackerContextWork(options?.contextKey);
|
|
@@ -4738,7 +4754,7 @@ export async function createSessionLogs(logs: {
|
|
|
4738
4754
|
for (let start = 0; start < logs.lines.length; start += BATCH_SIZE) {
|
|
4739
4755
|
// Yield to the event loop between batches.
|
|
4740
4756
|
if (start > 0) await new Promise<void>((r) => setTimeout(r, 5));
|
|
4741
|
-
const batch:
|
|
4757
|
+
const batch: string[] = [];
|
|
4742
4758
|
const end = Math.min(start + BATCH_SIZE, logs.lines.length);
|
|
4743
4759
|
for (let i = start; i < end; i++) {
|
|
4744
4760
|
const line = logs.lines[i];
|
|
@@ -4747,10 +4763,22 @@ export async function createSessionLogs(logs: {
|
|
|
4747
4763
|
// content that is already scrubbed at the adapter emit site. We scrub
|
|
4748
4764
|
// again here so any future write path that bypasses the adapter still
|
|
4749
4765
|
// lands clean text in the persistent session_logs table.
|
|
4750
|
-
batch.push(
|
|
4766
|
+
batch.push(scrubSecrets(line));
|
|
4751
4767
|
}
|
|
4752
4768
|
await getDbClient().transaction(async (tx) => {
|
|
4753
|
-
|
|
4769
|
+
// The worker flushes one (sessionId, iteration) in many POSTs, so the
|
|
4770
|
+
// lineNumber continues from the newest stored row instead of restarting
|
|
4771
|
+
// at the POST's index. Reading the newest row by rowid walks the
|
|
4772
|
+
// sessionId index backwards and stops at the first hit, where MAX()
|
|
4773
|
+
// would scan the whole session. Rows written here are monotonic in
|
|
4774
|
+
// rowid, so this equals MAX + 1 for them.
|
|
4775
|
+
const last = await tx.get<{ lineNumber: number }>(
|
|
4776
|
+
`SELECT lineNumber FROM session_logs WHERE sessionId = ? AND iteration = ?
|
|
4777
|
+
ORDER BY rowid DESC LIMIT 1`,
|
|
4778
|
+
[logs.sessionId, logs.iteration],
|
|
4779
|
+
);
|
|
4780
|
+
let lineNumber = last ? last.lineNumber + 1 : 0;
|
|
4781
|
+
for (const content of batch) {
|
|
4754
4782
|
await tx.run(
|
|
4755
4783
|
`INSERT INTO session_logs (id, taskId, sessionId, iteration, cli, content, lineNumber, createdAt)
|
|
4756
4784
|
VALUES (?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`,
|
|
@@ -4761,7 +4789,7 @@ export async function createSessionLogs(logs: {
|
|
|
4761
4789
|
logs.iteration,
|
|
4762
4790
|
logs.cli,
|
|
4763
4791
|
content,
|
|
4764
|
-
lineNumber
|
|
4792
|
+
lineNumber++,
|
|
4765
4793
|
],
|
|
4766
4794
|
);
|
|
4767
4795
|
}
|
|
@@ -4769,6 +4797,10 @@ export async function createSessionLogs(logs: {
|
|
|
4769
4797
|
}
|
|
4770
4798
|
}
|
|
4771
4799
|
|
|
4800
|
+
// Session log reads order by rowid, which is insert order. Rows written before
|
|
4801
|
+
// lineNumber was assigned server-side restart at 0 on every worker flush, so
|
|
4802
|
+
// only rowid orders them. The API never runs VACUUM (runbooks/db-retention.md);
|
|
4803
|
+
// a manual VACUUM may renumber rowids but copies rows in rowid order.
|
|
4772
4804
|
export async function getSessionLogsByTaskId(
|
|
4773
4805
|
taskId: string,
|
|
4774
4806
|
limit?: number,
|
|
@@ -4776,16 +4808,16 @@ export async function getSessionLogsByTaskId(
|
|
|
4776
4808
|
if (typeof limit === "number" && limit > 0) {
|
|
4777
4809
|
const rows = await getDbClient().query<SessionLogRow>(
|
|
4778
4810
|
`SELECT * FROM (
|
|
4779
|
-
SELECT * FROM session_logs WHERE taskId = ?
|
|
4780
|
-
ORDER BY
|
|
4811
|
+
SELECT rowid AS seq, * FROM session_logs WHERE taskId = ?
|
|
4812
|
+
ORDER BY rowid DESC
|
|
4781
4813
|
LIMIT ?
|
|
4782
|
-
) ORDER BY
|
|
4814
|
+
) ORDER BY seq ASC`,
|
|
4783
4815
|
[taskId, limit],
|
|
4784
4816
|
);
|
|
4785
4817
|
return rows.map(rowToSessionLog);
|
|
4786
4818
|
}
|
|
4787
4819
|
const rows = await getDbClient().query<SessionLogRow>(
|
|
4788
|
-
"SELECT * FROM session_logs WHERE taskId = ? ORDER BY
|
|
4820
|
+
"SELECT * FROM session_logs WHERE taskId = ? ORDER BY rowid ASC",
|
|
4789
4821
|
[taskId],
|
|
4790
4822
|
);
|
|
4791
4823
|
return rows.map(rowToSessionLog);
|
|
@@ -4796,7 +4828,7 @@ export async function getSessionLogsBySession(
|
|
|
4796
4828
|
iteration: number,
|
|
4797
4829
|
): Promise<SessionLog[]> {
|
|
4798
4830
|
const rows = await getDbClient().query<SessionLogRow>(
|
|
4799
|
-
"SELECT * FROM session_logs WHERE sessionId = ? AND iteration = ? ORDER BY
|
|
4831
|
+
"SELECT * FROM session_logs WHERE sessionId = ? AND iteration = ? ORDER BY rowid ASC",
|
|
4800
4832
|
[sessionId, iteration],
|
|
4801
4833
|
);
|
|
4802
4834
|
return rows.map(rowToSessionLog);
|
|
@@ -7789,7 +7821,9 @@ async function deleteWorkflowRows(id: string, source?: "api" | "mcp"): Promise<b
|
|
|
7789
7821
|
await reclassifyTaskHumanFree(linkedTaskIds);
|
|
7790
7822
|
// 4. Delete every user's favorite of it (no FK to the workflow)
|
|
7791
7823
|
await client.run("DELETE FROM user_favorites WHERE itemType = 'workflow' AND itemId = ?", [id]);
|
|
7792
|
-
// 5.
|
|
7824
|
+
// 5. Keep Idempotency-Keys that created or triggered it taken, without the stored response
|
|
7825
|
+
await tombstoneWorkflowIdempotencyKeys(id);
|
|
7826
|
+
// 6. Delete workflow
|
|
7793
7827
|
const result = await client.run("DELETE FROM workflows WHERE id = ?", [id]);
|
|
7794
7828
|
const deleted = result.changes > 0;
|
|
7795
7829
|
if (deleted) {
|
|
@@ -7842,6 +7876,8 @@ type WorkflowRunRow = {
|
|
|
7842
7876
|
finishedAt: string | null;
|
|
7843
7877
|
/** The `sub-workflow` step that started this run (migration 203). */
|
|
7844
7878
|
parentStepId?: string | null;
|
|
7879
|
+
/** That step's attempt (`retryCount`) when it started this run (migration 240). */
|
|
7880
|
+
parentStepAttempt?: number | null;
|
|
7845
7881
|
};
|
|
7846
7882
|
|
|
7847
7883
|
/**
|
|
@@ -7862,6 +7898,7 @@ function rowToWorkflowRun(row: WorkflowRunRow, view: WorkflowPayloadView = "repl
|
|
|
7862
7898
|
lastUpdatedAt: normalizeDateRequired(row.lastUpdatedAt),
|
|
7863
7899
|
finishedAt: normalizeDate(row.finishedAt) ?? undefined,
|
|
7864
7900
|
parentStepId: row.parentStepId ?? undefined,
|
|
7901
|
+
parentStepAttempt: row.parentStepId ? (row.parentStepAttempt ?? 0) : undefined,
|
|
7865
7902
|
};
|
|
7866
7903
|
return defineWorkflowPayload(run, "context", row.context, row.context_replay, view, {
|
|
7867
7904
|
runId: row.id,
|
|
@@ -7876,10 +7913,12 @@ export async function createWorkflowRun(data: {
|
|
|
7876
7913
|
triggerData?: unknown;
|
|
7877
7914
|
createdBy?: string;
|
|
7878
7915
|
parentStepId?: string;
|
|
7916
|
+
/** The parent step's `retryCount` when it started this run (migration 240). */
|
|
7917
|
+
parentStepAttempt?: number;
|
|
7879
7918
|
}): Promise<WorkflowRun> {
|
|
7880
7919
|
const now = new Date().toISOString();
|
|
7881
7920
|
const row = await getDbClient().get<WorkflowRunRow>(
|
|
7882
|
-
`INSERT INTO workflow_runs (id, workflowId, triggerType, startedAt, triggerData, created_by, parentStepId) VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
7921
|
+
`INSERT INTO workflow_runs (id, workflowId, triggerType, startedAt, triggerData, created_by, parentStepId, parentStepAttempt) VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
7883
7922
|
[
|
|
7884
7923
|
data.id,
|
|
7885
7924
|
data.workflowId,
|
|
@@ -7888,6 +7927,7 @@ export async function createWorkflowRun(data: {
|
|
|
7888
7927
|
data.triggerData ? scrubJsonValue(data.triggerData) : null,
|
|
7889
7928
|
data.createdBy ?? null,
|
|
7890
7929
|
data.parentStepId ?? null,
|
|
7930
|
+
data.parentStepAttempt ?? 0,
|
|
7891
7931
|
],
|
|
7892
7932
|
);
|
|
7893
7933
|
if (!row) throw new Error("Failed to create workflow run");
|
|
@@ -7992,9 +8032,10 @@ export type WorkflowRunListOptions = {
|
|
|
7992
8032
|
};
|
|
7993
8033
|
|
|
7994
8034
|
// Everything but `context`. `triggerData` stays: it is small, and stored
|
|
7995
|
-
// scripts read it off list rows (dedupe by trigger, failure reports).
|
|
8035
|
+
// scripts read it off list rows (dedupe by trigger, failure reports). The
|
|
8036
|
+
// parent link and attempt stay so a list row identifies a child's invocation.
|
|
7996
8037
|
const WORKFLOW_RUN_SUMMARY_COLUMNS =
|
|
7997
|
-
"id, workflowId, status, triggerType, triggerData, error, created_by, startedAt, lastUpdatedAt, finishedAt";
|
|
8038
|
+
"id, workflowId, status, triggerType, triggerData, error, created_by, startedAt, lastUpdatedAt, finishedAt, parentStepId, parentStepAttempt";
|
|
7998
8039
|
|
|
7999
8040
|
export type WorkflowRunPage = {
|
|
8000
8041
|
runs: WorkflowRun[];
|
|
@@ -9779,15 +9820,29 @@ export async function createApprovalRequest(data: {
|
|
|
9779
9820
|
const row = await getDbClient().transaction(async () => {
|
|
9780
9821
|
let status: ApprovalRequest["status"] = "pending";
|
|
9781
9822
|
let resolutionReason: string | null = null;
|
|
9782
|
-
|
|
9783
|
-
|
|
9784
|
-
|
|
9823
|
+
// A request linked to an existing run, with or without a step, is born
|
|
9824
|
+
// cancelled unless the run is live and its tree is not cancelled: a
|
|
9825
|
+
// pending request in a cancelled tree would be open work no cancel may
|
|
9826
|
+
// leave behind. A missing run is refused only on the workflow (HITL) path.
|
|
9827
|
+
const actionable = data.requireActionableWorkflow && data.workflowRunStepId;
|
|
9828
|
+
const run = data.workflowRunId ? await getWorkflowRun(data.workflowRunId, "display") : null;
|
|
9829
|
+
if (data.workflowRunId && (run || actionable)) {
|
|
9830
|
+
const step = actionable ? await getWorkflowRunStep(data.workflowRunStepId!) : undefined;
|
|
9785
9831
|
if (!run || (run.status !== "running" && run.status !== "waiting")) {
|
|
9786
9832
|
status = "cancelled";
|
|
9787
9833
|
resolutionReason = run?.error ?? `Workflow run is ${run?.status ?? "missing"}`;
|
|
9788
|
-
} else if (
|
|
9834
|
+
} else if (
|
|
9835
|
+
step !== undefined &&
|
|
9836
|
+
(!step || step.runId !== run.id || step.status !== "running")
|
|
9837
|
+
) {
|
|
9789
9838
|
status = "cancelled";
|
|
9790
9839
|
resolutionReason = `Human-in-the-loop step is ${step?.status ?? "missing"}`;
|
|
9840
|
+
} else {
|
|
9841
|
+
const blocked = await getRunLineageBlock(run.id);
|
|
9842
|
+
if (blocked) {
|
|
9843
|
+
status = "cancelled";
|
|
9844
|
+
resolutionReason = blocked;
|
|
9845
|
+
}
|
|
9791
9846
|
}
|
|
9792
9847
|
}
|
|
9793
9848
|
|
|
@@ -10989,11 +11044,23 @@ export async function listSkills(filters?: SkillFilters): Promise<Skill[]> {
|
|
|
10989
11044
|
return rows.map(rowToSkill);
|
|
10990
11045
|
}
|
|
10991
11046
|
|
|
11047
|
+
/**
|
|
11048
|
+
* Ranked skill search (see searchSkillRowsRanked). Falls back to the unranked
|
|
11049
|
+
* substring match when the ranked query finds nothing, e.g. a partial word or
|
|
11050
|
+
* a query of only stopwords. The fallback also ranks an exact name first.
|
|
11051
|
+
*/
|
|
10992
11052
|
export async function searchSkills(
|
|
10993
11053
|
query: string,
|
|
10994
11054
|
limit = 20,
|
|
10995
11055
|
includeContent = true,
|
|
10996
11056
|
): Promise<Skill[]> {
|
|
11057
|
+
const ranked = await searchSkillRowsRanked<SkillRow>(
|
|
11058
|
+
query,
|
|
11059
|
+
limit,
|
|
11060
|
+
includeContent === false ? SKILL_SLIM_COLUMNS : "skills.*",
|
|
11061
|
+
);
|
|
11062
|
+
if (ranked.length > 0) return ranked.map(rowToSkill);
|
|
11063
|
+
|
|
10997
11064
|
const tokens = query.split(/\s+/).filter(Boolean);
|
|
10998
11065
|
const columns = includeContent === false ? SKILL_SLIM_COLUMNS : "*";
|
|
10999
11066
|
const searchClauses = tokens.map(() => "(name LIKE ? OR description LIKE ? OR content LIKE ?)");
|
|
@@ -11002,8 +11069,8 @@ export async function searchSkills(
|
|
|
11002
11069
|
return [term, term, term];
|
|
11003
11070
|
});
|
|
11004
11071
|
const rows = await getDbClient().query<SkillRow>(
|
|
11005
|
-
`SELECT ${columns} FROM skills WHERE ${[...searchClauses, "isEnabled = 1"].join(" AND ")} ORDER BY name ASC LIMIT ?`,
|
|
11006
|
-
[...params, limit],
|
|
11072
|
+
`SELECT ${columns} FROM skills WHERE ${[...searchClauses, "isEnabled = 1"].join(" AND ")} ORDER BY lower(name) = lower(?) DESC, name ASC LIMIT ?`,
|
|
11073
|
+
[...params, query.trim(), limit],
|
|
11007
11074
|
);
|
|
11008
11075
|
return rows.map(rowToSkill);
|
|
11009
11076
|
}
|
|
@@ -12872,7 +12939,8 @@ export async function listTaskTemplates(opts?: {
|
|
|
12872
12939
|
/**
|
|
12873
12940
|
* Walk the chain rooted at `rootTaskId`, ordered by `createdAt` (root first; `rowid` breaks
|
|
12874
12941
|
* ties as idx_agent_tasks_created did). `CROSS JOIN` keeps the chain as the outer loop: with
|
|
12875
|
-
* a plain JOIN, SQLite scanned all of agent_tasks to skip the sort.
|
|
12942
|
+
* a plain JOIN, SQLite scanned all of agent_tasks to skip the sort. Each task carries its
|
|
12943
|
+
* `totalCostUsd`, so the session header can total the whole chain, not only the root.
|
|
12876
12944
|
*/
|
|
12877
12945
|
export async function getRootTaskChain(rootTaskId: string): Promise<AgentTask[]> {
|
|
12878
12946
|
const rows = await getDbClient().query<AgentTaskRow>(
|
|
@@ -12882,7 +12950,9 @@ export async function getRootTaskChain(rootTaskId: string): Promise<AgentTask[]>
|
|
|
12882
12950
|
SELECT t.id FROM agent_tasks t
|
|
12883
12951
|
JOIN chain c ON t.parentTaskId = c.id
|
|
12884
12952
|
)
|
|
12885
|
-
SELECT t
|
|
12953
|
+
SELECT t.*,
|
|
12954
|
+
(SELECT SUM(totalCostUsd) FROM session_costs WHERE session_costs.taskId = t.id) AS totalCostUsd
|
|
12955
|
+
FROM chain
|
|
12886
12956
|
CROSS JOIN agent_tasks t ON t.id = chain.id
|
|
12887
12957
|
ORDER BY t.createdAt, t.rowid`,
|
|
12888
12958
|
[rootTaskId],
|