@desplega.ai/agent-swarm 2.5.3 → 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-gr2fcyyj.js → acp-adapter-ek38cwsv.js} +4 -4
- package/dist/{actions-wv766ghf.js → actions-92mepq03.js} +17 -17
- package/dist/{amp-adapter-speb906t.js → amp-adapter-9gaxg015.js} +2 -2
- package/dist/{app-ag29n14f.js → app-ctn8gsgx.js} +3 -3
- package/dist/{assistant-3x8bz3eg.js → assistant-zyr5kz47.js} +19 -19
- package/dist/{boot-reembed-jsmfrpzf.js → boot-reembed-dwzfttdr.js} +1 -1
- package/dist/{boot-reembed-5b3d2eea.js → boot-reembed-w75maga2.js} +1 -1
- package/dist/{boot-scrub-sweep-s73gerxy.js → boot-scrub-sweep-e9kq43kf.js} +3 -3
- package/dist/{boot-seeds-5zmax2wr.js → boot-seeds-x28v3fhb.js} +4 -4
- package/dist/{claude-adapter-hswdnm3t.js → claude-adapter-f0afqnne.js} +4 -4
- package/dist/{claude-managed-adapter-cvbzj1h6.js → claude-managed-adapter-x81a7r65.js} +3 -3
- package/dist/{claude-managed-setup-3k8m3hhc.js → claude-managed-setup-bqzwjd9x.js} +1 -1
- package/dist/{claude-sdk-session-fkbyk66x.js → claude-sdk-session-7b7whkaq.js} +9307 -9254
- package/dist/{cli-a5ebc95n.js → cli-0shx9j93.js} +16 -13
- package/dist/{cli-vwn81jbs.js → cli-19202kew.js} +3 -3
- package/dist/{cli-svrj71pg.js → cli-1r5ad7j5.js} +7 -7
- package/dist/{cli-hhxjnjzs.js → cli-25n3mv5d.js} +8 -6
- package/dist/cli-2655k16q.js +140 -0
- package/dist/{cli-hcpj2xqz.js → cli-277c31dw.js} +1 -1
- package/dist/{cli-35gtk1p6.js → cli-4b2z6n0m.js} +29 -7
- package/dist/{cli-40jsnrjm.js → cli-5bnsc0e1.js} +7 -5
- package/dist/{cli-fzcn3d87.js → cli-621ce7kw.js} +1 -1
- package/dist/{cli-s49hpn37.js → cli-64mh9mf3.js} +19 -5
- package/dist/{cli-e8jyq7d8.js → cli-6a1xg1p7.js} +6 -2
- package/dist/{cli-p9rafdrb.js → cli-7p4jxgtt.js} +4 -4
- package/dist/{cli-w1zwctj2.js → cli-9j129brq.js} +9 -3
- package/dist/{cli-ce0ak9ms.js → cli-abs8ndtr.js} +11 -11
- package/dist/{cli-aecdcr77.js → cli-asq5ydm4.js} +123 -50
- package/dist/{cli-n45mr7zh.js → cli-bvpgr9ny.js} +2 -2
- package/dist/{cli-210xd1f4.js → cli-c61mbdds.js} +3 -3
- 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-s53vp709.js → cli-eftzyh9y.js} +1 -1
- package/dist/{cli-3ngdk5te.js → cli-ethgy7ha.js} +1 -1
- package/dist/{cli-99xgw26r.js → cli-ff1tm9n9.js} +1 -1
- package/dist/{cli-ewm3pwbq.js → cli-g9vd9ece.js} +37 -12
- package/dist/{cli-jjznk3f7.js → cli-gbfxp48k.js} +62 -28
- package/dist/{cli-z21whrrg.js → cli-gn5af0x5.js} +14 -14
- package/dist/{cli-1fc85mvt.js → cli-gwhjmh8g.js} +4 -4
- package/dist/{cli-avdvgcay.js → cli-hh5we352.js} +24 -5
- package/dist/{cli-0ebey9hg.js → cli-j6rktc91.js} +1 -1
- package/dist/{cli-kxawjrxg.js → cli-jb30dzvk.js} +332 -36
- package/dist/{cli-25j01dpj.js → cli-jbnn4vxb.js} +3 -3
- package/dist/{cli-qzme5949.js → cli-k4tjsdx9.js} +44 -39
- package/dist/{cli-8s5wx3nz.js → cli-m4ecdxvz.js} +40 -13
- package/dist/{cli-ta8bd7th.js → cli-pzatvw94.js} +1 -1
- package/dist/{cli-p6zs9h4y.js → cli-rr7zx19j.js} +767 -249
- package/dist/{cli-502eqv93.js → cli-s682zgqz.js} +1 -1
- package/dist/{cli-fsdprwrg.js → cli-sbwsxvtd.js} +1 -1
- package/dist/{cli-0xk505e8.js → cli-sxgtf5fw.js} +1 -1
- package/dist/{cli-egjggehm.js → cli-tsma7p8v.js} +1 -1
- package/dist/{cli-qgf8emrn.js → cli-wghjkr0e.js} +5 -5
- package/dist/{cli-p0rzjkya.js → cli-wx2nj19m.js} +29 -10
- package/dist/{cli-e2ar4bp1.js → cli-x0f2v69c.js} +111 -42
- package/dist/{cli-y561h5a9.js → cli-xtrz6bzs.js} +5 -5
- package/dist/{cli-gpsz7g88.js → cli-xzfq4at3.js} +7 -7
- package/dist/cli.js +18 -18
- package/dist/{codex-adapter-n8ka66vf.js → codex-adapter-wch12q99.js} +2 -2
- package/dist/{codex-hook-e2he7aca.js → codex-hook-brpnycn5.js} +1 -1
- package/dist/{codex-session-runner-fsebts37.js → codex-session-runner-41pfcr95.js} +2 -2
- package/dist/{commands-t04rt2pc.js → commands-9849dkj6.js} +2 -2
- package/dist/{controller-rgna04r2.js → controller-ja0sd95p.js} +27 -1
- package/dist/{controllers-connect-bqm1rtft.js → controllers-connect-xp05dj6x.js} +16 -16
- package/dist/{cursor-adapter-swr2k95y.js → cursor-adapter-623qxqew.js} +1 -2
- package/dist/{db-a4m7haw0.js → db-47vj40dy.js} +3 -3
- package/dist/{db-24ck09xy.js → db-w759qmx0.js} +2 -2
- package/dist/{dispatcher-nwf9ss0q.js → dispatcher-7b8e4z2j.js} +15 -15
- package/dist/{docker-nfy2gc8t.js → docker-s84hjs91.js} +2 -2
- package/dist/{dsh-adapter-kpdnhjst.js → dsh-adapter-2ezw3xea.js} +1 -2
- package/dist/{external-channel-j1e2p34p.js → external-channel-e2s028da.js} +2 -2
- package/dist/{grok-adapter-qtjz0whk.js → grok-adapter-ys7gaqee.js} +5 -5
- package/dist/{handlers-19xr6dnz.js → handlers-wy1dr6ja.js} +18 -18
- package/dist/{heartbeat-vtesda5d.js → heartbeat-w3mntghj.js} +17 -17
- package/dist/{hook-8wqaszda.js → hook-3jjywnpb.js} +9 -3
- package/dist/{hook-b2hhj6rd.js → hook-g7kvv6x9.js} +3 -2
- package/dist/{http-mxcqhhcs.js → http-tkp7d4kb.js} +267 -75
- package/dist/{index-srg522m6.js → index-1ef1myq2.js} +15 -15
- package/dist/{index-1r8wfpzs.js → index-3n8m9ea2.js} +1 -1
- package/dist/{index-kw2mmat7.js → index-nmm79qgr.js} +17 -17
- package/dist/{index-n1vbvta9.js → index-q7m26kvs.js} +24 -22
- package/dist/{index-spwxjgay.js → index-xdaahthb.js} +15 -15
- package/dist/{keepalive-dsd8rtgx.js → keepalive-v4ta8wgg.js} +3 -3
- package/dist/{lead-zyerk1x2.js → lead-sawnyqxk.js} +33 -33
- package/dist/{maintenance-jqmqwnet.js → maintenance-84n1ex21.js} +1 -1
- package/dist/{oauth-refresh-sweep-q2c3bnvm.js → oauth-refresh-sweep-vjnnandw.js} +2 -2
- package/dist/{onboard-w4yd7s8t.js → onboard-sfmpe4np.js} +2 -2
- package/dist/{opencode-adapter-9tppvxar.js → opencode-adapter-jmpst4q4.js} +3 -3
- package/dist/{otel-impl-f36byzbd.js → otel-impl-v6cc0zve.js} +1 -1
- package/dist/{pi-mono-adapter-tqsge1ay.js → pi-mono-adapter-fk8fkcf6.js} +33 -6
- package/dist/{pricing-refresh-xvak5p4v.js → pricing-refresh-fsk1e1zc.js} +3 -3
- package/dist/{rbac-roles-gfn30fb8.js → rbac-roles-prmkdnkw.js} +2 -2
- package/dist/{rbac-roles-682k2mt5.js → rbac-roles-zxmxqa39.js} +1 -1
- package/dist/{render-v2-hgytz8nn.js → render-v2-pjwxvnkw.js} +3 -3
- package/dist/{resume-2dvq348g.js → resume-frdtgfa0.js} +8 -6
- package/dist/{session-access-7f21ekq9.js → session-access-fpez4y5q.js} +4 -4
- package/dist/{setup-epffxtbk.js → setup-mq6m3m8q.js} +1 -1
- package/dist/{work-objects-xz1a5rqk.js → work-objects-kfahhjsq.js} +18 -18
- package/dist/{worker-42t8emjt.js → worker-v5e5rxj3.js} +33 -33
- package/openapi.json +68 -3
- package/package.json +5 -5
- package/plugin/commands/create-pr.md +2 -2
- package/plugin/pi-skills/create-pr/SKILL.md +2 -2
- package/src/be/db/approvals.ts +48 -0
- 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 +318 -7
- package/src/be/db.ts +67 -17
- 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 +2 -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/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/config.ts +48 -16
- package/src/http/idempotency-key.ts +117 -0
- package/src/http/workflows.ts +129 -24
- 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/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-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/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/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/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 +3 -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 +37 -0
- 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/tools/workflows/cancel-workflow-run.ts +1 -1
- package/src/utils/context-window.ts +8 -6
- package/src/workflows/approval-cancel.ts +15 -13
- package/src/workflows/approval-notifications.ts +5 -1
- package/src/workflows/checkpoint.ts +6 -3
- package/src/workflows/engine.ts +30 -8
- package/src/workflows/executors/foreach.ts +2 -2
- package/src/workflows/foreach-driver.ts +10 -3
- package/src/workflows/foreach-join.ts +29 -6
- package/src/workflows/recovery.ts +6 -1
- package/src/workflows/resume.ts +168 -56
- package/src/workflows/retry-poller.ts +49 -3
- 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/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/work-on-task/SKILL.md +1 -1
- package/templates/skills/work-on-task/content.md +1 -1
package/src/workflows/resume.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type ApprovalRequest,
|
|
2
3
|
cancelPendingApprovalRequestsForRun,
|
|
3
4
|
cancelTask,
|
|
5
|
+
getApprovalRequestById,
|
|
4
6
|
getCompletedStepNodeIds,
|
|
5
7
|
getDbClient,
|
|
6
8
|
getPendingEventWaitNames,
|
|
@@ -11,13 +13,25 @@ import {
|
|
|
11
13
|
getWorkflowRun,
|
|
12
14
|
getWorkflowRunStep,
|
|
13
15
|
getWorkflowRunStepsByRunId,
|
|
14
|
-
listCancelledApprovalRequestsForRun,
|
|
15
16
|
listCancelledApprovalRequestsForStep,
|
|
16
17
|
resolveWaitState,
|
|
17
18
|
updateWorkflowRun,
|
|
18
19
|
updateWorkflowRunStep,
|
|
19
20
|
} from "../be/db";
|
|
20
|
-
import {
|
|
21
|
+
import { listUndeliveredCancelledApprovalRequestIds } from "../be/db/approvals";
|
|
22
|
+
import {
|
|
23
|
+
cancelLiveWorkflowTreeRuns,
|
|
24
|
+
cancelOpenWorkflowTreeSteps,
|
|
25
|
+
createForeachAttemptSteps,
|
|
26
|
+
disarmWorkflowTreeRetries,
|
|
27
|
+
getChildWorkflowRunId,
|
|
28
|
+
getOpenWorkflowTreeTaskIds,
|
|
29
|
+
getRunLineageBlock,
|
|
30
|
+
getTaskIdsWithDependents,
|
|
31
|
+
getWorkflowRunStatus,
|
|
32
|
+
getWorkflowRunTreeIds,
|
|
33
|
+
workflowRunTreeHasOpenWork,
|
|
34
|
+
} from "../be/db/workflow-runs";
|
|
21
35
|
import {
|
|
22
36
|
type RetryPolicy,
|
|
23
37
|
RetryPolicySchema,
|
|
@@ -48,6 +62,7 @@ import { startForeachDrive } from "./foreach-driver";
|
|
|
48
62
|
import {
|
|
49
63
|
FOREACH_TERMINAL_STEP_STATUSES,
|
|
50
64
|
type ForeachChildOutcome,
|
|
65
|
+
isRetryPending,
|
|
51
66
|
parseSyntheticNodeId,
|
|
52
67
|
resolveForeachParent,
|
|
53
68
|
settleForeachChild,
|
|
@@ -278,12 +293,16 @@ export async function finalizeOrWait(runId: string): Promise<void> {
|
|
|
278
293
|
// move a step into `waiting` between the read and the write, and a run
|
|
279
294
|
// finalized from a stale snapshot strands that branch.
|
|
280
295
|
await getDbClient().transaction(async () => {
|
|
296
|
+
// A cancel (or another finalizer) that committed after the caller's claim
|
|
297
|
+
// wins: a terminal run is never re-parked or completed from here.
|
|
298
|
+
const status = await getWorkflowRunStatus(runId);
|
|
299
|
+
if (status !== "running" && status !== "waiting") return;
|
|
300
|
+
if (await getRunLineageBlock(runId)) return;
|
|
281
301
|
const steps = await getWorkflowRunStepsByRunId(runId);
|
|
282
302
|
// A branch still executing belongs to a live walk, which finalizes the
|
|
283
303
|
// run itself; its finalizer only acts on a `running` run.
|
|
284
304
|
if (hasRunningStep(steps)) {
|
|
285
|
-
|
|
286
|
-
if (run?.status === "waiting") await updateWorkflowRun(runId, { status: "running" });
|
|
305
|
+
if (status === "waiting") await updateWorkflowRun(runId, { status: "running" });
|
|
287
306
|
return;
|
|
288
307
|
}
|
|
289
308
|
// A step queued for the retry poller is still live, like a waiting one.
|
|
@@ -521,6 +540,10 @@ export async function retryFailedRun(runId: string, registry: ExecutorRegistry):
|
|
|
521
540
|
const attempts = await getDbClient().transaction(async () => {
|
|
522
541
|
const current = await getWorkflowRun(runId);
|
|
523
542
|
if (!current || current.status !== "failed") return null;
|
|
543
|
+
// A failed run inside a cancelled tree keeps its failure; the cancel
|
|
544
|
+
// forbids starting it again.
|
|
545
|
+
const blocked = await getRunLineageBlock(runId);
|
|
546
|
+
if (blocked) throw new Error(`Retry not started, run left failed: ${blocked}`);
|
|
524
547
|
// Fan-outs with failed or cancelled items get one new attempt row per such
|
|
525
548
|
// item; completed items are never rerun. Selected from the rows as of this
|
|
526
549
|
// transaction: an in-flight sibling of a fail-fast fan-out can record its
|
|
@@ -531,6 +554,13 @@ export async function retryFailedRun(runId: string, registry: ExecutorRegistry):
|
|
|
531
554
|
]);
|
|
532
555
|
const capError = foreachRetryCapError(currentSteps.length, attempts);
|
|
533
556
|
if (capError) throw new Error(`Retry not started, run left failed: ${capError}`);
|
|
557
|
+
// An item retry still queued when the run failed gets a new attempt row
|
|
558
|
+
// below. Retire the queued one, or the poller starts the old row beside it.
|
|
559
|
+
for (const step of currentSteps) {
|
|
560
|
+
if (isRetryPending(step) && resolveForeachParent(workflow.definition, step.nodeId)) {
|
|
561
|
+
await updateWorkflowRunStep(step.id, { nextRetryAt: null });
|
|
562
|
+
}
|
|
563
|
+
}
|
|
534
564
|
// A foreach item retries as a new attempt row instead of a reset: a fresh
|
|
535
565
|
// step id is a fresh invocation identity for every body type, and the
|
|
536
566
|
// failed row stays as history.
|
|
@@ -639,82 +669,162 @@ function foreachRetryCapError(
|
|
|
639
669
|
return `foreach retry would reach ${maxSteps} total steps (WORKFLOW_MAX_STEPS_PER_RUN): ${existingSteps} existing + ${added} new attempts leaves no room for the retry walk`;
|
|
640
670
|
}
|
|
641
671
|
|
|
672
|
+
const TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "cancelled", "skipped"]);
|
|
673
|
+
|
|
674
|
+
/** What one `cancelWorkflowRunRows` call covered. */
|
|
675
|
+
export interface WorkflowRunCancelResult {
|
|
676
|
+
/** True when this call moved the root run to `cancelled`. */
|
|
677
|
+
cancelled: boolean;
|
|
678
|
+
/**
|
|
679
|
+
* The root and every run below it, root first, for the caller's Slack
|
|
680
|
+
* posts. Also set for a repeat cancel that found nothing open, so posts a
|
|
681
|
+
* failed earlier cancel left undelivered are retried. Empty when the root
|
|
682
|
+
* is missing or completed, failed or skipped.
|
|
683
|
+
*/
|
|
684
|
+
runIds: string[];
|
|
685
|
+
}
|
|
686
|
+
|
|
642
687
|
/**
|
|
643
|
-
* The DB writes of a run cancel, with no Slack post.
|
|
644
|
-
*
|
|
645
|
-
*
|
|
688
|
+
* The DB writes of a run cancel, with no Slack post. Cancels the run and its
|
|
689
|
+
* whole child tree (every attempt's child, through terminal runs) in one
|
|
690
|
+
* transaction:
|
|
691
|
+
* - every non-terminal step, its task, and the run's other open tasks;
|
|
692
|
+
* - pending approvals and queued step retries;
|
|
693
|
+
* - every non-terminal run. A terminal descendant keeps its status, result
|
|
694
|
+
* and reason; only its leftover open work is cancelled.
|
|
695
|
+
*
|
|
696
|
+
* A missing root, or one that is completed, failed or skipped, cancels
|
|
697
|
+
* nothing. An already cancelled root is walked again, so a repeat cancel
|
|
698
|
+
* finishes descendants an earlier cancel missed. A tree with a cycle or past
|
|
699
|
+
* its bounds throws and nothing commits.
|
|
700
|
+
*
|
|
701
|
+
* Called inside a caller's transaction, the writes join it (as a SAVEPOINT)
|
|
702
|
+
* and commit or roll back with it. Task lifecycle events and worker nudges
|
|
703
|
+
* queue behind COMMIT (afterCommit) and drop on rollback.
|
|
646
704
|
*/
|
|
647
|
-
export async function cancelWorkflowRunRows(
|
|
648
|
-
|
|
705
|
+
export async function cancelWorkflowRunRows(
|
|
706
|
+
runId: string,
|
|
707
|
+
cancelReason: string,
|
|
708
|
+
): Promise<WorkflowRunCancelResult> {
|
|
649
709
|
const now = new Date().toISOString();
|
|
650
710
|
|
|
651
|
-
//
|
|
652
|
-
//
|
|
653
|
-
//
|
|
654
|
-
//
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
const task = await getTaskByWorkflowRunStepId(step.id);
|
|
666
|
-
if (task) {
|
|
667
|
-
await cancelTask(task.id, cancelReason);
|
|
668
|
-
}
|
|
711
|
+
// A repeat cancel of a tree with nothing left open is answered by reads
|
|
712
|
+
// alone, without the write lock. The guards keep a cancelled tree from
|
|
713
|
+
// gaining new work, so the answer still holds when this returns.
|
|
714
|
+
// Its run ids still go back, so the caller retries Slack posts an earlier
|
|
715
|
+
// cancel could not deliver.
|
|
716
|
+
const settledTree = await getDbClient().transaction(
|
|
717
|
+
async () => {
|
|
718
|
+
if ((await getWorkflowRunStatus(runId)) !== "cancelled") return null;
|
|
719
|
+
const ids = await getWorkflowRunTreeIds(runId);
|
|
720
|
+
return (await workflowRunTreeHasOpenWork(ids)) ? null : ids;
|
|
721
|
+
},
|
|
722
|
+
{ readOnly: true },
|
|
723
|
+
);
|
|
724
|
+
if (settledTree) return { cancelled: false, runIds: settledTree };
|
|
669
725
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
726
|
+
// The tree snapshot and every write commit together. A child run, task or
|
|
727
|
+
// step that commits first is in the snapshot; one that commits after sees
|
|
728
|
+
// the cancelled rows in its own guard (getRunLineageBlock and the step
|
|
729
|
+
// claims) and is refused.
|
|
730
|
+
return await getDbClient().transaction(async () => {
|
|
731
|
+
const rootStatus = await getWorkflowRunStatus(runId);
|
|
732
|
+
if (!rootStatus || (TERMINAL_RUN_STATUSES.has(rootStatus) && rootStatus !== "cancelled")) {
|
|
733
|
+
return { cancelled: false, runIds: [] };
|
|
675
734
|
}
|
|
676
735
|
|
|
677
|
-
await
|
|
678
|
-
|
|
679
|
-
//
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
736
|
+
const runIds = await getWorkflowRunTreeIds(runId);
|
|
737
|
+
// Every step task carries the run link (indexed), and a retry detaches
|
|
738
|
+
// only the step link, so this covers current and earlier attempts' tasks.
|
|
739
|
+
// Dependents are looked up once for the whole tree, and only when needed.
|
|
740
|
+
const openTaskIds = await getOpenWorkflowTreeTaskIds(runIds);
|
|
741
|
+
const withDependents =
|
|
742
|
+
openTaskIds.length > 0 ? await getTaskIdsWithDependents() : new Set<string>();
|
|
743
|
+
for (const taskId of openTaskIds) {
|
|
744
|
+
await cancelTask(taskId, cancelReason, { cascadeDependents: withDependents.has(taskId) });
|
|
745
|
+
}
|
|
746
|
+
await cancelOpenWorkflowTreeSteps(runIds, cancelReason, now);
|
|
747
|
+
await disarmWorkflowTreeRetries(runIds, now);
|
|
748
|
+
for (const id of runIds) await cancelPendingApprovalRequestsForRun(id, cancelReason);
|
|
749
|
+
// Descendants' parent steps were just cancelled, so only the root's own
|
|
750
|
+
// parent (when the root is a child) needs the child-finished wake-up.
|
|
751
|
+
await cancelLiveWorkflowTreeRuns(runIds.slice(1), cancelReason, now);
|
|
752
|
+
if (rootStatus !== "cancelled") {
|
|
753
|
+
await updateWorkflowRun(runId, { status: "cancelled", error: cancelReason, finishedAt: now });
|
|
754
|
+
}
|
|
755
|
+
return { cancelled: rootStatus !== "cancelled", runIds };
|
|
686
756
|
});
|
|
687
757
|
}
|
|
688
758
|
|
|
759
|
+
// Cancels in flight in this process, by run id. The API process owns the
|
|
760
|
+
// database, so this bounds concurrent walks of one tree.
|
|
761
|
+
const inFlightCancels = new Map<string, Promise<void>>();
|
|
762
|
+
|
|
689
763
|
/**
|
|
690
|
-
* Cancel a workflow run and
|
|
691
|
-
*
|
|
764
|
+
* Cancel a workflow run and its child tree (see `cancelWorkflowRunRows`), then
|
|
765
|
+
* post the Slack updates for every approval the cancel closed. The posts run
|
|
766
|
+
* after COMMIT, and a failed post is logged without undoing the cancel.
|
|
767
|
+
* Concurrent calls for the same run share one cancel and its result.
|
|
692
768
|
*/
|
|
693
|
-
export
|
|
694
|
-
const
|
|
769
|
+
export function cancelWorkflowRun(runId: string, reason?: string): Promise<void> {
|
|
770
|
+
const inFlight = inFlightCancels.get(runId);
|
|
771
|
+
if (inFlight) return inFlight;
|
|
772
|
+
const cancel = cancelWorkflowRunOnce(runId, reason).finally(() => {
|
|
773
|
+
inFlightCancels.delete(runId);
|
|
774
|
+
});
|
|
775
|
+
inFlightCancels.set(runId, cancel);
|
|
776
|
+
return cancel;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
async function cancelWorkflowRunOnce(runId: string, reason?: string): Promise<void> {
|
|
780
|
+
const run = await getWorkflowRun(runId, "display");
|
|
695
781
|
if (!run) throw new Error("Workflow run not found");
|
|
696
782
|
|
|
697
|
-
|
|
698
|
-
if (terminalStatuses.includes(run.status) && run.status !== "cancelled") {
|
|
783
|
+
if (TERMINAL_RUN_STATUSES.has(run.status) && run.status !== "cancelled") {
|
|
699
784
|
throw new Error(`Cannot cancel run in '${run.status}' state`);
|
|
700
785
|
}
|
|
701
786
|
|
|
702
787
|
const cancelReason = reason ?? "Cancelled by user";
|
|
703
|
-
const
|
|
788
|
+
const { cancelled, runIds } = await cancelWorkflowRunRows(runId, cancelReason);
|
|
704
789
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
const cancelledApprovals = await listCancelledApprovalRequestsForRun(runId);
|
|
708
|
-
const latestRun = await getWorkflowRun(runId);
|
|
709
|
-
if (!applied && latestRun?.status !== "cancelled") {
|
|
790
|
+
const latestRun = await getWorkflowRun(runId, "display");
|
|
791
|
+
if (!cancelled && latestRun?.status !== "cancelled") {
|
|
710
792
|
throw new Error(`Cannot cancel run in '${latestRun?.status ?? "missing"}' state`);
|
|
711
793
|
}
|
|
712
|
-
await
|
|
713
|
-
|
|
714
|
-
|
|
794
|
+
await postTreeApprovalCancellations(
|
|
795
|
+
runIds,
|
|
796
|
+
cancelled ? cancelReason : (latestRun?.error ?? cancelReason),
|
|
715
797
|
);
|
|
716
798
|
}
|
|
717
799
|
|
|
800
|
+
/**
|
|
801
|
+
* Post the cancellation update of every cancelled approval in the given runs
|
|
802
|
+
* whose Slack post is not delivered yet. Re-read after commit: the migration
|
|
803
|
+
* trigger also cancels the approvals of a directly cancelled step. Each post
|
|
804
|
+
* is claimed, and a failed post releases its claim, so the next cancel of the
|
|
805
|
+
* tree retries it and a delivered post is never repeated.
|
|
806
|
+
*/
|
|
807
|
+
export async function postTreeApprovalCancellations(
|
|
808
|
+
runIds: string[],
|
|
809
|
+
reason: string,
|
|
810
|
+
options: { exceptRequestId?: string } = {},
|
|
811
|
+
): Promise<void> {
|
|
812
|
+
try {
|
|
813
|
+
const approvals: ApprovalRequest[] = [];
|
|
814
|
+
for (const id of await listUndeliveredCancelledApprovalRequestIds(runIds)) {
|
|
815
|
+
if (id === options.exceptRequestId) continue;
|
|
816
|
+
const approval = await getApprovalRequestById(id);
|
|
817
|
+
if (approval) approvals.push(approval);
|
|
818
|
+
}
|
|
819
|
+
if (approvals.length > 0) await postApprovalCancellationUpdates(approvals, reason);
|
|
820
|
+
} catch (err) {
|
|
821
|
+
console.error(
|
|
822
|
+
"[workflows] approval cancellation updates not posted:",
|
|
823
|
+
scrubSecrets(err instanceof Error ? err.message : String(err)),
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
718
828
|
/** Cancel one HITL step and close any approval request it gates. */
|
|
719
829
|
export async function cancelWorkflowRunStep(
|
|
720
830
|
stepId: string,
|
|
@@ -993,6 +1103,8 @@ export async function resumeFromChildRun(
|
|
|
993
1103
|
const routing = await getDbClient().transaction(async () => {
|
|
994
1104
|
const current = await getWorkflowRunStep(step.id);
|
|
995
1105
|
if (!current || current.status !== "waiting") return null;
|
|
1106
|
+
// Inside a cancelled tree a child's end settles nothing and queues no retry.
|
|
1107
|
+
if (await getRunLineageBlock(run.id)) return null;
|
|
996
1108
|
const childId = await getChildWorkflowRunId(step.id, current.retryCount);
|
|
997
1109
|
const child = childId ? await getWorkflowRun(childId) : null;
|
|
998
1110
|
// The child row exists before the step parks, so a missing one was deleted.
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
getWorkflowRunStepsByRunId,
|
|
7
7
|
updateWorkflowRun,
|
|
8
8
|
} from "../be/db";
|
|
9
|
+
import { disarmWorkflowTreeRetries, getRunLineageBlock } from "../be/db/workflow-runs";
|
|
9
10
|
import type { RetryPolicy } from "../types";
|
|
10
11
|
import { checkpointStep, checkpointStepFailure, checkpointStepWaiting } from "./checkpoint";
|
|
11
12
|
import { getSuccessors, resolveValidationPort } from "./definition";
|
|
@@ -17,7 +18,8 @@ import {
|
|
|
17
18
|
} from "./engine";
|
|
18
19
|
import type { AsyncExecutorResult } from "./executors/base";
|
|
19
20
|
import type { ExecutorRegistry } from "./executors/registry";
|
|
20
|
-
import { FOREACH_ITEMS_WAIT_FOR, parkAndDriveForeach } from "./foreach-driver";
|
|
21
|
+
import { FOREACH_ITEMS_WAIT_FOR, parkAndDriveForeach, runForeachDrive } from "./foreach-driver";
|
|
22
|
+
import { resolveForeachParent } from "./foreach-join";
|
|
21
23
|
import { rebuildNodeExecutionInput } from "./step-input";
|
|
22
24
|
import { runStepValidation } from "./validation";
|
|
23
25
|
|
|
@@ -45,6 +47,19 @@ export function startRetryPoller(registry: ExecutorRegistry, intervalMs = 5000):
|
|
|
45
47
|
const workflow = await getWorkflow(run.workflowId);
|
|
46
48
|
if (!workflow) continue;
|
|
47
49
|
|
|
50
|
+
// A foreach child goes back to its fan-out's queue: the driver
|
|
51
|
+
// rebuilds the per-item config and starts it under the concurrency
|
|
52
|
+
// cap, and the item settles into the join like any other.
|
|
53
|
+
const foreachParent = resolveForeachParent(workflow.definition, step.nodeId);
|
|
54
|
+
if (foreachParent) {
|
|
55
|
+
if (!(await claimRetry(run.id, step.id, { requeueForeachChild: true }))) continue;
|
|
56
|
+
console.log(
|
|
57
|
+
`[workflows] Retrying foreach item ${step.nodeId} (attempt ${step.retryCount}) for run ${step.runId}`,
|
|
58
|
+
);
|
|
59
|
+
await runForeachDrive(run.id, foreachParent.id, registry);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
48
63
|
// Find the node definition for this step
|
|
49
64
|
const node = workflow.definition.nodes.find((n) => n.id === step.nodeId);
|
|
50
65
|
if (!node) continue;
|
|
@@ -264,14 +279,44 @@ export function startRetryPoller(registry: ExecutorRegistry, intervalMs = 5000):
|
|
|
264
279
|
/**
|
|
265
280
|
* Flip a retry-pending step to `running` only while it is still retry-pending
|
|
266
281
|
* and its run is live, and revive a run failed by this step.
|
|
282
|
+
*
|
|
283
|
+
* `requeueForeachChild` flips a foreach child to `pending` instead, for the
|
|
284
|
+
* driver to claim, and clears the cut's error: the join reads `error` on a
|
|
285
|
+
* completed child as a failed item, so a stale one would count the retry's
|
|
286
|
+
* success as a failure.
|
|
287
|
+
*
|
|
288
|
+
* A queued child never revives a failed run: its retry does not settle, so
|
|
289
|
+
* something else failed the run. The queued retry is retired instead, leaving
|
|
290
|
+
* the item `failed` with the cut's error for a run retry to give a new attempt.
|
|
291
|
+
*
|
|
292
|
+
* A run inside a cancelled tree is never revived: its queued retry is dropped.
|
|
267
293
|
*/
|
|
268
|
-
async function claimRetry(
|
|
294
|
+
async function claimRetry(
|
|
295
|
+
runId: string,
|
|
296
|
+
stepId: string,
|
|
297
|
+
options: { requeueForeachChild?: boolean } = {},
|
|
298
|
+
): Promise<boolean> {
|
|
269
299
|
return getDbClient().transaction(async () => {
|
|
270
300
|
const run = await getWorkflowRun(runId);
|
|
271
301
|
if (!run || !RETRYABLE_RUN_STATUSES.has(run.status)) return false;
|
|
302
|
+
if (await getRunLineageBlock(runId)) {
|
|
303
|
+
await disarmWorkflowTreeRetries([runId], new Date().toISOString());
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
if (options.requeueForeachChild && run.status === "failed") {
|
|
307
|
+
await getDbClient().run(
|
|
308
|
+
`UPDATE workflow_run_steps SET nextRetryAt = NULL, finishedAt = ?
|
|
309
|
+
WHERE id = ? AND runId = ? AND status = 'failed' AND nextRetryAt IS NOT NULL`,
|
|
310
|
+
[new Date().toISOString(), stepId, runId],
|
|
311
|
+
);
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
const set = options.requeueForeachChild
|
|
315
|
+
? "status = 'pending', nextRetryAt = NULL, error = NULL"
|
|
316
|
+
: "status = 'running', nextRetryAt = NULL";
|
|
272
317
|
const claimed = await getDbClient().get<{ id: string }>(
|
|
273
318
|
`UPDATE workflow_run_steps
|
|
274
|
-
SET
|
|
319
|
+
SET ${set}
|
|
275
320
|
WHERE id = ? AND runId = ? AND status = 'failed' AND nextRetryAt IS NOT NULL
|
|
276
321
|
RETURNING id`,
|
|
277
322
|
[stepId, runId],
|
|
@@ -293,6 +338,7 @@ async function completeRunIfSettled(runId: string, ctx: Record<string, unknown>)
|
|
|
293
338
|
await getDbClient().transaction(async () => {
|
|
294
339
|
const run = await getWorkflowRun(runId);
|
|
295
340
|
if (!run || (run.status !== "running" && run.status !== "waiting")) return;
|
|
341
|
+
if (await getRunLineageBlock(runId)) return;
|
|
296
342
|
const steps = await getWorkflowRunStepsByRunId(runId);
|
|
297
343
|
const live = steps.some(
|
|
298
344
|
(s) =>
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
updateWorkflowRun,
|
|
8
8
|
updateWorkflowRunStep,
|
|
9
9
|
} from "../be/db";
|
|
10
|
+
import { getRunLineageBlock } from "../be/db/workflow-runs";
|
|
10
11
|
import {
|
|
11
12
|
type RetryPolicy,
|
|
12
13
|
RetryPolicySchema,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
import { checkpointStep, checkpointStepFailure } from "./checkpoint";
|
|
19
20
|
import { getSuccessors } from "./definition";
|
|
20
21
|
import type { ExecutorRegistry } from "./executors/registry";
|
|
22
|
+
import { resolveStepNode } from "./foreach-body";
|
|
21
23
|
import { type ForeachSettleResult, resolveForeachParent, settleForeachChild } from "./foreach-join";
|
|
22
24
|
|
|
23
25
|
export interface TaskStepRoutingResult {
|
|
@@ -146,6 +148,7 @@ export async function checkpointPortStepAndResolveSuccessors(
|
|
|
146
148
|
if (!run || (run.status !== "waiting" && run.status !== "running")) {
|
|
147
149
|
return UNCLAIMED;
|
|
148
150
|
}
|
|
151
|
+
if (await getRunLineageBlock(runId)) return UNCLAIMED;
|
|
149
152
|
// A foreach child's port (approval rejected/timeout, wait event/timeout)
|
|
150
153
|
// is stored for display and still counts as a completed item: the body's
|
|
151
154
|
// ports do not route, the item's consumer reads the output.
|
|
@@ -182,8 +185,8 @@ export async function checkpointPortStepAndResolveSuccessors(
|
|
|
182
185
|
* or the failed task is no longer the one bound to the step (it was
|
|
183
186
|
* already retried and a fresh task now owns it); the caller stops.
|
|
184
187
|
* - `not-eligible`: no retry applies (no `node.retry` and not a supersede
|
|
185
|
-
* cut, not an agent-task node,
|
|
186
|
-
* `onNodeFailure` as before.
|
|
188
|
+
* cut, not an agent-task node, retries exhausted, or a foreach child whose
|
|
189
|
+
* run is no longer live); the caller applies `onNodeFailure` as before.
|
|
187
190
|
*/
|
|
188
191
|
export type TaskStepRetryOutcome = "scheduled" | "not-claimed" | "not-eligible";
|
|
189
192
|
|
|
@@ -217,8 +220,13 @@ export const SUPERSEDE_RETRY_POLICY: RetryPolicy = {
|
|
|
217
220
|
* only after ownership is confirmed: a stale caller's step snapshot may carry
|
|
218
221
|
* the counter of a newer attempt.
|
|
219
222
|
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
223
|
+
* A foreach child (`<foreachId>#<key>`) with an `agent-task` body takes the
|
|
224
|
+
* parent foreach node's `retry`, else the same supersede default. While queued
|
|
225
|
+
* it does not settle, so the join stays open; the retry poller re-queues it and
|
|
226
|
+
* the foreach driver starts it under the fan-out's concurrency cap. A child is
|
|
227
|
+
* queued only while its run is live. Exhausted retries, a run that is no longer
|
|
228
|
+
* live and every other failure settle the item through `onNodeFailure` and the
|
|
229
|
+
* fan-out's failure policy as before.
|
|
222
230
|
*/
|
|
223
231
|
export async function scheduleTaskStepRetry(
|
|
224
232
|
def: WorkflowDefinition,
|
|
@@ -227,11 +235,14 @@ export async function scheduleTaskStepRetry(
|
|
|
227
235
|
taskId: string,
|
|
228
236
|
reason: string,
|
|
229
237
|
): Promise<TaskStepRetryOutcome> {
|
|
230
|
-
const
|
|
238
|
+
const foreachParent = resolveForeachParent(def, step.nodeId);
|
|
239
|
+
const node = resolveStepNode(def, step.nodeId);
|
|
231
240
|
if (!node || node.type !== "agent-task") return "not-eligible";
|
|
241
|
+
// A synthesized child node carries only the body; its retry lives on the parent.
|
|
242
|
+
const declaredRetry = foreachParent ? foreachParent.retry : node.retry;
|
|
232
243
|
let policy: RetryPolicy;
|
|
233
|
-
if (
|
|
234
|
-
const parsed = RetryPolicySchema.safeParse(
|
|
244
|
+
if (declaredRetry) {
|
|
245
|
+
const parsed = RetryPolicySchema.safeParse(declaredRetry);
|
|
235
246
|
if (!parsed.success) return "not-eligible";
|
|
236
247
|
policy = parsed.data;
|
|
237
248
|
} else {
|
|
@@ -243,6 +254,15 @@ export async function scheduleTaskStepRetry(
|
|
|
243
254
|
const current = await getWorkflowRunStep(step.id);
|
|
244
255
|
if (!current || current.status !== "waiting") return "not-claimed";
|
|
245
256
|
if (!(await isTaskBoundToStep(taskId, step.id))) return "not-claimed";
|
|
257
|
+
// A sibling's fail-fast or another branch can fail the run while the
|
|
258
|
+
// caller awaited. A child queued then would revive the failed run, so the
|
|
259
|
+
// caller records the cut as the item's late outcome instead.
|
|
260
|
+
if (foreachParent) {
|
|
261
|
+
const run = await getWorkflowRun(runId);
|
|
262
|
+
if (run?.status !== "running" && run?.status !== "waiting") return "not-eligible";
|
|
263
|
+
}
|
|
264
|
+
// No retry is queued inside a cancelled tree.
|
|
265
|
+
if (await getRunLineageBlock(runId)) return "not-claimed";
|
|
246
266
|
if (current.retryCount >= policy.maxRetries) return "not-eligible";
|
|
247
267
|
await detachTaskFromWorkflowRunStep(taskId);
|
|
248
268
|
await checkpointStepFailure(runId, step.id, reason, current.retryCount, policy);
|
|
@@ -280,6 +300,8 @@ export async function completeTaskStepAndResolveSuccessors(
|
|
|
280
300
|
if (!current || current.status !== "waiting") return UNCLAIMED;
|
|
281
301
|
// A stale snapshot of a step since redriven to a fresh task must not route it.
|
|
282
302
|
if (ownerTaskId && !(await isTaskBoundToStep(ownerTaskId, step.id))) return UNCLAIMED;
|
|
303
|
+
// A late result inside a cancelled tree routes nothing and revives nothing.
|
|
304
|
+
if (await getRunLineageBlock(runId)) return UNCLAIMED;
|
|
283
305
|
|
|
284
306
|
if (resolveForeachParent(def, step.nodeId)) {
|
|
285
307
|
// onNodeFailure:"continue" completions persist the failure reason as
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
2
|
+
"commit": "5a577022800d9b43ccec5376bfe16c7b24eecd91",
|
|
3
3
|
"excludedSkills": [
|
|
4
4
|
"feedback"
|
|
5
5
|
],
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
"templates/skills/comms/files/references/ste-rules.md": "6ee2ed5d6b49d2e421b6c9d22310a5b589fa1abe81de06213e4b89fbc1271e74",
|
|
17
17
|
"templates/skills/comms/files/references/visual-shapes.md": "5e07e9250ad4cdc877af2281525185f0de89c01f90a077463a2c77c8a7b998e8",
|
|
18
18
|
"templates/skills/delegate-work/config.json": "5b1542eb0be359bb8a17117fcf87917e0607d1b939ae3266ddffacdc14efb281",
|
|
19
|
-
"templates/skills/delegate-work/content.md": "
|
|
20
|
-
"templates/skills/delegate-work/files/scripts/codex-exec.sh": "
|
|
19
|
+
"templates/skills/delegate-work/content.md": "1046da2fbc5603c4deed92beb6d98c443df21e87cf83a7d7b3ce58dbc9d01997",
|
|
20
|
+
"templates/skills/delegate-work/files/scripts/codex-exec.sh": "4524a224fe0657d2e2872d97a70357c4a3ce84d2241bd935ed83febf1fac6e37",
|
|
21
21
|
"templates/skills/design-docs/config.json": "b78141763541486d67a105e8fa23873c9ffee6a682832c4b7266b8320d55ead7",
|
|
22
22
|
"templates/skills/design-docs/content.md": "fc860031defbd192c1b8e1c024b46509d78dcf10dbf1b8c93cc4d62396153a52",
|
|
23
23
|
"templates/skills/design-docs/files/template.md": "72d2cdcf8713c08d525d69892c5fed536107594522e3e799ae44f51df7015a36",
|
|
@@ -38,13 +38,13 @@
|
|
|
38
38
|
"templates/skills/planning/content.md": "543476e4dbb5c352acd686c3929c45cc992d7d033d06f7e9a30f78009b50785c",
|
|
39
39
|
"templates/skills/planning/files/template.md": "c0d5ca63c07934546172322bbd1180e2901dc45e27b5d12ce3dc5c30f5e7a98b",
|
|
40
40
|
"templates/skills/qa/config.json": "c53b01558a691c0343b6270a548e6172af86600a429b789eedb518849f7ea741",
|
|
41
|
-
"templates/skills/qa/content.md": "
|
|
41
|
+
"templates/skills/qa/content.md": "46b1147d8af1a292d9bcd7091385d2c98264f10bb5afe4fb61073113eb0a6574",
|
|
42
42
|
"templates/skills/qa/files/template.md": "1902e4bb65b19ab58c54ab5b63693ad09adecee69079c2c70f58d7d6c6597896",
|
|
43
43
|
"templates/skills/questioning/config.json": "da2d5fe6dfdac1915a0476a7063e62ebeb996b84caff07cc7527a55477656a80",
|
|
44
44
|
"templates/skills/questioning/content.md": "66f826a191e96d0041adca00a7c197a458de8e6fe164c1599b8708098d4109ab",
|
|
45
45
|
"templates/skills/questioning/files/template.md": "a68d72801a0b10380bf773b07e9d2f8d4c5b4b1c609ce954289e38ffc78cc223",
|
|
46
46
|
"templates/skills/researching/config.json": "921c062cab599097cc7677d2dad4797ec4d5844d2ee73607fbd417ec3fa487d4",
|
|
47
|
-
"templates/skills/researching/content.md": "
|
|
47
|
+
"templates/skills/researching/content.md": "92951142327415696726a2577defdb2379cc611cdafa9cdac92431a5e60f618f",
|
|
48
48
|
"templates/skills/researching/files/template.md": "411d34b98c610ef715edf0f24d09f828ba2d0f65f688e7879ef33a9453007949",
|
|
49
49
|
"templates/skills/reviewing/config.json": "09c40bb098e9738c60a848995fa9fe7568d158631a089c76ba02c0dd955445c7",
|
|
50
50
|
"templates/skills/reviewing/content.md": "1bbc285c9ad8cc3312f1ee2c558b04ffee914ffb6f5c63fb3741fcbb3e300fa5",
|
|
@@ -130,13 +130,13 @@
|
|
|
130
130
|
},
|
|
131
131
|
{
|
|
132
132
|
"from": "`${CLAUDE_PLUGIN_ROOT}/skills/delegate-work/scripts/codex-exec.sh`",
|
|
133
|
-
"line":
|
|
133
|
+
"line": 70,
|
|
134
134
|
"skill": "delegate-work",
|
|
135
135
|
"to": "`bash ~/.claude/skills/delegate-work/scripts/codex-exec.sh`"
|
|
136
136
|
},
|
|
137
137
|
{
|
|
138
138
|
"from": "| codex-exec.sh -m",
|
|
139
|
-
"line":
|
|
139
|
+
"line": 73,
|
|
140
140
|
"skill": "delegate-work",
|
|
141
141
|
"to": "| bash ~/.claude/skills/delegate-work/scripts/codex-exec.sh -m"
|
|
142
142
|
},
|
|
@@ -178,7 +178,7 @@
|
|
|
178
178
|
},
|
|
179
179
|
{
|
|
180
180
|
"from": "cc-plugin/base/skills/researching/template.md",
|
|
181
|
-
"line":
|
|
181
|
+
"line": 115,
|
|
182
182
|
"skill": "researching",
|
|
183
183
|
"to": "template.md"
|
|
184
184
|
},
|
|
@@ -10,7 +10,7 @@ The worked example is a 24-second video about a model checker finding race condi
|
|
|
10
10
|
- Every number on screen read live from a named source into `facts.json` right before render.
|
|
11
11
|
- The real Agent Swarm logo on the end card, never a redrawn one.
|
|
12
12
|
- A contact sheet per critique round, so a reviewer sees what the maker saw.
|
|
13
|
-
- Delivery to agent-fs, with a
|
|
13
|
+
- Delivery to agent-fs, with a live app link per mp4.
|
|
14
14
|
|
|
15
15
|
## Template Files
|
|
16
16
|
|
|
@@ -120,10 +120,9 @@ The maker uploads to `thoughts/<maker-agent-id>/videos/<slug>/` (versions as `<s
|
|
|
120
120
|
|
|
121
121
|
```bash
|
|
122
122
|
agent-fs --org <ORG_ID> --drive <DRIVE_ID> write thoughts/<maker-agent-id>/videos/<slug>/<file>.mp4 --file ./out/<file>.mp4
|
|
123
|
-
agent-fs --org <ORG_ID> --drive <DRIVE_ID> share-create thoughts/<maker-agent-id>/videos/<slug>/<file>.mp4 --expires-in 604800 --json
|
|
124
123
|
```
|
|
125
124
|
|
|
126
|
-
|
|
125
|
+
Hand each mp4 over as its live app link `${AGENT_FS_LIVE_URL}/file/~/<org>/<drive>/<path>`: the viewer plays it for a signed-in teammate. Make a `share-create` link only when the brief asks for one or names a reader without an agent-fs login (a customer, a public post, an outsider). Then run `agent-fs --org <ORG_ID> --drive <DRIVE_ID> share-create <path> --expires-in 604800 --json`; the share page plays with no login for 7 days at most. Report the url, the share id and the expiry. A raw signed URL downloads instead of playing, and agent-fs serves a raw `.mp4` as `application/octet-stream`. For a byte-exact copy back down: `agent-fs download <path> -o <file>` (`cat` is paginated and truncates).
|
|
127
126
|
|
|
128
127
|
**Not run** for this playbook: `write` to a real drive and `share-create` (it publishes a public link). `write --file`, `share-create` and `signed-url --inline` flags were checked against the CLI's `--help`. `agent-fs download` was run on the example master.
|
|
129
128
|
|
|
@@ -261,8 +260,8 @@ The maker's output is not the finish line. Before relaying, the Lead:
|
|
|
261
260
|
2. Opens the end-card frame: real logo, credit line present and readable.
|
|
262
261
|
3. Checks every on-screen number against the maker's `facts.json`, and `facts.json` against the source named in the brief.
|
|
263
262
|
4. Checks the ffprobe line: `h264`, `yuv420p`, exactly 1920x1080 (and 1080x1350), duration as briefed, and an `aac` audio stream when the video has music. (`ffprobe` after `setup.sh`, or `ffmpeg -i` stderr in the stock image.)
|
|
264
|
-
5. Plays the
|
|
265
|
-
6. Relays the
|
|
263
|
+
5. Plays the live app link once.
|
|
264
|
+
6. Relays the live app link (plus the share url, id and expiry when one was asked for) and the `attribution` string from `music/current.json` (source link, license link, changes made). The maker never posts.
|
|
266
265
|
|
|
267
266
|
## Known Gotchas
|
|
268
267
|
|
|
@@ -274,7 +273,7 @@ The maker's output is not the finish line. Before relaying, the Lead:
|
|
|
274
273
|
| PNG frames encoded by libx264 without `-pix_fmt` | The output is `yuv444p` (High 4:4:4 Predictive), which many players and uploaders reject | Always `-pix_fmt yuv420p`. `render.mjs` does. |
|
|
275
274
|
| Full-range `yuvj420p` | Rendered by `remotion render` by default | `--color-space=bt709`, or `x-reencode.sh` |
|
|
276
275
|
| Remotion's bundled ffmpeg is stripped | no `fps`, `tile`, `hstack` filters | Use the image's `/usr/local/bin/ffmpeg` for analysis and sheets |
|
|
277
|
-
| agent-fs serves `.mp4` as `application/octet-stream` | A content-type check on a raw link fails |
|
|
276
|
+
| agent-fs serves `.mp4` as `application/octet-stream` | A content-type check on a raw link fails | Open it through the live app link, or a `share-create` link for a reader without a login. Do not retry the content-type check. |
|
|
278
277
|
| Signed URLs expire and default to attachment | A `<video src>` pointing at one downloads instead of playing, and stops working at the expiry | Never use one as a public URL. For a page, commit a compressed copy. `signed-url --inline` renders in a browser tab but still expires. |
|
|
279
278
|
| `agent-fs cat` truncates | Pagination hides the tail of a big file | `agent-fs download <path> -o <file>` for exact bytes |
|
|
280
279
|
| Container restart mid-render | The scratch dir is the only thing that survives | Keep it in `/workspace/personal/<slug>`, name it in the first progress note, upload an interim cut before the long render |
|
|
@@ -65,7 +65,7 @@ How:
|
|
|
65
65
|
|
|
66
66
|
Deliver to agent-fs thoughts/<maker-agent-id>/videos/<slug>/: <1920x1080 [+ 1080x1350]> h264 yuv420p mp4,
|
|
67
67
|
contact sheet(s), facts.json, reel.html.
|
|
68
|
-
Output: agent-fs paths, a
|
|
68
|
+
Output: agent-fs paths, a live app link per mp4, ffprobe line, model you ran on,
|
|
69
69
|
each on-screen number with its source, music attribution (source link, license link, changes made).
|
|
70
70
|
```
|
|
71
71
|
|
|
@@ -54,7 +54,7 @@ How:
|
|
|
54
54
|
|
|
55
55
|
Deliver to agent-fs thoughts/<maker-agent-id>/videos/<slug>/: <1920x1080 [+ 1080x1350]> h264 yuv420p mp4,
|
|
56
56
|
contact sheet(s), facts.json.
|
|
57
|
-
Output: agent-fs paths, a
|
|
57
|
+
Output: agent-fs paths, a live app link per mp4, ffprobe line, model you ran on, each on-screen number with its source, music attribution (source link, license link, changes made).
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
Optional style add-on for a calmer, premium cut (the "Apple framework"): 3 colors, one background, one font, no HUD/grain; one shot = one idea; ~3 s open and ~2 s held end; key object centered; eased, overlapping keyframes; no hard cuts; ~108 BPM, phrase hit on the first product frame; no SFX. Add it only when asked.
|
|
@@ -127,9 +127,9 @@ window.frame = (f) => { render(f); return cv.toDataURL("image/png"); };
|
|
|
127
127
|
|
|
128
128
|
- agent-fs: `thoughts/<maker-agent-id>/videos/<slug>/` (versions as `<slug>-v2`, `<slug>-v3`, never overwrite an earlier version).
|
|
129
129
|
- Upload the mp4s, contact sheet(s), `facts.json`, the music json, and `reel.html` so the next round can start from it. After every non-trivial write, `agent-fs stat <path> --json` and check the size.
|
|
130
|
-
-
|
|
131
|
-
- agent-fs serves a raw `.mp4` download as `application/octet-stream`; the share page
|
|
132
|
-
- Output (short): paths, share
|
|
130
|
+
- Link for each final mp4: the live app link `${AGENT_FS_LIVE_URL}/file/~/<org>/<drive>/<path>`; its viewer plays the mp4 for a signed-in teammate. Make a `share-create` link only when the brief asks for one or names a reader without an agent-fs login (a customer, a public post, an outsider). Then run `agent-fs --org <ORG_ID> --drive <DRIVE_ID> share-create <path> --expires-in 604800 --json`; the share page plays the video with no login for 7 days at most. Report the url, the share id and the expiry. Do not hand out raw signed URLs for video.
|
|
131
|
+
- agent-fs serves a raw `.mp4` download as `application/octet-stream`; the viewer and the share page play it. Mention it rather than retrying.
|
|
132
|
+
- Output (short): paths, live app links (plus share url, id and expiry only if asked), ffprobe line, model, each on-screen fact with its source and SHA/run id, music attribution (source link, license link, changes made), logo source. No Slack post by the maker; Lead relays.
|
|
133
133
|
|
|
134
134
|
## Feedback rounds (v2, v3)
|
|
135
135
|
|