@aicopycoders/exodus 2026.10.1700 → 2026.10.1900-beta.1
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/assets/skills/exodus-video/SKILL.md +12 -2
- package/dist/commands/video-customer.d.ts +1 -0
- package/dist/commands/video.d.ts +2 -0
- package/dist/commands/video.js +89 -13
- package/dist/commands/workflow.d.ts +3 -1
- package/dist/commands/workflow.js +24 -6
- package/dist/lib/runStatus.d.ts +7 -1
- package/dist/lib/runStatus.js +8 -2
- package/dist/lib/runVerdict.d.ts +1 -0
- package/dist/lib/runVerdict.js +2 -2
- package/package.json +1 -1
|
@@ -101,7 +101,7 @@ never how it is made. Every answer to them says `"view": "customer"`, and the
|
|
|
101
101
|
CLI words it the way the run page does. On their own brands they can:
|
|
102
102
|
|
|
103
103
|
- `video start --script <file> --conceit <key> --style <slug>` with
|
|
104
|
-
`--direction`, `--frame`, `--pacing`, `--music`, `--one-shot`, `--review-storyboard`, `--stop-at-script` and `--wait`. Any other start
|
|
104
|
+
`--direction`, `--frame`, `--pacing`, `--music`, `--one-shot`, `--review-storyboard`, `--stop-at-script`, `--rewrite-to-fit` and `--wait`. Any other start
|
|
105
105
|
flag is refused with one sentence naming it. The answer is the run id and the
|
|
106
106
|
link to the video's page, plus what the server did to the script: laid a
|
|
107
107
|
plain transcript out, or wrote a script from a description (see "The script
|
|
@@ -158,7 +158,7 @@ exodus workflow run <workflowId|name> --input <field>=@script.txt --voices @voic
|
|
|
158
158
|
exodus workflow run <workflowId|name> --input <field>=@script.txt --voice-treatment @voice-treatment.json --wait
|
|
159
159
|
same, with the video model speaking voices you WROTE
|
|
160
160
|
exodus video status <runId> where it is, per scene
|
|
161
|
-
exodus video start --script <file> --conceit <kind> --style <look> [--direction "…"] [--frame pov] [--pacing <jump-cuts|long-take|montage>] [--voice-path <path>] [--video-model <id>] [--voice native] [--music] [--one-shot] [--director] [--stop-at-script] [--wait]
|
|
161
|
+
exodus video start --script <file> --conceit <kind> --style <look> [--direction "…"] [--frame pov] [--pacing <jump-cuts|long-take|montage>] [--voice-path <path>] [--video-model <id>] [--voice native] [--music] [--one-shot] [--director] [--stop-at-script] [--rewrite-to-fit] [--wait]
|
|
162
162
|
start from a script and the kinds the server offers.
|
|
163
163
|
The file can be a plain transcript, a draft or a
|
|
164
164
|
description: the server tidies it (see "The script
|
|
@@ -397,6 +397,16 @@ What the CLI prints, and what you tell the user:
|
|
|
397
397
|
one-person kind of ad): the notes come first, then the refusal, and `--json`
|
|
398
398
|
carries the tidied script under `script`. Fix that script and send it. It is
|
|
399
399
|
already in the format, so it is not tidied or paid for again.
|
|
400
|
+
- **The script doesn't fit the kind of ad** (too many speakers, or a rule the
|
|
401
|
+
kind of ad has): the refusal says `--rewrite-to-fit` can fix it, and
|
|
402
|
+
`--json` carries `rewriteOffered: true`. Ask the user before you add the
|
|
403
|
+
flag: the server then writes a new script for that kind of ad from theirs
|
|
404
|
+
(the dashboard's "Rewrite to fit"), which can change every line and the
|
|
405
|
+
names, starts the run on it, and prints it (`rewritten` in `--json`). Show
|
|
406
|
+
the user that script. To let them read it first, add `--stop-at-script` too.
|
|
407
|
+
If the rewrite can't be made, nothing starts and the error says why
|
|
408
|
+
(`SCRIPT_NOT_REWRITTEN`). The other way out is a kind of ad made for the
|
|
409
|
+
script's cast.
|
|
400
410
|
|
|
401
411
|
**`--stop-at-script`** prints the script the run would start on and starts
|
|
402
412
|
nothing (exit 0). A question or a description too thin to write from answers
|
package/dist/commands/video.d.ts
CHANGED
|
@@ -298,6 +298,7 @@ export type RunStop = {
|
|
|
298
298
|
repair?: true;
|
|
299
299
|
nodeId?: string;
|
|
300
300
|
step?: string;
|
|
301
|
+
workerStopped?: true;
|
|
301
302
|
} | {
|
|
302
303
|
at: "finished";
|
|
303
304
|
status: string;
|
|
@@ -456,6 +457,7 @@ export interface ScriptStartOptions {
|
|
|
456
457
|
director?: true;
|
|
457
458
|
shots?: RunShots;
|
|
458
459
|
stopAtScript?: true;
|
|
460
|
+
rewriteToFit?: true;
|
|
459
461
|
wait: boolean;
|
|
460
462
|
json: boolean;
|
|
461
463
|
onProgressLine?: (line: string) => void;
|
package/dist/commands/video.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { apiGet, apiPost, getDashboardUrl } from "../lib/client.js";
|
|
5
5
|
import { displayRunStatus, formatApiError } from "../lib/format.js";
|
|
6
|
+
import { isWorkerStoppedPark } from "../lib/runStatus.js";
|
|
6
7
|
import { missingRouteLine } from "../lib/route-support.js";
|
|
7
8
|
import { hasBinary } from "../lib/preflight.js";
|
|
8
9
|
import { ASSET_UPLOAD_POLICY, pauseAheadLine, } from "./workflow.js";
|
|
@@ -167,6 +168,13 @@ Options:
|
|
|
167
168
|
--stop-at-script Print the script the run would start on, and start
|
|
168
169
|
nothing (start, with --conceit). To read a written or
|
|
169
170
|
laid-out script first; send it back with --script
|
|
171
|
+
--rewrite-to-fit If the script doesn't fit the kind of ad (more speakers
|
|
172
|
+
than it takes, or a rule it has), write a new script for
|
|
173
|
+
it from yours, print it, and start on it (start, with
|
|
174
|
+
--conceit). That can change every line and the names.
|
|
175
|
+
A script that fits starts as it is. With
|
|
176
|
+
--stop-at-script, it prints the new script and starts
|
|
177
|
+
nothing
|
|
170
178
|
--conceit <kind> Kind of ad (start)
|
|
171
179
|
--style <look> Look (start)
|
|
172
180
|
The server lists the kinds and looks it offers. An unknown word prints them.
|
|
@@ -206,7 +214,7 @@ Options:
|
|
|
206
214
|
Who can use what. Admins get every command here. Beta testers with video
|
|
207
215
|
switched on get these, on their own brands: start (--script, --conceit,
|
|
208
216
|
--style, --direction, --frame, --pacing, --music, --one-shot,
|
|
209
|
-
--review-storyboard, --stop-at-script),
|
|
217
|
+
--review-storyboard, --stop-at-script, --rewrite-to-fit),
|
|
210
218
|
status, storyboard, approve (the storyboard), voices, retry-frame --scene <n> (redo one scene's
|
|
211
219
|
picture while the storyboard waits), pull (the finished video, each scene's
|
|
212
220
|
clip and voice, and the music) and upload. These stay admin-only:
|
|
@@ -411,6 +419,8 @@ export function classifyRun(run) {
|
|
|
411
419
|
if (run.isTerminal)
|
|
412
420
|
return { at: "finished", status: run.status };
|
|
413
421
|
if (isAwaitingApproval(run.status)) {
|
|
422
|
+
if (isWorkerStoppedPark(run))
|
|
423
|
+
return { at: "failed", repair: true, workerStopped: true };
|
|
414
424
|
if (run.pauseReason === "repair") {
|
|
415
425
|
const deadNode = run.nodes.find((n) => n.status === "failed");
|
|
416
426
|
const why = deadNode?.error ?? run.error;
|
|
@@ -591,6 +601,13 @@ export function stopLines(stop, runId, runUrl) {
|
|
|
591
601
|
];
|
|
592
602
|
}
|
|
593
603
|
if (stop.at === "failed") {
|
|
604
|
+
if (stop.workerStopped) {
|
|
605
|
+
return [
|
|
606
|
+
"The worker making this ad stopped partway. Finished steps are kept.",
|
|
607
|
+
`Carry on from where it stopped: exodus workflow repair ${runId} retry`,
|
|
608
|
+
`Or open the run: ${runUrl}`,
|
|
609
|
+
];
|
|
610
|
+
}
|
|
594
611
|
if (stop.repair) {
|
|
595
612
|
if (!stop.step) {
|
|
596
613
|
return [
|
|
@@ -1447,6 +1464,20 @@ export function planVideoStart(flags, occurrences) {
|
|
|
1447
1464
|
line: "--stop-at-script works with --conceit, not --show. A Show's script is read as you wrote it.",
|
|
1448
1465
|
};
|
|
1449
1466
|
}
|
|
1467
|
+
const refitFlags = occurrences.filter((o) => o.flag === "rewrite-to-fit");
|
|
1468
|
+
if (refitFlags.some((o) => o.value !== undefined && !o.value.startsWith("--"))) {
|
|
1469
|
+
return {
|
|
1470
|
+
kind: "usage",
|
|
1471
|
+
line: "--rewrite-to-fit takes no value. Leave it off to start the script as you wrote it.",
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
const rewriteToFit = refitFlags.length > 0;
|
|
1475
|
+
if (showId && rewriteToFit) {
|
|
1476
|
+
return {
|
|
1477
|
+
kind: "usage",
|
|
1478
|
+
line: "--rewrite-to-fit works with --conceit, not --show. A Show's script is read as you wrote it.",
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1450
1481
|
if (showId && director) {
|
|
1451
1482
|
return {
|
|
1452
1483
|
kind: "usage",
|
|
@@ -1521,6 +1552,8 @@ export function planVideoStart(flags, occurrences) {
|
|
|
1521
1552
|
opts.director = true;
|
|
1522
1553
|
if (stopAtScript)
|
|
1523
1554
|
opts.stopAtScript = true;
|
|
1555
|
+
if (rewriteToFit)
|
|
1556
|
+
opts.rewriteToFit = true;
|
|
1524
1557
|
if (frameFlag.value || pacingFlag.value) {
|
|
1525
1558
|
opts.shots = {
|
|
1526
1559
|
...(frameFlag.value ? { frame: frameFlag.value } : {}),
|
|
@@ -1634,6 +1667,20 @@ function scriptWrittenHead(written, started) {
|
|
|
1634
1667
|
function writtenOf(value) {
|
|
1635
1668
|
return typeof value === "string" && value.trim() !== "" ? value : null;
|
|
1636
1669
|
}
|
|
1670
|
+
function scriptRewrittenHead(rewritten, started) {
|
|
1671
|
+
if (rewritten === null)
|
|
1672
|
+
return [];
|
|
1673
|
+
return [
|
|
1674
|
+
started
|
|
1675
|
+
? "Your script didn't fit this kind of ad, so we wrote a new one from it and started the run on it:"
|
|
1676
|
+
: "Your script didn't fit this kind of ad, so we wrote a new one from it:",
|
|
1677
|
+
"",
|
|
1678
|
+
...rewritten.split("\n").map((line) => (line ? ` ${line}` : "")),
|
|
1679
|
+
"",
|
|
1680
|
+
];
|
|
1681
|
+
}
|
|
1682
|
+
const REWRITE_OFFER = "To have a new script written from yours for this kind of ad, run the same command with --rewrite-to-fit. " +
|
|
1683
|
+
"That can change every line and the names.";
|
|
1637
1684
|
function scriptNeedsCallerResult(res, json) {
|
|
1638
1685
|
const data = res.data;
|
|
1639
1686
|
if (!isRecord(data) || !isRecord(data.error))
|
|
@@ -1657,7 +1704,9 @@ function scriptStartRefused(res, json) {
|
|
|
1657
1704
|
const data = isRecord(res.data) ? res.data : {};
|
|
1658
1705
|
const tidied = data.tidied === undefined ? null : scriptTidiedOf(data.tidied);
|
|
1659
1706
|
const written = writtenOf(data.written);
|
|
1660
|
-
|
|
1707
|
+
const rewritten = writtenOf(data.rewritten);
|
|
1708
|
+
const offered = data.rewriteOffered === true;
|
|
1709
|
+
if (!tidied && written === null && rewritten === null && !offered)
|
|
1661
1710
|
return errorResult(res, json);
|
|
1662
1711
|
const script = typeof data.script === "string" ? data.script : null;
|
|
1663
1712
|
const error = videoApiError(res);
|
|
@@ -1671,16 +1720,25 @@ function scriptStartRefused(res, json) {
|
|
|
1671
1720
|
error,
|
|
1672
1721
|
...(tidied ? { tidied } : {}),
|
|
1673
1722
|
...(written !== null ? { written } : {}),
|
|
1723
|
+
...(rewritten !== null ? { rewritten } : {}),
|
|
1674
1724
|
...(script !== null ? { script } : {}),
|
|
1725
|
+
...(offered ? { rewriteOffered: true } : {}),
|
|
1675
1726
|
}),
|
|
1676
1727
|
]
|
|
1677
|
-
: [
|
|
1728
|
+
: [
|
|
1729
|
+
...scriptTidiedHead(tidied),
|
|
1730
|
+
...scriptWrittenHead(written, false),
|
|
1731
|
+
...scriptRewrittenHead(rewritten, false),
|
|
1732
|
+
error,
|
|
1733
|
+
...(offered ? [REWRITE_OFFER] : []),
|
|
1734
|
+
],
|
|
1678
1735
|
};
|
|
1679
1736
|
}
|
|
1680
1737
|
function stoppedAtScriptResult(data, json) {
|
|
1681
1738
|
const script = typeof data.script === "string" ? data.script : "";
|
|
1682
1739
|
const tidied = data.tidied === undefined ? null : scriptTidiedOf(data.tidied);
|
|
1683
1740
|
const written = writtenOf(data.written);
|
|
1741
|
+
const rewritten = writtenOf(data.rewritten);
|
|
1684
1742
|
if (json) {
|
|
1685
1743
|
return {
|
|
1686
1744
|
code: 0,
|
|
@@ -1690,6 +1748,7 @@ function stoppedAtScriptResult(data, json) {
|
|
|
1690
1748
|
script,
|
|
1691
1749
|
...(tidied ? { tidied } : {}),
|
|
1692
1750
|
...(written !== null ? { written } : {}),
|
|
1751
|
+
...(rewritten !== null ? { rewritten } : {}),
|
|
1693
1752
|
}),
|
|
1694
1753
|
],
|
|
1695
1754
|
};
|
|
@@ -1698,9 +1757,11 @@ function stoppedAtScriptResult(data, json) {
|
|
|
1698
1757
|
code: 0,
|
|
1699
1758
|
lines: [
|
|
1700
1759
|
...scriptTidiedHead(tidied),
|
|
1701
|
-
|
|
1702
|
-
? "Your
|
|
1703
|
-
:
|
|
1760
|
+
rewritten !== null
|
|
1761
|
+
? "Your script didn't fit this kind of ad, so we wrote a new one from it. Nothing has started."
|
|
1762
|
+
: written !== null
|
|
1763
|
+
? "Your file described the ad, so we wrote this script from it. Nothing has started."
|
|
1764
|
+
: "This is the script the run would start on. Nothing has started.",
|
|
1704
1765
|
"",
|
|
1705
1766
|
...script.split("\n").map((line) => (line ? ` ${line}` : "")),
|
|
1706
1767
|
"",
|
|
@@ -1782,6 +1843,7 @@ export async function startScriptFlow(opts, deps) {
|
|
|
1782
1843
|
...(opts.director ? { director: true } : {}),
|
|
1783
1844
|
...(opts.shots ? { shots: opts.shots } : {}),
|
|
1784
1845
|
...(opts.stopAtScript ? { stopAtScript: true } : {}),
|
|
1846
|
+
...(opts.rewriteToFit ? { rewriteToFit: true } : {}),
|
|
1785
1847
|
});
|
|
1786
1848
|
if (!res.ok)
|
|
1787
1849
|
return scriptNeedsCallerResult(res, opts.json) ?? scriptStartRefused(res, opts.json);
|
|
@@ -1801,13 +1863,19 @@ export async function startScriptFlow(opts, deps) {
|
|
|
1801
1863
|
const video = startedVideoChoice(started.videoChoice);
|
|
1802
1864
|
const tidied = started.tidied === undefined ? null : scriptTidiedOf(started.tidied);
|
|
1803
1865
|
const written = writtenOf(started.written);
|
|
1866
|
+
const rewritten = writtenOf(started.rewritten);
|
|
1804
1867
|
const facts = {
|
|
1805
1868
|
card,
|
|
1806
1869
|
...(video ? { videoChoice: video } : {}),
|
|
1807
1870
|
...(tidied ? { tidied } : {}),
|
|
1808
1871
|
...(written !== null ? { written } : {}),
|
|
1872
|
+
...(rewritten !== null ? { rewritten } : {}),
|
|
1809
1873
|
};
|
|
1810
|
-
const head = [
|
|
1874
|
+
const head = [
|
|
1875
|
+
...scriptTidiedHead(tidied),
|
|
1876
|
+
...scriptWrittenHead(written, true),
|
|
1877
|
+
...scriptRewrittenHead(rewritten, true),
|
|
1878
|
+
];
|
|
1811
1879
|
if (!opts.wait) {
|
|
1812
1880
|
return {
|
|
1813
1881
|
code: 0,
|
|
@@ -1834,16 +1902,22 @@ async function customerStartedResult(started, opts, deps) {
|
|
|
1834
1902
|
const { runId, dashboardUrl } = started;
|
|
1835
1903
|
const tidied = started.tidied === undefined ? null : scriptTidiedOf(started.tidied);
|
|
1836
1904
|
const written = writtenOf(started.written);
|
|
1905
|
+
const rewritten = writtenOf(started.rewritten);
|
|
1837
1906
|
const head = [
|
|
1838
1907
|
...scriptTidiedHead(tidied),
|
|
1839
1908
|
...scriptWrittenHead(written, true),
|
|
1909
|
+
...scriptRewrittenHead(rewritten, true),
|
|
1840
1910
|
`Started video ${runId}`,
|
|
1841
1911
|
`Watch it: ${dashboardUrl}`,
|
|
1842
1912
|
];
|
|
1843
1913
|
if (opts.wait) {
|
|
1844
1914
|
const waited = await startedThenWait(runId, head, { ...opts, url: dashboardUrl }, deps);
|
|
1845
|
-
const facts = {
|
|
1846
|
-
|
|
1915
|
+
const facts = {
|
|
1916
|
+
...(tidied ? { tidied } : {}),
|
|
1917
|
+
...(written !== null ? { written } : {}),
|
|
1918
|
+
...(rewritten !== null ? { rewritten } : {}),
|
|
1919
|
+
};
|
|
1920
|
+
return opts.json && (tidied || written !== null || rewritten !== null) ? waitJsonWith(waited, facts) : waited;
|
|
1847
1921
|
}
|
|
1848
1922
|
if (opts.json)
|
|
1849
1923
|
return { code: 0, lines: [JSON.stringify(started)] };
|
|
@@ -2035,7 +2109,7 @@ export async function statusFlow(runId, json, deps) {
|
|
|
2035
2109
|
if (item.itemKind !== "clip" && item.itemKind !== "voiceover" && item.itemKind !== "frame") {
|
|
2036
2110
|
continue;
|
|
2037
2111
|
}
|
|
2038
|
-
if (item.sceneIndex
|
|
2112
|
+
if (!isPullSceneIndex(item.sceneIndex))
|
|
2039
2113
|
continue;
|
|
2040
2114
|
if (item.itemKind === "frame" && !sceneFrameNodeIds.has(item.nodeId))
|
|
2041
2115
|
continue;
|
|
@@ -2043,9 +2117,11 @@ export async function statusFlow(runId, json, deps) {
|
|
|
2043
2117
|
row[item.itemKind] = item;
|
|
2044
2118
|
byScene.set(item.sceneIndex, row);
|
|
2045
2119
|
}
|
|
2046
|
-
const headline = stop.at === "failed" && stop.
|
|
2047
|
-
? `Ad run ${runId} — Stopped,
|
|
2048
|
-
:
|
|
2120
|
+
const headline = stop.at === "failed" && stop.workerStopped
|
|
2121
|
+
? `Ad run ${runId} — Stopped, its worker went quiet`
|
|
2122
|
+
: stop.at === "failed" && stop.repair
|
|
2123
|
+
? `Ad run ${runId} — Stopped, a step failed`
|
|
2124
|
+
: `Ad run ${runId} — ${displayRunStatus(run.status)}`;
|
|
2049
2125
|
const lines = [
|
|
2050
2126
|
headline,
|
|
2051
2127
|
...(carryingOn(run) ? [CARRYING_ON_LINE] : []),
|
|
@@ -508,7 +508,9 @@ interface RunFlowOptions {
|
|
|
508
508
|
onProgressLine?: (line: string) => void;
|
|
509
509
|
onWarningLine?: (line: string) => void;
|
|
510
510
|
}
|
|
511
|
-
export declare function formatPauseNotice(pauseReason: WorkflowPauseReason | undefined, runId: string, dashboardUrl: string
|
|
511
|
+
export declare function formatPauseNotice(pauseReason: WorkflowPauseReason | undefined, runId: string, dashboardUrl: string, { workerStopped }?: {
|
|
512
|
+
workerStopped?: boolean;
|
|
513
|
+
}): string[];
|
|
512
514
|
export declare function parseRawInputFlags(occurrences: FlagOccurrence[]): Record<string, string>;
|
|
513
515
|
export declare function parseInputFlags(occurrences: FlagOccurrence[], readFile?: (path: string) => string): Record<string, string>;
|
|
514
516
|
export declare const ASSET_UPLOAD_POLICY: Record<WorkflowMediaType, {
|
|
@@ -4,7 +4,7 @@ import nodePath from "node:path";
|
|
|
4
4
|
import { apiGet, apiGetText, apiPost, apiPostDashboard, getDashboardUrl, } from "../lib/client.js";
|
|
5
5
|
import { formatApiError } from "../lib/format.js";
|
|
6
6
|
import { pollUntilDone } from "../lib/poll.js";
|
|
7
|
-
import { normalizeRunStatus, isTerminalRunStatus, storedWorkflowStatusForms, TERMINAL_RUN_STATUSES, } from "../lib/runStatus.js";
|
|
7
|
+
import { normalizeRunStatus, isTerminalRunStatus, storedWorkflowStatusForms, isWorkerStoppedPark, TERMINAL_RUN_STATUSES, } from "../lib/runStatus.js";
|
|
8
8
|
import { runVerdict } from "../lib/runVerdict.js";
|
|
9
9
|
import { runDisplayName, runHeadingWord, runTypeWord } from "../lib/runNames.js";
|
|
10
10
|
import { workflowToYaml, parseWorkflowText } from "../lib/workflowText.js";
|
|
@@ -354,9 +354,11 @@ Notes:
|
|
|
354
354
|
its own box on the canvas, sitting on a wire between two steps, so the diagram
|
|
355
355
|
shows exactly where a run will stop. A require-all collector that stalled
|
|
356
356
|
on a dead input is a "repair" park — retry it, skip the dead input, or kill the
|
|
357
|
-
run. A
|
|
358
|
-
|
|
359
|
-
|
|
357
|
+
run. A run whose worker stopped partway is also a "repair" park, with nothing
|
|
358
|
+
dead in it: retry carries it on from where it stopped and keeps every finished
|
|
359
|
+
step, and skip is refused. (#3820) A nested sub-workflow waiting on inputs is a
|
|
360
|
+
"slots" park — answer it with repeatable --slot key=value flags (run "answer"
|
|
361
|
+
with no --slot to list the slot ids it wants). The "gate" verbs retired in 2.0 along with the Gate node — the
|
|
360
362
|
command now prints a pointer at "checkpoint" and exits 1.
|
|
361
363
|
`.trim();
|
|
362
364
|
export function formatRulesLine(provenance) {
|
|
@@ -567,7 +569,7 @@ function gateParkedNodeId(status, pauseReason, pausedNodeId) {
|
|
|
567
569
|
}
|
|
568
570
|
const RETIRED_GATE_VERB_POINTER = "The Gate node retired in 2.0 — runs now pause at a Checkpoint box on the canvas. " +
|
|
569
571
|
"Use: exodus workflow checkpoint <runId> [approve|edit|retry|cancel]";
|
|
570
|
-
export function formatPauseNotice(pauseReason, runId, dashboardUrl) {
|
|
572
|
+
export function formatPauseNotice(pauseReason, runId, dashboardUrl, { workerStopped = false } = {}) {
|
|
571
573
|
if (!pauseReason) {
|
|
572
574
|
return [
|
|
573
575
|
" ⏸ paused at the cost gate — approve or edit the storyboard in the web app to continue.",
|
|
@@ -591,6 +593,13 @@ export function formatPauseNotice(pauseReason, runId, dashboardUrl) {
|
|
|
591
593
|
" Then run the workflow again.",
|
|
592
594
|
];
|
|
593
595
|
}
|
|
596
|
+
if (pauseReason === "repair" && workerStopped) {
|
|
597
|
+
return [
|
|
598
|
+
" ⏸ the worker stopped partway — retry carries on from where it stopped, finished steps are kept.",
|
|
599
|
+
` Resolve here: exodus workflow repair ${runId} retry|kill`,
|
|
600
|
+
` Or in the app: ${dashboardUrl}${RUN_PAGE_PREFIX}${runId}`,
|
|
601
|
+
];
|
|
602
|
+
}
|
|
594
603
|
const resolveVerb = pauseReason === "repair"
|
|
595
604
|
? `exodus workflow repair ${runId} retry|skip|kill`
|
|
596
605
|
: `exodus workflow answer ${runId} --slot key=value`;
|
|
@@ -2121,7 +2130,12 @@ async function waitForRun(runId, opts, deps) {
|
|
|
2121
2130
|
pausedNotified = true;
|
|
2122
2131
|
const dashboardUrl = deps.dashboardUrl ?? getDashboardUrl();
|
|
2123
2132
|
const pauseReason = raw["pauseReason"];
|
|
2124
|
-
|
|
2133
|
+
const workerStopped = isWorkerStoppedPark({
|
|
2134
|
+
status: raw["status"],
|
|
2135
|
+
pauseReason,
|
|
2136
|
+
error: raw["error"],
|
|
2137
|
+
});
|
|
2138
|
+
for (const line of formatPauseNotice(pauseReason, runId, dashboardUrl, { workerStopped })) {
|
|
2125
2139
|
opts.onProgressLine(line);
|
|
2126
2140
|
}
|
|
2127
2141
|
}
|
|
@@ -2996,6 +3010,10 @@ export async function repairFlow(runId, action, opts, deps) {
|
|
|
2996
3010
|
const pf = await preflightPark(runId, "repair", verb, opts.json, deps);
|
|
2997
3011
|
if (!pf.ok)
|
|
2998
3012
|
return pf.result;
|
|
3013
|
+
if (action === "skip" && isWorkerStoppedPark(pf.run)) {
|
|
3014
|
+
return errLine(`Run ${runId} has nothing to skip — its worker stopped partway. ` +
|
|
3015
|
+
`Carry on from where it stopped (finished steps are kept): exodus workflow repair ${runId} retry`, opts.json);
|
|
3016
|
+
}
|
|
2999
3017
|
if (action === "kill") {
|
|
3000
3018
|
const res = await deps.post(CANCEL_PATH, { runId });
|
|
3001
3019
|
if (!res.ok)
|
package/dist/lib/runStatus.d.ts
CHANGED
|
@@ -14,9 +14,15 @@ export declare function isActiveRunStatus(raw: string | null | undefined): boole
|
|
|
14
14
|
export declare const WORKFLOW_RUN_STATUS_VALUES: readonly ["queued", "running", "awaiting-approval", "succeeded", "succeeded-with-warnings", "failed", "cancelled"];
|
|
15
15
|
export declare const LEGACY_WORKFLOW_RUN_STATUS_VALUES: readonly ["awaiting-review", "completed", "partial", "canceled"];
|
|
16
16
|
export declare function storedWorkflowStatusForms(status: RunStatus): string[];
|
|
17
|
-
export declare function workflowRunPresentation(rawStatus: string | null | undefined, pauseReason?: string | null): {
|
|
17
|
+
export declare function workflowRunPresentation(rawStatus: string | null | undefined, pauseReason?: string | null, workerStopped?: boolean): {
|
|
18
18
|
status: RunStatus;
|
|
19
19
|
label: string;
|
|
20
20
|
tone: RunStatusTone;
|
|
21
21
|
detail?: string;
|
|
22
22
|
};
|
|
23
|
+
export declare const WORKER_STOPPED_PARK_ERROR = "timeout-zombie-sweep: worker stopped heartbeating; finished steps are kept";
|
|
24
|
+
export declare function isWorkerStoppedPark(run: {
|
|
25
|
+
status?: string | null;
|
|
26
|
+
pauseReason?: string | null;
|
|
27
|
+
error?: string | null;
|
|
28
|
+
}): boolean;
|
package/dist/lib/runStatus.js
CHANGED
|
@@ -115,7 +115,7 @@ export function storedWorkflowStatusForms(status) {
|
|
|
115
115
|
const twin = LEGACY_WORKFLOW_TWIN[status];
|
|
116
116
|
return twin ? [status, twin] : [status];
|
|
117
117
|
}
|
|
118
|
-
export function workflowRunPresentation(rawStatus, pauseReason) {
|
|
118
|
+
export function workflowRunPresentation(rawStatus, pauseReason, workerStopped) {
|
|
119
119
|
let status = normalizeRunStatus(rawStatus);
|
|
120
120
|
let detail;
|
|
121
121
|
if (status === "awaiting-approval") {
|
|
@@ -127,7 +127,7 @@ export function workflowRunPresentation(rawStatus, pauseReason) {
|
|
|
127
127
|
detail = "needs inputs";
|
|
128
128
|
}
|
|
129
129
|
else if (pauseReason === "repair") {
|
|
130
|
-
detail = "needs repair";
|
|
130
|
+
detail = workerStopped ? "worker stopped" : "needs repair";
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
return {
|
|
@@ -137,3 +137,9 @@ export function workflowRunPresentation(rawStatus, pauseReason) {
|
|
|
137
137
|
detail,
|
|
138
138
|
};
|
|
139
139
|
}
|
|
140
|
+
export const WORKER_STOPPED_PARK_ERROR = "timeout-zombie-sweep: worker stopped heartbeating; finished steps are kept";
|
|
141
|
+
export function isWorkerStoppedPark(run) {
|
|
142
|
+
return (normalizeRunStatus(run.status) === "awaiting-approval" &&
|
|
143
|
+
run.pauseReason === "repair" &&
|
|
144
|
+
run.error === WORKER_STOPPED_PARK_ERROR);
|
|
145
|
+
}
|
package/dist/lib/runVerdict.d.ts
CHANGED
package/dist/lib/runVerdict.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RUN_STATUS_LABELS, workflowRunPresentation } from "./runStatus.js";
|
|
1
|
+
import { RUN_STATUS_LABELS, isWorkerStoppedPark, workflowRunPresentation, } from "./runStatus.js";
|
|
2
2
|
export function summarizeDeliveries(deliveries) {
|
|
3
3
|
if (deliveries === undefined || deliveries === null)
|
|
4
4
|
return undefined;
|
|
@@ -8,7 +8,7 @@ export function summarizeDeliveries(deliveries) {
|
|
|
8
8
|
};
|
|
9
9
|
}
|
|
10
10
|
export function runVerdict(run) {
|
|
11
|
-
const presented = workflowRunPresentation(run.status, run.pauseReason);
|
|
11
|
+
const presented = workflowRunPresentation(run.status, run.pauseReason, isWorkerStoppedPark(run));
|
|
12
12
|
const delivery = summarizeDeliveries(run.deliveries) ?? run.deliverySummary ?? undefined;
|
|
13
13
|
if ((presented.status === "succeeded" || presented.status === "succeeded-with-warnings") &&
|
|
14
14
|
(run.counts?.failed ?? 0) > 0 &&
|