@selesai/code 0.8.6 → 0.8.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/dist/cli/args.d.ts +1 -0
  3. package/dist/cli/args.js +11 -0
  4. package/dist/cli/args.test.d.ts +1 -0
  5. package/dist/cli/args.test.js +24 -0
  6. package/dist/core/agent-session.d.ts +4 -1
  7. package/dist/core/agent-session.js +18 -7
  8. package/dist/core/experimental.d.ts +4 -0
  9. package/dist/core/experimental.js +4 -0
  10. package/dist/core/extensions/types.d.ts +6 -0
  11. package/dist/core/keybindings.d.ts +24 -0
  12. package/dist/core/model-config.d.ts +5 -0
  13. package/dist/core/model-config.js +1 -0
  14. package/dist/core/project-trust.js +2 -2
  15. package/dist/core/provider-composer.js +17 -4
  16. package/dist/core/sdk.d.ts +5 -3
  17. package/dist/core/sdk.js +2 -1
  18. package/dist/core/session-manager.js +2 -2
  19. package/dist/core/settings-manager.d.ts +8 -0
  20. package/dist/core/settings-manager.js +15 -0
  21. package/dist/core/system-prompt.js +3 -9
  22. package/dist/core/system-prompt.test.js +6 -0
  23. package/dist/core/tools/bash.js +2 -0
  24. package/dist/core/tools/edit.js +2 -0
  25. package/dist/core/tools/find.js +1 -1
  26. package/dist/core/tools/grep.js +1 -1
  27. package/dist/core/tools/index.d.ts +1 -0
  28. package/dist/core/tools/index.js +1 -0
  29. package/dist/core/tools/read.js +2 -0
  30. package/dist/core/tools/schema-prune.d.ts +16 -0
  31. package/dist/core/tools/schema-prune.js +39 -0
  32. package/dist/core/tools/schema-prune.test.d.ts +1 -0
  33. package/dist/core/tools/schema-prune.test.js +111 -0
  34. package/dist/core/tools/write.js +2 -0
  35. package/dist/defaults/settings.json +1 -0
  36. package/dist/extensions/model-prompt-injector/config.json +10 -0
  37. package/dist/extensions/model-prompt-injector/index.test.ts +112 -0
  38. package/dist/extensions/model-prompt-injector/index.ts +151 -0
  39. package/dist/extensions/package.json +1 -0
  40. package/dist/extensions/pi-subagents/CHANGELOG.md +158 -90
  41. package/dist/extensions/pi-subagents/UPSTREAM-V0.50-MAPPING.md +172 -0
  42. package/dist/extensions/pi-subagents/agents/oracle.md +7 -5
  43. package/dist/extensions/pi-subagents/agents/reviewer.md +2 -2
  44. package/dist/extensions/pi-subagents/agents/scout.md +1 -1
  45. package/dist/extensions/pi-subagents/agents/worker.md +1 -1
  46. package/dist/extensions/pi-subagents/docs/configuration.md +78 -19
  47. package/dist/extensions/pi-subagents/docs/extension-api.md +36 -0
  48. package/dist/extensions/pi-subagents/docs/missions.md +5 -3
  49. package/dist/extensions/pi-subagents/docs/models.md +1 -1
  50. package/dist/extensions/pi-subagents/docs/observability.md +42 -2
  51. package/dist/extensions/pi-subagents/docs/tool-reference.md +20 -3
  52. package/dist/extensions/pi-subagents/docs/workflows.md +5 -5
  53. package/dist/extensions/pi-subagents/install.mjs +2 -2
  54. package/dist/extensions/pi-subagents/package-lock.json +1470 -1472
  55. package/dist/extensions/pi-subagents/package.json +1 -1
  56. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +1 -1
  57. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  58. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +15 -14
  59. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  60. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  61. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
  62. package/dist/extensions/pi-subagents/src/agents/agents.ts +37 -12
  63. package/dist/extensions/pi-subagents/src/api/external-runs.ts +174 -84
  64. package/dist/extensions/pi-subagents/src/api/preflight.ts +13 -7
  65. package/dist/extensions/pi-subagents/src/extension/config.ts +53 -0
  66. package/dist/extensions/pi-subagents/src/extension/index.ts +67 -13
  67. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +34 -6
  68. package/dist/extensions/pi-subagents/src/extension/rpc.ts +5 -1
  69. package/dist/extensions/pi-subagents/src/extension/schemas.ts +31 -30
  70. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +10 -9
  71. package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +13 -8
  72. package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +16 -3
  73. package/dist/extensions/pi-subagents/src/inspectors/herdr/project-panes.ts +2 -6
  74. package/dist/extensions/pi-subagents/src/inspectors/herdr/shell-command.ts +16 -0
  75. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +5 -4
  76. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +19 -42
  77. package/dist/extensions/pi-subagents/src/missions/goal-driver.ts +3 -1
  78. package/dist/extensions/pi-subagents/src/missions/store.ts +8 -3
  79. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +82 -25
  80. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +71 -1
  81. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +73 -43
  82. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +5 -0
  83. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -6
  84. package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +277 -0
  85. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +8 -3
  86. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +2 -2
  87. package/dist/extensions/pi-subagents/src/runs/background/completion-replay.ts +11 -1
  88. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +21 -6
  89. package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +437 -0
  90. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +187 -41
  91. package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +27 -7
  92. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +76 -19
  93. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +30 -24
  94. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +101 -4
  95. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +54 -28
  96. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +27 -13
  97. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +298 -33
  98. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +2 -0
  99. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +5 -2
  100. package/dist/extensions/pi-subagents/src/runs/foreground/async-dismiss-action.ts +2 -1
  101. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +16 -0
  102. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +219 -15
  103. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +4 -3
  104. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +333 -45
  105. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +107 -1
  106. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +4 -0
  107. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +39 -23
  108. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +16 -2
  109. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +66 -62
  110. package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +376 -0
  111. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
  112. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +15 -0
  113. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +1 -9
  114. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +12 -0
  115. package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +95 -0
  116. package/dist/extensions/pi-subagents/src/shared/agent-stream-options.ts +5 -0
  117. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +0 -4
  118. package/dist/extensions/pi-subagents/src/shared/display-text.ts +50 -0
  119. package/dist/extensions/pi-subagents/src/shared/node-executable.ts +21 -0
  120. package/dist/extensions/pi-subagents/src/shared/types.ts +50 -4
  121. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +34 -25
  122. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +4 -0
  123. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +160 -45
  124. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +1 -48
  125. package/dist/extensions/pi-subagents/src/tui/fleet.ts +128 -16
  126. package/dist/extensions/pi-subagents/src/tui/render.ts +122 -44
  127. package/dist/extensions/pi-subagents/src/watchdog/permission-arbiter.ts +2 -1
  128. package/dist/extensions/pi-subagents/src/watchdog/review.ts +4 -3
  129. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +10 -2
  130. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +272 -76
  131. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +2 -0
  132. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2 -2
  133. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +68 -9
  134. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +27 -1
  135. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1 -1
  136. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +49 -0
  137. package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
  138. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +26 -1
  139. package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +144 -0
  140. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +26 -0
  141. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +395 -98
  142. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +111 -78
  143. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +49 -1
  144. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +12 -0
  145. package/dist/extensions/pi-subagents/test/support/node-command.ts +15 -0
  146. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +50 -0
  147. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +28 -0
  148. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +61 -0
  149. package/dist/extensions/pi-subagents/test/unit/agent-stream-options.test.ts +14 -0
  150. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +76 -0
  151. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
  152. package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +162 -0
  153. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +209 -10
  154. package/dist/extensions/pi-subagents/test/unit/completion-replay.test.ts +41 -1
  155. package/dist/extensions/pi-subagents/test/unit/external-runs.test.ts +135 -42
  156. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +252 -1
  157. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +192 -6
  158. package/dist/extensions/pi-subagents/test/unit/herdr-inspector-bootstrap.test.ts +32 -0
  159. package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +59 -0
  160. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +69 -0
  161. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +23 -4
  162. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +56 -1
  163. package/dist/extensions/pi-subagents/test/unit/mission-goal-driver.test.ts +67 -3
  164. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
  165. package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +14 -0
  166. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +18 -0
  167. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +2 -2
  168. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +25 -2
  169. package/dist/extensions/pi-subagents/test/unit/node-executable.test.ts +26 -0
  170. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +8 -0
  171. package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +348 -0
  172. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +5 -2
  173. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +1 -1
  174. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +34 -0
  175. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +6 -1
  176. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +92 -0
  177. package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +175 -0
  178. package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +127 -17
  179. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +44 -0
  180. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +247 -0
  181. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +107 -2
  182. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +5 -8
  183. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +456 -4
  184. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +128 -0
  185. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +28 -0
  186. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +16 -7
  187. package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +15 -0
  188. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +79 -15
  189. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +12 -12
  190. package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +109 -0
  191. package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +34 -0
  192. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +94 -1
  193. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +42 -2
  194. package/dist/extensions/question/index.ts +21 -6
  195. package/dist/extensions/question/question-list.ts +7 -1
  196. package/dist/extensions/question/row-layout.ts +3 -0
  197. package/dist/extensions/question/tests/exp.test.ts +70 -0
  198. package/dist/extensions/question/tests/exp2.test.ts +59 -0
  199. package/dist/extensions/question/tests/exp3.test.ts +47 -0
  200. package/dist/extensions/question/tests/helpers.test.ts +7 -0
  201. package/dist/extensions/question/tests/question-list.test.ts +140 -0
  202. package/dist/extensions/question/tests/row-layout.test.ts +13 -0
  203. package/dist/extensions/question/tests/selection-mode.test.ts +54 -0
  204. package/dist/extensions/question/tests/zz-ig3.test.ts +20 -0
  205. package/dist/extensions/question/tests/zz-probe.test.ts +29 -0
  206. package/dist/extensions/tokenin-onboarding.ts +1 -1
  207. package/dist/index.d.ts +1 -1
  208. package/dist/main.js +4 -0
  209. package/dist/modes/interactive/components/model-selector.js +2 -1
  210. package/dist/modes/interactive/components/settings-selector.d.ts +3 -1
  211. package/dist/modes/interactive/components/settings-selector.js +10 -0
  212. package/dist/modes/interactive/components/tool-execution.js +10 -1
  213. package/dist/modes/interactive/interactive-mode.d.ts +8 -2
  214. package/dist/modes/interactive/interactive-mode.js +81 -25
  215. package/dist/modes/interactive/model-catalog-refresh.d.ts +6 -0
  216. package/dist/modes/interactive/model-catalog-refresh.js +33 -0
  217. package/dist/modes/interactive/theme/dark.json +2 -0
  218. package/dist/modes/interactive/theme/light.json +2 -0
  219. package/dist/modes/interactive/theme/theme-controller.d.ts +10 -2
  220. package/dist/modes/interactive/theme/theme-controller.js +25 -11
  221. package/dist/modes/interactive/theme/theme-schema.json +9 -1
  222. package/dist/modes/interactive/theme/theme.d.ts +5 -3
  223. package/dist/modes/interactive/theme/theme.js +12 -2
  224. package/dist/skills/pi-subagents/SKILL.md +1 -1
  225. package/dist/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  226. package/dist/skills/pi-subagents/references/execution-controls.md +15 -14
  227. package/dist/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  228. package/dist/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  229. package/dist/utils/abort.d.ts +4 -0
  230. package/dist/utils/abort.js +47 -0
  231. package/dist/utils/tools-manager.d.ts +5 -1
  232. package/dist/utils/tools-manager.js +12 -21
  233. package/docs/settings.md +6 -0
  234. package/package.json +6 -6
@@ -5,7 +5,8 @@ import * as path from "node:path";
5
5
  import { describe, it } from "node:test";
6
6
  import type { ExtensionContext } from "@selesai/code";
7
7
  import { buildCompletionKey } from "../../src/runs/background/completion-dedupe.ts";
8
- import { createResultWatcher } from "../../src/runs/background/result-watcher.ts";
8
+ import { createResultWatcher as createRawResultWatcher } from "../../src/runs/background/result-watcher.ts";
9
+ import { writeAsyncResultFile, writePendingAsyncResultFile } from "../../src/runs/background/result-files.ts";
9
10
  import { createScheduledRunManager, scheduledRunStorePath } from "../../src/runs/background/scheduled-runs.ts";
10
11
  import { prepareMissionLaunch, writeMissionAsyncBinding } from "../../src/missions/lifecycle.ts";
11
12
  import { readMission, updateMission } from "../../src/missions/store.ts";
@@ -32,6 +33,24 @@ function createState(): SubagentState {
32
33
  };
33
34
  }
34
35
 
36
+ function createResultWatcher(
37
+ pi: Parameters<typeof createRawResultWatcher>[0],
38
+ state: Parameters<typeof createRawResultWatcher>[1],
39
+ resultsDir: Parameters<typeof createRawResultWatcher>[2],
40
+ completionTtlMs: Parameters<typeof createRawResultWatcher>[3],
41
+ deps: Parameters<typeof createRawResultWatcher>[4] = {},
42
+ ): ReturnType<typeof createRawResultWatcher> {
43
+ return createRawResultWatcher(pi, state, resultsDir, completionTtlMs, { coalesceDelayMs: 0, ...deps });
44
+ }
45
+
46
+ function writeIndexedResult(filePath: string, data: Record<string, unknown>): void {
47
+ writeAsyncResultFile(filePath, data);
48
+ }
49
+
50
+ function pendingResultPath(resultsDir: string, sessionId: string, runId: string): string {
51
+ return path.join(resultsDir, "result-pending", encodeURIComponent(sessionId), `${encodeURIComponent(runId)}.json`);
52
+ }
53
+
35
54
  async function waitForPredicate(predicate: () => boolean, timeoutMs = 2_500): Promise<boolean> {
36
55
  const deadline = Date.now() + timeoutMs;
37
56
  while (!predicate()) {
@@ -55,24 +74,25 @@ describe("result watcher", () => {
55
74
  },
56
75
  };
57
76
  const state = createState();
77
+ state.asyncJobs.set("session-run", { asyncId: "session-run", asyncDir: path.join(resultsDir, "session-run"), status: "running", startedAt: Date.now(), updatedAt: Date.now() });
58
78
  const resultPath = path.join(resultsDir, "session-run.json");
59
- fs.writeFileSync(resultPath, JSON.stringify({
79
+ writeIndexedResult(resultPath, {
60
80
  id: "session-run",
61
81
  sessionId: "session-current",
62
82
  success: true,
63
83
  summary: "done",
64
- }), "utf-8");
84
+ });
65
85
 
66
86
  const watcher = createResultWatcher(pi, state, resultsDir, 60_000);
67
87
  try {
68
88
  watcher.primeExistingResults();
69
- await new Promise((resolve) => setTimeout(resolve, 100));
89
+ await new Promise((resolve) => setTimeout(resolve, 10));
70
90
  assert.equal(emitted.length, 0);
71
91
  assert.equal(fs.existsSync(resultPath), true);
72
92
 
73
93
  state.currentSessionId = "session-current";
74
94
  watcher.primeExistingResults();
75
- await new Promise((resolve) => setTimeout(resolve, 100));
95
+ await new Promise((resolve) => setTimeout(resolve, 10));
76
96
  } finally {
77
97
  watcher.stopResultWatcher();
78
98
  }
@@ -84,38 +104,54 @@ describe("result watcher", () => {
84
104
  }
85
105
  });
86
106
 
87
- it("does not inspect scheduled observers while priming current-session results", async () => {
107
+ it("includes scheduled observer ids while priming current-session results", async () => {
88
108
  const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-current-prime-"));
89
109
  try {
90
110
  const state = createState();
91
111
  state.currentSessionId = "session-current";
92
- let observedRunIdScans = 0;
93
- fs.writeFileSync(path.join(resultsDir, "current.json"), JSON.stringify({
94
- id: "current",
95
- sessionId: "session-current",
96
- success: true,
97
- summary: "done",
98
- }), "utf-8");
99
- const watcher = createResultWatcher({
100
- events: {
101
- on: () => () => {},
102
- emit() {},
103
- },
104
- }, state, resultsDir, 60_000, {
112
+ let observedRunIdLookups = 0;
113
+ writeIndexedResult(path.join(resultsDir, "current.json"), { id: "current", sessionId: "session-current", success: true, summary: "done" });
114
+ const watcher = createResultWatcher({ events: { on: () => () => {}, emit() {} } }, state, resultsDir, 60_000, {
105
115
  deliverIntercomResults: false,
106
116
  observedCompletionRunIds() {
107
- observedRunIdScans += 1;
108
- return [];
117
+ observedRunIdLookups += 1;
118
+ return ["scheduled-a"];
109
119
  },
110
120
  });
111
121
  watcher.primeExistingResults();
112
- assert.equal(observedRunIdScans, 0);
122
+ assert.equal(observedRunIdLookups, 1);
113
123
  watcher.stopResultWatcher();
114
124
  } finally {
115
125
  fs.rmSync(resultsDir, { recursive: true, force: true });
116
126
  }
117
127
  });
118
128
 
129
+ it("keeps slow indexed result scans silent", () => {
130
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-slow-scan-"));
131
+ const originalNow = Date.now;
132
+ const originalError = console.error;
133
+ const logged: unknown[][] = [];
134
+ try {
135
+ const state = createState();
136
+ state.currentSessionId = "session-current";
137
+ state.asyncJobs.set("slow-scan", { asyncId: "slow-scan", asyncDir: path.join(resultsDir, "slow-scan"), status: "running", startedAt: 0, updatedAt: 0 });
138
+ let nowCalls = 0;
139
+ Date.now = () => nowCalls++ === 0 ? 0 : 501;
140
+ console.error = (...args: unknown[]) => { logged.push(args); };
141
+ const watcher = createResultWatcher({ events: { on: () => () => {}, emit() {} } }, state, resultsDir, 60_000);
142
+ try {
143
+ watcher.primeExistingResults();
144
+ } finally {
145
+ watcher.stopResultWatcher();
146
+ }
147
+ assert.deepEqual(logged, []);
148
+ } finally {
149
+ Date.now = originalNow;
150
+ console.error = originalError;
151
+ fs.rmSync(resultsDir, { recursive: true, force: true });
152
+ }
153
+ });
154
+
119
155
  it("skips full parsing for unrelated sessions during priming, safety scans, and native watch events", async () => {
120
156
  const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-filter-"));
121
157
  try {
@@ -133,13 +169,13 @@ describe("result watcher", () => {
133
169
  unref() {},
134
170
  } as fs.FSWatcher;
135
171
  const writeResult = (id: string, sessionId: string) => {
136
- fs.writeFileSync(path.join(resultsDir, `${id}.json`), JSON.stringify({
172
+ writeIndexedResult(path.join(resultsDir, `${id}.json`), {
137
173
  id,
138
174
  results: [{ structuredOutput: { sessionId: "nested-not-owner" } }],
139
175
  sessionId,
140
176
  success: true,
141
177
  summary: "done",
142
- }), "utf-8");
178
+ });
143
179
  };
144
180
  writeResult("startup-current", "session-current");
145
181
  writeResult("startup-stale", "session-stale");
@@ -224,7 +260,7 @@ describe("result watcher", () => {
224
260
  const binding = prepareMissionLaunch({
225
261
  params: { mission: { title: "Workflow mission" }, task: "Run async child" },
226
262
  projectRoot: project,
227
- config: { globalIndexDir: path.join(root, "global-index") },
263
+ config: { directory: path.join(root, "missions"), globalIndexDir: path.join(root, "global-index") },
228
264
  ownerSessionId: "session-current",
229
265
  });
230
266
  assert.ok(binding);
@@ -239,7 +275,7 @@ describe("result watcher", () => {
239
275
  heartbeat: { status: "running" },
240
276
  }],
241
277
  });
242
- fs.writeFileSync(path.join(resultsDir, "async-child.json"), JSON.stringify({
278
+ writeIndexedResult(path.join(resultsDir, "async-child.json"), {
243
279
  id: "async-child",
244
280
  runId: "async-child",
245
281
  sessionId: "session-current",
@@ -251,7 +287,7 @@ describe("result watcher", () => {
251
287
  parentWorkflowRunId: "workflow-1",
252
288
  workflowKey: "background",
253
289
  results: [{ agent: "worker", success: true, output: "done", artifactPaths: { outputPath } }],
254
- }), "utf-8");
290
+ });
255
291
  const state = createState();
256
292
  state.currentSessionId = "session-current";
257
293
  const watcher = createResultWatcher({ events: { on: () => () => {}, emit() {} } }, state, resultsDir, 60_000, {
@@ -259,7 +295,7 @@ describe("result watcher", () => {
259
295
  });
260
296
  try {
261
297
  watcher.primeExistingResults();
262
- await new Promise((resolve) => setTimeout(resolve, 100));
298
+ await new Promise((resolve) => setTimeout(resolve, 10));
263
299
  } finally {
264
300
  watcher.stopResultWatcher();
265
301
  }
@@ -279,6 +315,91 @@ describe("result watcher", () => {
279
315
  }
280
316
  });
281
317
 
318
+ it("keeps result files until failed completion observers retry successfully", async () => {
319
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-observer-retry-"));
320
+ const originalError = console.error;
321
+ try {
322
+ console.error = () => {};
323
+ const resultPath = path.join(resultsDir, "observer-retry.json");
324
+ writeIndexedResult(resultPath, { id: "observer-retry", runId: "observer-retry", sessionId: "session-current", success: true, summary: "done" });
325
+ const state = createState();
326
+ state.currentSessionId = "session-current";
327
+ let observerCalls = 0;
328
+ let deliveries = 0;
329
+ const watcher = createResultWatcher({ events: { on: () => () => {}, emit() {} } }, state, resultsDir, 60_000, {
330
+ observeCompletion: () => {
331
+ observerCalls += 1;
332
+ if (observerCalls === 1) throw new Error("observer unavailable");
333
+ },
334
+ notifier: { deliver: async () => { deliveries += 1; return true; } },
335
+ });
336
+ try {
337
+ watcher.primeExistingResults();
338
+ assert.equal(await waitForPredicate(() => !fs.existsSync(resultPath) && observerCalls >= 2), true);
339
+ } finally {
340
+ watcher.stopResultWatcher();
341
+ }
342
+
343
+ assert.equal(observerCalls, 2);
344
+ assert.equal(deliveries, 1);
345
+ } finally {
346
+ console.error = originalError;
347
+ fs.rmSync(resultsDir, { recursive: true, force: true });
348
+ }
349
+ });
350
+
351
+ it("does not redeliver user notification after reload while observer retry is pending", async () => {
352
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-observer-reload-"));
353
+ const originalError = console.error;
354
+ try {
355
+ console.error = () => {};
356
+ const resultPath = path.join(resultsDir, "observer-reload.json");
357
+ writeIndexedResult(resultPath, { id: "observer-reload", runId: "observer-reload", sessionId: "session-current", success: true, summary: "done" });
358
+ let observerCalls = 0;
359
+ let deliveries = 0;
360
+ let emitted = 0;
361
+ const pi = { events: { on: () => () => {}, emit: () => { emitted += 1; } } };
362
+ const firstState = createState();
363
+ firstState.currentSessionId = "session-current";
364
+ const firstWatcher = createResultWatcher(pi, firstState, resultsDir, 60_000, {
365
+ observeCompletion: () => {
366
+ observerCalls += 1;
367
+ throw new Error("observer unavailable");
368
+ },
369
+ notifier: { deliver: async () => { deliveries += 1; return true; } },
370
+ });
371
+ try {
372
+ firstWatcher.primeExistingResults();
373
+ assert.equal(await waitForPredicate(() => {
374
+ if (deliveries !== 1 || !fs.existsSync(resultPath)) return false;
375
+ return typeof (JSON.parse(fs.readFileSync(resultPath, "utf-8")) as { notificationDeliveredAt?: unknown }).notificationDeliveredAt === "number";
376
+ }), true);
377
+ } finally {
378
+ firstWatcher.stopResultWatcher();
379
+ }
380
+
381
+ const secondState = createState();
382
+ secondState.currentSessionId = "session-current";
383
+ const secondWatcher = createResultWatcher(pi, secondState, resultsDir, 60_000, {
384
+ observeCompletion: () => { observerCalls += 1; },
385
+ notifier: { deliver: async () => { deliveries += 1; return true; } },
386
+ });
387
+ try {
388
+ secondWatcher.primeExistingResults();
389
+ assert.equal(await waitForPredicate(() => !fs.existsSync(resultPath)), true);
390
+ } finally {
391
+ secondWatcher.stopResultWatcher();
392
+ }
393
+
394
+ assert.equal(observerCalls, 2);
395
+ assert.equal(deliveries, 1);
396
+ assert.equal(emitted, 1);
397
+ } finally {
398
+ console.error = originalError;
399
+ fs.rmSync(resultsDir, { recursive: true, force: true });
400
+ }
401
+ });
402
+
282
403
  it("observes retained-project completions without changing active-session delivery", async () => {
283
404
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-scheduled-"));
284
405
  const resultsDir = path.join(root, "results");
@@ -308,7 +429,7 @@ describe("result watcher", () => {
308
429
  const state = createState();
309
430
  state.currentSessionId = "session-b";
310
431
  const resultPath = path.join(resultsDir, "scheduled-a.json");
311
- fs.writeFileSync(resultPath, JSON.stringify({ id: "scheduled-a", sessionId: "session-a", success: true, summary: "done" }), "utf-8");
432
+ writeIndexedResult(resultPath, { id: "scheduled-a", sessionId: "session-a", success: true, summary: "done" });
312
433
  const watcher = createResultWatcher({
313
434
  events: {
314
435
  on: () => () => {},
@@ -316,12 +437,13 @@ describe("result watcher", () => {
316
437
  },
317
438
  }, state, resultsDir, 60_000, {
318
439
  observeCompletion: (result) => manager.handleAsyncCompletion(result),
440
+ observedCompletionRunIds: () => manager.observedCompletionRunIds(),
319
441
  notifier: { deliver: async () => assert.fail("inactive-session completion must not reach the live notifier") },
320
442
  });
321
443
  try {
322
444
  watcher.startResultWatcher();
323
445
  watcher.primeExistingResults();
324
- await new Promise((resolve) => setTimeout(resolve, 100));
446
+ await new Promise((resolve) => setTimeout(resolve, 10));
325
447
  } finally {
326
448
  watcher.stopResultWatcher();
327
449
  }
@@ -337,6 +459,192 @@ describe("result watcher", () => {
337
459
  }
338
460
  });
339
461
 
462
+ it("delivers indexed pending results during reload when public promotion is blocked", async () => {
463
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-pending-index-"));
464
+ const originalError = console.error;
465
+ try {
466
+ console.error = () => {};
467
+ const emitted: Array<{ event: string; data: unknown }> = [];
468
+ const delivered: unknown[] = [];
469
+ const state = createState();
470
+ state.currentSessionId = "session-current";
471
+ const resultPath = path.join(resultsDir, "pending-run.json");
472
+ fs.mkdirSync(resultPath, { recursive: true });
473
+ writePendingAsyncResultFile(resultPath, {
474
+ id: "pending-run",
475
+ runId: "pending-run",
476
+ sessionId: "session-current",
477
+ success: true,
478
+ state: "complete",
479
+ summary: "done from pending",
480
+ });
481
+ const watcher = createResultWatcher({
482
+ events: {
483
+ on: () => () => {},
484
+ emit(event: string, data: unknown) { emitted.push({ event, data }); },
485
+ },
486
+ }, state, resultsDir, 60_000, {
487
+ deliverIntercomResults: false,
488
+ notifier: { deliver: async (result) => { delivered.push(result); return true; } },
489
+ });
490
+ try {
491
+ watcher.primeExistingResults();
492
+ assert.equal(await waitForPredicate(() => emitted.some((entry) => entry.event === "subagent:async-complete")), true);
493
+ } finally {
494
+ watcher.stopResultWatcher();
495
+ }
496
+
497
+ assert.equal((delivered[0] as { summary?: string } | undefined)?.summary, "done from pending");
498
+ assert.equal(fs.existsSync(pendingResultPath(resultsDir, "session-current", "pending-run")), false);
499
+ assert.equal(fs.statSync(resultPath).isDirectory(), true);
500
+ } finally {
501
+ console.error = originalError;
502
+ fs.rmSync(resultsDir, { recursive: true, force: true });
503
+ }
504
+ });
505
+
506
+ it("uses indexed result files and ignores unindexed stale files during reload", async () => {
507
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-index-"));
508
+ try {
509
+ for (let i = 0; i < 300; i += 1) {
510
+ fs.writeFileSync(path.join(resultsDir, `stale-${i}.json`), JSON.stringify({ id: `stale-${i}`, sessionId: "session-stale", success: true, summary: "done" }), "utf-8");
511
+ }
512
+ writeIndexedResult(path.join(resultsDir, "current.json"), { id: "current", runId: "current", sessionId: "session-current", success: true, summary: "done" });
513
+ const parsedIds: string[] = [];
514
+ const state = createState();
515
+ state.currentSessionId = "session-current";
516
+ const watcher = createResultWatcher({ events: { on: () => () => {}, emit() {} } }, state, resultsDir, 60_000, {
517
+ deliverIntercomResults: false,
518
+ parseResult(raw) {
519
+ const parsed = JSON.parse(raw) as { id: string };
520
+ parsedIds.push(parsed.id);
521
+ return parsed;
522
+ },
523
+ });
524
+ try {
525
+ watcher.primeExistingResults();
526
+ assert.equal(await waitForPredicate(() => !fs.existsSync(path.join(resultsDir, "current.json"))), true);
527
+ } finally {
528
+ watcher.stopResultWatcher();
529
+ }
530
+ assert.deepEqual(parsedIds, ["current"]);
531
+ assert.equal(fs.existsSync(path.join(resultsDir, "stale-0.json")), true);
532
+ } finally {
533
+ fs.rmSync(resultsDir, { recursive: true, force: true });
534
+ }
535
+ });
536
+
537
+ it("includes observer-known run ids when current-session results are also indexed", async () => {
538
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-mixed-observer-"));
539
+ try {
540
+ const resultsDir = path.join(root, "results");
541
+ writeIndexedResult(path.join(resultsDir, "current.json"), { id: "current", runId: "current", sessionId: "session-current", success: true, summary: "current" });
542
+ writeIndexedResult(path.join(resultsDir, "scheduled-a.json"), { id: "scheduled-a", runId: "scheduled-a", sessionId: "session-a", success: true, summary: "scheduled" });
543
+ let observations = 0;
544
+ const state = createState();
545
+ state.currentSessionId = "session-current";
546
+ const watcher = createResultWatcher({ events: { on: () => () => {}, emit() {} } }, state, resultsDir, 60_000, {
547
+ observedCompletionRunIds: () => ["scheduled-a"],
548
+ observeCompletion: (result) => { if (result.runId === "scheduled-a") observations += 1; },
549
+ notifier: { deliver: async () => true },
550
+ });
551
+ try {
552
+ watcher.primeExistingResults();
553
+ assert.equal(await waitForPredicate(() => observations === 1), true);
554
+ assert.equal(await waitForPredicate(() => !fs.existsSync(path.join(resultsDir, "current.json"))), true);
555
+ } finally {
556
+ watcher.stopResultWatcher();
557
+ }
558
+ } finally {
559
+ fs.rmSync(root, { recursive: true, force: true });
560
+ }
561
+ });
562
+
563
+ it("delivers observed indexed pending results when public promotion is blocked", async () => {
564
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-observed-pending-"));
565
+ const originalError = console.error;
566
+ try {
567
+ console.error = () => {};
568
+ for (let i = 0; i < 300; i += 1) {
569
+ fs.writeFileSync(path.join(resultsDir, `stale-${i}.json`), JSON.stringify({ id: `stale-${i}`, sessionId: "session-stale", success: true, summary: "done" }), "utf-8");
570
+ }
571
+ const resultPath = path.join(resultsDir, "scheduled-pending.json");
572
+ fs.mkdirSync(resultPath, { recursive: true });
573
+ writePendingAsyncResultFile(resultPath, {
574
+ id: "scheduled-pending",
575
+ runId: "scheduled-pending",
576
+ sessionId: "session-a",
577
+ success: true,
578
+ state: "complete",
579
+ summary: "scheduled pending done",
580
+ });
581
+ let observations = 0;
582
+ const state = createState();
583
+ state.currentSessionId = "session-b";
584
+ const watcher = createResultWatcher({ events: { on: () => () => {}, emit() {} } }, state, resultsDir, 60_000, {
585
+ observedCompletionRunIds: () => ["scheduled-pending"],
586
+ observeCompletion: (result) => {
587
+ if (result.runId === "scheduled-pending") observations += 1;
588
+ },
589
+ notifier: { deliver: async () => assert.fail("observer-owned completion must not reach active delivery") },
590
+ });
591
+ try {
592
+ watcher.primeExistingResults();
593
+ assert.equal(await waitForPredicate(() => observations === 1), true);
594
+ } finally {
595
+ watcher.stopResultWatcher();
596
+ }
597
+
598
+ assert.equal(fs.existsSync(pendingResultPath(resultsDir, "session-a", "scheduled-pending")), true);
599
+ assert.equal(fs.statSync(resultPath).isDirectory(), true);
600
+ assert.equal(fs.existsSync(path.join(resultsDir, "stale-0.json")), true);
601
+ } finally {
602
+ console.error = originalError;
603
+ fs.rmSync(resultsDir, { recursive: true, force: true });
604
+ }
605
+ });
606
+
607
+ it("uses observed run ids directly without scanning a stale result pile", async () => {
608
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-observed-index-"));
609
+ try {
610
+ for (let i = 0; i < 300; i += 1) {
611
+ fs.writeFileSync(path.join(resultsDir, `stale-${i}.json`), JSON.stringify({ id: `stale-${i}`, sessionId: "session-stale", success: true, summary: "done" }), "utf-8");
612
+ }
613
+ const resultPath = path.join(resultsDir, "scheduled-a.json");
614
+ writeIndexedResult(resultPath, { id: "scheduled-a", runId: "scheduled-a", sessionId: "session-a", success: true, summary: "done" });
615
+ let observations = 0;
616
+ let observed = true;
617
+ const makeWatcher = () => {
618
+ const state = createState();
619
+ state.currentSessionId = "session-b";
620
+ return createResultWatcher({ events: { on: () => () => {}, emit() {} } }, state, resultsDir, 60_000, {
621
+ observedCompletionRunIds: () => observed ? ["scheduled-a"] : [],
622
+ observeCompletion: () => { observations += 1; observed = false; },
623
+ notifier: { deliver: async () => assert.fail("observer-owned completion must not reach active delivery") },
624
+ });
625
+ };
626
+ let watcher = makeWatcher();
627
+ try {
628
+ watcher.primeExistingResults();
629
+ await new Promise((resolve) => setTimeout(resolve, 10));
630
+ } finally {
631
+ watcher.stopResultWatcher();
632
+ }
633
+ watcher = makeWatcher();
634
+ try {
635
+ watcher.primeExistingResults();
636
+ await new Promise((resolve) => setTimeout(resolve, 10));
637
+ } finally {
638
+ watcher.stopResultWatcher();
639
+ }
640
+ assert.equal(observations, 1);
641
+ assert.equal(fs.existsSync(resultPath), true);
642
+ assert.equal(fs.existsSync(path.join(resultsDir, "stale-0.json")), true);
643
+ } finally {
644
+ fs.rmSync(resultsDir, { recursive: true, force: true });
645
+ }
646
+ });
647
+
340
648
  it("uses native completion delivery without attempting external grouped intercom when disabled", async () => {
341
649
  const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-native-delivery-"));
342
650
  try {
@@ -351,7 +659,7 @@ describe("result watcher", () => {
351
659
  const state = createState();
352
660
  state.currentSessionId = "session-native";
353
661
  const resultPath = path.join(resultsDir, "native-run.json");
354
- fs.writeFileSync(resultPath, JSON.stringify({
662
+ writeIndexedResult(resultPath, {
355
663
  id: "native-run",
356
664
  runId: "native-run",
357
665
  sessionId: "session-native",
@@ -361,14 +669,14 @@ describe("result watcher", () => {
361
669
  summary: "Subagent process terminated by signal SIGTERM.",
362
670
  results: [{ agent: "worker", output: "", success: false, exitCode: 1, processSignal: "SIGTERM" }],
363
671
  intercomTarget: "native-parent",
364
- }), "utf-8");
672
+ });
365
673
  const watcher = createResultWatcher(pi, state, resultsDir, 60_000, {
366
674
  deliverIntercomResults: false,
367
675
  notifier: { deliver: async (result) => { delivered.push(result); return true; } },
368
676
  });
369
677
  try {
370
678
  watcher.primeExistingResults();
371
- await new Promise((resolve) => setTimeout(resolve, 100));
679
+ await new Promise((resolve) => setTimeout(resolve, 10));
372
680
  } finally {
373
681
  watcher.stopResultWatcher();
374
682
  }
@@ -419,9 +727,8 @@ describe("result watcher", () => {
419
727
  const ownerWatcher = createResultWatcher(owner.pi, ownerState, resultsDir, 60_000);
420
728
  const otherWatcher = createResultWatcher(other.pi, otherState, resultsDir, 60_000);
421
729
  const ownerResultPath = path.join(resultsDir, "owner-run.json");
422
- const sessionlessResultPath = path.join(resultsDir, "sessionless-run.json");
423
730
  try {
424
- fs.writeFileSync(ownerResultPath, JSON.stringify({
731
+ writeIndexedResult(ownerResultPath, {
425
732
  id: "owner-run",
426
733
  agent: "worker",
427
734
  mode: "single",
@@ -432,23 +739,12 @@ describe("result watcher", () => {
432
739
  sessionId: "session-owner",
433
740
  cwd: "/repo",
434
741
  intercomTarget: "owner-target",
435
- }), "utf-8");
436
- fs.writeFileSync(sessionlessResultPath, JSON.stringify({
437
- id: "sessionless-run",
438
- agent: "worker",
439
- mode: "single",
440
- success: true,
441
- state: "complete",
442
- summary: "legacy cwd-scoped output",
443
- results: [{ agent: "worker", output: "legacy cwd-scoped output", success: true }],
444
- cwd: "/repo",
445
- intercomTarget: "sessionless-target",
446
- }), "utf-8");
742
+ });
447
743
 
448
744
  otherWatcher.primeExistingResults();
449
- await new Promise((resolve) => setTimeout(resolve, 100));
745
+ await new Promise((resolve) => setTimeout(resolve, 10));
450
746
  ownerWatcher.primeExistingResults();
451
- await new Promise((resolve) => setTimeout(resolve, 100));
747
+ assert.equal(await waitForPredicate(() => owner.emitted.some((entry) => entry.event === "subagent:async-complete")), true);
452
748
  } finally {
453
749
  ownerWatcher.stopResultWatcher();
454
750
  otherWatcher.stopResultWatcher();
@@ -462,7 +758,6 @@ describe("result watcher", () => {
462
758
  assert.equal(other.emitted.some((entry) => entry.event === "subagent:async-complete"), false);
463
759
  assert.equal(other.emitted.some((entry) => entry.event === "subagent:result-intercom"), false);
464
760
  assert.equal(fs.existsSync(ownerResultPath), false);
465
- assert.equal(fs.existsSync(sessionlessResultPath), true);
466
761
  } finally {
467
762
  fs.rmSync(resultsDir, { recursive: true, force: true });
468
763
  }
@@ -482,6 +777,7 @@ describe("result watcher", () => {
482
777
  },
483
778
  };
484
779
  const state = createState();
780
+ state.asyncJobs.set("bad", { asyncId: "bad", asyncDir: path.join(resultsDir, "bad"), status: "running", startedAt: Date.now(), updatedAt: Date.now() });
485
781
  const watcher = createResultWatcher(pi, state, resultsDir, 60_000);
486
782
  const originalError = console.error;
487
783
  const logged: unknown[][] = [];
@@ -490,7 +786,7 @@ describe("result watcher", () => {
490
786
  };
491
787
  try {
492
788
  watcher.primeExistingResults();
493
- await new Promise((resolve) => setTimeout(resolve, 100));
789
+ await new Promise((resolve) => setTimeout(resolve, 10));
494
790
  } finally {
495
791
  console.error = originalError;
496
792
  watcher.stopResultWatcher();
@@ -590,16 +886,16 @@ describe("result watcher", () => {
590
886
  watcher.startResultWatcher();
591
887
  assert.equal(state.watcher, fakeWatcher);
592
888
  for (const id of ["missed-a", "missed-b"]) {
593
- fs.writeFileSync(path.join(resultsDir, `${id}.json`), JSON.stringify({
889
+ writeIndexedResult(path.join(resultsDir, `${id}.json`), {
594
890
  id,
595
891
  sessionId: "session-1",
596
892
  success: true,
597
893
  state: "complete",
598
894
  summary: "done",
599
- }), "utf-8");
895
+ });
600
896
  }
601
897
  scan?.();
602
- await new Promise((resolve) => setTimeout(resolve, 100));
898
+ await new Promise((resolve) => setTimeout(resolve, 10));
603
899
  } finally {
604
900
  watcher.stopResultWatcher();
605
901
  }
@@ -662,6 +958,7 @@ describe("result watcher", () => {
662
958
  });
663
959
  const originalError = console.error;
664
960
  const childSessionPath = path.join(resultsDir, "a-session.jsonl");
961
+ const resultPath = path.join(resultsDir, "async-fallback.json");
665
962
  console.error = () => {};
666
963
  try {
667
964
  watcher.startResultWatcher();
@@ -669,7 +966,7 @@ describe("result watcher", () => {
669
966
  assert.notEqual(state.watcherRestartTimer, null);
670
967
 
671
968
  fs.writeFileSync(childSessionPath, "", "utf-8");
672
- fs.writeFileSync(path.join(resultsDir, "async-fallback.json"), JSON.stringify({
969
+ writeIndexedResult(resultPath, {
673
970
  id: "async-fallback",
674
971
  runId: "run-fallback",
675
972
  agent: "parallel:a+b",
@@ -683,9 +980,9 @@ describe("result watcher", () => {
683
980
  ],
684
981
  sessionId: "session-1",
685
982
  intercomTarget: "subagent-chat-main",
686
- }), "utf-8");
983
+ });
687
984
  poll?.();
688
- await new Promise((resolve) => setTimeout(resolve, 100));
985
+ assert.equal(await waitForPredicate(() => emitted.some((entry) => entry.event === "subagent:async-complete") && !fs.existsSync(resultPath)), true);
689
986
  } finally {
690
987
  console.error = originalError;
691
988
  watcher.stopResultWatcher();
@@ -694,7 +991,7 @@ describe("result watcher", () => {
694
991
  const intercomEvents = emitted.filter((entry) => entry.event === "subagent:result-intercom");
695
992
  assert.equal(intercomEvents.length, 1);
696
993
  assert.equal(emitted.some((entry) => entry.event === "subagent:async-complete"), true);
697
- assert.equal(fs.existsSync(path.join(resultsDir, "async-fallback.json")), false);
994
+ assert.equal(fs.existsSync(resultPath), false);
698
995
  const payload = intercomEvents[0]?.data as { mode?: string; status?: string; message?: string; children?: Array<{ status?: string; summary?: string; sessionPath?: string }> };
699
996
  const completion = emitted.find((entry) => entry.event === "subagent:async-complete")?.data as { results?: Array<{ status?: string; summary?: string; sessionPath?: string }> } | undefined;
700
997
  assert.equal(payload.mode, "parallel");
@@ -764,9 +1061,9 @@ describe("result watcher", () => {
764
1061
  assert.equal(state.watcher, null);
765
1062
  assert.notEqual(state.watcherRestartTimer, null);
766
1063
 
767
- fs.writeFileSync(path.join(resultsDir, "done.json"), JSON.stringify({ sessionId: "session-1", summary: "done" }), "utf-8");
1064
+ writeIndexedResult(path.join(resultsDir, "done.json"), { sessionId: "session-1", summary: "done" });
768
1065
  poll?.();
769
- await new Promise((resolve) => setTimeout(resolve, 75));
1066
+ await new Promise((resolve) => setTimeout(resolve, 10));
770
1067
  } finally {
771
1068
  console.error = originalError;
772
1069
  watcher.stopResultWatcher();
@@ -811,7 +1108,7 @@ describe("result watcher", () => {
811
1108
  const missingSession = path.join(resultsDir, "b-session.jsonl");
812
1109
  try {
813
1110
  fs.writeFileSync(firstSession, "", "utf-8");
814
- fs.writeFileSync(path.join(resultsDir, "async-1.json"), JSON.stringify({
1111
+ writeIndexedResult(path.join(resultsDir, "async-1.json"), {
815
1112
  id: "async-1",
816
1113
  runId: "run-123",
817
1114
  agent: "parallel:a+b",
@@ -828,9 +1125,9 @@ describe("result watcher", () => {
828
1125
  asyncDir: "/tmp/async-1",
829
1126
  parallelHandoff: { version: 1, path: "/tmp/async-1/handoff.json", groupCount: 1, childCount: 2, changedPatches: 1, cleanupState: "complete" },
830
1127
  intercomTarget: "subagent-chat-main",
831
- }), "utf-8");
1128
+ });
832
1129
  watcher.primeExistingResults();
833
- await new Promise((resolve) => setTimeout(resolve, 100));
1130
+ assert.equal(await waitForPredicate(() => emitted.some((entry) => entry.event === "subagent:async-complete") && emitted.some((entry) => entry.event === "subagent:result-intercom")), true);
834
1131
  } finally {
835
1132
  watcher.stopResultWatcher();
836
1133
  }
@@ -882,7 +1179,7 @@ describe("result watcher", () => {
882
1179
  state.currentSessionId = "session-1";
883
1180
  const watcher = createResultWatcher(pi, state, resultsDir, 60_000);
884
1181
  try {
885
- fs.writeFileSync(path.join(resultsDir, "async-stopped.json"), JSON.stringify({
1182
+ writeIndexedResult(path.join(resultsDir, "async-stopped.json"), {
886
1183
  id: "async-stopped",
887
1184
  runId: "async-stopped",
888
1185
  agent: "parallel:a+b",
@@ -897,9 +1194,9 @@ describe("result watcher", () => {
897
1194
  ],
898
1195
  sessionId: "session-1",
899
1196
  intercomTarget: "subagent-chat-main",
900
- }, null, 2), "utf-8");
1197
+ });
901
1198
  watcher.primeExistingResults();
902
- await new Promise((resolve) => setTimeout(resolve, 100));
1199
+ await new Promise((resolve) => setTimeout(resolve, 10));
903
1200
  } finally {
904
1201
  watcher.stopResultWatcher();
905
1202
  }
@@ -964,7 +1261,7 @@ describe("result watcher", () => {
964
1261
  const watcher = createResultWatcher(pi, state, resultsDir, 60_000);
965
1262
  const resultPath = path.join(resultsDir, "async-nested-root.json");
966
1263
  try {
967
- fs.writeFileSync(resultPath, JSON.stringify({
1264
+ writeIndexedResult(resultPath, {
968
1265
  id: "async-nested-root",
969
1266
  runId: "async-nested-root",
970
1267
  agent: "owner",
@@ -975,9 +1272,9 @@ describe("result watcher", () => {
975
1272
  results: [{ agent: "owner", output: "owner done", success: true }],
976
1273
  sessionId: "session-1",
977
1274
  intercomTarget: "subagent-chat-main",
978
- }), "utf-8");
1275
+ });
979
1276
  watcher.primeExistingResults();
980
- await new Promise((resolve) => setTimeout(resolve, 100));
1277
+ assert.equal(await waitForPredicate(() => !fs.existsSync(resultPath) && emitted.some((entry) => entry.event === "subagent:async-complete") && emitted.some((entry) => entry.event === "subagent:result-intercom")), true);
981
1278
  } finally {
982
1279
  watcher.stopResultWatcher();
983
1280
  }
@@ -1032,7 +1329,7 @@ describe("result watcher", () => {
1032
1329
  logged.push(args);
1033
1330
  };
1034
1331
  try {
1035
- fs.writeFileSync(resultPath, JSON.stringify({
1332
+ writeIndexedResult(resultPath, {
1036
1333
  id: "async-explicit-nested",
1037
1334
  runId: "async-explicit-nested",
1038
1335
  agent: "owner",
@@ -1055,9 +1352,9 @@ describe("result watcher", () => {
1055
1352
  ],
1056
1353
  sessionId: "session-1",
1057
1354
  intercomTarget: "subagent-chat-main",
1058
- }), "utf-8");
1355
+ });
1059
1356
  watcher.primeExistingResults();
1060
- await new Promise((resolve) => setTimeout(resolve, 100));
1357
+ assert.equal(await waitForPredicate(() => !fs.existsSync(resultPath) && emitted.some((entry) => entry.event === "subagent:async-complete")), true);
1061
1358
  } finally {
1062
1359
  console.error = originalError;
1063
1360
  watcher.stopResultWatcher();
@@ -1123,7 +1420,7 @@ describe("result watcher", () => {
1123
1420
  logged.push(args);
1124
1421
  };
1125
1422
  try {
1126
- fs.writeFileSync(resultPath, JSON.stringify({
1423
+ writeIndexedResult(resultPath, {
1127
1424
  id: "async-nested-retry",
1128
1425
  runId: "async-nested-retry",
1129
1426
  agent: "owner",
@@ -1132,9 +1429,9 @@ describe("result watcher", () => {
1132
1429
  summary: "owner done",
1133
1430
  sessionId: "session-1",
1134
1431
  intercomTarget: "subagent-chat-main",
1135
- }), "utf-8");
1432
+ });
1136
1433
  watcher.primeExistingResults();
1137
- await new Promise((resolve) => setTimeout(resolve, 100));
1434
+ await new Promise((resolve) => setTimeout(resolve, 10));
1138
1435
 
1139
1436
  assert.equal(fs.existsSync(resultPath), true);
1140
1437
  assert.equal(emitted.length, 0);
@@ -1186,7 +1483,7 @@ describe("result watcher", () => {
1186
1483
  state.currentSessionId = "session-1";
1187
1484
  const watcher = createResultWatcher(pi, state, resultsDir, 60_000);
1188
1485
  try {
1189
- fs.writeFileSync(path.join(resultsDir, "async-top-session.json"), JSON.stringify({
1486
+ writeIndexedResult(path.join(resultsDir, "async-top-session.json"), {
1190
1487
  id: "async-top-session",
1191
1488
  mode: "parallel",
1192
1489
  success: false,
@@ -1198,9 +1495,9 @@ describe("result watcher", () => {
1198
1495
  sessionId: "session-1",
1199
1496
  sessionFile: "/tmp/top-session.jsonl",
1200
1497
  intercomTarget: "subagent-chat-main",
1201
- }), "utf-8");
1498
+ });
1202
1499
  watcher.primeExistingResults();
1203
- await new Promise((resolve) => setTimeout(resolve, 100));
1500
+ await new Promise((resolve) => setTimeout(resolve, 10));
1204
1501
  } finally {
1205
1502
  watcher.stopResultWatcher();
1206
1503
  }
@@ -1243,7 +1540,7 @@ describe("result watcher", () => {
1243
1540
  state.currentSessionId = "session-1";
1244
1541
  const watcher = createResultWatcher(pi, state, resultsDir, 60_000);
1245
1542
  try {
1246
- fs.writeFileSync(path.join(resultsDir, "async-paused.json"), JSON.stringify({
1543
+ writeIndexedResult(path.join(resultsDir, "async-paused.json"), {
1247
1544
  id: "async-paused",
1248
1545
  runId: "run-paused",
1249
1546
  agent: "chain:a->b",
@@ -1257,9 +1554,9 @@ describe("result watcher", () => {
1257
1554
  ],
1258
1555
  sessionId: "session-1",
1259
1556
  intercomTarget: "subagent-chat-main",
1260
- }), "utf-8");
1557
+ });
1261
1558
  watcher.primeExistingResults();
1262
- await new Promise((resolve) => setTimeout(resolve, 100));
1559
+ await new Promise((resolve) => setTimeout(resolve, 10));
1263
1560
  } finally {
1264
1561
  watcher.stopResultWatcher();
1265
1562
  }
@@ -1301,7 +1598,7 @@ describe("result watcher", () => {
1301
1598
  logged.push(args);
1302
1599
  };
1303
1600
  try {
1304
- fs.writeFileSync(path.join(resultsDir, "async-2.json"), JSON.stringify({
1601
+ writeIndexedResult(path.join(resultsDir, "async-2.json"), {
1305
1602
  id: "async-2",
1306
1603
  runId: "run-456",
1307
1604
  agent: "worker",
@@ -1310,9 +1607,9 @@ describe("result watcher", () => {
1310
1607
  summary: "Worker summary",
1311
1608
  sessionId: "session-1",
1312
1609
  intercomTarget: "orchestrator",
1313
- }), "utf-8");
1610
+ });
1314
1611
  watcher.primeExistingResults();
1315
- await new Promise((resolve) => setTimeout(resolve, 100));
1612
+ await new Promise((resolve) => setTimeout(resolve, 10));
1316
1613
  } finally {
1317
1614
  console.error = originalError;
1318
1615
  watcher.stopResultWatcher();
@@ -1354,7 +1651,7 @@ describe("result watcher", () => {
1354
1651
  const loggedWarning = () => logged.some((entry) => /Subagent async grouped result intercom delivery was not acknowledged/.test(String(entry[0] ?? "")));
1355
1652
  let warned = false;
1356
1653
  try {
1357
- fs.writeFileSync(path.join(resultsDir, "unacknowledged.json"), JSON.stringify({
1654
+ writeIndexedResult(path.join(resultsDir, "unacknowledged.json"), {
1358
1655
  id: "unacknowledged",
1359
1656
  runId: "run-unacknowledged",
1360
1657
  agent: "worker",
@@ -1363,7 +1660,7 @@ describe("result watcher", () => {
1363
1660
  summary: "Worker summary",
1364
1661
  sessionId: "session-1",
1365
1662
  intercomTarget: "orchestrator",
1366
- }), "utf-8");
1663
+ });
1367
1664
  watcher.primeExistingResults();
1368
1665
  warned = await waitForPredicate(loggedWarning);
1369
1666
  } finally {
@@ -1410,7 +1707,7 @@ describe("result watcher", () => {
1410
1707
  notifier: { async deliver(result) { delivered.push({ intercomDelivered: result.intercomDelivered }); return true; } },
1411
1708
  });
1412
1709
  try {
1413
- fs.writeFileSync(path.join(resultsDir, "acknowledged.json"), JSON.stringify({
1710
+ writeIndexedResult(path.join(resultsDir, "acknowledged.json"), {
1414
1711
  id: "acknowledged",
1415
1712
  runId: "run-acknowledged",
1416
1713
  agent: "worker",
@@ -1419,9 +1716,9 @@ describe("result watcher", () => {
1419
1716
  summary: "Worker summary",
1420
1717
  sessionId: "session-1",
1421
1718
  intercomTarget: "orchestrator",
1422
- }), "utf-8");
1719
+ });
1423
1720
  watcher.primeExistingResults();
1424
- await new Promise((resolve) => setTimeout(resolve, 100));
1721
+ await new Promise((resolve) => setTimeout(resolve, 10));
1425
1722
  } finally {
1426
1723
  watcher.stopResultWatcher();
1427
1724
  }
@@ -1444,11 +1741,11 @@ describe("result watcher", () => {
1444
1741
  const resultFile = "expired.json";
1445
1742
  const result = { sessionId: "session-1", agent: "worker", success: true, summary: "new result", timestamp: 123 };
1446
1743
  const resultPath = path.join(resultsDir, resultFile);
1447
- fs.writeFileSync(resultPath, JSON.stringify(result), "utf-8");
1744
+ writeIndexedResult(resultPath, result);
1448
1745
  state.completionSeen.set(buildCompletionKey(result, `result:${resultFile}`), Date.now() - 61_000);
1449
1746
  try {
1450
1747
  watcher.primeExistingResults();
1451
- await new Promise((resolve) => setTimeout(resolve, 75));
1748
+ await new Promise((resolve) => setTimeout(resolve, 10));
1452
1749
  } finally {
1453
1750
  watcher.stopResultWatcher();
1454
1751
  }
@@ -1471,10 +1768,10 @@ describe("result watcher", () => {
1471
1768
  notifier: { async deliver() { attempts += 1; return attempts > 1; } },
1472
1769
  });
1473
1770
  const resultPath = path.join(resultsDir, "retry.json");
1474
- fs.writeFileSync(resultPath, JSON.stringify({ id: "retry", sessionId: "session-1", agent: "worker", success: true, summary: "done", timestamp: 1 }), "utf-8");
1771
+ writeIndexedResult(resultPath, { id: "retry", sessionId: "session-1", agent: "worker", success: true, summary: "done", timestamp: 1 });
1475
1772
  try {
1476
1773
  watcher.primeExistingResults();
1477
- await new Promise((resolve) => setTimeout(resolve, 50));
1774
+ await new Promise((resolve) => setTimeout(resolve, 10));
1478
1775
  assert.equal(fs.existsSync(resultPath), true);
1479
1776
  await new Promise((resolve) => setTimeout(resolve, 180));
1480
1777
  } finally {
@@ -1499,12 +1796,12 @@ describe("result watcher", () => {
1499
1796
  notifier: { deliver: () => new Promise<boolean>((resolve) => { accept = resolve; }) },
1500
1797
  });
1501
1798
  const resultPath = path.join(resultsDir, "stale.json");
1502
- fs.writeFileSync(resultPath, JSON.stringify({ id: "stale", sessionId: "session-1", agent: "worker", success: true, summary: "done", timestamp: 1 }), "utf-8");
1799
+ writeIndexedResult(resultPath, { id: "stale", sessionId: "session-1", agent: "worker", success: true, summary: "done", timestamp: 1 });
1503
1800
  watcher.primeExistingResults();
1504
- await new Promise((resolve) => setTimeout(resolve, 50));
1801
+ await new Promise((resolve) => setTimeout(resolve, 10));
1505
1802
  watcher.stopResultWatcher();
1506
1803
  accept(true);
1507
- await new Promise((resolve) => setTimeout(resolve, 50));
1804
+ await new Promise((resolve) => setTimeout(resolve, 10));
1508
1805
  assert.equal(fs.existsSync(resultPath), true);
1509
1806
  assert.deepEqual(emitted, []);
1510
1807
  } finally {
@@ -1522,10 +1819,10 @@ describe("result watcher", () => {
1522
1819
  const watcher = createResultWatcher({ events: { on: () => () => {}, emit(event) { emitted.push(event); } } }, state, resultsDir, 60_000, {
1523
1820
  notifier: { async deliver(result) { delivered.push({ triggerTurn: result.triggerTurn }); return true; } },
1524
1821
  });
1525
- fs.writeFileSync(path.join(resultsDir, "quiet.json"), JSON.stringify({ id: "quiet", sessionId: "session-1", agent: "worker", success: true, summary: "done", timestamp: 1, intercomTarget: "host" }), "utf-8");
1822
+ writeIndexedResult(path.join(resultsDir, "quiet.json"), { id: "quiet", sessionId: "session-1", agent: "worker", success: true, summary: "done", timestamp: 1, intercomTarget: "host" });
1526
1823
  try {
1527
1824
  watcher.primeExistingResults({ triggerTurn: false });
1528
- await new Promise((resolve) => setTimeout(resolve, 75));
1825
+ await new Promise((resolve) => setTimeout(resolve, 10));
1529
1826
  } finally {
1530
1827
  watcher.stopResultWatcher();
1531
1828
  }