@selesai/code 0.8.6 → 0.8.7
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/CHANGELOG.md +16 -0
- package/dist/core/agent-session.js +13 -1
- package/dist/core/extensions/types.d.ts +2 -0
- package/dist/core/provider-composer.js +17 -4
- package/dist/core/settings-manager.d.ts +2 -0
- package/dist/core/settings-manager.js +3 -0
- package/dist/core/system-prompt.js +2 -8
- package/dist/core/system-prompt.test.js +6 -0
- package/dist/core/tools/index.d.ts +1 -0
- package/dist/core/tools/index.js +1 -0
- package/dist/core/tools/schema-prune.d.ts +16 -0
- package/dist/core/tools/schema-prune.js +39 -0
- package/dist/core/tools/schema-prune.test.d.ts +1 -0
- package/dist/core/tools/schema-prune.test.js +111 -0
- package/dist/defaults/settings.json +1 -0
- package/dist/extensions/model-prompt-injector/config.json +10 -0
- package/dist/extensions/model-prompt-injector/index.test.ts +112 -0
- package/dist/extensions/model-prompt-injector/index.ts +151 -0
- package/dist/extensions/package.json +1 -0
- package/dist/extensions/pi-subagents/CHANGELOG.md +158 -90
- package/dist/extensions/pi-subagents/UPSTREAM-V0.50-MAPPING.md +172 -0
- package/dist/extensions/pi-subagents/agents/oracle.md +7 -5
- package/dist/extensions/pi-subagents/agents/reviewer.md +2 -2
- package/dist/extensions/pi-subagents/agents/scout.md +1 -1
- package/dist/extensions/pi-subagents/agents/worker.md +1 -1
- package/dist/extensions/pi-subagents/docs/configuration.md +78 -19
- package/dist/extensions/pi-subagents/docs/extension-api.md +36 -0
- package/dist/extensions/pi-subagents/docs/missions.md +5 -3
- package/dist/extensions/pi-subagents/docs/models.md +1 -1
- package/dist/extensions/pi-subagents/docs/observability.md +42 -2
- package/dist/extensions/pi-subagents/docs/tool-reference.md +20 -3
- package/dist/extensions/pi-subagents/docs/workflows.md +5 -5
- package/dist/extensions/pi-subagents/install.mjs +2 -2
- package/dist/extensions/pi-subagents/package-lock.json +1470 -1472
- package/dist/extensions/pi-subagents/package.json +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +15 -14
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +46 -26
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
- package/dist/extensions/pi-subagents/src/agents/agents.ts +37 -12
- package/dist/extensions/pi-subagents/src/api/external-runs.ts +174 -84
- package/dist/extensions/pi-subagents/src/api/preflight.ts +13 -7
- package/dist/extensions/pi-subagents/src/extension/config.ts +53 -0
- package/dist/extensions/pi-subagents/src/extension/index.ts +67 -13
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +34 -6
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +5 -1
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +31 -30
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +10 -9
- package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +13 -8
- package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +16 -3
- package/dist/extensions/pi-subagents/src/inspectors/herdr/project-panes.ts +2 -6
- package/dist/extensions/pi-subagents/src/inspectors/herdr/shell-command.ts +16 -0
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +5 -4
- package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +19 -42
- package/dist/extensions/pi-subagents/src/missions/goal-driver.ts +3 -1
- package/dist/extensions/pi-subagents/src/missions/store.ts +8 -3
- package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +82 -25
- package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +71 -1
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +73 -43
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +5 -0
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -6
- package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +277 -0
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +8 -3
- package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/background/completion-replay.ts +11 -1
- package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +21 -6
- package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +437 -0
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +205 -41
- package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +27 -7
- package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +76 -19
- package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +30 -24
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +101 -4
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +54 -28
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +27 -13
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +298 -33
- package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +5 -2
- package/dist/extensions/pi-subagents/src/runs/foreground/async-dismiss-action.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +16 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +219 -15
- package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +4 -3
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +333 -45
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +107 -1
- package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +4 -0
- package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +39 -23
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +16 -2
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +66 -62
- package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +376 -0
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +15 -0
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +1 -9
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +12 -0
- package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +95 -0
- package/dist/extensions/pi-subagents/src/shared/agent-stream-options.ts +5 -0
- package/dist/extensions/pi-subagents/src/shared/artifacts.ts +0 -4
- package/dist/extensions/pi-subagents/src/shared/display-text.ts +50 -0
- package/dist/extensions/pi-subagents/src/shared/node-executable.ts +21 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +50 -4
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +34 -25
- package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +4 -0
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +160 -45
- package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +1 -48
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +128 -16
- package/dist/extensions/pi-subagents/src/tui/render.ts +122 -44
- package/dist/extensions/pi-subagents/src/watchdog/permission-arbiter.ts +2 -1
- package/dist/extensions/pi-subagents/src/watchdog/review.ts +4 -3
- package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +10 -2
- package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +272 -76
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +2 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +68 -9
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +27 -1
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +49 -0
- package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +26 -1
- package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +144 -0
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +26 -0
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +369 -98
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +111 -78
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +49 -1
- package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +12 -0
- package/dist/extensions/pi-subagents/test/support/node-command.ts +15 -0
- package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +50 -0
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +28 -0
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +61 -0
- package/dist/extensions/pi-subagents/test/unit/agent-stream-options.test.ts +14 -0
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +76 -0
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
- package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +162 -0
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +209 -10
- package/dist/extensions/pi-subagents/test/unit/completion-replay.test.ts +41 -1
- package/dist/extensions/pi-subagents/test/unit/external-runs.test.ts +135 -42
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +252 -1
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +192 -6
- package/dist/extensions/pi-subagents/test/unit/herdr-inspector-bootstrap.test.ts +32 -0
- package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +59 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +69 -0
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +23 -4
- package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +56 -1
- package/dist/extensions/pi-subagents/test/unit/mission-goal-driver.test.ts +67 -3
- package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +14 -0
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +18 -0
- package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +25 -2
- package/dist/extensions/pi-subagents/test/unit/node-executable.test.ts +26 -0
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +8 -0
- package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +348 -0
- package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +5 -2
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +6 -1
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +92 -0
- package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +175 -0
- package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +127 -17
- package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +44 -0
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +247 -0
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +107 -2
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +5 -8
- package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +456 -4
- package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +128 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +28 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +16 -7
- package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +15 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +79 -15
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +12 -12
- package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +109 -0
- package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +94 -1
- package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +42 -2
- package/dist/extensions/question/index.ts +21 -6
- package/dist/extensions/question/question-list.ts +7 -1
- package/dist/extensions/question/row-layout.ts +3 -0
- package/dist/extensions/question/tests/exp.test.ts +70 -0
- package/dist/extensions/question/tests/exp2.test.ts +59 -0
- package/dist/extensions/question/tests/exp3.test.ts +47 -0
- package/dist/extensions/question/tests/helpers.test.ts +7 -0
- package/dist/extensions/question/tests/question-list.test.ts +140 -0
- package/dist/extensions/question/tests/row-layout.test.ts +13 -0
- package/dist/extensions/question/tests/selection-mode.test.ts +54 -0
- package/dist/extensions/question/tests/zz-ig3.test.ts +20 -0
- package/dist/extensions/question/tests/zz-probe.test.ts +29 -0
- package/dist/skills/pi-subagents/SKILL.md +1 -1
- package/dist/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
- package/dist/skills/pi-subagents/references/execution-controls.md +15 -14
- package/dist/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
- package/dist/skills/pi-subagents/references/prompting-and-roles.md +46 -26
- package/docs/settings.md +6 -0
- package/package.json +3 -3
|
@@ -135,6 +135,20 @@ describe("scripted workflow runtime", () => {
|
|
|
135
135
|
assert.deepEqual(emitSnapshots, [1]);
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
+
it("passes a per-run intercom bridge override to the host launch", async () => {
|
|
139
|
+
let launchParams: Record<string, unknown> | undefined;
|
|
140
|
+
await runWorkflowScript({
|
|
141
|
+
script: `return runs.run("isolated", { agent: "worker", task: "Run", intercomBridge: { mode: "off" } });`,
|
|
142
|
+
async launch(key, params) {
|
|
143
|
+
launchParams = params;
|
|
144
|
+
return { key, ok: true, output: "done", artifactPaths: [] };
|
|
145
|
+
},
|
|
146
|
+
async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
assert.deepEqual(launchParams?.intercomBridge, { mode: "off" });
|
|
150
|
+
});
|
|
151
|
+
|
|
138
152
|
it("renders prompt text before passing it explicitly to a child", async () => {
|
|
139
153
|
let launchTask: unknown;
|
|
140
154
|
const result = await runWorkflowScript({
|
|
@@ -277,6 +291,34 @@ describe("scripted workflow runtime", () => {
|
|
|
277
291
|
);
|
|
278
292
|
});
|
|
279
293
|
|
|
294
|
+
it("tags only fail-fast detached child errors as detached-child", async () => {
|
|
295
|
+
await assert.rejects(
|
|
296
|
+
runWorkflowScript({
|
|
297
|
+
script: `return await runs.run("detaches", { agent: "worker", task: "ask" });`,
|
|
298
|
+
timeoutMs: 2_000,
|
|
299
|
+
async launch(key) { return { key, ok: false, detached: true, output: "reply first", error: "reply first", artifactPaths: [], results: [] }; },
|
|
300
|
+
async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
301
|
+
}),
|
|
302
|
+
(error: unknown) => error instanceof WorkflowScriptError && error.errorKind === "detached-child" && /Run 'detaches' detached/.test(error.message),
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
await assert.rejects(
|
|
306
|
+
runWorkflowScript({
|
|
307
|
+
script: `
|
|
308
|
+
await runs.all([{ key: "detaches", agent: "worker", task: "ask" }]);
|
|
309
|
+
throw new Error("manual hard failure");
|
|
310
|
+
`,
|
|
311
|
+
timeoutMs: 2_000,
|
|
312
|
+
async launch(key) { return { key, ok: false, detached: true, output: "reply first", error: "reply first", artifactPaths: [], results: [] }; },
|
|
313
|
+
async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
314
|
+
}),
|
|
315
|
+
(error: unknown) => error instanceof WorkflowScriptError
|
|
316
|
+
&& error.errorKind === undefined
|
|
317
|
+
&& /manual hard failure/.test(error.message)
|
|
318
|
+
&& error.partial.children[0]?.detached === true,
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
|
|
280
322
|
it("validates every runs.all item before launching children", async () => {
|
|
281
323
|
const malformedScripts = [
|
|
282
324
|
`return await runs.all([{ key: "valid", agent: "worker", task: "run" }, null]);`,
|
|
@@ -577,6 +619,133 @@ describe("scripted workflow runtime", () => {
|
|
|
577
619
|
);
|
|
578
620
|
});
|
|
579
621
|
|
|
622
|
+
it("rejects an unawaited new Promise wrapper over runs.run", async () => {
|
|
623
|
+
await assert.rejects(
|
|
624
|
+
runWorkflowScript({
|
|
625
|
+
script: `new Promise((resolve) => resolve(runs.run("wrapped", { agent: "worker", task: "fire" }))); return "done";`,
|
|
626
|
+
timeoutMs: 2_000,
|
|
627
|
+
async launch(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
628
|
+
async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
629
|
+
}),
|
|
630
|
+
(error: unknown) => error instanceof WorkflowScriptError && error.message.includes("unawaited runs.run launch(es): 'wrapped'"),
|
|
631
|
+
);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it("rejects an unawaited async helper wrapper over runs.run", async () => {
|
|
635
|
+
await assert.rejects(
|
|
636
|
+
runWorkflowScript({
|
|
637
|
+
script: `async function helper() { return runs.run("async-bare", { agent: "worker", task: "run" }); } helper(); return "done";`,
|
|
638
|
+
timeoutMs: 2_000,
|
|
639
|
+
async launch(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
640
|
+
async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
641
|
+
}),
|
|
642
|
+
(error: unknown) => error instanceof WorkflowScriptError && error.message.includes("unawaited runs.run launch(es): 'async-bare'"),
|
|
643
|
+
);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it("accepts an awaited async helper wrapper over runs.run", async () => {
|
|
647
|
+
const result = await runWorkflowScript({
|
|
648
|
+
script: `async function helper() { return runs.run("async-awaited", { agent: "worker", task: "run" }); } const child = await helper(); return child.output;`,
|
|
649
|
+
timeoutMs: 2_000,
|
|
650
|
+
async launch(key) { return { key, ok: true, output: "helper output", artifactPaths: [] }; },
|
|
651
|
+
async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
652
|
+
});
|
|
653
|
+
assert.equal(result.value, "helper output");
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it("accepts awaited and returned handlers on async helper wrappers", async () => {
|
|
657
|
+
const handlers = [
|
|
658
|
+
{ name: "then", chain: "then((value) => value)" },
|
|
659
|
+
{ name: "catch", chain: "catch(() => ({ output: 'fallback' }))" },
|
|
660
|
+
{ name: "finally", chain: "finally(() => {})" },
|
|
661
|
+
];
|
|
662
|
+
for (const mode of ["await", "return"] as const) {
|
|
663
|
+
for (const { name, chain } of handlers) {
|
|
664
|
+
const key = `${mode}-${name}`;
|
|
665
|
+
const expression = `helper().${chain}`;
|
|
666
|
+
const result = await runWorkflowScript({
|
|
667
|
+
script: `async function helper() { return runs.run("${key}", { agent: "worker", task: "run" }); } ${mode === "await" ? `const child = await ${expression}; return child.output;` : `return ${expression};`}`,
|
|
668
|
+
timeoutMs: 2_000,
|
|
669
|
+
async launch(key) { return { key, ok: true, output: "helper chain output", artifactPaths: [] }; },
|
|
670
|
+
async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
671
|
+
});
|
|
672
|
+
assert.equal(mode === "await" ? result.value : (result.value as { output: string }).output, "helper chain output");
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
it("rejects a nested dropped async helper propagation wrapper", async () => {
|
|
678
|
+
await assert.rejects(
|
|
679
|
+
runWorkflowScript({
|
|
680
|
+
script: `async function inner() { return runs.run("nested-bare", { agent: "worker", task: "run" }); } async function outer() { return inner(); } outer(); return "done";`,
|
|
681
|
+
timeoutMs: 2_000,
|
|
682
|
+
async launch(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
683
|
+
async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
684
|
+
}),
|
|
685
|
+
(error: unknown) => error instanceof WorkflowScriptError && error.message.includes("unawaited runs.run launch(es): 'nested-bare'"),
|
|
686
|
+
);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it("accepts awaited and returned nested async helper wrappers", async () => {
|
|
690
|
+
for (const mode of ["await", "return"] as const) {
|
|
691
|
+
const key = `nested-${mode}`;
|
|
692
|
+
const result = await runWorkflowScript({
|
|
693
|
+
script: `async function inner() { return runs.run("${key}", { agent: "worker", task: "run" }); } async function outer() { return inner(); } ${mode === "await" ? "const child = await outer(); return child.output;" : "return outer();"}`,
|
|
694
|
+
timeoutMs: 2_000,
|
|
695
|
+
async launch(key) { return { key, ok: true, output: "nested output", artifactPaths: [] }; },
|
|
696
|
+
async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
697
|
+
});
|
|
698
|
+
assert.equal(mode === "await" ? result.value : (result.value as { output: string }).output, "nested output");
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
it("counts an explicit internal helper await as observed", async () => {
|
|
703
|
+
const result = await runWorkflowScript({
|
|
704
|
+
script: `async function helper() { return await runs.run("internal-await", { agent: "worker", task: "run" }); } helper(); return "done";`,
|
|
705
|
+
timeoutMs: 2_000,
|
|
706
|
+
async launch(key) { return { key, ok: true, output: "helper output", artifactPaths: [] }; },
|
|
707
|
+
async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
708
|
+
});
|
|
709
|
+
assert.equal(result.value, "done");
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
it("rejects detached handlers on async helper wrappers", async () => {
|
|
713
|
+
const handlers = [
|
|
714
|
+
{ key: "async-then", chain: "then(() => {})" },
|
|
715
|
+
{ key: "async-catch", chain: "catch(() => {})" },
|
|
716
|
+
{ key: "async-finally", chain: "finally(() => {})" },
|
|
717
|
+
];
|
|
718
|
+
for (const { key, chain } of handlers) {
|
|
719
|
+
await assert.rejects(
|
|
720
|
+
runWorkflowScript({
|
|
721
|
+
script: `async function helper() { return runs.run("${key}", { agent: "worker", task: "run" }); } helper().${chain}; return "done";`,
|
|
722
|
+
timeoutMs: 2_000,
|
|
723
|
+
async launch(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
724
|
+
async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
725
|
+
}),
|
|
726
|
+
(error: unknown) => error instanceof WorkflowScriptError && error.message.includes(`unawaited runs.run launch(es): '${key}'`),
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
it("rejects a launch passed to an ignored repeated Promise resolution", async () => {
|
|
732
|
+
await assert.rejects(
|
|
733
|
+
runWorkflowScript({
|
|
734
|
+
script: `
|
|
735
|
+
await new Promise((resolve) => {
|
|
736
|
+
resolve("done");
|
|
737
|
+
Promise.resolve().then(() => resolve(runs.run("ignored", { agent: "worker", task: "fire" })));
|
|
738
|
+
});
|
|
739
|
+
return "done";
|
|
740
|
+
`,
|
|
741
|
+
timeoutMs: 2_000,
|
|
742
|
+
async launch(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
743
|
+
async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
744
|
+
}),
|
|
745
|
+
(error: unknown) => error instanceof WorkflowScriptError && error.message.includes("unawaited runs.run launch(es): 'ignored'"),
|
|
746
|
+
);
|
|
747
|
+
});
|
|
748
|
+
|
|
580
749
|
it("rejects fire-and-forget callbacks on a runs.run promise", async () => {
|
|
581
750
|
await assert.rejects(
|
|
582
751
|
runWorkflowScript({
|
|
@@ -589,6 +758,25 @@ describe("scripted workflow runtime", () => {
|
|
|
589
758
|
);
|
|
590
759
|
});
|
|
591
760
|
|
|
761
|
+
it("rejects detached promise handlers after an await", async () => {
|
|
762
|
+
const handlers = [
|
|
763
|
+
{ key: "bg-then", chain: "then(() => {})" },
|
|
764
|
+
{ key: "bg-catch", chain: "catch(() => {})" },
|
|
765
|
+
{ key: "bg-finally", chain: "finally(() => {})" },
|
|
766
|
+
];
|
|
767
|
+
for (const { key, chain } of handlers) {
|
|
768
|
+
await assert.rejects(
|
|
769
|
+
runWorkflowScript({
|
|
770
|
+
script: `await Promise.resolve(); runs.run("${key}", { agent: "worker", task: "fire" }).${chain}; return "done";`,
|
|
771
|
+
timeoutMs: 2_000,
|
|
772
|
+
async launch(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
773
|
+
async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
774
|
+
}),
|
|
775
|
+
(error: unknown) => error instanceof WorkflowScriptError && error.message.includes(`unawaited runs.run launch(es): '${key}'`),
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
|
|
592
780
|
it("rejects reading output from an unawaited runs.run promise", async () => {
|
|
593
781
|
await assert.rejects(
|
|
594
782
|
runWorkflowScript({
|
|
@@ -629,9 +817,9 @@ describe("scripted workflow runtime", () => {
|
|
|
629
817
|
assert.equal((result.value as { output?: string }).output, "direct output");
|
|
630
818
|
});
|
|
631
819
|
|
|
632
|
-
it("accepts
|
|
820
|
+
it("accepts a docs-style awaited runs.run launch", async () => {
|
|
633
821
|
const result = await runWorkflowScript({
|
|
634
|
-
script: `
|
|
822
|
+
script: `const child = await runs.run("awaited", { agent: "worker", task: "run" }); return child.output;`,
|
|
635
823
|
timeoutMs: 2_000,
|
|
636
824
|
async launch(key) { return { key, ok: true, output: "awaited output", artifactPaths: [] }; },
|
|
637
825
|
async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
@@ -639,6 +827,103 @@ describe("scripted workflow runtime", () => {
|
|
|
639
827
|
assert.equal(result.value, "awaited output");
|
|
640
828
|
});
|
|
641
829
|
|
|
830
|
+
it("accepts a docs-style awaited runs.all launch group", async () => {
|
|
831
|
+
const result = await runWorkflowScript({
|
|
832
|
+
script: `const children = await runs.all([{ key: "one", agent: "worker", task: "run" }]); return children[0].output;`,
|
|
833
|
+
timeoutMs: 2_000,
|
|
834
|
+
async launch(key) { return { key, ok: true, output: "group output", artifactPaths: [] }; },
|
|
835
|
+
async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
836
|
+
});
|
|
837
|
+
assert.equal(result.value, "group output");
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
it("accepts sequential awaited launches that use the first output", async () => {
|
|
841
|
+
const tasks: unknown[] = [];
|
|
842
|
+
const result = await runWorkflowScript({
|
|
843
|
+
script: `
|
|
844
|
+
const first = await runs.run("first", { agent: "worker", task: "plan" });
|
|
845
|
+
const second = await runs.run("second", { agent: "worker", task: first.output });
|
|
846
|
+
return second.output;
|
|
847
|
+
`,
|
|
848
|
+
timeoutMs: 2_000,
|
|
849
|
+
async launch(key, params) {
|
|
850
|
+
tasks.push(params.task);
|
|
851
|
+
return { key, ok: true, output: key === "first" ? "first output" : "second output", artifactPaths: [] };
|
|
852
|
+
},
|
|
853
|
+
async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
854
|
+
});
|
|
855
|
+
assert.deepEqual(tasks, ["plan", "first output"]);
|
|
856
|
+
assert.equal(result.value, "second output");
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
it("accepts an awaited native Promise combinator over launches", async () => {
|
|
860
|
+
const result = await runWorkflowScript({
|
|
861
|
+
script: `const children = await Promise.all([runs.run("native", { agent: "worker", task: "run" })]); return children[0].output;`,
|
|
862
|
+
timeoutMs: 2_000,
|
|
863
|
+
async launch(key) { return { key, ok: true, output: "native output", artifactPaths: [] }; },
|
|
864
|
+
async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
865
|
+
});
|
|
866
|
+
assert.equal(result.value, "native output");
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
it("accepts Promise.resolve over a pending helper wrapper", async () => {
|
|
870
|
+
const result = await runWorkflowScript({
|
|
871
|
+
script: `
|
|
872
|
+
const helper = new Promise((resolve) => Promise.resolve().then(() =>
|
|
873
|
+
resolve(runs.run("resolve-helper", { agent: "worker", task: "run" }))
|
|
874
|
+
));
|
|
875
|
+
const child = await Promise.resolve(helper);
|
|
876
|
+
return child.output;
|
|
877
|
+
`,
|
|
878
|
+
timeoutMs: 2_000,
|
|
879
|
+
async launch(key) { return { key, ok: true, output: "resolved helper output", artifactPaths: [] }; },
|
|
880
|
+
async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
881
|
+
});
|
|
882
|
+
assert.equal(result.value, "resolved helper output");
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
it("accepts Promise.all over a pending helper wrapper", async () => {
|
|
886
|
+
const result = await runWorkflowScript({
|
|
887
|
+
script: `
|
|
888
|
+
const helper = new Promise((resolve) => Promise.resolve().then(() =>
|
|
889
|
+
resolve(runs.run("combo-later", { agent: "worker", task: "run" }))
|
|
890
|
+
));
|
|
891
|
+
const children = await Promise.all([helper]);
|
|
892
|
+
return children[0].output;
|
|
893
|
+
`,
|
|
894
|
+
timeoutMs: 2_000,
|
|
895
|
+
async launch(key) { return { key, ok: true, output: "pending helper output", artifactPaths: [] }; },
|
|
896
|
+
async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
897
|
+
});
|
|
898
|
+
assert.equal(result.value, "pending helper output");
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
it("accepts an awaited then chain over a pending helper wrapper", async () => {
|
|
902
|
+
const result = await runWorkflowScript({
|
|
903
|
+
script: `
|
|
904
|
+
const helper = new Promise((resolve) => Promise.resolve().then(() =>
|
|
905
|
+
resolve(runs.run("chain-later", { agent: "worker", task: "run" }))
|
|
906
|
+
));
|
|
907
|
+
const child = await helper.then((value) => value);
|
|
908
|
+
return child.output;
|
|
909
|
+
`,
|
|
910
|
+
timeoutMs: 2_000,
|
|
911
|
+
async launch(key) { return { key, ok: true, output: "chain output", artifactPaths: [] }; },
|
|
912
|
+
async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
913
|
+
});
|
|
914
|
+
assert.equal(result.value, "chain output");
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
it("accepts an awaited new Promise wrapper over runs.run", async () => {
|
|
918
|
+
const result = await runWorkflowScript({
|
|
919
|
+
script: `const child = await new Promise((resolve) => resolve(runs.run("wrapped", { agent: "worker", task: "run" }))); return child.output;`,
|
|
920
|
+
timeoutMs: 2_000,
|
|
921
|
+
async launch(key) { return { key, ok: true, output: "wrapped output", artifactPaths: [] }; },
|
|
922
|
+
async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
923
|
+
});
|
|
924
|
+
assert.equal(result.value, "wrapped output");
|
|
925
|
+
});
|
|
926
|
+
|
|
642
927
|
it("rejects non-JSON-safe emitted values without persisting them", async () => {
|
|
643
928
|
const invalidScripts = [
|
|
644
929
|
`emit(undefined);`,
|
|
@@ -748,6 +1033,113 @@ describe("scripted workflow runtime", () => {
|
|
|
748
1033
|
assert.equal(childAborted, true);
|
|
749
1034
|
});
|
|
750
1035
|
|
|
1036
|
+
it("ignores a queued child launch message after workflow abort", async () => {
|
|
1037
|
+
const originalOn = Worker.prototype.on;
|
|
1038
|
+
const controller = new AbortController();
|
|
1039
|
+
let launchCount = 0;
|
|
1040
|
+
let deliverCapturedRunMessage: (() => void) | undefined;
|
|
1041
|
+
let markRunMessageCaptured!: () => void;
|
|
1042
|
+
const runMessageCaptured = new Promise<void>((resolve) => { markRunMessageCaptured = resolve; });
|
|
1043
|
+
|
|
1044
|
+
(Worker.prototype as unknown as { on: typeof Worker.prototype.on }).on = function (event: string | symbol, listener: (...args: unknown[]) => void) {
|
|
1045
|
+
if (event !== "message") return originalOn.call(this, event, listener);
|
|
1046
|
+
const wrapped = (message: Record<string, unknown>) => {
|
|
1047
|
+
if (message.type === "call" && message.method === "run") {
|
|
1048
|
+
deliverCapturedRunMessage = () => listener.call(this, message);
|
|
1049
|
+
markRunMessageCaptured();
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
listener.call(this, message);
|
|
1053
|
+
};
|
|
1054
|
+
return originalOn.call(this, event, wrapped);
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
try {
|
|
1058
|
+
const workflow = runWorkflowScript({
|
|
1059
|
+
script: `await runs.run("late", { agent: "worker", task: "wait" });`,
|
|
1060
|
+
signal: controller.signal,
|
|
1061
|
+
async launch(key) {
|
|
1062
|
+
launchCount += 1;
|
|
1063
|
+
return { key, ok: true, output: "too late", artifactPaths: [] };
|
|
1064
|
+
},
|
|
1065
|
+
async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
await runMessageCaptured;
|
|
1069
|
+
controller.abort(new Error("Workflow stopped by user."));
|
|
1070
|
+
await assert.rejects(workflow, (error: unknown) => error instanceof WorkflowScriptError && error.message === "Workflow stopped by user.");
|
|
1071
|
+
deliverCapturedRunMessage?.();
|
|
1072
|
+
await new Promise((resolve) => queueMicrotask(resolve));
|
|
1073
|
+
assert.equal(launchCount, 0);
|
|
1074
|
+
} finally {
|
|
1075
|
+
Worker.prototype.on = originalOn;
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
it("marks a child stopped when abort fires during the started trace callback", async () => {
|
|
1080
|
+
const controller = new AbortController();
|
|
1081
|
+
let admitCount = 0;
|
|
1082
|
+
let launchCount = 0;
|
|
1083
|
+
|
|
1084
|
+
await assert.rejects(
|
|
1085
|
+
runWorkflowScript({
|
|
1086
|
+
script: `await runs.run("slow", { agent: "worker", task: "wait" });`,
|
|
1087
|
+
signal: controller.signal,
|
|
1088
|
+
onTrace(trace) {
|
|
1089
|
+
const started = trace.some((entry) => entry.operation === "run" && entry.key === "slow" && entry.state === "started");
|
|
1090
|
+
if (started && !controller.signal.aborted) controller.abort(new Error("Workflow stopped by user."));
|
|
1091
|
+
},
|
|
1092
|
+
admit() {
|
|
1093
|
+
admitCount += 1;
|
|
1094
|
+
},
|
|
1095
|
+
async launch(key) {
|
|
1096
|
+
launchCount += 1;
|
|
1097
|
+
return { key, ok: true, output: "done", artifactPaths: [] };
|
|
1098
|
+
},
|
|
1099
|
+
async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
1100
|
+
}),
|
|
1101
|
+
(error: unknown) => error instanceof WorkflowScriptError
|
|
1102
|
+
&& error.message === "Workflow stopped by user."
|
|
1103
|
+
&& error.partial.trace.some((entry) => entry.operation === "run" && entry.key === "slow" && entry.state === "stopped")
|
|
1104
|
+
&& !error.partial.trace.some((entry) => entry.operation === "run" && entry.key === "slow" && entry.state === "failed"),
|
|
1105
|
+
);
|
|
1106
|
+
await new Promise((resolve) => queueMicrotask(resolve));
|
|
1107
|
+
assert.equal(admitCount, 0);
|
|
1108
|
+
assert.equal(launchCount, 0);
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
it("does not launch a child after admission settles following workflow abort", async () => {
|
|
1112
|
+
const controller = new AbortController();
|
|
1113
|
+
let launchCount = 0;
|
|
1114
|
+
let resolveAdmission!: () => void;
|
|
1115
|
+
let markAdmissionStarted!: () => void;
|
|
1116
|
+
const admissionStarted = new Promise<void>((resolve) => { markAdmissionStarted = resolve; });
|
|
1117
|
+
|
|
1118
|
+
const workflow = runWorkflowScript({
|
|
1119
|
+
script: `await runs.run("slow", { agent: "worker", task: "wait" });`,
|
|
1120
|
+
signal: controller.signal,
|
|
1121
|
+
admit() {
|
|
1122
|
+
markAdmissionStarted();
|
|
1123
|
+
return new Promise<void>((resolve) => { resolveAdmission = resolve; });
|
|
1124
|
+
},
|
|
1125
|
+
async launch(key) {
|
|
1126
|
+
launchCount += 1;
|
|
1127
|
+
return { key, ok: true, output: "done", artifactPaths: [] };
|
|
1128
|
+
},
|
|
1129
|
+
async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
await admissionStarted;
|
|
1133
|
+
controller.abort(new Error("Workflow stopped by user."));
|
|
1134
|
+
await assert.rejects(workflow, (error: unknown) => error instanceof WorkflowScriptError
|
|
1135
|
+
&& error.message === "Workflow stopped by user."
|
|
1136
|
+
&& error.partial.trace.some((entry) => entry.operation === "run" && entry.key === "slow" && entry.state === "stopped")
|
|
1137
|
+
&& !error.partial.trace.some((entry) => entry.operation === "run" && entry.key === "slow" && entry.state === "failed"));
|
|
1138
|
+
resolveAdmission();
|
|
1139
|
+
await new Promise((resolve) => queueMicrotask(resolve));
|
|
1140
|
+
assert.equal(launchCount, 0);
|
|
1141
|
+
});
|
|
1142
|
+
|
|
751
1143
|
it("drops a child response that settles after the workflow aborts", async () => {
|
|
752
1144
|
const workerPrototype = Worker.prototype as unknown as { postMessage(value: unknown, ...args: unknown[]): void };
|
|
753
1145
|
const originalPostMessage = workerPrototype.postMessage;
|
|
@@ -773,8 +1165,11 @@ describe("scripted workflow runtime", () => {
|
|
|
773
1165
|
async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
774
1166
|
});
|
|
775
1167
|
await launchStarted;
|
|
776
|
-
controller.abort();
|
|
777
|
-
await assert.rejects(workflow, (error: unknown) => error instanceof WorkflowScriptError
|
|
1168
|
+
controller.abort(new Error("Workflow stopped by user."));
|
|
1169
|
+
await assert.rejects(workflow, (error: unknown) => error instanceof WorkflowScriptError
|
|
1170
|
+
&& error.message === "Workflow stopped by user."
|
|
1171
|
+
&& error.partial.trace.some((entry) => entry.operation === "run" && entry.key === "slow" && entry.state === "stopped" && entry.error === "Workflow stopped by user.")
|
|
1172
|
+
&& !error.partial.trace.some((entry) => entry.operation === "run" && entry.key === "slow" && entry.state === "failed"));
|
|
778
1173
|
workflowSettled = true;
|
|
779
1174
|
resolveLaunch({ key: "slow", ok: true, output: "done", artifactPaths: [], results: [] });
|
|
780
1175
|
await new Promise((resolve) => queueMicrotask(resolve));
|
|
@@ -783,4 +1178,61 @@ describe("scripted workflow runtime", () => {
|
|
|
783
1178
|
workerPrototype.postMessage = originalPostMessage;
|
|
784
1179
|
}
|
|
785
1180
|
});
|
|
1181
|
+
|
|
1182
|
+
it("does not dispatch status after abort fires during the status started trace callback", async () => {
|
|
1183
|
+
const controller = new AbortController();
|
|
1184
|
+
let statusCount = 0;
|
|
1185
|
+
|
|
1186
|
+
await assert.rejects(
|
|
1187
|
+
runWorkflowScript({
|
|
1188
|
+
script: `await runs.status("probe");`,
|
|
1189
|
+
signal: controller.signal,
|
|
1190
|
+
onTrace(trace) {
|
|
1191
|
+
const started = trace.some((entry) => entry.operation === "status" && entry.key === "probe" && entry.state === "started");
|
|
1192
|
+
if (started && !controller.signal.aborted) controller.abort(new Error("Workflow stopped by user."));
|
|
1193
|
+
},
|
|
1194
|
+
async launch(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
1195
|
+
async status(key) {
|
|
1196
|
+
statusCount += 1;
|
|
1197
|
+
return { key, ok: true, output: "done", artifactPaths: [] };
|
|
1198
|
+
},
|
|
1199
|
+
}),
|
|
1200
|
+
(error: unknown) => error instanceof WorkflowScriptError
|
|
1201
|
+
&& error.message === "Workflow stopped by user."
|
|
1202
|
+
&& error.partial.trace.some((entry) => entry.operation === "status" && entry.key === "probe" && entry.state === "started")
|
|
1203
|
+
&& !error.partial.trace.some((entry) => entry.operation === "status" && entry.key === "probe" && entry.state === "completed"),
|
|
1204
|
+
);
|
|
1205
|
+
await new Promise((resolve) => queueMicrotask(resolve));
|
|
1206
|
+
assert.equal(statusCount, 0);
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
it("drops a status response that settles after the workflow aborts", async () => {
|
|
1210
|
+
const controller = new AbortController();
|
|
1211
|
+
let traceLengths: number[] = [];
|
|
1212
|
+
let resolveStatus!: (result: { key: string; ok: true; output: string; artifactPaths: string[] }) => void;
|
|
1213
|
+
let markStatusStarted!: () => void;
|
|
1214
|
+
const statusStarted = new Promise<void>((resolve) => { markStatusStarted = resolve; });
|
|
1215
|
+
|
|
1216
|
+
const workflow = runWorkflowScript({
|
|
1217
|
+
script: `await runs.status("probe");`,
|
|
1218
|
+
signal: controller.signal,
|
|
1219
|
+
onTrace(trace) { traceLengths.push(trace.length); },
|
|
1220
|
+
async launch(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
1221
|
+
status(key) {
|
|
1222
|
+
markStatusStarted();
|
|
1223
|
+
return new Promise((resolve) => { resolveStatus = resolve; });
|
|
1224
|
+
},
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
await statusStarted;
|
|
1228
|
+
controller.abort(new Error("Workflow stopped by user."));
|
|
1229
|
+
await assert.rejects(workflow, (error: unknown) => error instanceof WorkflowScriptError
|
|
1230
|
+
&& error.message === "Workflow stopped by user."
|
|
1231
|
+
&& error.partial.trace.some((entry) => entry.operation === "status" && entry.key === "probe" && entry.state === "started")
|
|
1232
|
+
&& !error.partial.trace.some((entry) => entry.operation === "status" && entry.key === "probe" && entry.state === "completed"));
|
|
1233
|
+
const finalTraceLength = traceLengths.at(-1);
|
|
1234
|
+
resolveStatus({ key: "probe", ok: true, output: "done", artifactPaths: [] });
|
|
1235
|
+
await new Promise((resolve) => queueMicrotask(resolve));
|
|
1236
|
+
assert.equal(traceLengths.at(-1), finalTraceLength);
|
|
1237
|
+
});
|
|
786
1238
|
});
|
|
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { describe, it } from "node:test";
|
|
6
|
+
import { writeAsyncResultFile, writePendingAsyncResultFile } from "../../src/runs/background/result-files.ts";
|
|
6
7
|
import { checkPidLiveness, reconcileAsyncRun } from "../../src/runs/background/stale-run-reconciler.ts";
|
|
7
8
|
|
|
8
9
|
function tempRoot(prefix: string): string {
|
|
@@ -71,6 +72,37 @@ describe("async stale-run reconciliation", () => {
|
|
|
71
72
|
}
|
|
72
73
|
});
|
|
73
74
|
|
|
75
|
+
it("repairs sessionless stale status without writing an unindexed result", () => {
|
|
76
|
+
const root = tempRoot("pi-stale-run-sessionless-");
|
|
77
|
+
try {
|
|
78
|
+
const asyncDir = path.join(root, "run-sessionless");
|
|
79
|
+
const resultsDir = path.join(root, "results");
|
|
80
|
+
writeStatus(asyncDir, {
|
|
81
|
+
runId: "run-sessionless",
|
|
82
|
+
mode: "single",
|
|
83
|
+
state: "running",
|
|
84
|
+
pid: 12345,
|
|
85
|
+
startedAt: 1000,
|
|
86
|
+
lastUpdate: 1000,
|
|
87
|
+
currentStep: 0,
|
|
88
|
+
steps: [{ agent: "scout", status: "running", startedAt: 1000 }],
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const result = reconcileAsyncRun(asyncDir, {
|
|
92
|
+
resultsDir,
|
|
93
|
+
kill: () => { throw errno("ESRCH"); },
|
|
94
|
+
now: () => 2000,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
assert.equal(result.repaired, true);
|
|
98
|
+
assert.equal(result.resultPath, undefined);
|
|
99
|
+
assert.equal(JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8")).state, "failed");
|
|
100
|
+
assert.equal(fs.existsSync(path.join(resultsDir, "run-sessionless.json")), false);
|
|
101
|
+
} finally {
|
|
102
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
74
106
|
it("includes runner stderr diagnostics when repairing a stale startup crash", () => {
|
|
75
107
|
const root = tempRoot("pi-stale-run-stderr-");
|
|
76
108
|
try {
|
|
@@ -78,6 +110,7 @@ describe("async stale-run reconciliation", () => {
|
|
|
78
110
|
const resultsDir = path.join(root, "results");
|
|
79
111
|
writeStatus(asyncDir, {
|
|
80
112
|
runId: "run-dead-stderr",
|
|
113
|
+
sessionId: "session-current",
|
|
81
114
|
mode: "single",
|
|
82
115
|
state: "running",
|
|
83
116
|
pid: 12345,
|
|
@@ -113,6 +146,7 @@ describe("async stale-run reconciliation", () => {
|
|
|
113
146
|
const resultsDir = path.join(root, "results");
|
|
114
147
|
writeStatus(asyncDir, {
|
|
115
148
|
runId: "run-dead-events-dir",
|
|
149
|
+
sessionId: "session-current",
|
|
116
150
|
mode: "single",
|
|
117
151
|
state: "running",
|
|
118
152
|
pid: 12345,
|
|
@@ -193,6 +227,100 @@ describe("async stale-run reconciliation", () => {
|
|
|
193
227
|
}
|
|
194
228
|
});
|
|
195
229
|
|
|
230
|
+
it("repairs stale running status from an indexed pending result", () => {
|
|
231
|
+
const root = tempRoot("pi-stale-indexed-pending-result-");
|
|
232
|
+
const originalError = console.error;
|
|
233
|
+
try {
|
|
234
|
+
console.error = () => {};
|
|
235
|
+
const asyncDir = path.join(root, "run-pending-result");
|
|
236
|
+
const resultsDir = path.join(root, "results");
|
|
237
|
+
writeStatus(asyncDir, {
|
|
238
|
+
runId: "run-pending-result",
|
|
239
|
+
sessionId: "session-current",
|
|
240
|
+
mode: "single",
|
|
241
|
+
state: "running",
|
|
242
|
+
pid: 12345,
|
|
243
|
+
startedAt: 1000,
|
|
244
|
+
lastUpdate: 1000,
|
|
245
|
+
currentStep: 0,
|
|
246
|
+
steps: [{ agent: "worker", status: "running", startedAt: 1000 }],
|
|
247
|
+
});
|
|
248
|
+
const publicResultPath = path.join(resultsDir, "run-pending-result.json");
|
|
249
|
+
fs.mkdirSync(publicResultPath, { recursive: true });
|
|
250
|
+
assert.deepEqual(writeAsyncResultFile(publicResultPath, {
|
|
251
|
+
id: "run-pending-result",
|
|
252
|
+
runId: "run-pending-result",
|
|
253
|
+
sessionId: "session-current",
|
|
254
|
+
success: true,
|
|
255
|
+
state: "complete",
|
|
256
|
+
summary: "already done",
|
|
257
|
+
}), { state: "pending" });
|
|
258
|
+
|
|
259
|
+
const result = reconcileAsyncRun(asyncDir, {
|
|
260
|
+
resultsDir,
|
|
261
|
+
kill: () => { throw errno("ESRCH"); },
|
|
262
|
+
now: () => 2000,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
assert.equal(result.repaired, true);
|
|
266
|
+
assert.equal(result.status?.state, "complete");
|
|
267
|
+
assert.equal(result.status?.steps?.[0]?.status, "complete");
|
|
268
|
+
assert.equal(JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8")).state, "complete");
|
|
269
|
+
assert.equal(fs.statSync(publicResultPath).isDirectory(), true);
|
|
270
|
+
assert.match(fs.readFileSync(result.resultPath!, "utf-8"), /already done/);
|
|
271
|
+
} finally {
|
|
272
|
+
console.error = originalError;
|
|
273
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("repairs stale rejected status from an indexed pending status snapshot", () => {
|
|
278
|
+
const root = tempRoot("pi-stale-rejected-pending-result-");
|
|
279
|
+
const originalError = console.error;
|
|
280
|
+
try {
|
|
281
|
+
console.error = () => {};
|
|
282
|
+
const asyncDir = path.join(root, "run-rejected");
|
|
283
|
+
const resultsDir = path.join(root, "results");
|
|
284
|
+
writeStatus(asyncDir, {
|
|
285
|
+
runId: "run-rejected",
|
|
286
|
+
sessionId: "session-current",
|
|
287
|
+
mode: "chain",
|
|
288
|
+
state: "rejected",
|
|
289
|
+
pid: 12345,
|
|
290
|
+
startedAt: 1000,
|
|
291
|
+
lastUpdate: 1000,
|
|
292
|
+
currentStep: 0,
|
|
293
|
+
checkpoint: { name: "review", status: "rejected", stepIndex: 0 },
|
|
294
|
+
steps: [{ agent: "checkpoint:review", status: "rejected", startedAt: 1000, error: "Checkpoint 'review' rejected." }],
|
|
295
|
+
});
|
|
296
|
+
const publicResultPath = path.join(resultsDir, "run-rejected.json");
|
|
297
|
+
fs.mkdirSync(publicResultPath, { recursive: true });
|
|
298
|
+
writePendingAsyncResultFile(publicResultPath, {
|
|
299
|
+
id: "run-rejected",
|
|
300
|
+
runId: "run-rejected",
|
|
301
|
+
sessionId: "session-current",
|
|
302
|
+
success: false,
|
|
303
|
+
state: "rejected",
|
|
304
|
+
summary: "Checkpoint 'review' rejected.",
|
|
305
|
+
results: [{ agent: "checkpoint:review", success: false, error: "Checkpoint 'review' rejected." }],
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const result = reconcileAsyncRun(asyncDir, {
|
|
309
|
+
resultsDir,
|
|
310
|
+
kill: () => { throw errno("ESRCH"); },
|
|
311
|
+
now: () => 2000,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
assert.equal(result.repaired, false);
|
|
315
|
+
assert.equal(result.status?.state, "rejected");
|
|
316
|
+
assert.equal(result.status?.steps?.[0]?.status, "rejected");
|
|
317
|
+
assert.match(fs.readFileSync(result.resultPath!, "utf-8"), /review/);
|
|
318
|
+
} finally {
|
|
319
|
+
console.error = originalError;
|
|
320
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
196
324
|
it("fails a stale run when a live pid has not updated beyond the stale threshold", () => {
|
|
197
325
|
const root = tempRoot("pi-stale-live-pid-");
|
|
198
326
|
try {
|