@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.
Files changed (192) hide show
  1. package/CHANGELOG.md +16 -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/settings.json +1 -0
  16. package/dist/extensions/model-prompt-injector/config.json +10 -0
  17. package/dist/extensions/model-prompt-injector/index.test.ts +112 -0
  18. package/dist/extensions/model-prompt-injector/index.ts +151 -0
  19. package/dist/extensions/package.json +1 -0
  20. package/dist/extensions/pi-subagents/CHANGELOG.md +158 -90
  21. package/dist/extensions/pi-subagents/UPSTREAM-V0.50-MAPPING.md +172 -0
  22. package/dist/extensions/pi-subagents/agents/oracle.md +7 -5
  23. package/dist/extensions/pi-subagents/agents/reviewer.md +2 -2
  24. package/dist/extensions/pi-subagents/agents/scout.md +1 -1
  25. package/dist/extensions/pi-subagents/agents/worker.md +1 -1
  26. package/dist/extensions/pi-subagents/docs/configuration.md +78 -19
  27. package/dist/extensions/pi-subagents/docs/extension-api.md +36 -0
  28. package/dist/extensions/pi-subagents/docs/missions.md +5 -3
  29. package/dist/extensions/pi-subagents/docs/models.md +1 -1
  30. package/dist/extensions/pi-subagents/docs/observability.md +42 -2
  31. package/dist/extensions/pi-subagents/docs/tool-reference.md +20 -3
  32. package/dist/extensions/pi-subagents/docs/workflows.md +5 -5
  33. package/dist/extensions/pi-subagents/install.mjs +2 -2
  34. package/dist/extensions/pi-subagents/package-lock.json +1470 -1472
  35. package/dist/extensions/pi-subagents/package.json +1 -1
  36. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +1 -1
  37. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  38. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +15 -14
  39. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  40. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  41. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
  42. package/dist/extensions/pi-subagents/src/agents/agents.ts +37 -12
  43. package/dist/extensions/pi-subagents/src/api/external-runs.ts +174 -84
  44. package/dist/extensions/pi-subagents/src/api/preflight.ts +13 -7
  45. package/dist/extensions/pi-subagents/src/extension/config.ts +53 -0
  46. package/dist/extensions/pi-subagents/src/extension/index.ts +67 -13
  47. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +34 -6
  48. package/dist/extensions/pi-subagents/src/extension/rpc.ts +5 -1
  49. package/dist/extensions/pi-subagents/src/extension/schemas.ts +31 -30
  50. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +10 -9
  51. package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +13 -8
  52. package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +16 -3
  53. package/dist/extensions/pi-subagents/src/inspectors/herdr/project-panes.ts +2 -6
  54. package/dist/extensions/pi-subagents/src/inspectors/herdr/shell-command.ts +16 -0
  55. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +5 -4
  56. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +19 -42
  57. package/dist/extensions/pi-subagents/src/missions/goal-driver.ts +3 -1
  58. package/dist/extensions/pi-subagents/src/missions/store.ts +8 -3
  59. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +82 -25
  60. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +71 -1
  61. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +73 -43
  62. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +5 -0
  63. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -6
  64. package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +277 -0
  65. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +8 -3
  66. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +2 -2
  67. package/dist/extensions/pi-subagents/src/runs/background/completion-replay.ts +11 -1
  68. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +21 -6
  69. package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +437 -0
  70. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +205 -41
  71. package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +27 -7
  72. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +76 -19
  73. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +30 -24
  74. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +101 -4
  75. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +54 -28
  76. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +27 -13
  77. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +298 -33
  78. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +2 -0
  79. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +5 -2
  80. package/dist/extensions/pi-subagents/src/runs/foreground/async-dismiss-action.ts +2 -1
  81. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +16 -0
  82. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +219 -15
  83. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +4 -3
  84. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +333 -45
  85. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +107 -1
  86. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +4 -0
  87. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +39 -23
  88. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +16 -2
  89. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +66 -62
  90. package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +376 -0
  91. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
  92. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +15 -0
  93. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +1 -9
  94. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +12 -0
  95. package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +95 -0
  96. package/dist/extensions/pi-subagents/src/shared/agent-stream-options.ts +5 -0
  97. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +0 -4
  98. package/dist/extensions/pi-subagents/src/shared/display-text.ts +50 -0
  99. package/dist/extensions/pi-subagents/src/shared/node-executable.ts +21 -0
  100. package/dist/extensions/pi-subagents/src/shared/types.ts +50 -4
  101. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +34 -25
  102. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +4 -0
  103. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +160 -45
  104. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +1 -48
  105. package/dist/extensions/pi-subagents/src/tui/fleet.ts +128 -16
  106. package/dist/extensions/pi-subagents/src/tui/render.ts +122 -44
  107. package/dist/extensions/pi-subagents/src/watchdog/permission-arbiter.ts +2 -1
  108. package/dist/extensions/pi-subagents/src/watchdog/review.ts +4 -3
  109. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +10 -2
  110. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +272 -76
  111. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +2 -0
  112. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2 -2
  113. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +68 -9
  114. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +27 -1
  115. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1 -1
  116. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +49 -0
  117. package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
  118. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +26 -1
  119. package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +144 -0
  120. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +26 -0
  121. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +369 -98
  122. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +111 -78
  123. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +49 -1
  124. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +12 -0
  125. package/dist/extensions/pi-subagents/test/support/node-command.ts +15 -0
  126. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +50 -0
  127. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +28 -0
  128. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +61 -0
  129. package/dist/extensions/pi-subagents/test/unit/agent-stream-options.test.ts +14 -0
  130. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +76 -0
  131. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
  132. package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +162 -0
  133. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +209 -10
  134. package/dist/extensions/pi-subagents/test/unit/completion-replay.test.ts +41 -1
  135. package/dist/extensions/pi-subagents/test/unit/external-runs.test.ts +135 -42
  136. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +252 -1
  137. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +192 -6
  138. package/dist/extensions/pi-subagents/test/unit/herdr-inspector-bootstrap.test.ts +32 -0
  139. package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +59 -0
  140. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +69 -0
  141. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +23 -4
  142. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +56 -1
  143. package/dist/extensions/pi-subagents/test/unit/mission-goal-driver.test.ts +67 -3
  144. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
  145. package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +14 -0
  146. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +18 -0
  147. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +2 -2
  148. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +25 -2
  149. package/dist/extensions/pi-subagents/test/unit/node-executable.test.ts +26 -0
  150. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +8 -0
  151. package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +348 -0
  152. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +5 -2
  153. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +1 -1
  154. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +34 -0
  155. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +6 -1
  156. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +92 -0
  157. package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +175 -0
  158. package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +127 -17
  159. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +44 -0
  160. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +247 -0
  161. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +107 -2
  162. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +5 -8
  163. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +456 -4
  164. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +128 -0
  165. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +28 -0
  166. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +16 -7
  167. package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +15 -0
  168. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +79 -15
  169. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +12 -12
  170. package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +109 -0
  171. package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +34 -0
  172. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +94 -1
  173. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +42 -2
  174. package/dist/extensions/question/index.ts +21 -6
  175. package/dist/extensions/question/question-list.ts +7 -1
  176. package/dist/extensions/question/row-layout.ts +3 -0
  177. package/dist/extensions/question/tests/exp.test.ts +70 -0
  178. package/dist/extensions/question/tests/exp2.test.ts +59 -0
  179. package/dist/extensions/question/tests/exp3.test.ts +47 -0
  180. package/dist/extensions/question/tests/helpers.test.ts +7 -0
  181. package/dist/extensions/question/tests/question-list.test.ts +140 -0
  182. package/dist/extensions/question/tests/row-layout.test.ts +13 -0
  183. package/dist/extensions/question/tests/selection-mode.test.ts +54 -0
  184. package/dist/extensions/question/tests/zz-ig3.test.ts +20 -0
  185. package/dist/extensions/question/tests/zz-probe.test.ts +29 -0
  186. package/dist/skills/pi-subagents/SKILL.md +1 -1
  187. package/dist/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  188. package/dist/skills/pi-subagents/references/execution-controls.md +15 -14
  189. package/dist/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  190. package/dist/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  191. package/docs/settings.md +6 -0
  192. package/package.json +3 -3
@@ -214,6 +214,7 @@ Package skill content.
214
214
  it("resolves configured artifact directory preferences", () => {
215
215
  const sessionFile = path.join(agentDir, "sessions", "session-1", "session.jsonl");
216
216
 
217
+ // Selesai fork default: project-scoped .pi-subagents artifacts.
217
218
  assert.equal(getArtifactsDir(sessionFile, cwd), getProjectArtifactsDir(cwd));
218
219
  assert.equal(getArtifactsDir(sessionFile, cwd, "project"), getProjectArtifactsDir(cwd));
219
220
  assert.equal(getArtifactsDir(sessionFile, cwd, "session"), path.join(path.dirname(sessionFile), "subagent-artifacts"));
@@ -241,6 +242,39 @@ Package skill content.
241
242
  assert.throws(() => updateConfig((config) => config), /config\.fleetKeybindings\.pageUp entries must be non-empty strings/);
242
243
  });
243
244
 
245
+ it("loads and validates the foreground detach shortcut", () => {
246
+ const configPath = path.join(agentDir, "extensions", "subagent", "config.json");
247
+ writeFile(configPath, JSON.stringify({ foregroundDetachShortcut: "ctrl+b" }));
248
+ assert.equal(loadConfig().foregroundDetachShortcut, "ctrl+b");
249
+
250
+ writeFile(configPath, JSON.stringify({ foregroundDetachShortcut: "banana" }));
251
+ assert.throws(() => updateConfig((config) => config), /config\.foregroundDetachShortcut must be a valid keybinding string/);
252
+ });
253
+
254
+ it("loads and validates main-window renderer density config", () => {
255
+ const configPath = path.join(agentDir, "extensions", "subagent", "config.json");
256
+ writeFile(configPath, JSON.stringify({ mainWindowRenderer: { horizontalSpacing: 0, compactResultMaxLines: 4 } }));
257
+ assert.deepEqual(loadConfig().mainWindowRenderer, { horizontalSpacing: 0, compactResultMaxLines: 4 });
258
+
259
+ writeFile(configPath, JSON.stringify({ mainWindowRenderer: { horizontalSpacing: -1 } }));
260
+ assert.throws(() => updateConfig((config) => config), /config\.mainWindowRenderer\.horizontalSpacing must be an integer from 0 to 4/);
261
+
262
+ writeFile(configPath, JSON.stringify({ mainWindowRenderer: { compactResultMaxLines: 0 } }));
263
+ assert.throws(() => updateConfig((config) => config), /config\.mainWindowRenderer\.compactResultMaxLines must be a positive integer/);
264
+ });
265
+
266
+ it("loads and validates experimental Orca progress-tab config", () => {
267
+ const configPath = path.join(agentDir, "extensions", "subagent", "config.json");
268
+ writeFile(configPath, JSON.stringify({ orcaProgressTabs: { enabled: true } }));
269
+ assert.deepEqual(loadConfig().orcaProgressTabs, { enabled: true });
270
+
271
+ writeFile(configPath, JSON.stringify({ orcaProgressTabs: { enabled: "yes" } }));
272
+ assert.throws(() => updateConfig((config) => config), /config\.orcaProgressTabs\.enabled must be a boolean/);
273
+
274
+ writeFile(configPath, JSON.stringify({ orcaProgressTabs: { enabled: true, focus: true } }));
275
+ assert.throws(() => updateConfig((config) => config), /config\.orcaProgressTabs\.focus is not supported/);
276
+ });
277
+
244
278
  it("hardens and redacts existing run history while recording", () => {
245
279
  const historyPath = path.join(agentDir, "run-history.jsonl");
246
280
  fs.mkdirSync(agentDir, { recursive: true, mode: 0o755 });
@@ -5,7 +5,12 @@ import { normalizePublicSubagentExecution } from "../../src/extension/public-exe
5
5
  describe("public subagent execution normalization", () => {
6
6
  it("accepts all four execution modes, management, and direct-field schedules", () => {
7
7
  assert.deepEqual(normalizePublicSubagentExecution({ workflowScript: "return 1" }), { ok: true, params: { workflowScript: "return 1" } });
8
- assert.deepEqual(normalizePublicSubagentExecution({ agent: "worker", task: "work" }), { ok: true, params: { agent: "worker", task: "work" } });
8
+ const single = normalizePublicSubagentExecution({ agent: "worker", task: "work" });
9
+ assert.equal(single.ok, true);
10
+ if (single.ok) {
11
+ assert.equal(typeof (single.params as { workflowScript?: unknown }).workflowScript, "string");
12
+ assert.match((single.params as { workflowScript: string }).workflowScript, /runs\.run\("main", \{"agent":"worker","task":"work","output":true\}\)/);
13
+ }
9
14
  assert.deepEqual(normalizePublicSubagentExecution({ tasks: [{ agent: "worker" }] }), { ok: true, params: { tasks: [{ agent: "worker" }] } });
10
15
  assert.deepEqual(normalizePublicSubagentExecution({ chain: [{ agent: "worker" }] }), { ok: true, params: { chain: [{ agent: "worker" }] } });
11
16
  assert.deepEqual(normalizePublicSubagentExecution({ action: " list " }), { ok: true, params: { action: "list" } });
@@ -80,6 +80,50 @@ test("multiline rendering omits two-column graphemes at one-column width", () =>
80
80
  }
81
81
  });
82
82
 
83
+ test("running single-subagent cards show the configured detach shortcut", () => {
84
+ const running = {
85
+ ...result("reviewer", ""),
86
+ progress: { status: "running", index: 0, agent: "reviewer", toolCount: 0, tokens: 0, durationMs: 0 },
87
+ };
88
+ const toolResult = {
89
+ content: [{ type: "text", text: "running" }],
90
+ details: { mode: "single", results: [running] },
91
+ };
92
+
93
+ const configured = componentText(renderSubagentResult(
94
+ toolResult as never,
95
+ { expanded: false },
96
+ theme as any,
97
+ undefined,
98
+ undefined,
99
+ "ctrl+b",
100
+ ));
101
+ assert.match(configured, /Ctrl\+B to run in background/);
102
+
103
+ const unconfigured = componentText(renderSubagentResult(toolResult as never, { expanded: false }, theme as any));
104
+ assert.doesNotMatch(unconfigured, /run in background/);
105
+
106
+ const alreadyBackground = componentText(renderSubagentResult(
107
+ { ...toolResult, details: { ...toolResult.details, asyncId: "async-123" } } as never,
108
+ { expanded: false },
109
+ theme as any,
110
+ undefined,
111
+ undefined,
112
+ "ctrl+b",
113
+ ));
114
+ assert.doesNotMatch(alreadyBackground, /run in background/);
115
+
116
+ const pendingBackground = componentText(renderSubagentResult(
117
+ { ...toolResult, details: { ...toolResult.details, background: true } } as never,
118
+ { expanded: false },
119
+ theme as any,
120
+ undefined,
121
+ undefined,
122
+ "ctrl+b",
123
+ ));
124
+ assert.doesNotMatch(pendingBackground, /run in background/);
125
+ });
126
+
83
127
  test("compact chain rendering uses workflow graph spans for dynamic fanout results", () => {
84
128
  const component = renderSubagentResult({
85
129
  content: [{ type: "text", text: "done" }],
@@ -238,3 +282,51 @@ test("static sequential and static parallel chain rendering keep existing labels
238
282
  assert.match(parallel, /Agent 2\/2: auditor/);
239
283
  assert.match(parallel, /Step 3: writer/);
240
284
  });
285
+
286
+ test("main-window renderer config removes compact result indentation without changing status glyphs", () => {
287
+ const component = renderSubagentResult({
288
+ content: [{ type: "text", text: "done" }],
289
+ details: {
290
+ mode: "parallel",
291
+ results: [result("scout", "a"), { ...result("reviewer", ""), exitCode: 1, error: "failed" }],
292
+ },
293
+ }, { expanded: false }, theme as any, undefined, { horizontalSpacing: 0 });
294
+
295
+ const text = componentText(component);
296
+ assert.match(text, /^✗ parallel/m);
297
+ assert.match(text, /^✓ Agent 1\/2: scout/m);
298
+ assert.match(text, /^✗ Agent 2\/2: reviewer/m);
299
+ assert.match(text, /^⎿ Error: failed/m);
300
+ });
301
+
302
+ test("main-window renderer config caps only collapsed rich result rows", () => {
303
+ const rendered = renderSubagentResult({
304
+ content: [{ type: "text", text: "done" }],
305
+ details: {
306
+ mode: "parallel",
307
+ results: [
308
+ result("scout", "a"),
309
+ result("reviewer", "b"),
310
+ result("writer", "c"),
311
+ ],
312
+ },
313
+ }, { expanded: false }, theme as any, undefined, { compactResultMaxLines: 3 }).render(120);
314
+
315
+ assert.equal(rendered.length, 3);
316
+ assert.match(rendered[2]!, /rows hidden/);
317
+
318
+ const expanded = renderSubagentResult({
319
+ content: [{ type: "text", text: "done" }],
320
+ details: {
321
+ mode: "parallel",
322
+ results: [
323
+ result("scout", "a"),
324
+ result("reviewer", "b"),
325
+ result("writer", "c"),
326
+ ],
327
+ },
328
+ }, { expanded: true }, theme as any, undefined, { compactResultMaxLines: 3 }).render(120);
329
+
330
+ assert.ok(expanded.length > 3);
331
+ assert.doesNotMatch(expanded.join("\n"), /rows hidden/);
332
+ });
@@ -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/);