@selesai/code 0.8.5 → 0.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/core/agent-session.js +13 -1
  3. package/dist/core/extensions/types.d.ts +2 -0
  4. package/dist/core/provider-composer.js +17 -4
  5. package/dist/core/settings-manager.d.ts +2 -0
  6. package/dist/core/settings-manager.js +3 -0
  7. package/dist/core/system-prompt.js +2 -8
  8. package/dist/core/system-prompt.test.js +6 -0
  9. package/dist/core/tools/index.d.ts +1 -0
  10. package/dist/core/tools/index.js +1 -0
  11. package/dist/core/tools/schema-prune.d.ts +16 -0
  12. package/dist/core/tools/schema-prune.js +39 -0
  13. package/dist/core/tools/schema-prune.test.d.ts +1 -0
  14. package/dist/core/tools/schema-prune.test.js +111 -0
  15. package/dist/defaults/models.json +5 -0
  16. package/dist/defaults/settings.json +1 -0
  17. package/dist/extensions/agent-browser.test.ts +178 -2
  18. package/dist/extensions/agent-browser.ts +2 -0
  19. package/dist/extensions/copy-turn.test.ts +229 -0
  20. package/dist/extensions/copy-turn.ts +11 -0
  21. package/dist/extensions/grep-app/index.test.ts +356 -1
  22. package/dist/extensions/grep-app/index.ts +6 -1
  23. package/dist/extensions/handoff-new.test.ts +351 -161
  24. package/dist/extensions/handoff-new.ts +3 -0
  25. package/dist/extensions/inline-skills.test.ts +93 -0
  26. package/dist/extensions/inline-skills.ts +3 -0
  27. package/dist/extensions/model-prompt-injector/config.json +10 -0
  28. package/dist/extensions/model-prompt-injector/index.test.ts +112 -0
  29. package/dist/extensions/model-prompt-injector/index.ts +151 -0
  30. package/dist/extensions/package.json +1 -0
  31. package/dist/extensions/pi-subagents/CHANGELOG.md +158 -90
  32. package/dist/extensions/pi-subagents/UPSTREAM-V0.50-MAPPING.md +172 -0
  33. package/dist/extensions/pi-subagents/agents/builder.md +5 -2
  34. package/dist/extensions/pi-subagents/agents/commentator.md +1 -1
  35. package/dist/extensions/pi-subagents/agents/oracle.md +7 -5
  36. package/dist/extensions/pi-subagents/agents/reviewer.md +2 -2
  37. package/dist/extensions/pi-subagents/agents/scout.md +1 -1
  38. package/dist/extensions/pi-subagents/agents/worker.md +1 -1
  39. package/dist/extensions/pi-subagents/docs/configuration.md +78 -11
  40. package/dist/extensions/pi-subagents/docs/extension-api.md +36 -0
  41. package/dist/extensions/pi-subagents/docs/missions.md +5 -3
  42. package/dist/extensions/pi-subagents/docs/models.md +1 -1
  43. package/dist/extensions/pi-subagents/docs/observability.md +42 -2
  44. package/dist/extensions/pi-subagents/docs/tool-reference.md +20 -3
  45. package/dist/extensions/pi-subagents/docs/workflows.md +4 -4
  46. package/dist/extensions/pi-subagents/install.mjs +2 -2
  47. package/dist/extensions/pi-subagents/package-lock.json +1470 -1472
  48. package/dist/extensions/pi-subagents/package.json +1 -1
  49. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +1 -1
  50. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  51. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +16 -15
  52. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  53. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  54. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
  55. package/dist/extensions/pi-subagents/src/agents/agents.ts +37 -12
  56. package/dist/extensions/pi-subagents/src/api/external-runs.ts +174 -84
  57. package/dist/extensions/pi-subagents/src/api/preflight.ts +13 -7
  58. package/dist/extensions/pi-subagents/src/extension/config.ts +59 -0
  59. package/dist/extensions/pi-subagents/src/extension/index.ts +146 -23
  60. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +34 -6
  61. package/dist/extensions/pi-subagents/src/extension/rpc.ts +5 -1
  62. package/dist/extensions/pi-subagents/src/extension/schemas.ts +31 -30
  63. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +10 -9
  64. package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +13 -8
  65. package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +16 -3
  66. package/dist/extensions/pi-subagents/src/inspectors/herdr/project-panes.ts +2 -6
  67. package/dist/extensions/pi-subagents/src/inspectors/herdr/shell-command.ts +16 -0
  68. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +5 -4
  69. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +19 -42
  70. package/dist/extensions/pi-subagents/src/missions/goal-driver.ts +3 -1
  71. package/dist/extensions/pi-subagents/src/missions/store.ts +8 -3
  72. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +82 -25
  73. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +71 -1
  74. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +73 -43
  75. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +17 -6
  76. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -6
  77. package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +277 -0
  78. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +11 -3
  79. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +2 -2
  80. package/dist/extensions/pi-subagents/src/runs/background/completion-replay.ts +11 -1
  81. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +21 -6
  82. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +5 -2
  83. package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +437 -0
  84. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +224 -42
  85. package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +27 -7
  86. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +76 -19
  87. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +35 -26
  88. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +108 -9
  89. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +54 -28
  90. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +27 -13
  91. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +298 -33
  92. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +2 -0
  93. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +5 -2
  94. package/dist/extensions/pi-subagents/src/runs/background/wait-subscriptions.ts +2 -1
  95. package/dist/extensions/pi-subagents/src/runs/foreground/async-dismiss-action.ts +2 -1
  96. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +16 -0
  97. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +219 -15
  98. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +7 -4
  99. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +4 -3
  100. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +368 -50
  101. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +107 -1
  102. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +4 -0
  103. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +39 -23
  104. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +16 -2
  105. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +66 -62
  106. package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +376 -0
  107. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
  108. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +15 -0
  109. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +1 -9
  110. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +12 -0
  111. package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +95 -0
  112. package/dist/extensions/pi-subagents/src/shared/agent-stream-options.ts +5 -0
  113. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +0 -4
  114. package/dist/extensions/pi-subagents/src/shared/display-text.ts +50 -0
  115. package/dist/extensions/pi-subagents/src/shared/node-executable.ts +21 -0
  116. package/dist/extensions/pi-subagents/src/shared/session-lineage.ts +71 -0
  117. package/dist/extensions/pi-subagents/src/shared/types.ts +66 -4
  118. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +34 -25
  119. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +4 -0
  120. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +160 -45
  121. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +1 -48
  122. package/dist/extensions/pi-subagents/src/tui/fleet.ts +129 -17
  123. package/dist/extensions/pi-subagents/src/tui/render.ts +122 -44
  124. package/dist/extensions/pi-subagents/src/watchdog/permission-arbiter.ts +2 -1
  125. package/dist/extensions/pi-subagents/src/watchdog/review.ts +4 -3
  126. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +10 -2
  127. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +272 -76
  128. package/dist/extensions/pi-subagents/src/workflows/workflow-auto-relaunch.ts +28 -0
  129. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +2 -0
  130. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2 -2
  131. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +68 -9
  132. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +27 -1
  133. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1 -1
  134. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +49 -0
  135. package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
  136. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +26 -1
  137. package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +144 -0
  138. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +26 -0
  139. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +369 -98
  140. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +111 -78
  141. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +49 -1
  142. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +12 -0
  143. package/dist/extensions/pi-subagents/test/support/node-command.ts +15 -0
  144. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +50 -0
  145. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +28 -0
  146. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +61 -0
  147. package/dist/extensions/pi-subagents/test/unit/agent-stream-options.test.ts +14 -0
  148. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +76 -0
  149. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
  150. package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +162 -0
  151. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +209 -10
  152. package/dist/extensions/pi-subagents/test/unit/completion-replay.test.ts +41 -1
  153. package/dist/extensions/pi-subagents/test/unit/external-runs.test.ts +135 -42
  154. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +252 -1
  155. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +192 -6
  156. package/dist/extensions/pi-subagents/test/unit/handoff-adoption.test.ts +103 -0
  157. package/dist/extensions/pi-subagents/test/unit/herdr-inspector-bootstrap.test.ts +32 -0
  158. package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +59 -0
  159. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +91 -0
  160. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +23 -4
  161. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +56 -1
  162. package/dist/extensions/pi-subagents/test/unit/mission-goal-driver.test.ts +67 -3
  163. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
  164. package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +14 -0
  165. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +18 -0
  166. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +2 -2
  167. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +25 -2
  168. package/dist/extensions/pi-subagents/test/unit/node-executable.test.ts +26 -0
  169. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +8 -0
  170. package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +348 -0
  171. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +5 -2
  172. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +1 -1
  173. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +34 -0
  174. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +6 -1
  175. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +92 -0
  176. package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +175 -0
  177. package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +127 -17
  178. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +44 -0
  179. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +247 -0
  180. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +107 -2
  181. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +5 -8
  182. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +456 -4
  183. package/dist/extensions/pi-subagents/test/unit/session-lineage.test.ts +73 -0
  184. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +128 -0
  185. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +28 -0
  186. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +16 -7
  187. package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +15 -0
  188. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +79 -15
  189. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +12 -12
  190. package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +109 -0
  191. package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +34 -0
  192. package/dist/extensions/pi-subagents/test/unit/workflow-auto-relaunch.test.ts +45 -0
  193. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +94 -1
  194. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +42 -2
  195. package/dist/extensions/ponytail/index.js +11 -0
  196. package/dist/extensions/ponytail/ponytail-config.cjs +2 -0
  197. package/dist/extensions/ponytail/ponytail-instructions.cjs +6 -0
  198. package/dist/extensions/ponytail/test/extension.test.js +274 -140
  199. package/dist/extensions/ponytail/test/helpers.test.js +280 -92
  200. package/dist/extensions/question/index.ts +33 -0
  201. package/dist/extensions/question/question-list.ts +28 -0
  202. package/dist/extensions/question/row-layout.ts +3 -0
  203. package/dist/extensions/question/tests/batch.test.ts +103 -65
  204. package/dist/extensions/question/tests/exp.test.ts +70 -0
  205. package/dist/extensions/question/tests/exp2.test.ts +59 -0
  206. package/dist/extensions/question/tests/exp3.test.ts +47 -0
  207. package/dist/extensions/question/tests/helpers.test.ts +75 -35
  208. package/dist/extensions/question/tests/question-list.test.ts +640 -198
  209. package/dist/extensions/question/tests/row-layout.test.ts +193 -114
  210. package/dist/extensions/question/tests/schemas.test.ts +42 -19
  211. package/dist/extensions/question/tests/selection-mode.test.ts +54 -0
  212. package/dist/extensions/question/tests/shortcuts.test.ts +106 -84
  213. package/dist/extensions/question/tests/wizard.test.ts +771 -50
  214. package/dist/extensions/question/tests/zz-ig3.test.ts +20 -0
  215. package/dist/extensions/question/tests/zz-probe.test.ts +29 -0
  216. package/dist/extensions/rtk.test.ts +180 -1
  217. package/dist/extensions/tokenin-onboarding.ts +3 -0
  218. package/dist/extensions/undo.test.ts +720 -0
  219. package/dist/extensions/undo.ts +6 -0
  220. package/dist/extensions/web-agent-onboarding.test.ts +415 -0
  221. package/dist/extensions/workflow/extension.ts +3 -3
  222. package/dist/extensions/workflow/modes.ts +41 -19
  223. package/dist/skills/pi-subagents/SKILL.md +1 -1
  224. package/dist/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  225. package/dist/skills/pi-subagents/references/execution-controls.md +16 -15
  226. package/dist/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  227. package/dist/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  228. package/docs/plans/workflow-autoloop-reference.md +1 -2
  229. package/docs/plans/workflow-handoff-carryover.md +108 -0
  230. package/docs/settings.md +6 -0
  231. package/package.json +4 -1
@@ -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,32 +104,22 @@ 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 });
@@ -133,13 +143,13 @@ describe("result watcher", () => {
133
143
  unref() {},
134
144
  } as fs.FSWatcher;
135
145
  const writeResult = (id: string, sessionId: string) => {
136
- fs.writeFileSync(path.join(resultsDir, `${id}.json`), JSON.stringify({
146
+ writeIndexedResult(path.join(resultsDir, `${id}.json`), {
137
147
  id,
138
148
  results: [{ structuredOutput: { sessionId: "nested-not-owner" } }],
139
149
  sessionId,
140
150
  success: true,
141
151
  summary: "done",
142
- }), "utf-8");
152
+ });
143
153
  };
144
154
  writeResult("startup-current", "session-current");
145
155
  writeResult("startup-stale", "session-stale");
@@ -224,7 +234,7 @@ describe("result watcher", () => {
224
234
  const binding = prepareMissionLaunch({
225
235
  params: { mission: { title: "Workflow mission" }, task: "Run async child" },
226
236
  projectRoot: project,
227
- config: { globalIndexDir: path.join(root, "global-index") },
237
+ config: { directory: path.join(root, "missions"), globalIndexDir: path.join(root, "global-index") },
228
238
  ownerSessionId: "session-current",
229
239
  });
230
240
  assert.ok(binding);
@@ -239,7 +249,7 @@ describe("result watcher", () => {
239
249
  heartbeat: { status: "running" },
240
250
  }],
241
251
  });
242
- fs.writeFileSync(path.join(resultsDir, "async-child.json"), JSON.stringify({
252
+ writeIndexedResult(path.join(resultsDir, "async-child.json"), {
243
253
  id: "async-child",
244
254
  runId: "async-child",
245
255
  sessionId: "session-current",
@@ -251,7 +261,7 @@ describe("result watcher", () => {
251
261
  parentWorkflowRunId: "workflow-1",
252
262
  workflowKey: "background",
253
263
  results: [{ agent: "worker", success: true, output: "done", artifactPaths: { outputPath } }],
254
- }), "utf-8");
264
+ });
255
265
  const state = createState();
256
266
  state.currentSessionId = "session-current";
257
267
  const watcher = createResultWatcher({ events: { on: () => () => {}, emit() {} } }, state, resultsDir, 60_000, {
@@ -259,7 +269,7 @@ describe("result watcher", () => {
259
269
  });
260
270
  try {
261
271
  watcher.primeExistingResults();
262
- await new Promise((resolve) => setTimeout(resolve, 100));
272
+ await new Promise((resolve) => setTimeout(resolve, 10));
263
273
  } finally {
264
274
  watcher.stopResultWatcher();
265
275
  }
@@ -279,6 +289,91 @@ describe("result watcher", () => {
279
289
  }
280
290
  });
281
291
 
292
+ it("keeps result files until failed completion observers retry successfully", async () => {
293
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-observer-retry-"));
294
+ const originalError = console.error;
295
+ try {
296
+ console.error = () => {};
297
+ const resultPath = path.join(resultsDir, "observer-retry.json");
298
+ writeIndexedResult(resultPath, { id: "observer-retry", runId: "observer-retry", sessionId: "session-current", success: true, summary: "done" });
299
+ const state = createState();
300
+ state.currentSessionId = "session-current";
301
+ let observerCalls = 0;
302
+ let deliveries = 0;
303
+ const watcher = createResultWatcher({ events: { on: () => () => {}, emit() {} } }, state, resultsDir, 60_000, {
304
+ observeCompletion: () => {
305
+ observerCalls += 1;
306
+ if (observerCalls === 1) throw new Error("observer unavailable");
307
+ },
308
+ notifier: { deliver: async () => { deliveries += 1; return true; } },
309
+ });
310
+ try {
311
+ watcher.primeExistingResults();
312
+ assert.equal(await waitForPredicate(() => !fs.existsSync(resultPath) && observerCalls >= 2), true);
313
+ } finally {
314
+ watcher.stopResultWatcher();
315
+ }
316
+
317
+ assert.equal(observerCalls, 2);
318
+ assert.equal(deliveries, 1);
319
+ } finally {
320
+ console.error = originalError;
321
+ fs.rmSync(resultsDir, { recursive: true, force: true });
322
+ }
323
+ });
324
+
325
+ it("does not redeliver user notification after reload while observer retry is pending", async () => {
326
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-observer-reload-"));
327
+ const originalError = console.error;
328
+ try {
329
+ console.error = () => {};
330
+ const resultPath = path.join(resultsDir, "observer-reload.json");
331
+ writeIndexedResult(resultPath, { id: "observer-reload", runId: "observer-reload", sessionId: "session-current", success: true, summary: "done" });
332
+ let observerCalls = 0;
333
+ let deliveries = 0;
334
+ let emitted = 0;
335
+ const pi = { events: { on: () => () => {}, emit: () => { emitted += 1; } } };
336
+ const firstState = createState();
337
+ firstState.currentSessionId = "session-current";
338
+ const firstWatcher = createResultWatcher(pi, firstState, resultsDir, 60_000, {
339
+ observeCompletion: () => {
340
+ observerCalls += 1;
341
+ throw new Error("observer unavailable");
342
+ },
343
+ notifier: { deliver: async () => { deliveries += 1; return true; } },
344
+ });
345
+ try {
346
+ firstWatcher.primeExistingResults();
347
+ assert.equal(await waitForPredicate(() => {
348
+ if (deliveries !== 1 || !fs.existsSync(resultPath)) return false;
349
+ return typeof (JSON.parse(fs.readFileSync(resultPath, "utf-8")) as { notificationDeliveredAt?: unknown }).notificationDeliveredAt === "number";
350
+ }), true);
351
+ } finally {
352
+ firstWatcher.stopResultWatcher();
353
+ }
354
+
355
+ const secondState = createState();
356
+ secondState.currentSessionId = "session-current";
357
+ const secondWatcher = createResultWatcher(pi, secondState, resultsDir, 60_000, {
358
+ observeCompletion: () => { observerCalls += 1; },
359
+ notifier: { deliver: async () => { deliveries += 1; return true; } },
360
+ });
361
+ try {
362
+ secondWatcher.primeExistingResults();
363
+ assert.equal(await waitForPredicate(() => !fs.existsSync(resultPath)), true);
364
+ } finally {
365
+ secondWatcher.stopResultWatcher();
366
+ }
367
+
368
+ assert.equal(observerCalls, 2);
369
+ assert.equal(deliveries, 1);
370
+ assert.equal(emitted, 1);
371
+ } finally {
372
+ console.error = originalError;
373
+ fs.rmSync(resultsDir, { recursive: true, force: true });
374
+ }
375
+ });
376
+
282
377
  it("observes retained-project completions without changing active-session delivery", async () => {
283
378
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-scheduled-"));
284
379
  const resultsDir = path.join(root, "results");
@@ -308,7 +403,7 @@ describe("result watcher", () => {
308
403
  const state = createState();
309
404
  state.currentSessionId = "session-b";
310
405
  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");
406
+ writeIndexedResult(resultPath, { id: "scheduled-a", sessionId: "session-a", success: true, summary: "done" });
312
407
  const watcher = createResultWatcher({
313
408
  events: {
314
409
  on: () => () => {},
@@ -316,12 +411,13 @@ describe("result watcher", () => {
316
411
  },
317
412
  }, state, resultsDir, 60_000, {
318
413
  observeCompletion: (result) => manager.handleAsyncCompletion(result),
414
+ observedCompletionRunIds: () => manager.observedCompletionRunIds(),
319
415
  notifier: { deliver: async () => assert.fail("inactive-session completion must not reach the live notifier") },
320
416
  });
321
417
  try {
322
418
  watcher.startResultWatcher();
323
419
  watcher.primeExistingResults();
324
- await new Promise((resolve) => setTimeout(resolve, 100));
420
+ await new Promise((resolve) => setTimeout(resolve, 10));
325
421
  } finally {
326
422
  watcher.stopResultWatcher();
327
423
  }
@@ -337,6 +433,192 @@ describe("result watcher", () => {
337
433
  }
338
434
  });
339
435
 
436
+ it("delivers indexed pending results during reload when public promotion is blocked", async () => {
437
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-pending-index-"));
438
+ const originalError = console.error;
439
+ try {
440
+ console.error = () => {};
441
+ const emitted: Array<{ event: string; data: unknown }> = [];
442
+ const delivered: unknown[] = [];
443
+ const state = createState();
444
+ state.currentSessionId = "session-current";
445
+ const resultPath = path.join(resultsDir, "pending-run.json");
446
+ fs.mkdirSync(resultPath, { recursive: true });
447
+ writePendingAsyncResultFile(resultPath, {
448
+ id: "pending-run",
449
+ runId: "pending-run",
450
+ sessionId: "session-current",
451
+ success: true,
452
+ state: "complete",
453
+ summary: "done from pending",
454
+ });
455
+ const watcher = createResultWatcher({
456
+ events: {
457
+ on: () => () => {},
458
+ emit(event: string, data: unknown) { emitted.push({ event, data }); },
459
+ },
460
+ }, state, resultsDir, 60_000, {
461
+ deliverIntercomResults: false,
462
+ notifier: { deliver: async (result) => { delivered.push(result); return true; } },
463
+ });
464
+ try {
465
+ watcher.primeExistingResults();
466
+ assert.equal(await waitForPredicate(() => emitted.some((entry) => entry.event === "subagent:async-complete")), true);
467
+ } finally {
468
+ watcher.stopResultWatcher();
469
+ }
470
+
471
+ assert.equal((delivered[0] as { summary?: string } | undefined)?.summary, "done from pending");
472
+ assert.equal(fs.existsSync(pendingResultPath(resultsDir, "session-current", "pending-run")), false);
473
+ assert.equal(fs.statSync(resultPath).isDirectory(), true);
474
+ } finally {
475
+ console.error = originalError;
476
+ fs.rmSync(resultsDir, { recursive: true, force: true });
477
+ }
478
+ });
479
+
480
+ it("uses indexed result files and ignores unindexed stale files during reload", async () => {
481
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-index-"));
482
+ try {
483
+ for (let i = 0; i < 300; i += 1) {
484
+ fs.writeFileSync(path.join(resultsDir, `stale-${i}.json`), JSON.stringify({ id: `stale-${i}`, sessionId: "session-stale", success: true, summary: "done" }), "utf-8");
485
+ }
486
+ writeIndexedResult(path.join(resultsDir, "current.json"), { id: "current", runId: "current", sessionId: "session-current", success: true, summary: "done" });
487
+ const parsedIds: string[] = [];
488
+ const state = createState();
489
+ state.currentSessionId = "session-current";
490
+ const watcher = createResultWatcher({ events: { on: () => () => {}, emit() {} } }, state, resultsDir, 60_000, {
491
+ deliverIntercomResults: false,
492
+ parseResult(raw) {
493
+ const parsed = JSON.parse(raw) as { id: string };
494
+ parsedIds.push(parsed.id);
495
+ return parsed;
496
+ },
497
+ });
498
+ try {
499
+ watcher.primeExistingResults();
500
+ assert.equal(await waitForPredicate(() => !fs.existsSync(path.join(resultsDir, "current.json"))), true);
501
+ } finally {
502
+ watcher.stopResultWatcher();
503
+ }
504
+ assert.deepEqual(parsedIds, ["current"]);
505
+ assert.equal(fs.existsSync(path.join(resultsDir, "stale-0.json")), true);
506
+ } finally {
507
+ fs.rmSync(resultsDir, { recursive: true, force: true });
508
+ }
509
+ });
510
+
511
+ it("includes observer-known run ids when current-session results are also indexed", async () => {
512
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-mixed-observer-"));
513
+ try {
514
+ const resultsDir = path.join(root, "results");
515
+ writeIndexedResult(path.join(resultsDir, "current.json"), { id: "current", runId: "current", sessionId: "session-current", success: true, summary: "current" });
516
+ writeIndexedResult(path.join(resultsDir, "scheduled-a.json"), { id: "scheduled-a", runId: "scheduled-a", sessionId: "session-a", success: true, summary: "scheduled" });
517
+ let observations = 0;
518
+ const state = createState();
519
+ state.currentSessionId = "session-current";
520
+ const watcher = createResultWatcher({ events: { on: () => () => {}, emit() {} } }, state, resultsDir, 60_000, {
521
+ observedCompletionRunIds: () => ["scheduled-a"],
522
+ observeCompletion: (result) => { if (result.runId === "scheduled-a") observations += 1; },
523
+ notifier: { deliver: async () => true },
524
+ });
525
+ try {
526
+ watcher.primeExistingResults();
527
+ assert.equal(await waitForPredicate(() => observations === 1), true);
528
+ assert.equal(await waitForPredicate(() => !fs.existsSync(path.join(resultsDir, "current.json"))), true);
529
+ } finally {
530
+ watcher.stopResultWatcher();
531
+ }
532
+ } finally {
533
+ fs.rmSync(root, { recursive: true, force: true });
534
+ }
535
+ });
536
+
537
+ it("delivers observed indexed pending results when public promotion is blocked", async () => {
538
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-observed-pending-"));
539
+ const originalError = console.error;
540
+ try {
541
+ console.error = () => {};
542
+ for (let i = 0; i < 300; i += 1) {
543
+ fs.writeFileSync(path.join(resultsDir, `stale-${i}.json`), JSON.stringify({ id: `stale-${i}`, sessionId: "session-stale", success: true, summary: "done" }), "utf-8");
544
+ }
545
+ const resultPath = path.join(resultsDir, "scheduled-pending.json");
546
+ fs.mkdirSync(resultPath, { recursive: true });
547
+ writePendingAsyncResultFile(resultPath, {
548
+ id: "scheduled-pending",
549
+ runId: "scheduled-pending",
550
+ sessionId: "session-a",
551
+ success: true,
552
+ state: "complete",
553
+ summary: "scheduled pending done",
554
+ });
555
+ let observations = 0;
556
+ const state = createState();
557
+ state.currentSessionId = "session-b";
558
+ const watcher = createResultWatcher({ events: { on: () => () => {}, emit() {} } }, state, resultsDir, 60_000, {
559
+ observedCompletionRunIds: () => ["scheduled-pending"],
560
+ observeCompletion: (result) => {
561
+ if (result.runId === "scheduled-pending") observations += 1;
562
+ },
563
+ notifier: { deliver: async () => assert.fail("observer-owned completion must not reach active delivery") },
564
+ });
565
+ try {
566
+ watcher.primeExistingResults();
567
+ assert.equal(await waitForPredicate(() => observations === 1), true);
568
+ } finally {
569
+ watcher.stopResultWatcher();
570
+ }
571
+
572
+ assert.equal(fs.existsSync(pendingResultPath(resultsDir, "session-a", "scheduled-pending")), true);
573
+ assert.equal(fs.statSync(resultPath).isDirectory(), true);
574
+ assert.equal(fs.existsSync(path.join(resultsDir, "stale-0.json")), true);
575
+ } finally {
576
+ console.error = originalError;
577
+ fs.rmSync(resultsDir, { recursive: true, force: true });
578
+ }
579
+ });
580
+
581
+ it("uses observed run ids directly without scanning a stale result pile", async () => {
582
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-observed-index-"));
583
+ try {
584
+ for (let i = 0; i < 300; i += 1) {
585
+ fs.writeFileSync(path.join(resultsDir, `stale-${i}.json`), JSON.stringify({ id: `stale-${i}`, sessionId: "session-stale", success: true, summary: "done" }), "utf-8");
586
+ }
587
+ const resultPath = path.join(resultsDir, "scheduled-a.json");
588
+ writeIndexedResult(resultPath, { id: "scheduled-a", runId: "scheduled-a", sessionId: "session-a", success: true, summary: "done" });
589
+ let observations = 0;
590
+ let observed = true;
591
+ const makeWatcher = () => {
592
+ const state = createState();
593
+ state.currentSessionId = "session-b";
594
+ return createResultWatcher({ events: { on: () => () => {}, emit() {} } }, state, resultsDir, 60_000, {
595
+ observedCompletionRunIds: () => observed ? ["scheduled-a"] : [],
596
+ observeCompletion: () => { observations += 1; observed = false; },
597
+ notifier: { deliver: async () => assert.fail("observer-owned completion must not reach active delivery") },
598
+ });
599
+ };
600
+ let watcher = makeWatcher();
601
+ try {
602
+ watcher.primeExistingResults();
603
+ await new Promise((resolve) => setTimeout(resolve, 10));
604
+ } finally {
605
+ watcher.stopResultWatcher();
606
+ }
607
+ watcher = makeWatcher();
608
+ try {
609
+ watcher.primeExistingResults();
610
+ await new Promise((resolve) => setTimeout(resolve, 10));
611
+ } finally {
612
+ watcher.stopResultWatcher();
613
+ }
614
+ assert.equal(observations, 1);
615
+ assert.equal(fs.existsSync(resultPath), true);
616
+ assert.equal(fs.existsSync(path.join(resultsDir, "stale-0.json")), true);
617
+ } finally {
618
+ fs.rmSync(resultsDir, { recursive: true, force: true });
619
+ }
620
+ });
621
+
340
622
  it("uses native completion delivery without attempting external grouped intercom when disabled", async () => {
341
623
  const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-native-delivery-"));
342
624
  try {
@@ -351,7 +633,7 @@ describe("result watcher", () => {
351
633
  const state = createState();
352
634
  state.currentSessionId = "session-native";
353
635
  const resultPath = path.join(resultsDir, "native-run.json");
354
- fs.writeFileSync(resultPath, JSON.stringify({
636
+ writeIndexedResult(resultPath, {
355
637
  id: "native-run",
356
638
  runId: "native-run",
357
639
  sessionId: "session-native",
@@ -361,14 +643,14 @@ describe("result watcher", () => {
361
643
  summary: "Subagent process terminated by signal SIGTERM.",
362
644
  results: [{ agent: "worker", output: "", success: false, exitCode: 1, processSignal: "SIGTERM" }],
363
645
  intercomTarget: "native-parent",
364
- }), "utf-8");
646
+ });
365
647
  const watcher = createResultWatcher(pi, state, resultsDir, 60_000, {
366
648
  deliverIntercomResults: false,
367
649
  notifier: { deliver: async (result) => { delivered.push(result); return true; } },
368
650
  });
369
651
  try {
370
652
  watcher.primeExistingResults();
371
- await new Promise((resolve) => setTimeout(resolve, 100));
653
+ await new Promise((resolve) => setTimeout(resolve, 10));
372
654
  } finally {
373
655
  watcher.stopResultWatcher();
374
656
  }
@@ -419,9 +701,8 @@ describe("result watcher", () => {
419
701
  const ownerWatcher = createResultWatcher(owner.pi, ownerState, resultsDir, 60_000);
420
702
  const otherWatcher = createResultWatcher(other.pi, otherState, resultsDir, 60_000);
421
703
  const ownerResultPath = path.join(resultsDir, "owner-run.json");
422
- const sessionlessResultPath = path.join(resultsDir, "sessionless-run.json");
423
704
  try {
424
- fs.writeFileSync(ownerResultPath, JSON.stringify({
705
+ writeIndexedResult(ownerResultPath, {
425
706
  id: "owner-run",
426
707
  agent: "worker",
427
708
  mode: "single",
@@ -432,23 +713,12 @@ describe("result watcher", () => {
432
713
  sessionId: "session-owner",
433
714
  cwd: "/repo",
434
715
  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");
716
+ });
447
717
 
448
718
  otherWatcher.primeExistingResults();
449
- await new Promise((resolve) => setTimeout(resolve, 100));
719
+ await new Promise((resolve) => setTimeout(resolve, 10));
450
720
  ownerWatcher.primeExistingResults();
451
- await new Promise((resolve) => setTimeout(resolve, 100));
721
+ assert.equal(await waitForPredicate(() => owner.emitted.some((entry) => entry.event === "subagent:async-complete")), true);
452
722
  } finally {
453
723
  ownerWatcher.stopResultWatcher();
454
724
  otherWatcher.stopResultWatcher();
@@ -462,7 +732,6 @@ describe("result watcher", () => {
462
732
  assert.equal(other.emitted.some((entry) => entry.event === "subagent:async-complete"), false);
463
733
  assert.equal(other.emitted.some((entry) => entry.event === "subagent:result-intercom"), false);
464
734
  assert.equal(fs.existsSync(ownerResultPath), false);
465
- assert.equal(fs.existsSync(sessionlessResultPath), true);
466
735
  } finally {
467
736
  fs.rmSync(resultsDir, { recursive: true, force: true });
468
737
  }
@@ -482,6 +751,7 @@ describe("result watcher", () => {
482
751
  },
483
752
  };
484
753
  const state = createState();
754
+ state.asyncJobs.set("bad", { asyncId: "bad", asyncDir: path.join(resultsDir, "bad"), status: "running", startedAt: Date.now(), updatedAt: Date.now() });
485
755
  const watcher = createResultWatcher(pi, state, resultsDir, 60_000);
486
756
  const originalError = console.error;
487
757
  const logged: unknown[][] = [];
@@ -490,7 +760,7 @@ describe("result watcher", () => {
490
760
  };
491
761
  try {
492
762
  watcher.primeExistingResults();
493
- await new Promise((resolve) => setTimeout(resolve, 100));
763
+ await new Promise((resolve) => setTimeout(resolve, 10));
494
764
  } finally {
495
765
  console.error = originalError;
496
766
  watcher.stopResultWatcher();
@@ -590,16 +860,16 @@ describe("result watcher", () => {
590
860
  watcher.startResultWatcher();
591
861
  assert.equal(state.watcher, fakeWatcher);
592
862
  for (const id of ["missed-a", "missed-b"]) {
593
- fs.writeFileSync(path.join(resultsDir, `${id}.json`), JSON.stringify({
863
+ writeIndexedResult(path.join(resultsDir, `${id}.json`), {
594
864
  id,
595
865
  sessionId: "session-1",
596
866
  success: true,
597
867
  state: "complete",
598
868
  summary: "done",
599
- }), "utf-8");
869
+ });
600
870
  }
601
871
  scan?.();
602
- await new Promise((resolve) => setTimeout(resolve, 100));
872
+ await new Promise((resolve) => setTimeout(resolve, 10));
603
873
  } finally {
604
874
  watcher.stopResultWatcher();
605
875
  }
@@ -662,6 +932,7 @@ describe("result watcher", () => {
662
932
  });
663
933
  const originalError = console.error;
664
934
  const childSessionPath = path.join(resultsDir, "a-session.jsonl");
935
+ const resultPath = path.join(resultsDir, "async-fallback.json");
665
936
  console.error = () => {};
666
937
  try {
667
938
  watcher.startResultWatcher();
@@ -669,7 +940,7 @@ describe("result watcher", () => {
669
940
  assert.notEqual(state.watcherRestartTimer, null);
670
941
 
671
942
  fs.writeFileSync(childSessionPath, "", "utf-8");
672
- fs.writeFileSync(path.join(resultsDir, "async-fallback.json"), JSON.stringify({
943
+ writeIndexedResult(resultPath, {
673
944
  id: "async-fallback",
674
945
  runId: "run-fallback",
675
946
  agent: "parallel:a+b",
@@ -683,9 +954,9 @@ describe("result watcher", () => {
683
954
  ],
684
955
  sessionId: "session-1",
685
956
  intercomTarget: "subagent-chat-main",
686
- }), "utf-8");
957
+ });
687
958
  poll?.();
688
- await new Promise((resolve) => setTimeout(resolve, 100));
959
+ assert.equal(await waitForPredicate(() => emitted.some((entry) => entry.event === "subagent:async-complete") && !fs.existsSync(resultPath)), true);
689
960
  } finally {
690
961
  console.error = originalError;
691
962
  watcher.stopResultWatcher();
@@ -694,7 +965,7 @@ describe("result watcher", () => {
694
965
  const intercomEvents = emitted.filter((entry) => entry.event === "subagent:result-intercom");
695
966
  assert.equal(intercomEvents.length, 1);
696
967
  assert.equal(emitted.some((entry) => entry.event === "subagent:async-complete"), true);
697
- assert.equal(fs.existsSync(path.join(resultsDir, "async-fallback.json")), false);
968
+ assert.equal(fs.existsSync(resultPath), false);
698
969
  const payload = intercomEvents[0]?.data as { mode?: string; status?: string; message?: string; children?: Array<{ status?: string; summary?: string; sessionPath?: string }> };
699
970
  const completion = emitted.find((entry) => entry.event === "subagent:async-complete")?.data as { results?: Array<{ status?: string; summary?: string; sessionPath?: string }> } | undefined;
700
971
  assert.equal(payload.mode, "parallel");
@@ -764,9 +1035,9 @@ describe("result watcher", () => {
764
1035
  assert.equal(state.watcher, null);
765
1036
  assert.notEqual(state.watcherRestartTimer, null);
766
1037
 
767
- fs.writeFileSync(path.join(resultsDir, "done.json"), JSON.stringify({ sessionId: "session-1", summary: "done" }), "utf-8");
1038
+ writeIndexedResult(path.join(resultsDir, "done.json"), { sessionId: "session-1", summary: "done" });
768
1039
  poll?.();
769
- await new Promise((resolve) => setTimeout(resolve, 75));
1040
+ await new Promise((resolve) => setTimeout(resolve, 10));
770
1041
  } finally {
771
1042
  console.error = originalError;
772
1043
  watcher.stopResultWatcher();
@@ -811,7 +1082,7 @@ describe("result watcher", () => {
811
1082
  const missingSession = path.join(resultsDir, "b-session.jsonl");
812
1083
  try {
813
1084
  fs.writeFileSync(firstSession, "", "utf-8");
814
- fs.writeFileSync(path.join(resultsDir, "async-1.json"), JSON.stringify({
1085
+ writeIndexedResult(path.join(resultsDir, "async-1.json"), {
815
1086
  id: "async-1",
816
1087
  runId: "run-123",
817
1088
  agent: "parallel:a+b",
@@ -828,9 +1099,9 @@ describe("result watcher", () => {
828
1099
  asyncDir: "/tmp/async-1",
829
1100
  parallelHandoff: { version: 1, path: "/tmp/async-1/handoff.json", groupCount: 1, childCount: 2, changedPatches: 1, cleanupState: "complete" },
830
1101
  intercomTarget: "subagent-chat-main",
831
- }), "utf-8");
1102
+ });
832
1103
  watcher.primeExistingResults();
833
- await new Promise((resolve) => setTimeout(resolve, 100));
1104
+ assert.equal(await waitForPredicate(() => emitted.some((entry) => entry.event === "subagent:async-complete") && emitted.some((entry) => entry.event === "subagent:result-intercom")), true);
834
1105
  } finally {
835
1106
  watcher.stopResultWatcher();
836
1107
  }
@@ -882,7 +1153,7 @@ describe("result watcher", () => {
882
1153
  state.currentSessionId = "session-1";
883
1154
  const watcher = createResultWatcher(pi, state, resultsDir, 60_000);
884
1155
  try {
885
- fs.writeFileSync(path.join(resultsDir, "async-stopped.json"), JSON.stringify({
1156
+ writeIndexedResult(path.join(resultsDir, "async-stopped.json"), {
886
1157
  id: "async-stopped",
887
1158
  runId: "async-stopped",
888
1159
  agent: "parallel:a+b",
@@ -897,9 +1168,9 @@ describe("result watcher", () => {
897
1168
  ],
898
1169
  sessionId: "session-1",
899
1170
  intercomTarget: "subagent-chat-main",
900
- }, null, 2), "utf-8");
1171
+ });
901
1172
  watcher.primeExistingResults();
902
- await new Promise((resolve) => setTimeout(resolve, 100));
1173
+ await new Promise((resolve) => setTimeout(resolve, 10));
903
1174
  } finally {
904
1175
  watcher.stopResultWatcher();
905
1176
  }
@@ -964,7 +1235,7 @@ describe("result watcher", () => {
964
1235
  const watcher = createResultWatcher(pi, state, resultsDir, 60_000);
965
1236
  const resultPath = path.join(resultsDir, "async-nested-root.json");
966
1237
  try {
967
- fs.writeFileSync(resultPath, JSON.stringify({
1238
+ writeIndexedResult(resultPath, {
968
1239
  id: "async-nested-root",
969
1240
  runId: "async-nested-root",
970
1241
  agent: "owner",
@@ -975,9 +1246,9 @@ describe("result watcher", () => {
975
1246
  results: [{ agent: "owner", output: "owner done", success: true }],
976
1247
  sessionId: "session-1",
977
1248
  intercomTarget: "subagent-chat-main",
978
- }), "utf-8");
1249
+ });
979
1250
  watcher.primeExistingResults();
980
- await new Promise((resolve) => setTimeout(resolve, 100));
1251
+ 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
1252
  } finally {
982
1253
  watcher.stopResultWatcher();
983
1254
  }
@@ -1032,7 +1303,7 @@ describe("result watcher", () => {
1032
1303
  logged.push(args);
1033
1304
  };
1034
1305
  try {
1035
- fs.writeFileSync(resultPath, JSON.stringify({
1306
+ writeIndexedResult(resultPath, {
1036
1307
  id: "async-explicit-nested",
1037
1308
  runId: "async-explicit-nested",
1038
1309
  agent: "owner",
@@ -1055,9 +1326,9 @@ describe("result watcher", () => {
1055
1326
  ],
1056
1327
  sessionId: "session-1",
1057
1328
  intercomTarget: "subagent-chat-main",
1058
- }), "utf-8");
1329
+ });
1059
1330
  watcher.primeExistingResults();
1060
- await new Promise((resolve) => setTimeout(resolve, 100));
1331
+ assert.equal(await waitForPredicate(() => !fs.existsSync(resultPath) && emitted.some((entry) => entry.event === "subagent:async-complete")), true);
1061
1332
  } finally {
1062
1333
  console.error = originalError;
1063
1334
  watcher.stopResultWatcher();
@@ -1123,7 +1394,7 @@ describe("result watcher", () => {
1123
1394
  logged.push(args);
1124
1395
  };
1125
1396
  try {
1126
- fs.writeFileSync(resultPath, JSON.stringify({
1397
+ writeIndexedResult(resultPath, {
1127
1398
  id: "async-nested-retry",
1128
1399
  runId: "async-nested-retry",
1129
1400
  agent: "owner",
@@ -1132,9 +1403,9 @@ describe("result watcher", () => {
1132
1403
  summary: "owner done",
1133
1404
  sessionId: "session-1",
1134
1405
  intercomTarget: "subagent-chat-main",
1135
- }), "utf-8");
1406
+ });
1136
1407
  watcher.primeExistingResults();
1137
- await new Promise((resolve) => setTimeout(resolve, 100));
1408
+ await new Promise((resolve) => setTimeout(resolve, 10));
1138
1409
 
1139
1410
  assert.equal(fs.existsSync(resultPath), true);
1140
1411
  assert.equal(emitted.length, 0);
@@ -1186,7 +1457,7 @@ describe("result watcher", () => {
1186
1457
  state.currentSessionId = "session-1";
1187
1458
  const watcher = createResultWatcher(pi, state, resultsDir, 60_000);
1188
1459
  try {
1189
- fs.writeFileSync(path.join(resultsDir, "async-top-session.json"), JSON.stringify({
1460
+ writeIndexedResult(path.join(resultsDir, "async-top-session.json"), {
1190
1461
  id: "async-top-session",
1191
1462
  mode: "parallel",
1192
1463
  success: false,
@@ -1198,9 +1469,9 @@ describe("result watcher", () => {
1198
1469
  sessionId: "session-1",
1199
1470
  sessionFile: "/tmp/top-session.jsonl",
1200
1471
  intercomTarget: "subagent-chat-main",
1201
- }), "utf-8");
1472
+ });
1202
1473
  watcher.primeExistingResults();
1203
- await new Promise((resolve) => setTimeout(resolve, 100));
1474
+ await new Promise((resolve) => setTimeout(resolve, 10));
1204
1475
  } finally {
1205
1476
  watcher.stopResultWatcher();
1206
1477
  }
@@ -1243,7 +1514,7 @@ describe("result watcher", () => {
1243
1514
  state.currentSessionId = "session-1";
1244
1515
  const watcher = createResultWatcher(pi, state, resultsDir, 60_000);
1245
1516
  try {
1246
- fs.writeFileSync(path.join(resultsDir, "async-paused.json"), JSON.stringify({
1517
+ writeIndexedResult(path.join(resultsDir, "async-paused.json"), {
1247
1518
  id: "async-paused",
1248
1519
  runId: "run-paused",
1249
1520
  agent: "chain:a->b",
@@ -1257,9 +1528,9 @@ describe("result watcher", () => {
1257
1528
  ],
1258
1529
  sessionId: "session-1",
1259
1530
  intercomTarget: "subagent-chat-main",
1260
- }), "utf-8");
1531
+ });
1261
1532
  watcher.primeExistingResults();
1262
- await new Promise((resolve) => setTimeout(resolve, 100));
1533
+ await new Promise((resolve) => setTimeout(resolve, 10));
1263
1534
  } finally {
1264
1535
  watcher.stopResultWatcher();
1265
1536
  }
@@ -1301,7 +1572,7 @@ describe("result watcher", () => {
1301
1572
  logged.push(args);
1302
1573
  };
1303
1574
  try {
1304
- fs.writeFileSync(path.join(resultsDir, "async-2.json"), JSON.stringify({
1575
+ writeIndexedResult(path.join(resultsDir, "async-2.json"), {
1305
1576
  id: "async-2",
1306
1577
  runId: "run-456",
1307
1578
  agent: "worker",
@@ -1310,9 +1581,9 @@ describe("result watcher", () => {
1310
1581
  summary: "Worker summary",
1311
1582
  sessionId: "session-1",
1312
1583
  intercomTarget: "orchestrator",
1313
- }), "utf-8");
1584
+ });
1314
1585
  watcher.primeExistingResults();
1315
- await new Promise((resolve) => setTimeout(resolve, 100));
1586
+ await new Promise((resolve) => setTimeout(resolve, 10));
1316
1587
  } finally {
1317
1588
  console.error = originalError;
1318
1589
  watcher.stopResultWatcher();
@@ -1354,7 +1625,7 @@ describe("result watcher", () => {
1354
1625
  const loggedWarning = () => logged.some((entry) => /Subagent async grouped result intercom delivery was not acknowledged/.test(String(entry[0] ?? "")));
1355
1626
  let warned = false;
1356
1627
  try {
1357
- fs.writeFileSync(path.join(resultsDir, "unacknowledged.json"), JSON.stringify({
1628
+ writeIndexedResult(path.join(resultsDir, "unacknowledged.json"), {
1358
1629
  id: "unacknowledged",
1359
1630
  runId: "run-unacknowledged",
1360
1631
  agent: "worker",
@@ -1363,7 +1634,7 @@ describe("result watcher", () => {
1363
1634
  summary: "Worker summary",
1364
1635
  sessionId: "session-1",
1365
1636
  intercomTarget: "orchestrator",
1366
- }), "utf-8");
1637
+ });
1367
1638
  watcher.primeExistingResults();
1368
1639
  warned = await waitForPredicate(loggedWarning);
1369
1640
  } finally {
@@ -1410,7 +1681,7 @@ describe("result watcher", () => {
1410
1681
  notifier: { async deliver(result) { delivered.push({ intercomDelivered: result.intercomDelivered }); return true; } },
1411
1682
  });
1412
1683
  try {
1413
- fs.writeFileSync(path.join(resultsDir, "acknowledged.json"), JSON.stringify({
1684
+ writeIndexedResult(path.join(resultsDir, "acknowledged.json"), {
1414
1685
  id: "acknowledged",
1415
1686
  runId: "run-acknowledged",
1416
1687
  agent: "worker",
@@ -1419,9 +1690,9 @@ describe("result watcher", () => {
1419
1690
  summary: "Worker summary",
1420
1691
  sessionId: "session-1",
1421
1692
  intercomTarget: "orchestrator",
1422
- }), "utf-8");
1693
+ });
1423
1694
  watcher.primeExistingResults();
1424
- await new Promise((resolve) => setTimeout(resolve, 100));
1695
+ await new Promise((resolve) => setTimeout(resolve, 10));
1425
1696
  } finally {
1426
1697
  watcher.stopResultWatcher();
1427
1698
  }
@@ -1444,11 +1715,11 @@ describe("result watcher", () => {
1444
1715
  const resultFile = "expired.json";
1445
1716
  const result = { sessionId: "session-1", agent: "worker", success: true, summary: "new result", timestamp: 123 };
1446
1717
  const resultPath = path.join(resultsDir, resultFile);
1447
- fs.writeFileSync(resultPath, JSON.stringify(result), "utf-8");
1718
+ writeIndexedResult(resultPath, result);
1448
1719
  state.completionSeen.set(buildCompletionKey(result, `result:${resultFile}`), Date.now() - 61_000);
1449
1720
  try {
1450
1721
  watcher.primeExistingResults();
1451
- await new Promise((resolve) => setTimeout(resolve, 75));
1722
+ await new Promise((resolve) => setTimeout(resolve, 10));
1452
1723
  } finally {
1453
1724
  watcher.stopResultWatcher();
1454
1725
  }
@@ -1471,10 +1742,10 @@ describe("result watcher", () => {
1471
1742
  notifier: { async deliver() { attempts += 1; return attempts > 1; } },
1472
1743
  });
1473
1744
  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");
1745
+ writeIndexedResult(resultPath, { id: "retry", sessionId: "session-1", agent: "worker", success: true, summary: "done", timestamp: 1 });
1475
1746
  try {
1476
1747
  watcher.primeExistingResults();
1477
- await new Promise((resolve) => setTimeout(resolve, 50));
1748
+ await new Promise((resolve) => setTimeout(resolve, 10));
1478
1749
  assert.equal(fs.existsSync(resultPath), true);
1479
1750
  await new Promise((resolve) => setTimeout(resolve, 180));
1480
1751
  } finally {
@@ -1499,12 +1770,12 @@ describe("result watcher", () => {
1499
1770
  notifier: { deliver: () => new Promise<boolean>((resolve) => { accept = resolve; }) },
1500
1771
  });
1501
1772
  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");
1773
+ writeIndexedResult(resultPath, { id: "stale", sessionId: "session-1", agent: "worker", success: true, summary: "done", timestamp: 1 });
1503
1774
  watcher.primeExistingResults();
1504
- await new Promise((resolve) => setTimeout(resolve, 50));
1775
+ await new Promise((resolve) => setTimeout(resolve, 10));
1505
1776
  watcher.stopResultWatcher();
1506
1777
  accept(true);
1507
- await new Promise((resolve) => setTimeout(resolve, 50));
1778
+ await new Promise((resolve) => setTimeout(resolve, 10));
1508
1779
  assert.equal(fs.existsSync(resultPath), true);
1509
1780
  assert.deepEqual(emitted, []);
1510
1781
  } finally {
@@ -1522,10 +1793,10 @@ describe("result watcher", () => {
1522
1793
  const watcher = createResultWatcher({ events: { on: () => () => {}, emit(event) { emitted.push(event); } } }, state, resultsDir, 60_000, {
1523
1794
  notifier: { async deliver(result) { delivered.push({ triggerTurn: result.triggerTurn }); return true; } },
1524
1795
  });
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");
1796
+ writeIndexedResult(path.join(resultsDir, "quiet.json"), { id: "quiet", sessionId: "session-1", agent: "worker", success: true, summary: "done", timestamp: 1, intercomTarget: "host" });
1526
1797
  try {
1527
1798
  watcher.primeExistingResults({ triggerTurn: false });
1528
- await new Promise((resolve) => setTimeout(resolve, 75));
1799
+ await new Promise((resolve) => setTimeout(resolve, 10));
1529
1800
  } finally {
1530
1801
  watcher.stopResultWatcher();
1531
1802
  }