@selesai/code 0.8.6 → 0.8.8

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.
Files changed (234) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/dist/cli/args.d.ts +1 -0
  3. package/dist/cli/args.js +11 -0
  4. package/dist/cli/args.test.d.ts +1 -0
  5. package/dist/cli/args.test.js +24 -0
  6. package/dist/core/agent-session.d.ts +4 -1
  7. package/dist/core/agent-session.js +18 -7
  8. package/dist/core/experimental.d.ts +4 -0
  9. package/dist/core/experimental.js +4 -0
  10. package/dist/core/extensions/types.d.ts +6 -0
  11. package/dist/core/keybindings.d.ts +24 -0
  12. package/dist/core/model-config.d.ts +5 -0
  13. package/dist/core/model-config.js +1 -0
  14. package/dist/core/project-trust.js +2 -2
  15. package/dist/core/provider-composer.js +17 -4
  16. package/dist/core/sdk.d.ts +5 -3
  17. package/dist/core/sdk.js +2 -1
  18. package/dist/core/session-manager.js +2 -2
  19. package/dist/core/settings-manager.d.ts +8 -0
  20. package/dist/core/settings-manager.js +15 -0
  21. package/dist/core/system-prompt.js +3 -9
  22. package/dist/core/system-prompt.test.js +6 -0
  23. package/dist/core/tools/bash.js +2 -0
  24. package/dist/core/tools/edit.js +2 -0
  25. package/dist/core/tools/find.js +1 -1
  26. package/dist/core/tools/grep.js +1 -1
  27. package/dist/core/tools/index.d.ts +1 -0
  28. package/dist/core/tools/index.js +1 -0
  29. package/dist/core/tools/read.js +2 -0
  30. package/dist/core/tools/schema-prune.d.ts +16 -0
  31. package/dist/core/tools/schema-prune.js +39 -0
  32. package/dist/core/tools/schema-prune.test.d.ts +1 -0
  33. package/dist/core/tools/schema-prune.test.js +111 -0
  34. package/dist/core/tools/write.js +2 -0
  35. package/dist/defaults/settings.json +1 -0
  36. package/dist/extensions/model-prompt-injector/config.json +10 -0
  37. package/dist/extensions/model-prompt-injector/index.test.ts +112 -0
  38. package/dist/extensions/model-prompt-injector/index.ts +151 -0
  39. package/dist/extensions/package.json +1 -0
  40. package/dist/extensions/pi-subagents/CHANGELOG.md +158 -90
  41. package/dist/extensions/pi-subagents/UPSTREAM-V0.50-MAPPING.md +172 -0
  42. package/dist/extensions/pi-subagents/agents/oracle.md +7 -5
  43. package/dist/extensions/pi-subagents/agents/reviewer.md +2 -2
  44. package/dist/extensions/pi-subagents/agents/scout.md +1 -1
  45. package/dist/extensions/pi-subagents/agents/worker.md +1 -1
  46. package/dist/extensions/pi-subagents/docs/configuration.md +78 -19
  47. package/dist/extensions/pi-subagents/docs/extension-api.md +36 -0
  48. package/dist/extensions/pi-subagents/docs/missions.md +5 -3
  49. package/dist/extensions/pi-subagents/docs/models.md +1 -1
  50. package/dist/extensions/pi-subagents/docs/observability.md +42 -2
  51. package/dist/extensions/pi-subagents/docs/tool-reference.md +20 -3
  52. package/dist/extensions/pi-subagents/docs/workflows.md +5 -5
  53. package/dist/extensions/pi-subagents/install.mjs +2 -2
  54. package/dist/extensions/pi-subagents/package-lock.json +1470 -1472
  55. package/dist/extensions/pi-subagents/package.json +1 -1
  56. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +1 -1
  57. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  58. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +15 -14
  59. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  60. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  61. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
  62. package/dist/extensions/pi-subagents/src/agents/agents.ts +37 -12
  63. package/dist/extensions/pi-subagents/src/api/external-runs.ts +174 -84
  64. package/dist/extensions/pi-subagents/src/api/preflight.ts +13 -7
  65. package/dist/extensions/pi-subagents/src/extension/config.ts +53 -0
  66. package/dist/extensions/pi-subagents/src/extension/index.ts +67 -13
  67. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +34 -6
  68. package/dist/extensions/pi-subagents/src/extension/rpc.ts +5 -1
  69. package/dist/extensions/pi-subagents/src/extension/schemas.ts +31 -30
  70. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +10 -9
  71. package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +13 -8
  72. package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +16 -3
  73. package/dist/extensions/pi-subagents/src/inspectors/herdr/project-panes.ts +2 -6
  74. package/dist/extensions/pi-subagents/src/inspectors/herdr/shell-command.ts +16 -0
  75. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +5 -4
  76. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +19 -42
  77. package/dist/extensions/pi-subagents/src/missions/goal-driver.ts +3 -1
  78. package/dist/extensions/pi-subagents/src/missions/store.ts +8 -3
  79. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +82 -25
  80. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +71 -1
  81. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +73 -43
  82. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +5 -0
  83. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -6
  84. package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +277 -0
  85. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +8 -3
  86. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +2 -2
  87. package/dist/extensions/pi-subagents/src/runs/background/completion-replay.ts +11 -1
  88. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +21 -6
  89. package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +437 -0
  90. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +187 -41
  91. package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +27 -7
  92. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +76 -19
  93. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +30 -24
  94. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +101 -4
  95. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +54 -28
  96. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +27 -13
  97. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +298 -33
  98. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +2 -0
  99. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +5 -2
  100. package/dist/extensions/pi-subagents/src/runs/foreground/async-dismiss-action.ts +2 -1
  101. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +16 -0
  102. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +219 -15
  103. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +4 -3
  104. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +333 -45
  105. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +107 -1
  106. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +4 -0
  107. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +39 -23
  108. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +16 -2
  109. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +66 -62
  110. package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +376 -0
  111. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
  112. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +15 -0
  113. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +1 -9
  114. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +12 -0
  115. package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +95 -0
  116. package/dist/extensions/pi-subagents/src/shared/agent-stream-options.ts +5 -0
  117. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +0 -4
  118. package/dist/extensions/pi-subagents/src/shared/display-text.ts +50 -0
  119. package/dist/extensions/pi-subagents/src/shared/node-executable.ts +21 -0
  120. package/dist/extensions/pi-subagents/src/shared/types.ts +50 -4
  121. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +34 -25
  122. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +4 -0
  123. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +160 -45
  124. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +1 -48
  125. package/dist/extensions/pi-subagents/src/tui/fleet.ts +128 -16
  126. package/dist/extensions/pi-subagents/src/tui/render.ts +122 -44
  127. package/dist/extensions/pi-subagents/src/watchdog/permission-arbiter.ts +2 -1
  128. package/dist/extensions/pi-subagents/src/watchdog/review.ts +4 -3
  129. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +10 -2
  130. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +272 -76
  131. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +2 -0
  132. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2 -2
  133. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +68 -9
  134. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +27 -1
  135. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1 -1
  136. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +49 -0
  137. package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
  138. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +26 -1
  139. package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +144 -0
  140. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +26 -0
  141. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +395 -98
  142. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +111 -78
  143. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +49 -1
  144. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +12 -0
  145. package/dist/extensions/pi-subagents/test/support/node-command.ts +15 -0
  146. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +50 -0
  147. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +28 -0
  148. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +61 -0
  149. package/dist/extensions/pi-subagents/test/unit/agent-stream-options.test.ts +14 -0
  150. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +76 -0
  151. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
  152. package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +162 -0
  153. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +209 -10
  154. package/dist/extensions/pi-subagents/test/unit/completion-replay.test.ts +41 -1
  155. package/dist/extensions/pi-subagents/test/unit/external-runs.test.ts +135 -42
  156. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +252 -1
  157. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +192 -6
  158. package/dist/extensions/pi-subagents/test/unit/herdr-inspector-bootstrap.test.ts +32 -0
  159. package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +59 -0
  160. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +69 -0
  161. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +23 -4
  162. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +56 -1
  163. package/dist/extensions/pi-subagents/test/unit/mission-goal-driver.test.ts +67 -3
  164. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
  165. package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +14 -0
  166. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +18 -0
  167. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +2 -2
  168. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +25 -2
  169. package/dist/extensions/pi-subagents/test/unit/node-executable.test.ts +26 -0
  170. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +8 -0
  171. package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +348 -0
  172. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +5 -2
  173. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +1 -1
  174. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +34 -0
  175. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +6 -1
  176. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +92 -0
  177. package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +175 -0
  178. package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +127 -17
  179. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +44 -0
  180. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +247 -0
  181. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +107 -2
  182. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +5 -8
  183. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +456 -4
  184. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +128 -0
  185. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +28 -0
  186. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +16 -7
  187. package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +15 -0
  188. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +79 -15
  189. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +12 -12
  190. package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +109 -0
  191. package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +34 -0
  192. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +94 -1
  193. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +42 -2
  194. package/dist/extensions/question/index.ts +21 -6
  195. package/dist/extensions/question/question-list.ts +7 -1
  196. package/dist/extensions/question/row-layout.ts +3 -0
  197. package/dist/extensions/question/tests/exp.test.ts +70 -0
  198. package/dist/extensions/question/tests/exp2.test.ts +59 -0
  199. package/dist/extensions/question/tests/exp3.test.ts +47 -0
  200. package/dist/extensions/question/tests/helpers.test.ts +7 -0
  201. package/dist/extensions/question/tests/question-list.test.ts +140 -0
  202. package/dist/extensions/question/tests/row-layout.test.ts +13 -0
  203. package/dist/extensions/question/tests/selection-mode.test.ts +54 -0
  204. package/dist/extensions/question/tests/zz-ig3.test.ts +20 -0
  205. package/dist/extensions/question/tests/zz-probe.test.ts +29 -0
  206. package/dist/extensions/tokenin-onboarding.ts +1 -1
  207. package/dist/index.d.ts +1 -1
  208. package/dist/main.js +4 -0
  209. package/dist/modes/interactive/components/model-selector.js +2 -1
  210. package/dist/modes/interactive/components/settings-selector.d.ts +3 -1
  211. package/dist/modes/interactive/components/settings-selector.js +10 -0
  212. package/dist/modes/interactive/components/tool-execution.js +10 -1
  213. package/dist/modes/interactive/interactive-mode.d.ts +8 -2
  214. package/dist/modes/interactive/interactive-mode.js +81 -25
  215. package/dist/modes/interactive/model-catalog-refresh.d.ts +6 -0
  216. package/dist/modes/interactive/model-catalog-refresh.js +33 -0
  217. package/dist/modes/interactive/theme/dark.json +2 -0
  218. package/dist/modes/interactive/theme/light.json +2 -0
  219. package/dist/modes/interactive/theme/theme-controller.d.ts +10 -2
  220. package/dist/modes/interactive/theme/theme-controller.js +25 -11
  221. package/dist/modes/interactive/theme/theme-schema.json +9 -1
  222. package/dist/modes/interactive/theme/theme.d.ts +5 -3
  223. package/dist/modes/interactive/theme/theme.js +12 -2
  224. package/dist/skills/pi-subagents/SKILL.md +1 -1
  225. package/dist/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  226. package/dist/skills/pi-subagents/references/execution-controls.md +15 -14
  227. package/dist/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  228. package/dist/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  229. package/dist/utils/abort.d.ts +4 -0
  230. package/dist/utils/abort.js +47 -0
  231. package/dist/utils/tools-manager.d.ts +5 -1
  232. package/dist/utils/tools-manager.js +12 -21
  233. package/docs/settings.md +6 -0
  234. package/package.json +6 -6
@@ -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 output from an explicitly awaited runs.run promise", async () => {
820
+ it("accepts a docs-style awaited runs.run launch", async () => {
633
821
  const result = await runWorkflowScript({
634
- script: `return (await runs.run("awaited", { agent: "worker", task: "run" })).output;`,
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 && /aborted/.test(error.message));
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 {