@selesai/code 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/dist/core/diagnostics.d.ts +1 -0
  2. package/dist/core/diagnostics.d.ts.map +1 -1
  3. package/dist/core/diagnostics.js.map +1 -1
  4. package/dist/core/package-manager.d.ts +10 -0
  5. package/dist/core/package-manager.d.ts.map +1 -1
  6. package/dist/core/package-manager.js +93 -8
  7. package/dist/core/package-manager.js.map +1 -1
  8. package/dist/core/package-manager.test.d.ts +2 -0
  9. package/dist/core/package-manager.test.d.ts.map +1 -0
  10. package/dist/core/package-manager.test.js +20 -0
  11. package/dist/core/package-manager.test.js.map +1 -0
  12. package/dist/core/resource-loader.d.ts +3 -0
  13. package/dist/core/resource-loader.d.ts.map +1 -1
  14. package/dist/core/resource-loader.js +10 -0
  15. package/dist/core/resource-loader.js.map +1 -1
  16. package/dist/core/settings-manager.d.ts +1 -0
  17. package/dist/core/settings-manager.d.ts.map +1 -1
  18. package/dist/core/settings-manager.js.map +1 -1
  19. package/dist/extensions/caveman/caveman-instructions.cjs +39 -0
  20. package/dist/extensions/caveman/index.js +116 -0
  21. package/dist/extensions/caveman/package.json +8 -0
  22. package/dist/extensions/caveman/test/extension.test.js +185 -0
  23. package/dist/extensions/caveman/test/helpers.test.js +67 -0
  24. package/dist/extensions/pi-intercom/broker/broker.ts +2 -3
  25. package/dist/extensions/pi-intercom/broker/paths.ts +10 -5
  26. package/dist/extensions/pi-intercom/broker/spawn.ts +2 -3
  27. package/dist/extensions/pi-intercom/config.ts +2 -2
  28. package/dist/extensions/pi-intercom/index.ts +2 -2
  29. package/dist/extensions/pi-intercom/package.json +2 -2
  30. package/dist/extensions/pi-intercom/ui/compose.ts +3 -3
  31. package/dist/extensions/pi-intercom/ui/inline-message.ts +3 -3
  32. package/dist/extensions/pi-intercom/ui/session-list.ts +3 -3
  33. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +2 -1
  34. package/dist/extensions/pi-powerline-footer/index.ts +9 -16
  35. package/dist/extensions/pi-powerline-footer/welcome.ts +23 -16
  36. package/dist/extensions/pi-powerline-footer/working-vibes.ts +2 -9
  37. package/dist/extensions/pi-rewind-hook/index.ts +1 -1
  38. package/dist/extensions/pi-rewind-hook/package.json +1 -1
  39. package/dist/extensions/pi-subagents/README.md +224 -52
  40. package/dist/extensions/pi-subagents/install.mjs +93 -0
  41. package/dist/extensions/pi-subagents/package.json +4 -5
  42. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +1 -1
  43. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +1 -1
  44. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +76 -13
  45. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +179 -2
  46. package/dist/extensions/pi-subagents/src/agents/agent-memory.ts +254 -0
  47. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +11 -0
  48. package/dist/extensions/pi-subagents/src/agents/agents.ts +193 -19
  49. package/dist/extensions/pi-subagents/src/agents/chain-serializer.ts +27 -2
  50. package/dist/extensions/pi-subagents/src/extension/config.ts +27 -4
  51. package/dist/extensions/pi-subagents/src/extension/doctor.ts +1 -7
  52. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -2
  53. package/dist/extensions/pi-subagents/src/extension/index.ts +73 -71
  54. package/dist/extensions/pi-subagents/src/extension/rpc.ts +369 -0
  55. package/dist/extensions/pi-subagents/src/extension/schemas.ts +54 -10
  56. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +200 -0
  57. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +21 -253
  58. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +519 -0
  59. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +1 -1
  60. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +195 -105
  61. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +88 -2
  62. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +67 -10
  63. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +34 -4
  64. package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +166 -0
  65. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +156 -1
  66. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +515 -0
  67. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +161 -44
  68. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +1 -2
  69. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +3 -2
  70. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +167 -6
  71. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +514 -0
  72. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +28 -1
  73. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +873 -204
  74. package/dist/extensions/pi-subagents/src/runs/background/wait.ts +353 -0
  75. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +123 -27
  76. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +174 -70
  77. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +569 -81
  78. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +45 -22
  79. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +3 -1
  80. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +2 -2
  81. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +172 -21
  82. package/dist/extensions/pi-subagents/src/runs/shared/model-scope.ts +128 -0
  83. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +89 -0
  84. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +50 -1
  85. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +35 -4
  86. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +63 -45
  87. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +2 -0
  88. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +125 -5
  89. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +74 -0
  90. package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +52 -0
  91. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +28 -5
  92. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +16 -1
  93. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +15 -2
  94. package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +212 -0
  95. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +133 -22
  96. package/dist/extensions/pi-subagents/src/shared/settings.ts +3 -1
  97. package/dist/extensions/pi-subagents/src/shared/types.ts +197 -4
  98. package/dist/extensions/pi-subagents/src/shared/utils.ts +100 -15
  99. package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +330 -0
  100. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +135 -4
  101. package/dist/extensions/pi-subagents/src/tui/render.ts +32 -12
  102. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +94 -0
  103. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2772 -0
  104. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +1107 -0
  105. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +463 -0
  106. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +274 -0
  107. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1535 -0
  108. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +242 -0
  109. package/dist/extensions/pi-subagents/test/integration/doctor-executor.test.ts +128 -0
  110. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +252 -0
  111. package/dist/extensions/pi-subagents/test/integration/foreground-result-size.test.ts +188 -0
  112. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +1306 -0
  113. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +1033 -0
  114. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +397 -0
  115. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +583 -0
  116. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +691 -0
  117. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +892 -0
  118. package/dist/extensions/pi-subagents/test/integration/session-tokens.test.ts +62 -0
  119. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1824 -0
  120. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1284 -0
  121. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +127 -0
  122. package/dist/extensions/pi-subagents/test/integration/template-resolution.test.ts +299 -0
  123. package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +37 -0
  124. package/dist/extensions/pi-subagents/test/support/helpers.ts +189 -0
  125. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +335 -0
  126. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +129 -0
  127. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +184 -0
  128. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +285 -0
  129. package/dist/extensions/pi-subagents/test/support/register-loader.mjs +15 -0
  130. package/dist/extensions/pi-subagents/test/support/ts-loader.mjs +108 -0
  131. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +421 -0
  132. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +213 -0
  133. package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +343 -0
  134. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +1199 -0
  135. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +556 -0
  136. package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +420 -0
  137. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +579 -0
  138. package/dist/extensions/pi-subagents/test/unit/agent-scope.test.ts +20 -0
  139. package/dist/extensions/pi-subagents/test/unit/agent-selection.test.ts +88 -0
  140. package/dist/extensions/pi-subagents/test/unit/artifacts.test.ts +24 -0
  141. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +86 -0
  142. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +221 -0
  143. package/dist/extensions/pi-subagents/test/unit/async-permission-session.test.ts +98 -0
  144. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +285 -0
  145. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +112 -0
  146. package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +206 -0
  147. package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +159 -0
  148. package/dist/extensions/pi-subagents/test/unit/chain-serializer.test.ts +229 -0
  149. package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +225 -0
  150. package/dist/extensions/pi-subagents/test/unit/close-grace-timer.test.ts +101 -0
  151. package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +228 -0
  152. package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +36 -0
  153. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +152 -0
  154. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +72 -0
  155. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +327 -0
  156. package/dist/extensions/pi-subagents/test/unit/control-notices.test.ts +161 -0
  157. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +159 -0
  158. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +217 -0
  159. package/dist/extensions/pi-subagents/test/unit/extra-agent-dirs.test.ts +88 -0
  160. package/dist/extensions/pi-subagents/test/unit/file-coalescer.test.ts +68 -0
  161. package/dist/extensions/pi-subagents/test/unit/foreground-tool-call-compaction.test.ts +78 -0
  162. package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +415 -0
  163. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +126 -0
  164. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +271 -0
  165. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +173 -0
  166. package/dist/extensions/pi-subagents/test/unit/jsonl-writer.test.ts +119 -0
  167. package/dist/extensions/pi-subagents/test/unit/live-async-resume.test.ts +170 -0
  168. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +346 -0
  169. package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +62 -0
  170. package/dist/extensions/pi-subagents/test/unit/model-scope-settings.test.ts +87 -0
  171. package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +145 -0
  172. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +162 -0
  173. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +470 -0
  174. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +329 -0
  175. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +319 -0
  176. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +70 -0
  177. package/dist/extensions/pi-subagents/test/unit/parallel-utils.test.ts +247 -0
  178. package/dist/extensions/pi-subagents/test/unit/path-handling.test.ts +113 -0
  179. package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +94 -0
  180. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +803 -0
  181. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +209 -0
  182. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +238 -0
  183. package/dist/extensions/pi-subagents/test/unit/proactive-skills.test.ts +118 -0
  184. package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +299 -0
  185. package/dist/extensions/pi-subagents/test/unit/prompt-template-bridge.test.ts +408 -0
  186. package/dist/extensions/pi-subagents/test/unit/prompt-workflows.test.ts +134 -0
  187. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +257 -0
  188. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +209 -0
  189. package/dist/extensions/pi-subagents/test/unit/result-intercom.test.ts +226 -0
  190. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +294 -0
  191. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +147 -0
  192. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +997 -0
  193. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +478 -0
  194. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +562 -0
  195. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +213 -0
  196. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +438 -0
  197. package/dist/extensions/pi-subagents/test/unit/slash-bridge.test.ts +54 -0
  198. package/dist/extensions/pi-subagents/test/unit/slash-chain-groups.test.ts +464 -0
  199. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +255 -0
  200. package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +19 -0
  201. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +245 -0
  202. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +517 -0
  203. package/dist/extensions/pi-subagents/test/unit/temp-paths.test.ts +72 -0
  204. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +52 -0
  205. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +237 -0
  206. package/dist/extensions/pi-subagents/test/unit/total-cost.test.ts +60 -0
  207. package/dist/extensions/pi-subagents/test/unit/ts-loader.test.ts +33 -0
  208. package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +202 -0
  209. package/dist/extensions/pi-subagents/test/unit/types-fork-preamble.test.ts +23 -0
  210. package/dist/extensions/pi-subagents/test/unit/wait.test.ts +398 -0
  211. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +88 -0
  212. package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +26 -0
  213. package/dist/extensions/pi-subagents/test/unit/workflow-graph.test.ts +134 -0
  214. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +479 -0
  215. package/dist/extensions/pi-web-agent/src/changelog-notice.ts +5 -2
  216. package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +6 -4
  217. package/dist/main.d.ts.map +1 -1
  218. package/dist/main.js +4 -0
  219. package/dist/main.js.map +1 -1
  220. package/dist/modes/interactive/components/first-time-setup.d.ts +1 -1
  221. package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
  222. package/dist/modes/interactive/components/first-time-setup.js +19 -5
  223. package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
  224. package/dist/skills/caveman/SKILL.md +49 -0
  225. package/dist/skills/handoff-text/SKILL.md +13 -0
  226. package/docs/docs.json +4 -0
  227. package/docs/extensions.md +2 -0
  228. package/docs/settings.md +15 -0
  229. package/docs/shared-host-extensions.md +109 -0
  230. package/package.json +1 -1
@@ -0,0 +1,892 @@
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 { createResultWatcher } from "../../src/runs/background/result-watcher.ts";
7
+ import { createNestedRoute, writeNestedEvent } from "../../src/runs/shared/nested-events.ts";
8
+ import type { SubagentState } from "../../src/shared/types.ts";
9
+
10
+ function createState(): SubagentState {
11
+ return {
12
+ baseCwd: "/repo",
13
+ currentSessionId: null,
14
+ asyncJobs: new Map(),
15
+ foregroundControls: new Map(),
16
+ lastForegroundControlId: null,
17
+ cleanupTimers: new Map(),
18
+ lastUiContext: null,
19
+ poller: null,
20
+ completionSeen: new Map(),
21
+ watcher: null,
22
+ watcherRestartTimer: null,
23
+ resultFileCoalescer: {
24
+ schedule: () => false,
25
+ clear: () => {},
26
+ },
27
+ };
28
+ }
29
+
30
+ describe("result watcher", () => {
31
+ it("processes deferred session-scoped results after session identity is restored", async () => {
32
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-session-"));
33
+ try {
34
+ const emitted: Array<{ event: string; data: unknown }> = [];
35
+ const pi = {
36
+ events: {
37
+ on: () => () => {},
38
+ emit(event: string, data: unknown) {
39
+ emitted.push({ event, data });
40
+ },
41
+ },
42
+ };
43
+ const state = createState();
44
+ const resultPath = path.join(resultsDir, "session-run.json");
45
+ fs.writeFileSync(resultPath, JSON.stringify({
46
+ id: "session-run",
47
+ sessionId: "session-current",
48
+ success: true,
49
+ summary: "done",
50
+ }), "utf-8");
51
+
52
+ const watcher = createResultWatcher(pi, state, resultsDir, 60_000);
53
+ try {
54
+ watcher.primeExistingResults();
55
+ await new Promise((resolve) => setTimeout(resolve, 100));
56
+ assert.equal(emitted.length, 0);
57
+ assert.equal(fs.existsSync(resultPath), true);
58
+
59
+ state.currentSessionId = "session-current";
60
+ watcher.primeExistingResults();
61
+ await new Promise((resolve) => setTimeout(resolve, 100));
62
+ } finally {
63
+ watcher.stopResultWatcher();
64
+ }
65
+
66
+ assert.equal(emitted.filter((entry) => entry.event === "subagent:async-complete").length, 1);
67
+ assert.equal(fs.existsSync(resultPath), false);
68
+ } finally {
69
+ fs.rmSync(resultsDir, { recursive: true, force: true });
70
+ }
71
+ });
72
+
73
+ it("delivers result files only to the exact owning session when another watcher shares the same repo", async () => {
74
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-scope-"));
75
+ const createPi = () => {
76
+ const emitted: Array<{ event: string; data: unknown }> = [];
77
+ const listeners = new Map<string, Set<(payload: unknown) => void>>();
78
+ const pi = {
79
+ events: {
80
+ on(event: string, handler: (payload: unknown) => void) {
81
+ const eventListeners = listeners.get(event) ?? new Set();
82
+ eventListeners.add(handler);
83
+ listeners.set(event, eventListeners);
84
+ return () => eventListeners.delete(handler);
85
+ },
86
+ emit(event: string, data: unknown) {
87
+ emitted.push({ event, data });
88
+ for (const handler of listeners.get(event) ?? []) handler(data);
89
+ if (event === "subagent:result-intercom") {
90
+ const requestId = data && typeof data === "object" ? (data as { requestId?: unknown }).requestId : undefined;
91
+ if (typeof requestId === "string") {
92
+ setImmediate(() => pi.events.emit("subagent:result-intercom-delivery", { requestId, delivered: true }));
93
+ }
94
+ }
95
+ },
96
+ },
97
+ };
98
+ return { pi, emitted };
99
+ };
100
+ try {
101
+ const owner = createPi();
102
+ const other = createPi();
103
+ const ownerState = createState();
104
+ ownerState.currentSessionId = "session-owner";
105
+ const otherState = createState();
106
+ otherState.currentSessionId = "session-other";
107
+ const ownerWatcher = createResultWatcher(owner.pi, ownerState, resultsDir, 60_000);
108
+ const otherWatcher = createResultWatcher(other.pi, otherState, resultsDir, 60_000);
109
+ const ownerResultPath = path.join(resultsDir, "owner-run.json");
110
+ const sessionlessResultPath = path.join(resultsDir, "sessionless-run.json");
111
+ try {
112
+ fs.writeFileSync(ownerResultPath, JSON.stringify({
113
+ id: "owner-run",
114
+ agent: "worker",
115
+ mode: "single",
116
+ success: true,
117
+ state: "complete",
118
+ summary: "owner output",
119
+ results: [{ agent: "worker", output: "owner output", success: true }],
120
+ sessionId: "session-owner",
121
+ cwd: "/repo",
122
+ intercomTarget: "owner-target",
123
+ }), "utf-8");
124
+ fs.writeFileSync(sessionlessResultPath, JSON.stringify({
125
+ id: "sessionless-run",
126
+ agent: "worker",
127
+ mode: "single",
128
+ success: true,
129
+ state: "complete",
130
+ summary: "legacy cwd-scoped output",
131
+ results: [{ agent: "worker", output: "legacy cwd-scoped output", success: true }],
132
+ cwd: "/repo",
133
+ intercomTarget: "sessionless-target",
134
+ }), "utf-8");
135
+
136
+ otherWatcher.primeExistingResults();
137
+ await new Promise((resolve) => setTimeout(resolve, 100));
138
+ ownerWatcher.primeExistingResults();
139
+ await new Promise((resolve) => setTimeout(resolve, 100));
140
+ } finally {
141
+ ownerWatcher.stopResultWatcher();
142
+ otherWatcher.stopResultWatcher();
143
+ }
144
+
145
+ const ownerCompletions = owner.emitted.filter((entry) => entry.event === "subagent:async-complete");
146
+ const ownerIntercom = owner.emitted.filter((entry) => entry.event === "subagent:result-intercom");
147
+ assert.equal(ownerCompletions.length, 1);
148
+ assert.equal((ownerCompletions[0]?.data as { id?: string } | undefined)?.id, "owner-run");
149
+ assert.equal(ownerIntercom.length, 1);
150
+ assert.equal(other.emitted.some((entry) => entry.event === "subagent:async-complete"), false);
151
+ assert.equal(other.emitted.some((entry) => entry.event === "subagent:result-intercom"), false);
152
+ assert.equal(fs.existsSync(ownerResultPath), false);
153
+ assert.equal(fs.existsSync(sessionlessResultPath), true);
154
+ } finally {
155
+ fs.rmSync(resultsDir, { recursive: true, force: true });
156
+ }
157
+ });
158
+
159
+ it("logs malformed result files instead of swallowing them silently", async () => {
160
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-"));
161
+ try {
162
+ fs.writeFileSync(path.join(resultsDir, "bad.json"), "{bad-json", "utf-8");
163
+ const emitted: unknown[] = [];
164
+ const pi = {
165
+ events: {
166
+ on: () => () => {},
167
+ emit(_event: string, data: unknown) {
168
+ emitted.push(data);
169
+ },
170
+ },
171
+ };
172
+ const state = createState();
173
+ const watcher = createResultWatcher(pi, state, resultsDir, 60_000);
174
+ const originalError = console.error;
175
+ const logged: unknown[][] = [];
176
+ console.error = (...args: unknown[]) => {
177
+ logged.push(args);
178
+ };
179
+ try {
180
+ watcher.primeExistingResults();
181
+ await new Promise((resolve) => setTimeout(resolve, 100));
182
+ } finally {
183
+ console.error = originalError;
184
+ watcher.stopResultWatcher();
185
+ }
186
+
187
+ assert.equal(emitted.length, 0);
188
+ assert.ok(
189
+ logged.some((entry) => /Failed to process subagent result file/.test(String(entry[0] ?? ""))),
190
+ "expected watcher error to be logged",
191
+ );
192
+ } finally {
193
+ fs.rmSync(resultsDir, { recursive: true, force: true });
194
+ }
195
+ });
196
+
197
+ it("normalizes the native fs.watch path before watching result files", () => {
198
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-"));
199
+ try {
200
+ const nativeResultsDir = path.join(path.dirname(resultsDir), `${path.basename(resultsDir)}-native`);
201
+ const pi = {
202
+ events: {
203
+ on: () => () => {},
204
+ emit() {},
205
+ },
206
+ };
207
+ const state = createState();
208
+ let watchedDir: fs.PathLike | undefined;
209
+ const fakeWatcher = {
210
+ on() {
211
+ return fakeWatcher;
212
+ },
213
+ close() {},
214
+ unref() {},
215
+ } as fs.FSWatcher;
216
+ const realpathSync = ((target: fs.PathLike, options?: unknown) => fs.realpathSync(target, options as BufferEncoding)) as typeof fs.realpathSync;
217
+ realpathSync.native = ((target: fs.PathLike) => target === resultsDir ? nativeResultsDir : fs.realpathSync.native(target)) as typeof fs.realpathSync.native;
218
+ const watcher = createResultWatcher(pi, state, resultsDir, 60_000, {
219
+ fs: {
220
+ ...fs,
221
+ realpathSync,
222
+ watch(dir) {
223
+ watchedDir = dir;
224
+ return fakeWatcher;
225
+ },
226
+ },
227
+ });
228
+ try {
229
+ watcher.startResultWatcher();
230
+ } finally {
231
+ watcher.stopResultWatcher();
232
+ }
233
+
234
+ assert.equal(watchedDir, nativeResultsDir);
235
+ } finally {
236
+ fs.rmSync(resultsDir, { recursive: true, force: true });
237
+ }
238
+ });
239
+
240
+ it("falls back to polling when fs.watch throws EMFILE and preserves grouped intercom delivery", async () => {
241
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-"));
242
+ try {
243
+ const emitted: Array<{ event: string; data: unknown }> = [];
244
+ const listeners = new Map<string, Set<(payload: unknown) => void>>();
245
+ const pi = {
246
+ events: {
247
+ on(event: string, handler: (payload: unknown) => void) {
248
+ const eventListeners = listeners.get(event) ?? new Set();
249
+ eventListeners.add(handler);
250
+ listeners.set(event, eventListeners);
251
+ return () => eventListeners.delete(handler);
252
+ },
253
+ emit(event: string, data: unknown) {
254
+ emitted.push({ event, data });
255
+ for (const handler of listeners.get(event) ?? []) handler(data);
256
+ if (event === "subagent:result-intercom") {
257
+ const requestId = data && typeof data === "object" ? (data as { requestId?: unknown }).requestId : undefined;
258
+ if (typeof requestId === "string") {
259
+ setImmediate(() => pi.events.emit("subagent:result-intercom-delivery", { requestId, delivered: true }));
260
+ }
261
+ }
262
+ },
263
+ },
264
+ };
265
+ const state = createState();
266
+ state.currentSessionId = "session-1";
267
+ let poll: (() => void) | undefined;
268
+ const emfile = new Error("too many open files") as NodeJS.ErrnoException;
269
+ emfile.code = "EMFILE";
270
+ const watcher = createResultWatcher(pi, state, resultsDir, 60_000, {
271
+ fs: {
272
+ ...fs,
273
+ watch: () => {
274
+ throw emfile;
275
+ },
276
+ },
277
+ timers: {
278
+ setTimeout,
279
+ clearTimeout() {},
280
+ setInterval(handler: () => void) {
281
+ poll = handler;
282
+ return { unref() {} } as NodeJS.Timeout;
283
+ },
284
+ clearInterval() {
285
+ poll = undefined;
286
+ },
287
+ },
288
+ });
289
+ const originalError = console.error;
290
+ const childSessionPath = path.join(resultsDir, "a-session.jsonl");
291
+ console.error = () => {};
292
+ try {
293
+ watcher.startResultWatcher();
294
+ assert.equal(state.watcher, null);
295
+ assert.notEqual(state.watcherRestartTimer, null);
296
+
297
+ fs.writeFileSync(childSessionPath, "", "utf-8");
298
+ fs.writeFileSync(path.join(resultsDir, "async-fallback.json"), JSON.stringify({
299
+ id: "async-fallback",
300
+ runId: "run-fallback",
301
+ agent: "parallel:a+b",
302
+ mode: "parallel",
303
+ success: true,
304
+ state: "complete",
305
+ summary: "Combined summary",
306
+ results: [
307
+ { agent: "a", output: "Result from a", success: true, sessionFile: childSessionPath, intercomTarget: "subagent-a-run-fallback-1" },
308
+ { agent: "b", output: "Result from b", success: false, error: "B failed", intercomTarget: "subagent-b-run-fallback-2" },
309
+ ],
310
+ sessionId: "session-1",
311
+ intercomTarget: "subagent-chat-main",
312
+ }), "utf-8");
313
+ poll?.();
314
+ await new Promise((resolve) => setTimeout(resolve, 100));
315
+ } finally {
316
+ console.error = originalError;
317
+ watcher.stopResultWatcher();
318
+ }
319
+
320
+ const intercomEvents = emitted.filter((entry) => entry.event === "subagent:result-intercom");
321
+ assert.equal(intercomEvents.length, 1);
322
+ assert.equal(emitted.some((entry) => entry.event === "subagent:async-complete"), true);
323
+ assert.equal(fs.existsSync(path.join(resultsDir, "async-fallback.json")), false);
324
+ const payload = intercomEvents[0]?.data as { mode?: string; status?: string; message?: string; children?: Array<{ status?: string; summary?: string; sessionPath?: string }> };
325
+ const completion = emitted.find((entry) => entry.event === "subagent:async-complete")?.data as { results?: Array<{ status?: string; summary?: string; sessionPath?: string }> } | undefined;
326
+ assert.equal(payload.mode, "parallel");
327
+ assert.equal(payload.status, "failed");
328
+ assert.match(String(payload.message ?? ""), /Run: run-fallback/);
329
+ assert.match(String(payload.message ?? ""), /Children: 1 completed, 1 failed/);
330
+ assert.equal(payload.children?.[0]?.sessionPath, childSessionPath);
331
+ assert.equal(completion?.results?.[0]?.sessionPath, childSessionPath);
332
+ assert.equal(payload.children?.[1]?.status, "failed");
333
+ assert.equal(completion?.results?.[1]?.status, "failed");
334
+ assert.equal(payload.children?.[1]?.summary, "B failed\n\nOutput:\nResult from b");
335
+ assert.equal(completion?.results?.[1]?.summary, "B failed\n\nOutput:\nResult from b");
336
+ } finally {
337
+ fs.rmSync(resultsDir, { recursive: true, force: true });
338
+ }
339
+ });
340
+
341
+ it("falls back to polling when an active fs.watch emits ENOSPC", async () => {
342
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-"));
343
+ try {
344
+ const emitted: Array<{ event: string; data: unknown }> = [];
345
+ const pi = {
346
+ events: {
347
+ on: () => () => {},
348
+ emit(event: string, data: unknown) {
349
+ emitted.push({ event, data });
350
+ },
351
+ },
352
+ };
353
+ const state = createState();
354
+ state.currentSessionId = "session-1";
355
+ let poll: (() => void) | undefined;
356
+ let emitWatcherError: ((error: NodeJS.ErrnoException) => void) | undefined;
357
+ const fakeWatcher = {
358
+ on(event: string, handler: (error: NodeJS.ErrnoException) => void) {
359
+ if (event === "error") emitWatcherError = handler;
360
+ return fakeWatcher;
361
+ },
362
+ close() {},
363
+ unref() {},
364
+ } as fs.FSWatcher;
365
+ const watcher = createResultWatcher(pi, state, resultsDir, 60_000, {
366
+ fs: {
367
+ ...fs,
368
+ watch: () => fakeWatcher,
369
+ },
370
+ timers: {
371
+ setTimeout,
372
+ clearTimeout() {},
373
+ setInterval(handler: () => void) {
374
+ poll = handler;
375
+ return { unref() {} } as NodeJS.Timeout;
376
+ },
377
+ clearInterval() {
378
+ poll = undefined;
379
+ },
380
+ },
381
+ });
382
+ const originalError = console.error;
383
+ console.error = () => {};
384
+ try {
385
+ watcher.startResultWatcher();
386
+ assert.equal(state.watcher, fakeWatcher);
387
+ const enospc = new Error("inotify limit reached") as NodeJS.ErrnoException;
388
+ enospc.code = "ENOSPC";
389
+ emitWatcherError?.(enospc);
390
+ assert.equal(state.watcher, null);
391
+ assert.notEqual(state.watcherRestartTimer, null);
392
+
393
+ fs.writeFileSync(path.join(resultsDir, "done.json"), JSON.stringify({ sessionId: "session-1", summary: "done" }), "utf-8");
394
+ poll?.();
395
+ await new Promise((resolve) => setTimeout(resolve, 75));
396
+ } finally {
397
+ console.error = originalError;
398
+ watcher.stopResultWatcher();
399
+ }
400
+
401
+ assert.equal(emitted.filter((entry) => entry.event === "subagent:async-complete").length, 1);
402
+ assert.equal(fs.existsSync(path.join(resultsDir, "done.json")), false);
403
+ } finally {
404
+ fs.rmSync(resultsDir, { recursive: true, force: true });
405
+ }
406
+ });
407
+
408
+ it("emits async completion plus one grouped intercom result event when an intercom target is present", async () => {
409
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-"));
410
+ try {
411
+ const emitted: Array<{ event: string; data: unknown }> = [];
412
+ const listeners = new Map<string, Set<(payload: unknown) => void>>();
413
+ const pi = {
414
+ events: {
415
+ on(event: string, handler: (payload: unknown) => void) {
416
+ const eventListeners = listeners.get(event) ?? new Set();
417
+ eventListeners.add(handler);
418
+ listeners.set(event, eventListeners);
419
+ return () => eventListeners.delete(handler);
420
+ },
421
+ emit(event: string, data: unknown) {
422
+ emitted.push({ event, data });
423
+ for (const handler of listeners.get(event) ?? []) handler(data);
424
+ if (event === "subagent:result-intercom") {
425
+ const requestId = data && typeof data === "object" ? (data as { requestId?: unknown }).requestId : undefined;
426
+ if (typeof requestId === "string") {
427
+ setImmediate(() => pi.events.emit("subagent:result-intercom-delivery", { requestId, delivered: true }));
428
+ }
429
+ }
430
+ },
431
+ },
432
+ };
433
+ const state = createState();
434
+ state.currentSessionId = "session-1";
435
+ const watcher = createResultWatcher(pi, state, resultsDir, 60_000);
436
+ const firstSession = path.join(resultsDir, "a-session.jsonl");
437
+ const missingSession = path.join(resultsDir, "b-session.jsonl");
438
+ try {
439
+ fs.writeFileSync(firstSession, "", "utf-8");
440
+ fs.writeFileSync(path.join(resultsDir, "async-1.json"), JSON.stringify({
441
+ id: "async-1",
442
+ runId: "run-123",
443
+ agent: "parallel:a+b",
444
+ mode: "parallel",
445
+ success: true,
446
+ state: "complete",
447
+ summary: "Combined summary",
448
+ results: [
449
+ { agent: "a", output: "Result from a", success: true, sessionFile: firstSession, artifactPaths: { outputPath: "/tmp/a-output.md" }, intercomTarget: "subagent-a-run-123-1" },
450
+ { agent: "b", output: "Result from b", success: false, sessionFile: missingSession, artifactPaths: { outputPath: "/tmp/b-output.md" }, intercomTarget: "subagent-b-run-123-2" },
451
+ ],
452
+ sessionId: "session-1",
453
+ sessionFile: "/tmp/session.jsonl",
454
+ asyncDir: "/tmp/async-1",
455
+ intercomTarget: "subagent-chat-main",
456
+ }), "utf-8");
457
+ watcher.primeExistingResults();
458
+ await new Promise((resolve) => setTimeout(resolve, 100));
459
+ } finally {
460
+ watcher.stopResultWatcher();
461
+ }
462
+
463
+ const intercomEvents = emitted.filter((entry) => entry.event === "subagent:result-intercom");
464
+ assert.equal(intercomEvents.length, 1);
465
+ const eventData = intercomEvents[0]?.data as { message?: string; mode?: string; status?: string };
466
+ assert.equal(eventData.mode, "parallel");
467
+ assert.equal(eventData.status, "failed");
468
+ const message = String(eventData.message ?? "");
469
+ assert.match(message, /Revive child: subagent\(\{ action: "resume", id: "async-1", index: 0, message: "\.\.\." \}\)/);
470
+ assert.ok(message.includes(`Session: ${firstSession}`));
471
+ assert.equal(message.includes(missingSession), false);
472
+ assert.equal(emitted.some((entry) => entry.event === "subagent:async-complete"), true);
473
+ } finally {
474
+ fs.rmSync(resultsDir, { recursive: true, force: true });
475
+ }
476
+ });
477
+
478
+ it("enriches async completion and intercom payloads with nested registry children before deletion", async () => {
479
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-nested-"));
480
+ const route = createNestedRoute("async-nested-root");
481
+ try {
482
+ writeNestedEvent(route, {
483
+ type: "subagent.nested.completed",
484
+ ts: Date.now(),
485
+ parentRunId: "async-nested-root",
486
+ parentStepIndex: 0,
487
+ child: {
488
+ id: "nested-child",
489
+ parentRunId: "async-nested-root",
490
+ parentStepIndex: 0,
491
+ depth: 1,
492
+ path: [{ runId: "async-nested-root", stepIndex: 0 }],
493
+ state: "complete",
494
+ agent: "nested-reviewer",
495
+ sessionFile: path.join(resultsDir, "nested-child.jsonl"),
496
+ },
497
+ });
498
+ const emitted: Array<{ event: string; data: unknown }> = [];
499
+ const listeners = new Map<string, Set<(payload: unknown) => void>>();
500
+ const pi = {
501
+ events: {
502
+ on(event: string, handler: (payload: unknown) => void) {
503
+ const eventListeners = listeners.get(event) ?? new Set();
504
+ eventListeners.add(handler);
505
+ listeners.set(event, eventListeners);
506
+ return () => eventListeners.delete(handler);
507
+ },
508
+ emit(event: string, data: unknown) {
509
+ emitted.push({ event, data });
510
+ for (const handler of listeners.get(event) ?? []) handler(data);
511
+ if (event === "subagent:result-intercom") {
512
+ const requestId = data && typeof data === "object" ? (data as { requestId?: unknown }).requestId : undefined;
513
+ if (typeof requestId === "string") {
514
+ setImmediate(() => pi.events.emit("subagent:result-intercom-delivery", { requestId, delivered: true }));
515
+ }
516
+ }
517
+ },
518
+ },
519
+ };
520
+ const state = createState();
521
+ state.currentSessionId = "session-1";
522
+ const watcher = createResultWatcher(pi, state, resultsDir, 60_000);
523
+ const resultPath = path.join(resultsDir, "async-nested-root.json");
524
+ try {
525
+ fs.writeFileSync(resultPath, JSON.stringify({
526
+ id: "async-nested-root",
527
+ runId: "async-nested-root",
528
+ agent: "owner",
529
+ mode: "single",
530
+ success: true,
531
+ state: "complete",
532
+ summary: "owner done",
533
+ results: [{ agent: "owner", output: "owner done", success: true }],
534
+ sessionId: "session-1",
535
+ intercomTarget: "subagent-chat-main",
536
+ }), "utf-8");
537
+ watcher.primeExistingResults();
538
+ await new Promise((resolve) => setTimeout(resolve, 100));
539
+ } finally {
540
+ watcher.stopResultWatcher();
541
+ }
542
+
543
+ assert.equal(fs.existsSync(resultPath), false);
544
+ const intercomPayload = emitted.find((entry) => entry.event === "subagent:result-intercom")?.data as { children?: Array<{ children?: Array<{ id?: string; controlInbox?: string; capabilityToken?: string }> }>; message?: string } | undefined;
545
+ assert.equal(intercomPayload?.children?.[0]?.children?.[0]?.id, "nested-child");
546
+ assert.equal(intercomPayload?.children?.[0]?.children?.[0]?.controlInbox, undefined);
547
+ assert.equal(intercomPayload?.children?.[0]?.children?.[0]?.capabilityToken, undefined);
548
+ assert.match(String(intercomPayload?.message ?? ""), /Nested subagents:/);
549
+ const completion = emitted.find((entry) => entry.event === "subagent:async-complete")?.data as { nestedChildren?: Array<{ id?: string }>; results?: Array<{ children?: Array<{ id?: string }> }> } | undefined;
550
+ assert.equal(completion?.nestedChildren?.[0]?.id, "nested-child");
551
+ assert.equal(completion?.results?.[0]?.children?.[0]?.id, "nested-child");
552
+ } finally {
553
+ fs.rmSync(resultsDir, { recursive: true, force: true });
554
+ fs.rmSync(path.dirname(route.eventSink), { recursive: true, force: true });
555
+ }
556
+ });
557
+
558
+ it("filters malformed explicit nested children in result files before compacting", async () => {
559
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-nested-malformed-"));
560
+ try {
561
+ const emitted: Array<{ event: string; data: unknown }> = [];
562
+ const listeners = new Map<string, Set<(payload: unknown) => void>>();
563
+ const pi = {
564
+ events: {
565
+ on(event: string, handler: (payload: unknown) => void) {
566
+ const eventListeners = listeners.get(event) ?? new Set();
567
+ eventListeners.add(handler);
568
+ listeners.set(event, eventListeners);
569
+ return () => eventListeners.delete(handler);
570
+ },
571
+ emit(event: string, data: unknown) {
572
+ emitted.push({ event, data });
573
+ for (const handler of listeners.get(event) ?? []) handler(data);
574
+ if (event === "subagent:result-intercom") {
575
+ const requestId = data && typeof data === "object" ? (data as { requestId?: unknown }).requestId : undefined;
576
+ if (typeof requestId === "string") {
577
+ setImmediate(() => pi.events.emit("subagent:result-intercom-delivery", { requestId, delivered: true }));
578
+ }
579
+ }
580
+ },
581
+ },
582
+ };
583
+ const state = createState();
584
+ state.currentSessionId = "session-1";
585
+ const watcher = createResultWatcher(pi, state, resultsDir, 60_000);
586
+ const resultPath = path.join(resultsDir, "async-explicit-nested.json");
587
+ const originalError = console.error;
588
+ const logged: unknown[][] = [];
589
+ console.error = (...args: unknown[]) => {
590
+ logged.push(args);
591
+ };
592
+ try {
593
+ fs.writeFileSync(resultPath, JSON.stringify({
594
+ id: "async-explicit-nested",
595
+ runId: "async-explicit-nested",
596
+ agent: "owner",
597
+ mode: "single",
598
+ success: true,
599
+ state: "complete",
600
+ summary: "owner done",
601
+ results: [{
602
+ agent: "owner",
603
+ output: "owner done",
604
+ success: true,
605
+ children: [
606
+ { id: "child-explicit-good", parentRunId: "async-explicit-nested", depth: 1, path: [{ runId: "async-explicit-nested" }], state: "complete", agent: "child-good" },
607
+ { id: "child-explicit-bad", path: "not-an-array" },
608
+ ],
609
+ }],
610
+ nestedChildren: [
611
+ { id: "top-explicit-good", parentRunId: "async-explicit-nested", parentStepIndex: 0, depth: 1, path: [{ runId: "async-explicit-nested", stepIndex: 0 }], state: "complete", agent: "top-good" },
612
+ { id: "top-explicit-bad", path: "not-an-array" },
613
+ ],
614
+ sessionId: "session-1",
615
+ intercomTarget: "subagent-chat-main",
616
+ }), "utf-8");
617
+ watcher.primeExistingResults();
618
+ await new Promise((resolve) => setTimeout(resolve, 100));
619
+ } finally {
620
+ console.error = originalError;
621
+ watcher.stopResultWatcher();
622
+ }
623
+
624
+ assert.equal(fs.existsSync(resultPath), false);
625
+ assert.ok(logged.some((entry) => String(entry[0] ?? "").includes(resultPath) && /invalid nested child record/.test(String(entry[0] ?? ""))));
626
+ const intercomPayload = emitted.find((entry) => entry.event === "subagent:result-intercom")?.data as { children?: Array<{ children?: Array<{ id?: string }> }> } | undefined;
627
+ const intercomNestedIds = intercomPayload?.children?.[0]?.children?.map((child) => child.id) ?? [];
628
+ assert.deepEqual(intercomNestedIds.sort(), ["child-explicit-good", "top-explicit-good"].sort());
629
+ const completion = emitted.find((entry) => entry.event === "subagent:async-complete")?.data as { results?: Array<{ children?: Array<{ id?: string }> }>; nestedChildren?: Array<{ id?: string }> } | undefined;
630
+ assert.deepEqual(completion?.nestedChildren?.map((child) => child.id), ["top-explicit-good"]);
631
+ assert.deepEqual(completion?.results?.[0]?.children?.map((child) => child.id)?.sort(), ["child-explicit-good", "top-explicit-good"].sort());
632
+ } finally {
633
+ fs.rmSync(resultsDir, { recursive: true, force: true });
634
+ }
635
+ });
636
+
637
+ it("retries and delivers result files after nested registry enrichment recovers", async () => {
638
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-nested-retry-"));
639
+ const route = createNestedRoute("async-nested-retry");
640
+ try {
641
+ const registryPath = path.join(path.dirname(route.eventSink), "registry.json");
642
+ fs.writeFileSync(registryPath, "{", "utf-8");
643
+ writeNestedEvent(route, {
644
+ type: "subagent.nested.completed",
645
+ ts: 100,
646
+ parentRunId: "async-nested-retry",
647
+ parentStepIndex: 0,
648
+ child: {
649
+ id: "nested-retry-child",
650
+ parentRunId: "async-nested-retry",
651
+ parentStepIndex: 0,
652
+ depth: 1,
653
+ path: [{ runId: "async-nested-retry", stepIndex: 0 }],
654
+ state: "complete",
655
+ agent: "child",
656
+ },
657
+ });
658
+ const emitted: Array<{ event: string; data: unknown }> = [];
659
+ const listeners = new Map<string, Set<(payload: unknown) => void>>();
660
+ const pi = {
661
+ events: {
662
+ on(event: string, listener: (payload: unknown) => void) {
663
+ const set = listeners.get(event) ?? new Set();
664
+ set.add(listener);
665
+ listeners.set(event, set);
666
+ return () => set.delete(listener);
667
+ },
668
+ emit(event: string, data: unknown) {
669
+ emitted.push({ event, data });
670
+ for (const listener of listeners.get(event) ?? []) listener(data);
671
+ },
672
+ },
673
+ };
674
+ const state = createState();
675
+ state.currentSessionId = "session-1";
676
+ const watcher = createResultWatcher(pi, state, resultsDir, 60_000);
677
+ const resultPath = path.join(resultsDir, "async-nested-retry.json");
678
+ const originalError = console.error;
679
+ const logged: unknown[][] = [];
680
+ console.error = (...args: unknown[]) => {
681
+ logged.push(args);
682
+ };
683
+ try {
684
+ fs.writeFileSync(resultPath, JSON.stringify({
685
+ id: "async-nested-retry",
686
+ runId: "async-nested-retry",
687
+ agent: "owner",
688
+ success: true,
689
+ state: "complete",
690
+ summary: "owner done",
691
+ sessionId: "session-1",
692
+ intercomTarget: "subagent-chat-main",
693
+ }), "utf-8");
694
+ watcher.primeExistingResults();
695
+ await new Promise((resolve) => setTimeout(resolve, 100));
696
+
697
+ assert.equal(fs.existsSync(resultPath), true);
698
+ assert.equal(emitted.length, 0);
699
+ assert.ok(
700
+ logged.some((entry) => /will retry later/.test(String(entry[0] ?? ""))),
701
+ "expected nested enrichment retry warning to be logged",
702
+ );
703
+
704
+ fs.rmSync(registryPath, { force: true });
705
+ watcher.primeExistingResults();
706
+ await new Promise((resolve) => setTimeout(resolve, 650));
707
+ } finally {
708
+ console.error = originalError;
709
+ watcher.stopResultWatcher();
710
+ }
711
+
712
+ assert.equal(fs.existsSync(resultPath), false);
713
+ const completion = emitted.find((entry) => entry.event === "subagent:async-complete")?.data as { nestedChildren?: Array<{ id?: string }> } | undefined;
714
+ assert.deepEqual(completion?.nestedChildren?.map((child) => child.id), ["nested-retry-child"]);
715
+ const intercomPayload = emitted.find((entry) => entry.event === "subagent:result-intercom")?.data as { children?: Array<{ children?: Array<{ id?: string }> }> } | undefined;
716
+ assert.deepEqual(intercomPayload?.children?.[0]?.children?.map((child) => child.id), ["nested-retry-child"]);
717
+ } finally {
718
+ fs.rmSync(resultsDir, { recursive: true, force: true });
719
+ fs.rmSync(path.dirname(route.eventSink), { recursive: true, force: true });
720
+ }
721
+ });
722
+
723
+ it("does not advertise indexed revive from only a top-level async session file", async () => {
724
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-"));
725
+ try {
726
+ const emitted: Array<{ event: string; data: unknown }> = [];
727
+ const listeners = new Map<string, Set<(payload: unknown) => void>>();
728
+ const pi = {
729
+ events: {
730
+ emit: (event: string, data: unknown) => {
731
+ emitted.push({ event, data });
732
+ for (const listener of listeners.get(event) ?? []) listener(data);
733
+ return true;
734
+ },
735
+ on: (event: string, listener: (payload: unknown) => void) => {
736
+ const set = listeners.get(event) ?? new Set();
737
+ set.add(listener);
738
+ listeners.set(event, set);
739
+ return () => set.delete(listener);
740
+ },
741
+ },
742
+ };
743
+ const state = createState();
744
+ state.currentSessionId = "session-1";
745
+ const watcher = createResultWatcher(pi, state, resultsDir, 60_000);
746
+ try {
747
+ fs.writeFileSync(path.join(resultsDir, "async-top-session.json"), JSON.stringify({
748
+ id: "async-top-session",
749
+ mode: "parallel",
750
+ success: false,
751
+ state: "failed",
752
+ results: [
753
+ { agent: "a", output: "A", success: true },
754
+ { agent: "b", output: "B", success: false },
755
+ ],
756
+ sessionId: "session-1",
757
+ sessionFile: "/tmp/top-session.jsonl",
758
+ intercomTarget: "subagent-chat-main",
759
+ }), "utf-8");
760
+ watcher.primeExistingResults();
761
+ await new Promise((resolve) => setTimeout(resolve, 100));
762
+ } finally {
763
+ watcher.stopResultWatcher();
764
+ }
765
+
766
+ const eventData = emitted.find((entry) => entry.event === "subagent:result-intercom")?.data as { message?: string } | undefined;
767
+ assert.ok(eventData);
768
+ assert.doesNotMatch(String(eventData.message ?? ""), /Revive child:/);
769
+ assert.match(String(eventData.message ?? ""), /Resume: unavailable; no child session file was persisted/);
770
+ } finally {
771
+ fs.rmSync(resultsDir, { recursive: true, force: true });
772
+ }
773
+ });
774
+
775
+ it("marks grouped async results as paused when the result file is paused", async () => {
776
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-"));
777
+ try {
778
+ const emitted: Array<{ event: string; data: unknown }> = [];
779
+ const listeners = new Map<string, Set<(payload: unknown) => void>>();
780
+ const pi = {
781
+ events: {
782
+ on(event: string, handler: (payload: unknown) => void) {
783
+ const eventListeners = listeners.get(event) ?? new Set();
784
+ eventListeners.add(handler);
785
+ listeners.set(event, eventListeners);
786
+ return () => eventListeners.delete(handler);
787
+ },
788
+ emit(event: string, data: unknown) {
789
+ emitted.push({ event, data });
790
+ for (const handler of listeners.get(event) ?? []) handler(data);
791
+ if (event === "subagent:result-intercom") {
792
+ const requestId = data && typeof data === "object" ? (data as { requestId?: unknown }).requestId : undefined;
793
+ if (typeof requestId === "string") {
794
+ setImmediate(() => pi.events.emit("subagent:result-intercom-delivery", { requestId, delivered: true }));
795
+ }
796
+ }
797
+ },
798
+ },
799
+ };
800
+ const state = createState();
801
+ state.currentSessionId = "session-1";
802
+ const watcher = createResultWatcher(pi, state, resultsDir, 60_000);
803
+ try {
804
+ fs.writeFileSync(path.join(resultsDir, "async-paused.json"), JSON.stringify({
805
+ id: "async-paused",
806
+ runId: "run-paused",
807
+ agent: "chain:a->b",
808
+ mode: "chain",
809
+ success: false,
810
+ state: "paused",
811
+ summary: "Paused after interrupt. Waiting for explicit next action.",
812
+ results: [
813
+ { agent: "a", output: "Result from a", success: true, intercomTarget: "subagent-a-run-paused-1" },
814
+ { agent: "b", output: "Paused after interrupt", success: false, intercomTarget: "subagent-b-run-paused-2" },
815
+ ],
816
+ sessionId: "session-1",
817
+ intercomTarget: "subagent-chat-main",
818
+ }), "utf-8");
819
+ watcher.primeExistingResults();
820
+ await new Promise((resolve) => setTimeout(resolve, 100));
821
+ } finally {
822
+ watcher.stopResultWatcher();
823
+ }
824
+
825
+ const intercomEvents = emitted.filter((entry) => entry.event === "subagent:result-intercom");
826
+ assert.equal(intercomEvents.length, 1);
827
+ const payload = intercomEvents[0]?.data as { mode?: string; status?: string; message?: string; children?: Array<{ status?: string }> };
828
+ assert.equal(payload.mode, "chain");
829
+ assert.equal(payload.status, "paused");
830
+ assert.equal(payload.children?.every((child) => child.status === "paused"), true);
831
+ assert.match(String(payload.message ?? ""), /Status: paused/);
832
+ assert.match(String(payload.message ?? ""), /1\. a — paused/);
833
+ assert.match(String(payload.message ?? ""), /2\. b — paused/);
834
+ } finally {
835
+ fs.rmSync(resultsDir, { recursive: true, force: true });
836
+ }
837
+ });
838
+
839
+ it("logs one unacknowledged grouped async intercom delivery before completing", async () => {
840
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-"));
841
+ try {
842
+ const emitted: Array<{ event: string; data: unknown }> = [];
843
+ const pi = {
844
+ events: {
845
+ on(_event: string, _handler: (payload: unknown) => void) {
846
+ return () => {};
847
+ },
848
+ emit(event: string, data: unknown) {
849
+ emitted.push({ event, data });
850
+ },
851
+ },
852
+ };
853
+ const state = createState();
854
+ state.currentSessionId = "session-1";
855
+ const watcher = createResultWatcher(pi, state, resultsDir, 60_000);
856
+ const originalError = console.error;
857
+ const logged: unknown[][] = [];
858
+ console.error = (...args: unknown[]) => {
859
+ logged.push(args);
860
+ };
861
+ try {
862
+ fs.writeFileSync(path.join(resultsDir, "async-2.json"), JSON.stringify({
863
+ id: "async-2",
864
+ runId: "run-456",
865
+ agent: "worker",
866
+ success: true,
867
+ state: "complete",
868
+ summary: "Worker summary",
869
+ sessionId: "session-1",
870
+ intercomTarget: "orchestrator",
871
+ }), "utf-8");
872
+ watcher.primeExistingResults();
873
+ const deadline = Date.now() + 1000;
874
+ while (true) {
875
+ const sawWarning = logged.some((entry) => /Subagent async grouped result intercom delivery was not acknowledged/.test(String(entry[0] ?? "")));
876
+ const sawCompletion = emitted.some((entry) => entry.event === "subagent:async-complete");
877
+ if ((sawWarning && sawCompletion) || Date.now() > deadline) break;
878
+ await new Promise((resolve) => setTimeout(resolve, 25));
879
+ }
880
+ } finally {
881
+ console.error = originalError;
882
+ watcher.stopResultWatcher();
883
+ }
884
+
885
+ assert.equal(emitted.filter((entry) => entry.event === "subagent:result-intercom").length, 1);
886
+ assert.equal(emitted.some((entry) => entry.event === "subagent:async-complete"), true);
887
+ assert.equal(logged.some((entry) => /Subagent async grouped result intercom delivery was not acknowledged/.test(String(entry[0] ?? ""))), true);
888
+ } finally {
889
+ fs.rmSync(resultsDir, { recursive: true, force: true });
890
+ }
891
+ });
892
+ });