@selesai/code 0.8.5 → 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.
Files changed (231) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/core/agent-session.js +13 -1
  3. package/dist/core/extensions/types.d.ts +2 -0
  4. package/dist/core/provider-composer.js +17 -4
  5. package/dist/core/settings-manager.d.ts +2 -0
  6. package/dist/core/settings-manager.js +3 -0
  7. package/dist/core/system-prompt.js +2 -8
  8. package/dist/core/system-prompt.test.js +6 -0
  9. package/dist/core/tools/index.d.ts +1 -0
  10. package/dist/core/tools/index.js +1 -0
  11. package/dist/core/tools/schema-prune.d.ts +16 -0
  12. package/dist/core/tools/schema-prune.js +39 -0
  13. package/dist/core/tools/schema-prune.test.d.ts +1 -0
  14. package/dist/core/tools/schema-prune.test.js +111 -0
  15. package/dist/defaults/models.json +5 -0
  16. package/dist/defaults/settings.json +1 -0
  17. package/dist/extensions/agent-browser.test.ts +178 -2
  18. package/dist/extensions/agent-browser.ts +2 -0
  19. package/dist/extensions/copy-turn.test.ts +229 -0
  20. package/dist/extensions/copy-turn.ts +11 -0
  21. package/dist/extensions/grep-app/index.test.ts +356 -1
  22. package/dist/extensions/grep-app/index.ts +6 -1
  23. package/dist/extensions/handoff-new.test.ts +351 -161
  24. package/dist/extensions/handoff-new.ts +3 -0
  25. package/dist/extensions/inline-skills.test.ts +93 -0
  26. package/dist/extensions/inline-skills.ts +3 -0
  27. package/dist/extensions/model-prompt-injector/config.json +10 -0
  28. package/dist/extensions/model-prompt-injector/index.test.ts +112 -0
  29. package/dist/extensions/model-prompt-injector/index.ts +151 -0
  30. package/dist/extensions/package.json +1 -0
  31. package/dist/extensions/pi-subagents/CHANGELOG.md +158 -90
  32. package/dist/extensions/pi-subagents/UPSTREAM-V0.50-MAPPING.md +172 -0
  33. package/dist/extensions/pi-subagents/agents/builder.md +5 -2
  34. package/dist/extensions/pi-subagents/agents/commentator.md +1 -1
  35. package/dist/extensions/pi-subagents/agents/oracle.md +7 -5
  36. package/dist/extensions/pi-subagents/agents/reviewer.md +2 -2
  37. package/dist/extensions/pi-subagents/agents/scout.md +1 -1
  38. package/dist/extensions/pi-subagents/agents/worker.md +1 -1
  39. package/dist/extensions/pi-subagents/docs/configuration.md +78 -11
  40. package/dist/extensions/pi-subagents/docs/extension-api.md +36 -0
  41. package/dist/extensions/pi-subagents/docs/missions.md +5 -3
  42. package/dist/extensions/pi-subagents/docs/models.md +1 -1
  43. package/dist/extensions/pi-subagents/docs/observability.md +42 -2
  44. package/dist/extensions/pi-subagents/docs/tool-reference.md +20 -3
  45. package/dist/extensions/pi-subagents/docs/workflows.md +4 -4
  46. package/dist/extensions/pi-subagents/install.mjs +2 -2
  47. package/dist/extensions/pi-subagents/package-lock.json +1470 -1472
  48. package/dist/extensions/pi-subagents/package.json +1 -1
  49. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +1 -1
  50. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  51. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +16 -15
  52. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  53. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  54. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
  55. package/dist/extensions/pi-subagents/src/agents/agents.ts +37 -12
  56. package/dist/extensions/pi-subagents/src/api/external-runs.ts +174 -84
  57. package/dist/extensions/pi-subagents/src/api/preflight.ts +13 -7
  58. package/dist/extensions/pi-subagents/src/extension/config.ts +59 -0
  59. package/dist/extensions/pi-subagents/src/extension/index.ts +146 -23
  60. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +34 -6
  61. package/dist/extensions/pi-subagents/src/extension/rpc.ts +5 -1
  62. package/dist/extensions/pi-subagents/src/extension/schemas.ts +31 -30
  63. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +10 -9
  64. package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +13 -8
  65. package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +16 -3
  66. package/dist/extensions/pi-subagents/src/inspectors/herdr/project-panes.ts +2 -6
  67. package/dist/extensions/pi-subagents/src/inspectors/herdr/shell-command.ts +16 -0
  68. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +5 -4
  69. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +19 -42
  70. package/dist/extensions/pi-subagents/src/missions/goal-driver.ts +3 -1
  71. package/dist/extensions/pi-subagents/src/missions/store.ts +8 -3
  72. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +82 -25
  73. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +71 -1
  74. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +73 -43
  75. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +17 -6
  76. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -6
  77. package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +277 -0
  78. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +11 -3
  79. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +2 -2
  80. package/dist/extensions/pi-subagents/src/runs/background/completion-replay.ts +11 -1
  81. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +21 -6
  82. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +5 -2
  83. package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +437 -0
  84. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +224 -42
  85. package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +27 -7
  86. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +76 -19
  87. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +35 -26
  88. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +108 -9
  89. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +54 -28
  90. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +27 -13
  91. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +298 -33
  92. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +2 -0
  93. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +5 -2
  94. package/dist/extensions/pi-subagents/src/runs/background/wait-subscriptions.ts +2 -1
  95. package/dist/extensions/pi-subagents/src/runs/foreground/async-dismiss-action.ts +2 -1
  96. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +16 -0
  97. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +219 -15
  98. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +7 -4
  99. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +4 -3
  100. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +368 -50
  101. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +107 -1
  102. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +4 -0
  103. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +39 -23
  104. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +16 -2
  105. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +66 -62
  106. package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +376 -0
  107. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
  108. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +15 -0
  109. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +1 -9
  110. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +12 -0
  111. package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +95 -0
  112. package/dist/extensions/pi-subagents/src/shared/agent-stream-options.ts +5 -0
  113. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +0 -4
  114. package/dist/extensions/pi-subagents/src/shared/display-text.ts +50 -0
  115. package/dist/extensions/pi-subagents/src/shared/node-executable.ts +21 -0
  116. package/dist/extensions/pi-subagents/src/shared/session-lineage.ts +71 -0
  117. package/dist/extensions/pi-subagents/src/shared/types.ts +66 -4
  118. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +34 -25
  119. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +4 -0
  120. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +160 -45
  121. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +1 -48
  122. package/dist/extensions/pi-subagents/src/tui/fleet.ts +129 -17
  123. package/dist/extensions/pi-subagents/src/tui/render.ts +122 -44
  124. package/dist/extensions/pi-subagents/src/watchdog/permission-arbiter.ts +2 -1
  125. package/dist/extensions/pi-subagents/src/watchdog/review.ts +4 -3
  126. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +10 -2
  127. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +272 -76
  128. package/dist/extensions/pi-subagents/src/workflows/workflow-auto-relaunch.ts +28 -0
  129. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +2 -0
  130. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2 -2
  131. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +68 -9
  132. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +27 -1
  133. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1 -1
  134. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +49 -0
  135. package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
  136. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +26 -1
  137. package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +144 -0
  138. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +26 -0
  139. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +369 -98
  140. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +111 -78
  141. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +49 -1
  142. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +12 -0
  143. package/dist/extensions/pi-subagents/test/support/node-command.ts +15 -0
  144. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +50 -0
  145. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +28 -0
  146. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +61 -0
  147. package/dist/extensions/pi-subagents/test/unit/agent-stream-options.test.ts +14 -0
  148. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +76 -0
  149. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
  150. package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +162 -0
  151. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +209 -10
  152. package/dist/extensions/pi-subagents/test/unit/completion-replay.test.ts +41 -1
  153. package/dist/extensions/pi-subagents/test/unit/external-runs.test.ts +135 -42
  154. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +252 -1
  155. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +192 -6
  156. package/dist/extensions/pi-subagents/test/unit/handoff-adoption.test.ts +103 -0
  157. package/dist/extensions/pi-subagents/test/unit/herdr-inspector-bootstrap.test.ts +32 -0
  158. package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +59 -0
  159. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +91 -0
  160. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +23 -4
  161. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +56 -1
  162. package/dist/extensions/pi-subagents/test/unit/mission-goal-driver.test.ts +67 -3
  163. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
  164. package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +14 -0
  165. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +18 -0
  166. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +2 -2
  167. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +25 -2
  168. package/dist/extensions/pi-subagents/test/unit/node-executable.test.ts +26 -0
  169. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +8 -0
  170. package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +348 -0
  171. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +5 -2
  172. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +1 -1
  173. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +34 -0
  174. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +6 -1
  175. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +92 -0
  176. package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +175 -0
  177. package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +127 -17
  178. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +44 -0
  179. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +247 -0
  180. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +107 -2
  181. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +5 -8
  182. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +456 -4
  183. package/dist/extensions/pi-subagents/test/unit/session-lineage.test.ts +73 -0
  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-auto-relaunch.test.ts +45 -0
  193. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +94 -1
  194. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +42 -2
  195. package/dist/extensions/ponytail/index.js +11 -0
  196. package/dist/extensions/ponytail/ponytail-config.cjs +2 -0
  197. package/dist/extensions/ponytail/ponytail-instructions.cjs +6 -0
  198. package/dist/extensions/ponytail/test/extension.test.js +274 -140
  199. package/dist/extensions/ponytail/test/helpers.test.js +280 -92
  200. package/dist/extensions/question/index.ts +33 -0
  201. package/dist/extensions/question/question-list.ts +28 -0
  202. package/dist/extensions/question/row-layout.ts +3 -0
  203. package/dist/extensions/question/tests/batch.test.ts +103 -65
  204. package/dist/extensions/question/tests/exp.test.ts +70 -0
  205. package/dist/extensions/question/tests/exp2.test.ts +59 -0
  206. package/dist/extensions/question/tests/exp3.test.ts +47 -0
  207. package/dist/extensions/question/tests/helpers.test.ts +75 -35
  208. package/dist/extensions/question/tests/question-list.test.ts +640 -198
  209. package/dist/extensions/question/tests/row-layout.test.ts +193 -114
  210. package/dist/extensions/question/tests/schemas.test.ts +42 -19
  211. package/dist/extensions/question/tests/selection-mode.test.ts +54 -0
  212. package/dist/extensions/question/tests/shortcuts.test.ts +106 -84
  213. package/dist/extensions/question/tests/wizard.test.ts +771 -50
  214. package/dist/extensions/question/tests/zz-ig3.test.ts +20 -0
  215. package/dist/extensions/question/tests/zz-probe.test.ts +29 -0
  216. package/dist/extensions/rtk.test.ts +180 -1
  217. package/dist/extensions/tokenin-onboarding.ts +3 -0
  218. package/dist/extensions/undo.test.ts +720 -0
  219. package/dist/extensions/undo.ts +6 -0
  220. package/dist/extensions/web-agent-onboarding.test.ts +415 -0
  221. package/dist/extensions/workflow/extension.ts +3 -3
  222. package/dist/extensions/workflow/modes.ts +41 -19
  223. package/dist/skills/pi-subagents/SKILL.md +1 -1
  224. package/dist/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  225. package/dist/skills/pi-subagents/references/execution-controls.md +16 -15
  226. package/dist/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  227. package/dist/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  228. package/docs/plans/workflow-autoloop-reference.md +1 -2
  229. package/docs/plans/workflow-handoff-carryover.md +108 -0
  230. package/docs/settings.md +6 -0
  231. package/package.json +4 -1
@@ -0,0 +1,175 @@
1
+ import assert from "node:assert/strict";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { describe, it } from "node:test";
6
+ import { cleanupResultIndexes, removeResultIndex, resultCandidateFilesForSession, resultFilesForSession, resultPayloadPathForIndexedRun, resultPayloadPathForSessionRun, writeAsyncResultFile, writePendingAsyncResultFile, writeResultIndexForData } from "../../src/runs/background/result-files.ts";
7
+
8
+ function pendingPath(resultsDir: string, sessionId: string, runId: string): string {
9
+ return path.join(resultsDir, "result-pending", encodeURIComponent(sessionId), `${encodeURIComponent(runId)}.json`);
10
+ }
11
+
12
+ describe("result file indexes", () => {
13
+ it("removes orphan index entries without deleting flat result files", () => {
14
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-files-index-"));
15
+ try {
16
+ writeAsyncResultFile(path.join(resultsDir, "kept.json"), { id: "kept", runId: "kept", sessionId: "session-a", success: true });
17
+ writeAsyncResultFile(path.join(resultsDir, "missing.json"), { id: "missing", runId: "missing", sessionId: "session-a", toolCallId: "call-missing", success: true });
18
+ fs.rmSync(path.join(resultsDir, "missing.json"));
19
+ fs.writeFileSync(path.join(resultsDir, "unindexed.json"), JSON.stringify({ id: "unindexed", sessionId: "session-a" }), "utf-8");
20
+
21
+ assert.equal(cleanupResultIndexes(resultsDir, Date.now() + 86_400_001, 86_400_000), 2);
22
+
23
+ assert.deepEqual(resultFilesForSession(resultsDir, "session-a"), ["kept.json"]);
24
+ assert.equal(fs.existsSync(path.join(resultsDir, "kept.json")), true);
25
+ assert.equal(fs.existsSync(path.join(resultsDir, "unindexed.json")), true);
26
+ } finally {
27
+ fs.rmSync(resultsDir, { recursive: true, force: true });
28
+ }
29
+ });
30
+
31
+ it("promotes an indexed pending result payload", () => {
32
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-files-pending-payload-"));
33
+ const originalError = console.error;
34
+ try {
35
+ console.error = () => {};
36
+ const resultPath = path.join(resultsDir, "late.json");
37
+ fs.mkdirSync(resultPath, { recursive: true });
38
+
39
+ assert.deepEqual(writeAsyncResultFile(resultPath, { id: "late", runId: "late", sessionId: "session-a", success: true }), { state: "pending" });
40
+ assert.equal(fs.existsSync(pendingPath(resultsDir, "session-a", "late")), true);
41
+ fs.rmSync(resultPath, { recursive: true, force: true });
42
+
43
+ assert.deepEqual(resultFilesForSession(resultsDir, "session-a"), ["late.json"]);
44
+ assert.equal(JSON.parse(fs.readFileSync(resultPath, "utf-8")).success, true);
45
+ assert.equal(fs.existsSync(pendingPath(resultsDir, "session-a", "late")), false);
46
+ } finally {
47
+ console.error = originalError;
48
+ fs.rmSync(resultsDir, { recursive: true, force: true });
49
+ }
50
+ });
51
+
52
+ it("writes an indexed pending result without publishing it", () => {
53
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-files-pending-only-"));
54
+ const originalError = console.error;
55
+ try {
56
+ console.error = () => {};
57
+ const resultPath = path.join(resultsDir, "pending-only.json");
58
+ fs.mkdirSync(resultPath, { recursive: true });
59
+
60
+ writePendingAsyncResultFile(resultPath, { id: "pending-only", runId: "pending-only", sessionId: "session-a", success: true });
61
+
62
+ assert.equal(fs.statSync(resultPath).isDirectory(), true);
63
+ assert.deepEqual(resultFilesForSession(resultsDir, "session-a"), []);
64
+ assert.deepEqual(resultCandidateFilesForSession(resultsDir, "session-a"), ["pending-only.json"]);
65
+ assert.equal(resultPayloadPathForSessionRun(resultsDir, "session-a", "pending-only"), pendingPath(resultsDir, "session-a", "pending-only"));
66
+ assert.equal(resultPayloadPathForIndexedRun(resultsDir, "pending-only"), pendingPath(resultsDir, "session-a", "pending-only"));
67
+ } finally {
68
+ console.error = originalError;
69
+ fs.rmSync(resultsDir, { recursive: true, force: true });
70
+ }
71
+ });
72
+
73
+ it("keeps a legacy valid index while the result payload is not visible yet", () => {
74
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-files-legacy-late-payload-"));
75
+ try {
76
+ const resultPath = path.join(resultsDir, "late.json");
77
+ writeResultIndexForData(resultPath, { id: "late", runId: "late", sessionId: "session-a", success: true });
78
+
79
+ assert.deepEqual(resultFilesForSession(resultsDir, "session-a"), []);
80
+
81
+ fs.writeFileSync(resultPath, JSON.stringify({ id: "late", runId: "late", sessionId: "session-a", success: true }), "utf-8");
82
+ assert.deepEqual(resultFilesForSession(resultsDir, "session-a"), ["late.json"]);
83
+ } finally {
84
+ fs.rmSync(resultsDir, { recursive: true, force: true });
85
+ }
86
+ });
87
+
88
+ it("keeps an unindexed pending result recoverable when its session index cannot be written", () => {
89
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-files-index-failure-"));
90
+ try {
91
+ fs.writeFileSync(path.join(resultsDir, "result-index"), "not a directory", "utf-8");
92
+ const resultPath = path.join(resultsDir, "blocked.json");
93
+
94
+ assert.throws(() => writeAsyncResultFile(resultPath, { id: "blocked", runId: "blocked", sessionId: "session-a", success: true }));
95
+ assert.equal(fs.existsSync(resultPath), false);
96
+ assert.equal(fs.existsSync(pendingPath(resultsDir, "session-a", "blocked")), true);
97
+ assert.equal(resultPayloadPathForSessionRun(resultsDir, "session-a", "blocked"), pendingPath(resultsDir, "session-a", "blocked"));
98
+ assert.equal(resultPayloadPathForIndexedRun(resultsDir, "blocked"), pendingPath(resultsDir, "session-a", "blocked"));
99
+ assert.deepEqual(resultCandidateFilesForSession(resultsDir, "session-a"), ["blocked.json"]);
100
+ } finally {
101
+ fs.rmSync(resultsDir, { recursive: true, force: true });
102
+ }
103
+ });
104
+
105
+ it("does not commit a result payload without a session id", () => {
106
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-files-no-session-"));
107
+ try {
108
+ const resultPath = path.join(resultsDir, "blocked.json");
109
+
110
+ assert.throws(() => writeAsyncResultFile(resultPath, { id: "blocked", runId: "blocked", success: true }), /sessionId/);
111
+ assert.equal(fs.existsSync(resultPath), false);
112
+ } finally {
113
+ fs.rmSync(resultsDir, { recursive: true, force: true });
114
+ }
115
+ });
116
+
117
+ it("prefers pending payload over an older public result", () => {
118
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-files-pending-wins-"));
119
+ const originalError = console.error;
120
+ try {
121
+ console.error = () => {};
122
+ const resultPath = path.join(resultsDir, "blocked.json");
123
+ writeAsyncResultFile(resultPath, { id: "blocked", runId: "blocked", sessionId: "session-a", success: false });
124
+ fs.rmSync(resultPath, { force: true });
125
+ fs.mkdirSync(resultPath, { recursive: true });
126
+
127
+ assert.deepEqual(writeAsyncResultFile(resultPath, { id: "blocked", runId: "blocked", sessionId: "session-a", success: true }), { state: "pending" });
128
+ fs.rmSync(resultPath, { recursive: true, force: true });
129
+ fs.writeFileSync(resultPath, JSON.stringify({ id: "blocked", runId: "blocked", sessionId: "session-a", success: false }), "utf-8");
130
+
131
+ assert.deepEqual(resultFilesForSession(resultsDir, "session-a"), ["blocked.json"]);
132
+ assert.equal(JSON.parse(fs.readFileSync(resultPath, "utf-8")).success, true);
133
+ } finally {
134
+ console.error = originalError;
135
+ fs.rmSync(resultsDir, { recursive: true, force: true });
136
+ }
137
+ });
138
+
139
+ it("removes pending payloads with result indexes", () => {
140
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-files-pending-cleanup-"));
141
+ const originalError = console.error;
142
+ try {
143
+ console.error = () => {};
144
+ const resultPath = path.join(resultsDir, "pending-cleanup.json");
145
+ fs.mkdirSync(resultPath, { recursive: true });
146
+ writeAsyncResultFile(resultPath, { id: "pending-cleanup", runId: "pending-cleanup", sessionId: "session-a", success: true });
147
+
148
+ assert.equal(fs.existsSync(pendingPath(resultsDir, "session-a", "pending-cleanup")), true);
149
+ removeResultIndex(resultsDir, "session-a", "pending-cleanup");
150
+ assert.equal(fs.existsSync(pendingPath(resultsDir, "session-a", "pending-cleanup")), false);
151
+ } finally {
152
+ console.error = originalError;
153
+ fs.rmSync(resultsDir, { recursive: true, force: true });
154
+ }
155
+ });
156
+
157
+ it("commits a result payload when only an optional index fails", () => {
158
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-files-optional-index-"));
159
+ const originalError = console.error;
160
+ try {
161
+ console.error = () => {};
162
+ fs.mkdirSync(path.join(resultsDir, "result-index"), { recursive: true });
163
+ fs.writeFileSync(path.join(resultsDir, "result-index", "tool-calls"), "not a directory", "utf-8");
164
+ const resultPath = path.join(resultsDir, "kept.json");
165
+
166
+ writeAsyncResultFile(resultPath, { id: "kept", runId: "kept", sessionId: "session-a", toolCallId: "call-a", success: true });
167
+
168
+ assert.equal(fs.existsSync(resultPath), true);
169
+ assert.deepEqual(resultFilesForSession(resultsDir, "session-a"), ["kept.json"]);
170
+ } finally {
171
+ console.error = originalError;
172
+ fs.rmSync(resultsDir, { recursive: true, force: true });
173
+ }
174
+ });
175
+ });
@@ -3,55 +3,165 @@ 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 { listRetainedChildren } from "../../src/runs/background/retained-children.ts";
6
+ import { formatRetainedChildren, listRetainedChildren } from "../../src/runs/background/retained-children.ts";
7
+ import { createRunFanoutBudget } from "../../src/runs/shared/run-fanout-budget.ts";
7
8
 
8
- function writeRetainedRun(root: string, index: number, sessionId = "parent-a", parentWorkflowRunId: string | null = "workflow-a"): void {
9
+ interface WriteRunOptions {
10
+ sessionId?: string;
11
+ parentWorkflowRunId?: string | null;
12
+ state?: "complete" | "failed" | "paused" | "stopped";
13
+ stepStatus?: "complete" | "completed" | "failed" | "paused" | "stopped";
14
+ sessionFile?: "present" | "missing" | "omitted";
15
+ runner?: "external-cli";
16
+ recoveryDescriptor?: "present" | "missing" | "invalid";
17
+ recoverySourceRunId?: string;
18
+ recoveryAgent?: string;
19
+ }
20
+
21
+ function writeRetainedRun(root: string, index: number, options: WriteRunOptions = {}): void {
9
22
  const runId = `child-${index}`;
10
23
  const sessionFile = path.join(root, "sessions", `${runId}.jsonl`);
11
- fs.mkdirSync(path.dirname(sessionFile), { recursive: true });
12
- fs.writeFileSync(sessionFile, "", "utf-8");
24
+ if (options.sessionFile !== "missing" && options.sessionFile !== "omitted") {
25
+ fs.mkdirSync(path.dirname(sessionFile), { recursive: true });
26
+ fs.writeFileSync(sessionFile, "", "utf-8");
27
+ }
13
28
  const asyncDir = path.join(root, "runs", runId);
14
29
  fs.mkdirSync(asyncDir, { recursive: true });
30
+ const state = options.state ?? "complete";
31
+ const stepStatus = options.stepStatus ?? state;
15
32
  fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
16
33
  runId,
17
- sessionId,
34
+ sessionId: options.sessionId ?? "parent-a",
18
35
  mode: "single",
19
- state: "complete",
36
+ state,
20
37
  startedAt: index,
21
38
  endedAt: 1_000 + index,
22
39
  lastUpdate: 1_000 + index,
23
- ...(parentWorkflowRunId ? { parentWorkflowRunId } : {}),
40
+ cwd: root,
41
+ ...(options.parentWorkflowRunId === null ? {} : { parentWorkflowRunId: options.parentWorkflowRunId ?? "workflow-a", workflowKey: `lane-${index}` }),
24
42
  steps: [{
25
43
  agent: "worker",
26
- status: "complete",
44
+ status: stepStatus,
27
45
  description: ` Task ${index} with spacing ${"x".repeat(140)} `,
28
46
  endedAt: 1_000 + index,
29
- sessionFile,
47
+ ...(options.runner === "external-cli" ? { runner: { type: "external-cli", command: "codex", args: [], promptDelivery: "stdin", capabilities: { stop: true, steer: false, resume: false, structuredOutput: false, toolEvents: false } } } : {}),
48
+ ...(options.sessionFile === "omitted" ? {} : { sessionFile }),
30
49
  tokens: { input: index, output: index + 1, total: index * 2 + 1 },
31
50
  }],
32
51
  }), "utf-8");
52
+ if (options.recoveryDescriptor === "invalid") {
53
+ fs.writeFileSync(path.join(asyncDir, "recovery-descriptor.json"), JSON.stringify({
54
+ version: 2,
55
+ sourceRunId: runId,
56
+ agent: "worker",
57
+ cwd: root,
58
+ systemPromptMode: "replace",
59
+ outputMode: "inline",
60
+ }), "utf-8");
61
+ } else if (options.recoveryDescriptor !== "missing") {
62
+ fs.writeFileSync(path.join(asyncDir, "recovery-descriptor.json"), JSON.stringify({
63
+ version: 1,
64
+ runFanoutBudget: createRunFanoutBudget(runId, 64),
65
+ sourceRunId: options.recoverySourceRunId ?? runId,
66
+ agent: options.recoveryAgent ?? "worker",
67
+ cwd: root,
68
+ systemPromptMode: "replace",
69
+ inheritProjectContext: false,
70
+ inheritSkills: false,
71
+ outputMode: "inline",
72
+ maxSubagentDepth: 2,
73
+ share: false,
74
+ }), "utf-8");
75
+ }
33
76
  }
34
77
 
35
78
  describe("retained child roster", () => {
36
- it("returns only the newest 10 completed workflow children owned by the parent session", () => {
79
+ it("returns terminal workflow children newest first and formats a 10-row roster", () => {
37
80
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-retained-children-"));
38
81
  try {
39
82
  for (let index = 0; index < 12; index++) writeRetainedRun(root, index);
40
- writeRetainedRun(root, 50, "parent-b");
41
- writeRetainedRun(root, 51, "parent-a", null);
83
+ writeRetainedRun(root, 50, { sessionId: "parent-b" });
84
+ writeRetainedRun(root, 51, { parentWorkflowRunId: null });
85
+ writeRetainedRun(root, 52, { parentWorkflowRunId: "workflow-failed", state: "failed" });
42
86
 
43
87
  const children = listRetainedChildren(path.join(root, "runs"), "parent-a");
88
+ const formatted = formatRetainedChildren(children);
44
89
 
45
- assert.equal(children.length, 10);
46
- assert.deepEqual(children.map((child) => child.runId), Array.from({ length: 10 }, (_, offset) => `child-${11 - offset}`));
90
+ assert.equal(children.length, 13);
91
+ assert.deepEqual(children.map((child) => child.runId), ["child-52", ...Array.from({ length: 12 }, (_, offset) => `child-${11 - offset}`)]);
92
+ assert.equal(formatted.match(/^- child-/gm)?.length, 10);
93
+ assert.doesNotMatch(formatted, /^- child-2 /m);
47
94
  assert.equal(children[0]?.agent, "worker");
48
- assert.equal(children[0]?.completedAt, 1_011);
95
+ assert.equal(children[0]?.completedAt, 1_052);
96
+ assert.equal(children[0]?.state, "failed");
97
+ assert.equal(children[0]?.parentRunId, "workflow-failed");
98
+ assert.equal(children[0]?.workflowKey, "lane-52");
49
99
  assert.ok((children[0]?.taskSummary.length ?? 0) <= 120);
50
- assert.equal(children[0]?.taskSummary.startsWith("Task 11 with spacing"), true);
51
- assert.deepEqual(children[0]?.tokenTotals, { input: 11, output: 12, total: 23 });
100
+ assert.equal(children[0]?.taskSummary.startsWith("Task 52 with spacing"), true);
101
+ assert.deepEqual(children[0]?.tokenTotals, { input: 52, output: 53, total: 105 });
102
+ assert.equal(children[0]?.resumability.state, "resumable");
103
+ assert.match(formatted, /resumability: resumable\n session: .*child-52\.jsonl\n resume: subagent\(\{ action: "resume", id: "child-52", message: "\.\.\." \}\)/);
52
104
  assert.equal(children.some((child) => child.runId === "child-50" || child.runId === "child-51"), false);
53
105
  } finally {
54
106
  fs.rmSync(root, { recursive: true, force: true });
55
107
  }
56
108
  });
109
+
110
+ it("keeps an older resumable child visible behind ten newer non-resumable children", () => {
111
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-retained-children-"));
112
+ try {
113
+ writeRetainedRun(root, 0);
114
+ for (let index = 1; index <= 10; index++) writeRetainedRun(root, index, { recoveryDescriptor: "missing" });
115
+
116
+ const children = listRetainedChildren(path.join(root, "runs"), "parent-a");
117
+ const formatted = formatRetainedChildren(children);
118
+
119
+ assert.equal(children.length, 11);
120
+ assert.deepEqual(children.map((child) => child.runId), Array.from({ length: 11 }, (_, offset) => `child-${10 - offset}`));
121
+ assert.equal(formatted.match(/^- child-/gm)?.length, 10);
122
+ assert.match(formatted, /resume: subagent\(\{ action: "resume", id: "child-0"/);
123
+ assert.doesNotMatch(formatted, /No resumable retained child is listed/);
124
+ } finally {
125
+ fs.rmSync(root, { recursive: true, force: true });
126
+ }
127
+ });
128
+
129
+ it("keeps non-resumable retained children visible with exact reasons", () => {
130
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-retained-children-"));
131
+ try {
132
+ writeRetainedRun(root, 1, { sessionFile: "omitted" });
133
+ writeRetainedRun(root, 2, { sessionFile: "missing" });
134
+ writeRetainedRun(root, 3, { state: "stopped", stepStatus: "stopped" });
135
+ writeRetainedRun(root, 4, { runner: "external-cli" });
136
+ writeRetainedRun(root, 5, { recoveryDescriptor: "missing" });
137
+ writeRetainedRun(root, 6, { recoveryDescriptor: "invalid" });
138
+ writeRetainedRun(root, 7, { recoverySourceRunId: "other-run" });
139
+ writeRetainedRun(root, 8, { recoveryAgent: "reviewer" });
140
+
141
+ const children = listRetainedChildren(path.join(root, "runs"), "parent-a");
142
+ const formatted = formatRetainedChildren(children);
143
+
144
+ assert.deepEqual(children.map((child) => [child.runId, child.resumability]), [
145
+ ["child-8", { state: "not-resumable", reason: "recovery descriptor belongs to agent reviewer" }],
146
+ ["child-7", { state: "not-resumable", reason: "recovery descriptor belongs to run other-run" }],
147
+ ["child-6", { state: "not-resumable", reason: `invalid recovery descriptor: Invalid async recovery descriptor '${path.join(root, "runs", "child-6", "recovery-descriptor.json")}': version must be 1.` }],
148
+ ["child-5", { state: "not-resumable", reason: "missing recovery descriptor" }],
149
+ ["child-4", { state: "not-resumable", reason: "external CLI runner" }],
150
+ ["child-3", { state: "not-resumable", reason: "stopped run" }],
151
+ ["child-2", { state: "not-resumable", reason: `persisted session file is missing: ${path.join(root, "sessions", "child-2.jsonl")}` }],
152
+ ["child-1", { state: "not-resumable", reason: "no persisted session file" }],
153
+ ]);
154
+ assert.match(formatted, /resumability: not resumable \(recovery descriptor belongs to agent reviewer\)/);
155
+ assert.match(formatted, /resumability: not resumable \(recovery descriptor belongs to run other-run\)/);
156
+ assert.match(formatted, /resumability: not resumable \(invalid recovery descriptor:/);
157
+ assert.match(formatted, /resumability: not resumable \(missing recovery descriptor\)/);
158
+ assert.match(formatted, /resumability: not resumable \(external CLI runner\)/);
159
+ assert.match(formatted, /resumability: not resumable \(stopped run\)/);
160
+ assert.match(formatted, /resumability: not resumable \(no persisted session file\)/);
161
+ assert.doesNotMatch(formatted, /resume: subagent/);
162
+ assert.match(formatted, /No resumable retained child is listed\. Launch a same-role fallback challenge and label it as fallback\./);
163
+ } finally {
164
+ fs.rmSync(root, { recursive: true, force: true });
165
+ }
166
+ });
57
167
  });
@@ -4,6 +4,8 @@ import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import { afterEach, describe, it } from "node:test";
6
6
  import type { SubagentState } from "../../src/shared/types.ts";
7
+ import { releaseActiveRunIndex, updateActiveRunIndex } from "../../src/runs/background/active-run-index.ts";
8
+ import { resultFilePath, writeAsyncResultFile } from "../../src/runs/background/result-files.ts";
7
9
  import { resolveSubagentRunId } from "../../src/runs/background/run-id-resolver.ts";
8
10
  import { createNestedRoute, writeNestedEvent } from "../../src/runs/shared/nested-events.ts";
9
11
 
@@ -139,6 +141,48 @@ describe("subagent run id resolver", () => {
139
141
  }
140
142
  });
141
143
 
144
+ it("does not resolve unindexed result files by prefix", () => {
145
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-run-id-result-prefix-"));
146
+ try {
147
+ const asyncRoot = path.join(root, "runs");
148
+ const resultsDir = path.join(root, "results");
149
+ fs.mkdirSync(resultsDir, { recursive: true });
150
+ fs.writeFileSync(path.join(resultsDir, "legacy-run.json"), JSON.stringify({ id: "legacy-run", sessionId: "session-a", success: true }), "utf-8");
151
+
152
+ const exact = resolveSubagentRunId("legacy-run", { asyncDirRoot: asyncRoot, resultsDir });
153
+ assert.equal(exact?.kind, "async");
154
+ assert.equal(exact?.id, "legacy-run");
155
+ assert.equal(resolveSubagentRunId("legacy", { asyncDirRoot: asyncRoot, resultsDir }), undefined);
156
+ } finally {
157
+ fs.rmSync(root, { recursive: true, force: true });
158
+ }
159
+ });
160
+
161
+ it("resolves workflow tool-call ids through active and result indexes", () => {
162
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-run-resolver-tool-call-index-"));
163
+ try {
164
+ const asyncRoot = path.join(root, "async");
165
+ const resultsDir = path.join(root, "results");
166
+ const activeDir = path.join(asyncRoot, "workflow-active");
167
+ fs.mkdirSync(activeDir, { recursive: true });
168
+ fs.writeFileSync(path.join(activeDir, "status.json"), JSON.stringify({ runId: "workflow-active", toolCallId: "call-active", state: "running", mode: "workflow", startedAt: 1, lastUpdate: 1, steps: [] }), "utf-8");
169
+ updateActiveRunIndex(activeDir, "running", "call-active");
170
+
171
+ const active = resolveSubagentRunId("call-active", { asyncDirRoot: asyncRoot, resultsDir });
172
+ assert.equal(active?.kind, "async");
173
+ assert.equal(active?.id, "workflow-active");
174
+ releaseActiveRunIndex(activeDir);
175
+ assert.equal(resolveSubagentRunId("call-active", { asyncDirRoot: asyncRoot, resultsDir }), undefined);
176
+
177
+ writeAsyncResultFile(resultFilePath(resultsDir, "workflow-done"), { id: "workflow-done", runId: "workflow-done", toolCallId: "call-done", sessionId: "session-a", state: "complete", success: true });
178
+ const done = resolveSubagentRunId("call-done", { asyncDirRoot: asyncRoot, resultsDir });
179
+ assert.equal(done?.kind, "async");
180
+ assert.equal(done?.id, "workflow-done");
181
+ } finally {
182
+ fs.rmSync(root, { recursive: true, force: true });
183
+ }
184
+ });
185
+
142
186
  it("rejects unsafe nested id tokens before lookup", () => {
143
187
  assert.throws(() => resolveSubagentRunId("../run"), /safe id token/);
144
188
  assert.throws(() => resolveSubagentRunId("a/b"), /safe id token/);