@selesai/code 0.13.15 → 0.13.18

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 (473) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +2 -14
  3. package/dist/bun/cli.d.ts +3 -1
  4. package/dist/bun/cli.js +3 -9
  5. package/dist/bun/{register-bedrock.js → runtime-setup.js} +5 -0
  6. package/dist/bun/sandbox-env-setup.d.ts +1 -0
  7. package/dist/bun/sandbox-env-setup.js +3 -0
  8. package/dist/cli/auth-command.d.ts +23 -0
  9. package/dist/cli/auth-command.js +102 -0
  10. package/dist/cli/config-selector.js +2 -1
  11. package/dist/cli/setup.d.ts +1 -0
  12. package/dist/cli/setup.js +11 -0
  13. package/dist/cli.js +2 -14
  14. package/dist/config.d.ts +3 -2
  15. package/dist/config.js +9 -4
  16. package/dist/core/agent-session-runtime.js +15 -7
  17. package/dist/core/agent-session.d.ts +2 -6
  18. package/dist/core/agent-session.js +15 -4
  19. package/dist/core/compaction/branch-summarization.d.ts +1 -1
  20. package/dist/core/compaction/branch-summarization.js +2 -1
  21. package/dist/core/extensions/loader.js +3 -0
  22. package/dist/core/extensions/types.d.ts +16 -2
  23. package/dist/core/http-dispatcher.js +2 -0
  24. package/dist/core/keybindings.d.ts +5 -0
  25. package/dist/core/keybindings.js +4 -0
  26. package/dist/core/messages.d.ts +1 -1
  27. package/dist/core/model-config.d.ts +15 -0
  28. package/dist/core/model-config.js +3 -0
  29. package/dist/core/model-runtime.d.ts +1 -0
  30. package/dist/core/model-runtime.js +5 -2
  31. package/dist/core/provider-composer.js +8 -2
  32. package/dist/core/session-export.d.ts +3 -0
  33. package/dist/core/session-export.js +31 -0
  34. package/dist/core/session-manager.d.ts +3 -2
  35. package/dist/core/session-manager.js +40 -13
  36. package/dist/core/skills.d.ts +1 -1
  37. package/dist/core/skills.js +4 -2
  38. package/dist/core/system-prompt.js +10 -10
  39. package/dist/core/tools/bash.js +3 -117
  40. package/dist/core/tools/edit.d.ts +1 -12
  41. package/dist/core/tools/edit.js +5 -163
  42. package/dist/core/tools/find.js +4 -55
  43. package/dist/core/tools/grep.js +4 -60
  44. package/dist/core/tools/ls.js +4 -49
  45. package/dist/core/tools/read.d.ts +4 -0
  46. package/dist/core/tools/read.js +10 -122
  47. package/dist/core/tools/renderers/bash.d.ts +11 -0
  48. package/dist/core/tools/renderers/bash.js +129 -0
  49. package/dist/core/tools/renderers/edit.d.ts +22 -0
  50. package/dist/core/tools/renderers/edit.js +166 -0
  51. package/dist/core/tools/renderers/find.d.ts +9 -0
  52. package/dist/core/tools/renderers/find.js +63 -0
  53. package/dist/core/tools/renderers/grep.d.ts +9 -0
  54. package/dist/core/tools/renderers/grep.js +68 -0
  55. package/dist/core/tools/renderers/index.d.ts +27 -0
  56. package/dist/core/tools/renderers/index.js +46 -0
  57. package/dist/core/tools/renderers/ls.d.ts +9 -0
  58. package/dist/core/tools/renderers/ls.js +57 -0
  59. package/dist/core/tools/renderers/read.d.ts +10 -0
  60. package/dist/core/tools/renderers/read.js +128 -0
  61. package/dist/core/tools/renderers/write.d.ts +9 -0
  62. package/dist/core/tools/renderers/write.js +152 -0
  63. package/dist/core/tools/write.js +5 -146
  64. package/dist/extensions/capability-gateway/index.ts +12 -1
  65. package/dist/extensions/capability-gateway/integration.test.ts +23 -0
  66. package/dist/extensions/pi-hermes-memory/CHANGELOG.md +18 -0
  67. package/dist/extensions/pi-hermes-memory/README.md +16 -12
  68. package/dist/extensions/pi-hermes-memory/docs/ROADMAP.md +1 -1
  69. package/dist/extensions/pi-hermes-memory/package-lock.json +587 -62
  70. package/dist/extensions/pi-hermes-memory/package.json +4 -4
  71. package/dist/extensions/pi-hermes-memory/src/config.ts +32 -0
  72. package/dist/extensions/pi-hermes-memory/src/constants.ts +12 -2
  73. package/dist/extensions/pi-hermes-memory/src/handlers/background-review.ts +109 -51
  74. package/dist/extensions/pi-hermes-memory/src/handlers/index-sessions.ts +9 -3
  75. package/dist/extensions/pi-hermes-memory/src/handlers/review-memory-ops.ts +257 -98
  76. package/dist/extensions/pi-hermes-memory/src/handlers/session-backfill.ts +9 -2
  77. package/dist/extensions/pi-hermes-memory/src/handlers/sync-markdown-memories.ts +5 -0
  78. package/dist/extensions/pi-hermes-memory/src/index.ts +22 -2
  79. package/dist/extensions/pi-hermes-memory/src/paths.ts +1 -1
  80. package/dist/extensions/pi-hermes-memory/src/store/db.ts +6 -0
  81. package/dist/extensions/pi-hermes-memory/src/store/fts-query.ts +58 -1
  82. package/dist/extensions/pi-hermes-memory/src/store/memory-store.ts +60 -8
  83. package/dist/extensions/pi-hermes-memory/src/store/session-indexer.ts +169 -7
  84. package/dist/extensions/pi-hermes-memory/src/store/session-search.ts +7 -21
  85. package/dist/extensions/pi-hermes-memory/src/store/sqlite-memory-store.ts +134 -15
  86. package/dist/extensions/pi-hermes-memory/src/tools/memory-search-tool.ts +4 -2
  87. package/dist/extensions/pi-hermes-memory/src/tools/memory-tool.ts +24 -9
  88. package/dist/extensions/pi-hermes-memory/src/types.ts +11 -0
  89. package/dist/extensions/pi-hermes-memory/tests/config.test.ts +42 -0
  90. package/dist/extensions/pi-hermes-memory/tests/handlers/background-review.test.ts +159 -3
  91. package/dist/extensions/pi-hermes-memory/tests/handlers/review-memory-ops.test.ts +452 -0
  92. package/dist/extensions/pi-hermes-memory/tests/handlers/session-backfill.test.ts +39 -0
  93. package/dist/extensions/pi-hermes-memory/tests/integration/corruption-recovery-integration.test.ts +91 -0
  94. package/dist/extensions/pi-hermes-memory/tests/store/db.test.ts +29 -0
  95. package/dist/extensions/pi-hermes-memory/tests/store/fts-query.test.ts +116 -0
  96. package/dist/extensions/pi-hermes-memory/tests/store/memory-store.test.ts +163 -0
  97. package/dist/extensions/pi-hermes-memory/tests/store/session-indexer.test.ts +233 -0
  98. package/dist/extensions/pi-hermes-memory/tests/store/session-search.test.ts +12 -0
  99. package/dist/extensions/pi-hermes-memory/tests/store/sqlite-memory-store.test.ts +175 -0
  100. package/dist/extensions/pi-hermes-memory/tests/store/sqlite-native.test.ts +2 -2
  101. package/dist/extensions/pi-hermes-memory/tests/tools/memory-search-tool.test.ts +22 -0
  102. package/dist/extensions/pi-hermes-memory/tests/tools/memory-tool.test.ts +37 -1
  103. package/dist/extensions/pi-intercom/CHANGELOG.md +13 -0
  104. package/dist/extensions/pi-intercom/README.md +16 -2
  105. package/dist/extensions/pi-intercom/broker/spawn.test.ts +19 -2
  106. package/dist/extensions/pi-intercom/broker/spawn.ts +4 -4
  107. package/dist/extensions/pi-intercom/index.ts +1 -1
  108. package/dist/extensions/pi-intercom/intercom.integration.test.ts +121 -2
  109. package/dist/extensions/pi-intercom/package.json +1 -1
  110. package/dist/extensions/pi-intercom/project-agent.test.ts +1 -1
  111. package/dist/extensions/pi-intercom/project-agent.ts +1 -6
  112. package/dist/extensions/pi-intercom/skills/pi-intercom/SKILL.md +7 -6
  113. package/dist/extensions/pi-rewind-hook/index.test.ts +7 -0
  114. package/dist/extensions/pi-subagents/.github/workflows/test.yml +36 -4
  115. package/dist/extensions/pi-subagents/CHANGELOG.md +120 -0
  116. package/dist/extensions/pi-subagents/README.md +3 -3
  117. package/dist/extensions/pi-subagents/agents/researcher.md +23 -13
  118. package/dist/extensions/pi-subagents/agents/reviewer.md +1 -1
  119. package/dist/extensions/pi-subagents/agents/scout.md +1 -1
  120. package/dist/extensions/pi-subagents/docs/agents.md +32 -16
  121. package/dist/extensions/pi-subagents/docs/configuration.md +41 -15
  122. package/dist/extensions/pi-subagents/docs/extension-api.md +109 -6
  123. package/dist/extensions/pi-subagents/docs/missions.md +2 -0
  124. package/dist/extensions/pi-subagents/docs/models.md +58 -1
  125. package/dist/extensions/pi-subagents/docs/observability.md +50 -12
  126. package/dist/extensions/pi-subagents/docs/tool-reference.md +17 -10
  127. package/dist/extensions/pi-subagents/docs/watchdog.md +1 -1
  128. package/dist/extensions/pi-subagents/docs/workflows.md +26 -9
  129. package/dist/extensions/pi-subagents/package-lock.json +747 -177
  130. package/dist/extensions/pi-subagents/package.json +6 -4
  131. package/dist/extensions/pi-subagents/runner-server-preload.mjs +13 -0
  132. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +2 -1
  133. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  134. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +21 -4
  135. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
  136. package/dist/extensions/pi-subagents/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
  137. package/dist/extensions/pi-subagents/src/agents/advertised-agent-prompt.ts +63 -0
  138. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +61 -16
  139. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
  140. package/dist/extensions/pi-subagents/src/agents/agents.ts +8 -0
  141. package/dist/extensions/pi-subagents/src/api/capability-ceiling.ts +0 -1
  142. package/dist/extensions/pi-subagents/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
  143. package/dist/extensions/pi-subagents/src/api/preflight.ts +7 -4
  144. package/dist/extensions/pi-subagents/src/api/shared-types.ts +2 -2
  145. package/dist/extensions/pi-subagents/src/api/workflow-resources.ts +6 -0
  146. package/dist/extensions/pi-subagents/src/extension/config.ts +4 -2
  147. package/dist/extensions/pi-subagents/src/extension/doctor.ts +2 -10
  148. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +9 -11
  149. package/dist/extensions/pi-subagents/src/extension/index.ts +99 -10
  150. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +13 -0
  151. package/dist/extensions/pi-subagents/src/extension/rpc.ts +7 -23
  152. package/dist/extensions/pi-subagents/src/extension/schemas.ts +11 -8
  153. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +25 -7
  154. package/dist/extensions/pi-subagents/src/integrations/pi-web-session-liveness.ts +73 -0
  155. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +228 -136
  156. package/dist/extensions/pi-subagents/src/intercom/supervisor-ui.ts +244 -0
  157. package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +37 -16
  158. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +18 -18
  159. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +51 -25
  160. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +46 -3
  161. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -3
  162. package/dist/extensions/pi-subagents/src/runs/background/async-retention.ts +9 -0
  163. package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +10 -12
  164. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +18 -15
  165. package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +40 -29
  166. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +8 -0
  167. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +79 -247
  168. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +88 -12
  169. package/dist/extensions/pi-subagents/src/runs/background/owned-process-tree.ts +6 -6
  170. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +24 -24
  171. package/dist/extensions/pi-subagents/src/runs/background/retained-nested-route-tracker.ts +96 -0
  172. package/dist/extensions/pi-subagents/src/runs/background/run-child-session.ts +642 -0
  173. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +75 -6
  174. package/dist/extensions/pi-subagents/src/runs/background/runner-aliases.ts +163 -0
  175. package/dist/extensions/pi-subagents/src/runs/background/runner-child-launch.ts +86 -0
  176. package/dist/extensions/pi-subagents/src/runs/background/runner-child-sessions.ts +31 -0
  177. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +18 -4
  178. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +3 -1
  179. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +597 -1092
  180. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +3 -0
  181. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +4 -0
  182. package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +20 -17
  183. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +296 -394
  184. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +4 -0
  185. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +3 -1
  186. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +9 -5
  187. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +630 -283
  188. package/dist/extensions/pi-subagents/src/runs/foreground/workflow-detach-reconcile.ts +8 -5
  189. package/dist/extensions/pi-subagents/src/runs/foreground/workflow-foreground-steering.ts +78 -98
  190. package/dist/extensions/pi-subagents/src/runs/shared/abort-recovery.ts +3 -3
  191. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +16 -3
  192. package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +1 -1
  193. package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +47 -47
  194. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +1 -2
  195. package/dist/extensions/pi-subagents/src/runs/shared/child-hooks.ts +174 -0
  196. package/dist/extensions/pi-subagents/src/runs/shared/child-identity.ts +14 -3
  197. package/dist/extensions/pi-subagents/src/runs/shared/child-launch.ts +319 -0
  198. package/dist/extensions/pi-subagents/src/runs/shared/child-lifecycle.ts +25 -0
  199. package/dist/extensions/pi-subagents/src/runs/shared/child-runtime-config.ts +126 -0
  200. package/dist/extensions/pi-subagents/src/runs/shared/child-session.ts +373 -0
  201. package/dist/extensions/pi-subagents/src/runs/shared/child-tool-plan.ts +530 -0
  202. package/dist/extensions/pi-subagents/src/runs/shared/completion-evidence.ts +2 -2
  203. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +2 -1
  204. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-preflight.ts +16 -0
  205. package/dist/extensions/pi-subagents/src/runs/shared/host-step-status.ts +11 -11
  206. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +30 -20
  207. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
  208. package/dist/extensions/pi-subagents/src/runs/shared/model-exclusions.ts +84 -15
  209. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +88 -12
  210. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +37 -53
  211. package/dist/extensions/pi-subagents/src/runs/shared/nested-path.ts +1 -1
  212. package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +18 -7
  213. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +57 -12
  214. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +3 -4
  215. package/dist/extensions/pi-subagents/src/runs/shared/permissions.ts +0 -11
  216. package/dist/extensions/pi-subagents/src/runs/shared/process-signal.ts +4 -1
  217. package/dist/extensions/pi-subagents/src/runs/shared/readonly-drain-observation.ts +42 -0
  218. package/dist/extensions/pi-subagents/src/runs/shared/readonly-model-continuation.ts +69 -0
  219. package/dist/extensions/pi-subagents/src/runs/shared/readonly-session-evidence.ts +307 -0
  220. package/dist/extensions/pi-subagents/src/runs/shared/run-fanout-budget.ts +8 -21
  221. package/dist/extensions/pi-subagents/src/runs/shared/runtime-acknowledged-extensions.ts +3 -30
  222. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +17 -4
  223. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +2 -0
  224. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +99 -386
  225. package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +18 -62
  226. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +0 -14
  227. package/dist/extensions/pi-subagents/src/runs/shared/worktree-cleanup-plan.ts +31 -9
  228. package/dist/extensions/pi-subagents/src/runs/shared/worktree-setup-command.ts +190 -0
  229. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +506 -226
  230. package/dist/extensions/pi-subagents/src/shared/child-session-name.ts +1 -1
  231. package/dist/extensions/pi-subagents/src/shared/jsonl-writer.ts +11 -0
  232. package/dist/extensions/pi-subagents/src/shared/model-response-aliases.ts +13 -0
  233. package/dist/extensions/pi-subagents/src/shared/thinking-ceiling.ts +0 -6
  234. package/dist/extensions/pi-subagents/src/shared/types.ts +145 -89
  235. package/dist/extensions/pi-subagents/src/shared/utils.ts +13 -6
  236. package/dist/extensions/pi-subagents/src/shared/watch-strategy.ts +2 -0
  237. package/dist/extensions/pi-subagents/src/shared/workflow-child-permit.ts +18 -13
  238. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +0 -6
  239. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +1 -1
  240. package/dist/extensions/pi-subagents/src/tui/fleet.ts +11 -6
  241. package/dist/extensions/pi-subagents/src/tui/render.ts +277 -46
  242. package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +0 -1
  243. package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +12 -12
  244. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +3 -3
  245. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +116 -15
  246. package/dist/extensions/pi-subagents/src/workflows/workflow-checklist.ts +15 -18
  247. package/dist/extensions/pi-subagents/src/workflows/workflow-child-summary.ts +57 -8
  248. package/dist/extensions/pi-subagents/src/workflows/workflow-preflight.ts +19 -19
  249. package/dist/extensions/pi-subagents/src/workflows/workflow-receipt.ts +3 -3
  250. package/dist/extensions/pi-subagents/src/workflows/workflow-resources.ts +97 -22
  251. package/dist/extensions/pi-subagents/src/workflows/workflow-settlement.ts +3 -0
  252. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +336 -21
  253. package/dist/extensions/pi-subagents/test/integration/async-execution.part-1.test.ts +1181 -0
  254. package/dist/extensions/pi-subagents/test/integration/async-execution.part-2.test.ts +2240 -0
  255. package/dist/extensions/pi-subagents/test/integration/async-execution.part-3.test.ts +2268 -0
  256. package/dist/extensions/pi-subagents/test/integration/async-execution.part-4.test.ts +1512 -0
  257. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +231 -16
  258. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +4 -18
  259. package/dist/extensions/pi-subagents/test/integration/foreign-workflow-steering.test.ts +95 -0
  260. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +9 -15
  261. package/dist/extensions/pi-subagents/test/integration/in-process-child.test.ts +374 -0
  262. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +20 -10
  263. package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +18 -6
  264. package/dist/extensions/pi-subagents/test/integration/recovered-assistant-error.test.ts +151 -0
  265. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +42 -1
  266. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +150 -25
  267. package/dist/extensions/pi-subagents/test/integration/result-publication.test.ts +354 -0
  268. package/dist/extensions/pi-subagents/test/integration/shared-cwd-terminal-evidence.test.ts +117 -0
  269. package/dist/extensions/pi-subagents/test/integration/single-execution.part-1.test.ts +3678 -0
  270. package/dist/extensions/pi-subagents/test/integration/{single-execution.test.ts → single-execution.part-2.test.ts} +500 -4207
  271. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1 -2
  272. package/dist/extensions/pi-subagents/test/integration/workflow-result-publication.test.ts +127 -0
  273. package/dist/extensions/pi-subagents/test/integration/workflow-steer-inbox.test.ts +365 -0
  274. package/dist/extensions/pi-subagents/test/smoke/pi085-child.ts +31 -0
  275. package/dist/extensions/pi-subagents/test/smoke/pi085-clean-install.mjs +74 -0
  276. package/dist/extensions/pi-subagents/test/smoke/pi085-extension.ts +16 -0
  277. package/dist/extensions/pi-subagents/test/support/async-execution-fixture.ts +699 -0
  278. package/dist/extensions/pi-subagents/test/support/fake-child-session.ts +418 -0
  279. package/dist/extensions/pi-subagents/test/support/foreign-workflow-steer-host.mjs +13 -0
  280. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +30 -76
  281. package/dist/extensions/pi-subagents/test/support/notify-diagnostics-fixture.ts +84 -0
  282. package/dist/extensions/pi-subagents/test/support/result-publication-capacity-preload.mjs +70 -0
  283. package/dist/extensions/pi-subagents/test/support/runner-child-session-factory.ts +17 -0
  284. package/dist/extensions/pi-subagents/test/support/single-execution-fixture.ts +390 -0
  285. package/dist/extensions/pi-subagents/test/unit/acceptance-compaction.test.ts +120 -0
  286. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +24 -3
  287. package/dist/extensions/pi-subagents/test/unit/advertised-agent-prompt.test.ts +87 -0
  288. package/dist/extensions/pi-subagents/test/unit/advertised-agent-refresh.test.ts +163 -0
  289. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +70 -31
  290. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +78 -3
  291. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +2 -8
  292. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +1 -1
  293. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +73 -66
  294. package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +51 -1
  295. package/dist/extensions/pi-subagents/test/unit/async-retention.test.ts +49 -1
  296. package/dist/extensions/pi-subagents/test/unit/async-spawn-preload.test.ts +103 -0
  297. package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +3 -3
  298. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-agent-allowlist.test.ts +9 -8
  299. package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +10 -7
  300. package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +48 -6
  301. package/dist/extensions/pi-subagents/test/unit/child-runtime-config.test.ts +90 -0
  302. package/dist/extensions/pi-subagents/test/unit/{pi-args-permission-system.test.ts → child-tool-plan-permission-system.test.ts} +48 -86
  303. package/dist/extensions/pi-subagents/test/unit/child-tool-plan.test.ts +33 -0
  304. package/dist/extensions/pi-subagents/test/unit/compaction-resume.test.ts +18 -18
  305. package/dist/extensions/pi-subagents/test/unit/completion-evidence.test.ts +9 -9
  306. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +8 -7
  307. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +225 -65
  308. package/dist/extensions/pi-subagents/test/unit/extension-bindings.test.ts +17 -22
  309. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +11 -1
  310. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +47 -3
  311. package/dist/extensions/pi-subagents/test/unit/foreign-workflow-steering.test.ts +74 -0
  312. package/dist/extensions/pi-subagents/test/unit/fork-cache-key.test.ts +23 -29
  313. package/dist/extensions/pi-subagents/test/unit/host-peer-runtime-imports.test.ts +96 -4
  314. package/dist/extensions/pi-subagents/test/unit/host-step-status.test.ts +4 -4
  315. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +116 -55
  316. package/dist/extensions/pi-subagents/test/unit/index-segment.test.ts +58 -1
  317. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +104 -3
  318. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +10 -1
  319. package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +31 -1
  320. package/dist/extensions/pi-subagents/test/unit/model-exclusions.test.ts +102 -51
  321. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +196 -6
  322. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +181 -38
  323. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +62 -70
  324. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +58 -53
  325. package/dist/extensions/pi-subagents/test/unit/notify-diagnostics.test.ts +42 -0
  326. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +84 -0
  327. package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +13 -5
  328. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +21 -5
  329. package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +5 -4
  330. package/dist/extensions/pi-subagents/test/unit/permissions.test.ts +2 -6
  331. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +28 -2
  332. package/dist/extensions/pi-subagents/test/unit/pi-web-session-liveness.test.ts +209 -0
  333. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +38 -1
  334. package/dist/extensions/pi-subagents/test/unit/prompt-audit-optional-peers.test.ts +43 -0
  335. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +24 -2
  336. package/dist/extensions/pi-subagents/test/unit/readonly-drain-observation.test.ts +174 -0
  337. package/dist/extensions/pi-subagents/test/unit/readonly-model-continuation.test.ts +94 -0
  338. package/dist/extensions/pi-subagents/test/unit/readonly-session-evidence.test.ts +1329 -0
  339. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +29 -80
  340. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +11 -8
  341. package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +2 -2
  342. package/dist/extensions/pi-subagents/test/unit/retained-nested-route-tracker.test.ts +175 -0
  343. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +38 -12
  344. package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +2 -5
  345. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +89 -1
  346. package/dist/extensions/pi-subagents/test/unit/runtime-acknowledged-extensions.test.ts +2 -16
  347. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +27 -3
  348. package/dist/extensions/pi-subagents/test/unit/scheduled-supervisor-demand.test.ts +215 -0
  349. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +15 -11
  350. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +237 -1
  351. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +2 -5
  352. package/dist/extensions/pi-subagents/test/unit/streamed-progress-bounds.test.ts +2 -2
  353. package/dist/extensions/pi-subagents/test/unit/subagent-guide.test.ts +14 -1
  354. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +130 -739
  355. package/dist/extensions/pi-subagents/test/unit/supervisor-ask-registration.test.ts +682 -0
  356. package/dist/extensions/pi-subagents/test/unit/supervisor-ui.test.ts +137 -0
  357. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +1 -11
  358. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +23 -3
  359. package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +8 -1
  360. package/dist/extensions/pi-subagents/test/unit/watchdog-tool-actions.test.ts +19 -1
  361. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +72 -4
  362. package/dist/extensions/pi-subagents/test/unit/workflow-checklist.test.ts +17 -3
  363. package/dist/extensions/pi-subagents/test/unit/workflow-child-summary.test.ts +34 -0
  364. package/dist/extensions/pi-subagents/test/unit/workflow-detach-reconcile.test.ts +12 -2
  365. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +46 -6
  366. package/dist/extensions/pi-subagents/test/unit/workflow-receipt.test.ts +3 -3
  367. package/dist/extensions/pi-subagents/test/unit/workflow-resources.test.ts +133 -1
  368. package/dist/extensions/pi-subagents/test/unit/workflow-resume-hint.test.ts +53 -2
  369. package/dist/extensions/pi-subagents/test/unit/worktree-cleanup-plan.test.ts +222 -14
  370. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +526 -85
  371. package/dist/extensions/pi-web-agent/src/backends/config.ts +1 -0
  372. package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +5 -3
  373. package/dist/extensions/pi-web-agent/src/search/tokenin.ts +7 -3
  374. package/dist/extensions/pi-web-agent/src/types.ts +1 -1
  375. package/dist/extensions/pi-zentui/README.md +17 -0
  376. package/dist/extensions/pi-zentui/assets/screenshots/footer-status-hyperlink.png +0 -0
  377. package/dist/extensions/pi-zentui/docs/configuration.md +71 -2
  378. package/dist/extensions/pi-zentui/extensions/zentui/config.ts +11 -0
  379. package/dist/extensions/pi-zentui/extensions/zentui/editor-transfer.ts +14 -0
  380. package/dist/extensions/pi-zentui/extensions/zentui/extension-status.ts +29 -13
  381. package/dist/extensions/pi-zentui/extensions/zentui/footer-layout.ts +5 -4
  382. package/dist/extensions/pi-zentui/extensions/zentui/footer-text.ts +11 -0
  383. package/dist/extensions/pi-zentui/extensions/zentui/footer.ts +11 -10
  384. package/dist/extensions/pi-zentui/extensions/zentui/index.ts +77 -3
  385. package/dist/extensions/pi-zentui/extensions/zentui/presets.ts +77 -0
  386. package/dist/extensions/pi-zentui/extensions/zentui/settings-command.ts +103 -2
  387. package/dist/extensions/pi-zentui/extensions/zentui/working-line-extension-segments.ts +115 -0
  388. package/dist/extensions/pi-zentui/extensions/zentui/working-line.ts +87 -5
  389. package/dist/extensions/pi-zentui/package-lock.json +277 -517
  390. package/dist/extensions/pi-zentui/package.json +7 -4
  391. package/dist/extensions/pi-zentui/test/config-presets.test.ts +238 -0
  392. package/dist/extensions/pi-zentui/test/editor-transfer.test.ts +24 -1
  393. package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +570 -2
  394. package/dist/extensions/pi-zentui/test/extension-status.test.ts +90 -0
  395. package/dist/extensions/pi-zentui/test/footer-hyperlink.test.ts +243 -0
  396. package/dist/extensions/pi-zentui/test/package-contents.mjs +1 -0
  397. package/dist/extensions/pi-zentui/test/presets.test.ts +121 -0
  398. package/dist/extensions/pi-zentui/test/settings-command.test.ts +205 -1
  399. package/dist/extensions/pi-zentui/test/working-line-extension-segments.test.ts +139 -0
  400. package/dist/extensions/pi-zentui/test/working-line-lifecycle.integration.test.ts +237 -3
  401. package/dist/extensions/pi-zentui/test/working-line.test.ts +94 -2
  402. package/dist/index.d.ts +1 -1
  403. package/dist/main.js +4 -1
  404. package/dist/modes/interactive/chat-viewport.d.ts +19 -0
  405. package/dist/modes/interactive/chat-viewport.js +27 -0
  406. package/dist/modes/interactive/components/assistant-message.d.ts +6 -2
  407. package/dist/modes/interactive/components/assistant-message.js +30 -13
  408. package/dist/modes/interactive/components/custom-editor.d.ts +10 -1
  409. package/dist/modes/interactive/components/custom-editor.js +39 -1
  410. package/dist/modes/interactive/components/first-time-setup.js +1 -1
  411. package/dist/modes/interactive/components/index.d.ts +1 -1
  412. package/dist/modes/interactive/components/markdown-transform.d.ts +2 -0
  413. package/dist/modes/interactive/components/markdown-transform.js +18 -0
  414. package/dist/modes/interactive/components/model-selector.js +19 -17
  415. package/dist/modes/interactive/components/scoped-models-selector.js +21 -23
  416. package/dist/modes/interactive/components/settings-selector.d.ts +7 -1
  417. package/dist/modes/interactive/components/settings-selector.js +128 -20
  418. package/dist/modes/interactive/components/settings-submenu.d.ts +71 -0
  419. package/dist/modes/interactive/components/settings-submenu.js +164 -0
  420. package/dist/modes/interactive/components/status-indicator.d.ts +3 -1
  421. package/dist/modes/interactive/components/status-indicator.js +10 -3
  422. package/dist/modes/interactive/components/thinking-selector.d.ts +15 -3
  423. package/dist/modes/interactive/components/thinking-selector.js +75 -18
  424. package/dist/modes/interactive/components/tool-execution.d.ts +21 -4
  425. package/dist/modes/interactive/components/tool-execution.js +35 -33
  426. package/dist/modes/interactive/components/trust-selector.js +2 -2
  427. package/dist/modes/interactive/interactive-mode.d.ts +13 -15
  428. package/dist/modes/interactive/interactive-mode.js +144 -80
  429. package/dist/modes/interactive/session-share.d.ts +15 -0
  430. package/dist/modes/interactive/session-share.js +183 -0
  431. package/dist/modes/interactive/theme/dark.json +2 -1
  432. package/dist/modes/interactive/theme/light.json +2 -1
  433. package/dist/modes/interactive/theme/theme-controller.d.ts +1 -0
  434. package/dist/modes/interactive/theme/theme-controller.js +5 -0
  435. package/dist/modes/interactive/theme/theme-json.d.ts +83 -0
  436. package/dist/modes/interactive/theme/theme-json.js +129 -0
  437. package/dist/modes/interactive/theme/theme-schema.json +9 -1
  438. package/dist/modes/interactive/theme/theme.d.ts +14 -5
  439. package/dist/modes/interactive/theme/theme.js +18 -119
  440. package/dist/modes/interactive/tui-renderer.d.ts +20 -0
  441. package/dist/modes/interactive/tui-renderer.js +65 -0
  442. package/dist/utils/exif-orientation.js +2 -3
  443. package/dist/utils/management-http.d.ts +24 -0
  444. package/dist/utils/management-http.js +53 -0
  445. package/dist/utils/text.d.ts +7 -0
  446. package/dist/utils/text.js +8 -0
  447. package/dist/utils/tools-manager.d.ts +1 -0
  448. package/dist/utils/tools-manager.js +43 -21
  449. package/docs/docs.json +0 -4
  450. package/package.json +4 -4
  451. package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +0 -415
  452. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +0 -1059
  453. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +0 -116
  454. package/dist/extensions/pi-subagents/src/shared/post-exit-stdio-guard.ts +0 -85
  455. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +0 -224
  456. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +0 -6986
  457. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +0 -447
  458. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +0 -185
  459. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +0 -311
  460. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-pi-args.test.ts +0 -84
  461. package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +0 -202
  462. package/dist/extensions/pi-subagents/test/unit/close-grace-timer.test.ts +0 -101
  463. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +0 -2347
  464. package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +0 -104
  465. package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +0 -49
  466. package/dist/skills/imagegen-frontend-mobile/SKILL.md +0 -1465
  467. package/dist/skills/imagegen-frontend-web/SKILL.md +0 -987
  468. package/dist/skills/implanger/SKILL.md +0 -70
  469. package/dist/skills/output/SKILL.md +0 -49
  470. package/dist/skills/planger/SKILL.md +0 -167
  471. package/dist/skills/workflow/SKILL.md +0 -95
  472. package/docs/workflows.md +0 -18
  473. /package/dist/bun/{register-bedrock.d.ts → runtime-setup.d.ts} +0 -0
@@ -0,0 +1,1512 @@
1
+ /**
2
+ * Integration tests for async (background) agent execution.
3
+ *
4
+ * Tests the async support utilities: jiti availability check,
5
+ * status file reading/caching.
6
+ *
7
+ * Requires pi packages to be importable. Skips gracefully if unavailable.
8
+ */
9
+
10
+ import { describe, it } from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import { execFileSync } from "node:child_process";
13
+ import { once } from "node:events";
14
+ import { createServer, type Socket } from "node:net";
15
+ import * as fs from "node:fs";
16
+ import * as path from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ import { setChildSessionFactoryModule } from "../../src/runs/shared/child-session.ts";
19
+ import { createEventBus, createTempDir, events, makeAgent, removeTempDir } from "../support/helpers.ts";
20
+ import { deliverInterruptRequest, deliverStopRequest } from "../../src/runs/background/control-channel.ts";
21
+ import { SUBAGENT_PROCESS_TERMINAL_EVENT } from "../../src/shared/types.ts";
22
+ import { waitForSubagents } from "../../src/runs/background/subagent-wait.ts";
23
+ import type { AsyncResultPayload, AsyncStatusPayload } from "../support/async-execution-fixture.ts";
24
+ import {
25
+ installAsyncExecutionHooks, writeWatchdogSettings, withIsolatedWatchdogSettings,
26
+ childWatchdogStatus, available, isAsyncAvailable, executeAsyncSingle,
27
+ executeAsyncChain, ASYNC_DIR, RESULTS_DIR, TEMP_ROOT_DIR, escapeRegExp,
28
+ createRepo, waitForAsyncResultFile, waitForAsyncState, tempDir, mockPi,
29
+ readAsyncPayload, waitForMockPiCall,
30
+ } from "../support/async-execution-fixture.ts";
31
+
32
+ describe("async execution utilities", { skip: !available ? "pi packages not available" : undefined }, () => {
33
+ installAsyncExecutionHooks();
34
+
35
+ it("coalesces ordinary background status while publishing per-child activity transitions", { timeout: 30_000, skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async (t) => {
36
+ const id = `async-coalescing-${Date.now().toString(36)}`;
37
+ const statusPath = path.join(ASYNC_DIR, id, "status.json");
38
+ const reportPath = path.join(tempDir, `${id}-report.json`);
39
+ const factoryPath = path.join(tempDir, `${id}-factory.mjs`);
40
+ // Exercise the detached production runner through its child-session boundary.
41
+ // Only external time and successful filesystem publications are instrumented.
42
+ fs.writeFileSync(factoryPath, `
43
+ import fs from "node:fs";
44
+ import { syncBuiltinESMExports } from "node:module";
45
+ import { mock } from "node:test";
46
+ const statusPath = ${JSON.stringify(statusPath)};
47
+ const reportPath = ${JSON.stringify(reportPath)};
48
+ const children = [];
49
+ const report = { samples: [], transitions: [], terminal: [] };
50
+ process.once("exit", () => {
51
+ fs.writeFileSync(reportPath + ".tmp", JSON.stringify(report));
52
+ fs.renameSync(reportPath + ".tmp", reportPath);
53
+ });
54
+ let writes = 0, bytes = 0, replayed = false, finished = false;
55
+ const read = () => JSON.parse(fs.readFileSync(statusPath, "utf8"));
56
+ const rename = fs.renameSync;
57
+ fs.renameSync = function(source, target) {
58
+ rename.call(this, source, target);
59
+ if (target === statusPath) {
60
+ writes++;
61
+ bytes += fs.statSync(target).size;
62
+ if (replayed && !finished && read().state !== "running") {
63
+ finished = true;
64
+ queueMicrotask(() => {
65
+ report.terminal.push(read());
66
+ mock.timers.tick(100);
67
+ report.terminal.push(read());
68
+ mock.timers.reset();
69
+ });
70
+ }
71
+ }
72
+ };
73
+ syncBuiltinESMExports();
74
+ function replay() {
75
+ mock.timers.enable({ apis: ["Date", "setTimeout"], now: Date.now() });
76
+ const emit = (index, event) => children[index].listener(event);
77
+ const stream = () => emit(1, { type: "message_update", assistantMessageEvent: { type: "text_delta", delta: "x" } });
78
+ const transition = (index, event) => {
79
+ const before = writes;
80
+ emit(index, event);
81
+ report.transitions.push({ writes: writes - before, status: read() });
82
+ };
83
+ const sample = (state) => {
84
+ const beforeWrites = writes, beforeBytes = bytes;
85
+ for (let i = 0; i < 1000; i++) { stream(); mock.timers.tick(10); }
86
+ mock.timers.tick(100);
87
+ report.samples.push({ state, writes: writes - beforeWrites, bytes: bytes - beforeBytes, now: Date.now(), status: read() });
88
+ };
89
+ const start = { type: "tool_execution_start", toolName: "contact_supervisor", toolCallId: "decision", args: { reason: "need_decision", message: "Choose" } };
90
+ const end = { type: "tool_execution_end", toolName: "contact_supervisor", toolCallId: "decision" };
91
+ sample("unset");
92
+ transition(1, { type: "message_end", message: { role: "assistant", content: [{ type: "text", text: "working" }], stopReason: "tool_use" } });
93
+ sample("active_long_running");
94
+ transition(0, start);
95
+ sample("needs_attention");
96
+ transition(1, start); // Aggregate attention is unchanged by either sibling transition.
97
+ transition(1, end);
98
+ transition(0, end);
99
+ const beforePending = writes;
100
+ stream(); // Leave an ordinary update pending when both children fail.
101
+ report.pendingWrites = writes - beforePending;
102
+ replayed = true;
103
+ for (const child of children) child.reject(new Error("scripted terminal failure"));
104
+ }
105
+ export default function() {
106
+ return {
107
+ async create() {
108
+ const child = {};
109
+ children.push(child);
110
+ return {
111
+ sessionId: "coalescing-" + children.length, sessionFile: undefined, modelId: undefined, messages: [],
112
+ subscribe(listener) { child.listener = listener; return () => {}; },
113
+ prompt() { return new Promise((resolve, reject) => { child.reject = reject; if (children.length === 2 && children.every(c => c.reject)) replay(); }); },
114
+ async steer() {}, async followUp() {}, async abort() {}, async dispose() {},
115
+ };
116
+ },
117
+ async dispose() {},
118
+ };
119
+ }
120
+ `);
121
+ const reportReady = new Promise<void>((resolve) => {
122
+ // libuv compares expanded event paths against the watched directory (Windows 8.3 aliases differ).
123
+ const watcher = fs.watch(fs.realpathSync.native(tempDir), () => {
124
+ if (fs.existsSync(reportPath)) resolve();
125
+ });
126
+ t.after(() => watcher.close());
127
+ });
128
+ setChildSessionFactoryModule(factoryPath);
129
+ t.after(() => setChildSessionFactoryModule(fileURLToPath(new URL("../support/runner-child-session-factory.ts", import.meta.url))));
130
+ const launched = executeAsyncChain(id, {
131
+ chain: [{ parallel: [{ agent: "worker", task: "A" }, { agent: "worker", task: "B" }] }],
132
+ agents: [makeAgent("worker")],
133
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
134
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
135
+ shareEnabled: false,
136
+ sessionRoot: path.join(tempDir, "sessions"),
137
+ maxSubagentDepth: 2,
138
+ controlConfig: { enabled: true, needsAttentionAfterMs: 999_999, activeNoticeAfterTurns: 1, activeNoticeAfterMs: 999_999, activeNoticeAfterTokens: 999_999, failedToolAttemptsBeforeAttention: 3, notifyOn: ["active_long_running", "needs_attention"], notifyChannels: ["event", "async"] },
139
+ });
140
+ assert.notEqual(launched.isError, true);
141
+ await reportReady;
142
+ const report = JSON.parse(fs.readFileSync(reportPath, "utf8"));
143
+ t.diagnostic(JSON.stringify(report.samples.map(({ state, writes, bytes }: { state: string; writes: number; bytes: number }) => ({ state, writes, bytes }))));
144
+ assert.deepEqual(report.samples.map((sample: { writes: number }) => sample.writes), [100, 100, 100]);
145
+ for (const sample of report.samples) {
146
+ assert.equal(sample.status.lastActivityAt, sample.now - 110);
147
+ assert.equal(sample.status.steps[1].lastActivityAt, sample.now - 110);
148
+ }
149
+ assert.deepEqual(report.transitions.map((entry: { writes: number }) => entry.writes), [1, 1, 1, 1, 1]);
150
+ assert.deepEqual(report.transitions.map((entry: { status: AsyncStatusPayload }) => entry.status.steps?.map(step => step.activityState)), [
151
+ [undefined, "active_long_running"], ["needs_attention", "active_long_running"],
152
+ ["needs_attention", "needs_attention"], ["needs_attention", "active_long_running"], [undefined, "active_long_running"],
153
+ ]);
154
+ assert.equal(report.samples[2].status.steps[1].turnCount, 1);
155
+ assert.equal(report.transitions[2].status.activityState, "needs_attention");
156
+ assert.equal(report.transitions[3].status.activityState, "needs_attention");
157
+ assert.equal(report.pendingWrites, 0);
158
+ assert.equal(report.terminal[0].state, "failed");
159
+ assert.deepEqual(report.terminal[1], report.terminal[0]);
160
+ });
161
+
162
+ for (const mode of ["success", "stop", "pause", "deadline", "failure-before-JSON"] as const) {
163
+ it(`background setup lifecycle: ${mode}`, { skip: !isAsyncAvailable() || process.platform === "win32" ? "requires real POSIX setup executable" : undefined, timeout: 25_000 }, async (t) => {
164
+ const repo = createRepo("pi-background-setup-");
165
+ const baseDir = createTempDir();
166
+ const id = `async-setup-${mode}-${Date.now().toString(36)}`;
167
+ const asyncDir = path.join(ASYNC_DIR, id);
168
+ const marker = path.join(repo, ".git", "child-launched");
169
+ const allocatorFailure = mode === "failure-before-JSON";
170
+ const oldPath = process.env.PATH;
171
+ const server = createServer();
172
+ server.listen(0, "127.0.0.1");
173
+ await once(server, "listening");
174
+ const { port } = server.address() as { port: number };
175
+ const hook = path.join(baseDir, allocatorFailure ? "wt" : "setup-hook.cjs");
176
+ fs.writeFileSync(hook, `#!${process.execPath}
177
+ const args = process.argv.slice(2);
178
+ if (args.includes('--version')) { console.log('wt v0.75.0'); process.exit(0); }
179
+ if (args.includes('--help')) { console.log('--create --base --no-cd --no-hooks --format'); process.exit(0); }
180
+ if (${allocatorFailure}) require('node:child_process').execFileSync('git', ['branch', args[args.indexOf('--create') + 1]], { cwd: ${JSON.stringify(repo)} });
181
+ const socket = require('node:net').connect(${port}, '127.0.0.1', () => socket.write('ready'));
182
+ socket.on('data', data => {
183
+ if (data.toString() === 'release') { socket.end(); if (${allocatorFailure}) process.exitCode = 1; else console.log('{}'); }
184
+ });
185
+ setTimeout(() => process.exit(90), 15000).unref();
186
+ `, { mode: 0o755 });
187
+ if (allocatorFailure) process.env.PATH = `${baseDir}${path.delimiter}${oldPath}`;
188
+ const bus = createEventBus();
189
+ let closed = false;
190
+ const terminal = new Promise<unknown>((resolve) => bus.on(SUBAGENT_PROCESS_TERMINAL_EVENT, (proof) => { closed = true; resolve(proof); }));
191
+ let socket: Socket | undefined;
192
+ let started = false;
193
+ try {
194
+ mockPi.onCall({ output: "finite setup completed", writeFiles: [{ path: marker, content: "launched" }] });
195
+ const connection = once(server, "connection", { signal: AbortSignal.timeout(20_000) });
196
+ const receipt = executeAsyncChain(id, {
197
+ chain: [{ agent: "worker", task: "Do work", worktree: true }],
198
+ agents: [makeAgent("worker", { completionGuard: false })],
199
+ ctx: { pi: { events: bus }, cwd: repo, currentSessionId: "session-1" },
200
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
201
+ shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, acceptance: false,
202
+ ...(allocatorFailure ? { worktreeProvider: "worktrunk" } : { worktreeProvider: "native", worktreeBaseDir: path.join(baseDir, "trees"), worktreeSetupHook: hook }),
203
+ ...(mode === "deadline" ? { timeoutMs: 4_000 } : {}),
204
+ });
205
+ assert.equal(receipt.isError, undefined, receipt.content[0]?.text);
206
+ started = true;
207
+ [socket] = await Promise.race([connection, terminal.then((proof) => { throw new Error(`Runner closed before setup ready: ${JSON.stringify(proof)}`); })]) as [Socket];
208
+ assert.equal((await once(socket, "data"))[0].toString(), "ready");
209
+ const held = await waitForAsyncState(id, (status) => Boolean(status.parallelHandoff));
210
+ assert.equal(closed, false);
211
+ assert.equal(mockPi.callCount(), 0);
212
+ assert.equal(fs.existsSync(marker), false);
213
+ if (mode === "stop") deliverStopRequest({ asyncDir, source: "test" });
214
+ else if (mode === "pause") deliverInterruptRequest({ asyncDir, source: "test" });
215
+ else if (mode !== "deadline") socket.write("release");
216
+ const payload = await readAsyncPayload(id);
217
+ const proof = await terminal;
218
+ const status = JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf8"));
219
+ const expected = mode === "success" ? "complete" : mode === "stop" ? "stopped" : mode === "pause" ? "paused" : "failed";
220
+ if (payload.state !== expected || status.state !== expected || status.steps?.[0]?.status !== expected) {
221
+ // Failure-only, bounded projections: never print prompts, output, paths or raw stderr.
222
+ const record = (value: unknown): Record<string, unknown> => value !== null && typeof value === "object" ? value as Record<string, unknown> : {};
223
+ const token = (value: unknown) => typeof value === "string" && /^[a-z_-]{1,64}$/i.test(value) ? value : undefined;
224
+ const errors = (value: unknown) => {
225
+ const text = typeof value === "string" ? value.slice(0, 8192) : "";
226
+ return { present: Boolean(text), kinds: [
227
+ "EPIPE", "ENOENT", "EACCES", "ENOSPC", "ETIMEDOUT", "ABORT_ERR", "ENOBUFS",
228
+ "setup aborted", "setup deadline", "setup settlement unknown", "process tree settlement",
229
+ "empty stdout", "invalid JSON", "hook failed", "manual reconciliation required",
230
+ "not a git repository", "index.lock", "already exists", "Failed to write result",
231
+ "No model", "model not found", "completion", "acceptance",
232
+ ].filter((kind) => text.toLowerCase().includes(kind.toLowerCase())), exitCode: text.match(/exit (?:code |status )?(-?\d+)/i)?.[1] };
233
+ };
234
+ const project = (value: unknown) => {
235
+ const item = record(value);
236
+ return { state: token(item.state), status: token(item.status), reason: token(item.reason),
237
+ exitCode: typeof item.exitCode === "number" ? item.exitCode : undefined,
238
+ success: typeof item.success === "boolean" ? item.success : undefined,
239
+ timedOut: item.timedOut === true, stopped: item.stopped === true, error: errors(item.error) };
240
+ };
241
+ let cleanup: unknown;
242
+ try {
243
+ const handoffPath = held.parallelHandoff?.path;
244
+ if (handoffPath && fs.statSync(handoffPath).size <= 65_536) {
245
+ const handoff = JSON.parse(fs.readFileSync(handoffPath, "utf8"));
246
+ cleanup = (handoff.groups ?? []).slice(0, 2).map((group: { cleanup?: { state?: string; pruned?: boolean; tasks?: unknown[]; errors?: string[] } }) => ({
247
+ state: token(group.cleanup?.state), pruned: group.cleanup?.pruned,
248
+ taskCount: group.cleanup?.tasks?.length, errors: group.cleanup?.errors?.slice(0, 4).map(errors),
249
+ }));
250
+ }
251
+ } catch (error) { cleanup = { readError: token(record(error).code) }; }
252
+ t.diagnostic(JSON.stringify({ mode, expected, mockCalls: mockPi.callCount(), markerPresent: fs.existsSync(marker),
253
+ result: project(payload), status: project(status), steps: (status.steps ?? []).slice(0, 2).map(project),
254
+ children: (payload.results ?? []).slice(0, 2).map(project), cleanup, processTerminal: project(proof) }));
255
+ }
256
+ assert.equal(payload.state, expected);
257
+ assert.equal(status.state, expected);
258
+ assert.equal(status.steps[0].status, expected);
259
+ assert.equal(payload.success, mode === "success");
260
+ assert.equal(mockPi.callCount(), mode === "success" ? 1 : 0);
261
+ assert.equal(fs.existsSync(marker), mode === "success");
262
+ assert.ok(held.parallelHandoff?.path);
263
+ const handoff = JSON.parse(fs.readFileSync(held.parallelHandoff.path, "utf8"));
264
+ const group = handoff.groups[0];
265
+ if (allocatorFailure) {
266
+ assert.equal(status.steps[0].worktreePath, undefined);
267
+ assert.equal(status.steps[0].branch, undefined);
268
+ assert.deepEqual(group.cleanup.tasks, []);
269
+ assert.deepEqual(group.children, []);
270
+ assert.equal(group.cleanup.state, "partial");
271
+ assert.equal(group.cleanup.pruned, false);
272
+ assert.match(group.cleanup.errors.join("\n"), /manual reconciliation required/);
273
+ const attempt = group.cleanup.errors.find((entry: string) => entry.startsWith("Allocation attempt "));
274
+ const evidence = JSON.parse(attempt.slice("Allocation attempt ".length));
275
+ assert.equal(evidence.path, null);
276
+ assert.equal(evidence.validated, false);
277
+ assert.equal(evidence.command.status, 1);
278
+ assert.equal(evidence.command.processTree, "unknown");
279
+ assert.equal(execFileSync("git", ["branch", "--list", evidence.branch], { cwd: repo, encoding: "utf8" }).trim(), evidence.branch);
280
+ const persisted = JSON.parse(fs.readFileSync(path.join(asyncDir, "process-terminal.json"), "utf8"));
281
+ assert.equal(persisted.state, "unknown");
282
+ assert.equal(persisted.reason, "process-tree-unverified");
283
+ assert.deepEqual(proof, persisted, "actual runner close must not promote setup unknown with zero child writers");
284
+ const candidate = JSON.parse(fs.readFileSync(path.join(asyncDir, "process-terminal-candidate.json"), "utf8"));
285
+ assert.deepEqual(Object.values(candidate.writers).flat(), []);
286
+ assert.deepEqual(Object.values(candidate.expectedWriters), [0]);
287
+ } else if (mode === "deadline") {
288
+ assert.equal(payload.timedOut, true);
289
+ assert.equal(status.timedOut, true);
290
+ assert.ok(payload.deadlineAt! <= Date.now());
291
+ assert.equal(group.cleanup.pruned, false);
292
+ assert.equal(group.cleanup.tasks[0].preserved, true);
293
+ assert.equal(fs.existsSync(group.cleanup.tasks[0].path), true);
294
+ } else {
295
+ assert.equal(group.cleanup.state, "complete");
296
+ assert.equal(group.cleanup.tasks.length, 1);
297
+ assert.equal(group.cleanup.tasks[0].worktreeRemoved, true);
298
+ assert.equal(group.cleanup.tasks[0].branchRemoved, true);
299
+ assert.equal(fs.existsSync(group.cleanup.tasks[0].path), false);
300
+ assert.equal(execFileSync("git", ["branch", "--list", group.cleanup.tasks[0].branch], { cwd: repo, encoding: "utf8" }).trim(), "");
301
+ }
302
+ } finally {
303
+ socket?.end("release");
304
+ if (started && !closed) { deliverStopRequest({ asyncDir, source: "test-cleanup" }); await terminal; }
305
+ socket?.destroy();
306
+ await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
307
+ if (oldPath === undefined) delete process.env.PATH; else process.env.PATH = oldPath;
308
+ removeTempDir(baseDir);
309
+ removeTempDir(repo);
310
+ }
311
+ });
312
+ }
313
+
314
+ it("does not start child work when initial async status cannot be written", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
315
+ const id = `async-status-write-fail-${Date.now().toString(36)}`;
316
+ fs.mkdirSync(path.join(ASYNC_DIR, id, "status.json"), { recursive: true });
317
+ mockPi.onCall({ output: "must not run" });
318
+
319
+ const result = executeAsyncSingle(id, {
320
+ agent: "worker",
321
+ task: "Do not start",
322
+ agentConfig: makeAgent("worker"),
323
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
324
+ artifactConfig: {
325
+ enabled: false,
326
+ includeInput: false,
327
+ includeOutput: false,
328
+ includeJsonl: false,
329
+ includeMetadata: false,
330
+ cleanupDays: 7,
331
+ },
332
+ shareEnabled: false,
333
+ sessionRoot: path.join(tempDir, "sessions"),
334
+ maxSubagentDepth: 2,
335
+ });
336
+
337
+ assert.equal(result.isError, true);
338
+ assert.match(result.content[0]?.text ?? "", /Failed to persist initial async status/);
339
+ await new Promise((resolve) => setTimeout(resolve, 300));
340
+ assert.equal(mockPi.callCount(), 0);
341
+ });
342
+
343
+ it("returns a tool error when an async run uses a missing cwd", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, () => {
344
+ const id = `async-missing-cwd-${Date.now().toString(36)}`;
345
+ const missingCwd = path.join(tempDir, "missing-cwd");
346
+
347
+ const singleResult = executeAsyncSingle(id, {
348
+ agent: "worker",
349
+ task: "Do work",
350
+ agentConfig: makeAgent("worker"),
351
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
352
+ cwd: missingCwd,
353
+ artifactConfig: {
354
+ enabled: false,
355
+ includeInput: false,
356
+ includeOutput: false,
357
+ includeJsonl: false,
358
+ includeMetadata: false,
359
+ cleanupDays: 7,
360
+ },
361
+ shareEnabled: false,
362
+ sessionRoot: path.join(tempDir, "sessions"),
363
+ maxSubagentDepth: 2,
364
+ });
365
+
366
+ assert.equal(singleResult.isError, true);
367
+ assert.match(singleResult.content[0]?.text ?? "", /Failed to start async run/);
368
+ assert.match(singleResult.content[0]?.text ?? "", /cwd does not exist/);
369
+
370
+ const chainId = `async-missing-cwd-chain-${Date.now().toString(36)}`;
371
+ const chainResult = executeAsyncChain(chainId, {
372
+ chain: [{ agent: "worker", task: "Do work" }],
373
+ agents: [makeAgent("worker")],
374
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
375
+ cwd: missingCwd,
376
+ artifactConfig: {
377
+ enabled: false,
378
+ includeInput: false,
379
+ includeOutput: false,
380
+ includeJsonl: false,
381
+ includeMetadata: false,
382
+ cleanupDays: 7,
383
+ },
384
+ shareEnabled: false,
385
+ sessionRoot: path.join(tempDir, "sessions"),
386
+ maxSubagentDepth: 2,
387
+ });
388
+
389
+ assert.equal(chainResult.isError, true);
390
+ assert.match(chainResult.content[0]?.text ?? "", /Failed to start async chain/);
391
+ assert.match(chainResult.content[0]?.text ?? "", /cwd does not exist/);
392
+ });
393
+
394
+ it("returns a tool error when the async runner process cannot spawn", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, () => {
395
+ const originalExecPath = process.execPath;
396
+ const pathKey = process.platform === "win32" ? "Path" : "PATH";
397
+ const originalPath = process.env[pathKey];
398
+ process.execPath = path.join(tempDir, process.platform === "win32" ? "pi.exe" : "pi");
399
+ process.env[pathKey] = tempDir;
400
+ try {
401
+ const id = `async-spawn-fail-${Date.now().toString(36)}`;
402
+ const result = executeAsyncSingle(id, {
403
+ agent: "worker",
404
+ task: "Do work",
405
+ agentConfig: makeAgent("worker"),
406
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
407
+ artifactConfig: {
408
+ enabled: false,
409
+ includeInput: false,
410
+ includeOutput: false,
411
+ includeJsonl: false,
412
+ includeMetadata: false,
413
+ cleanupDays: 7,
414
+ },
415
+ shareEnabled: false,
416
+ sessionRoot: path.join(tempDir, "sessions"),
417
+ maxSubagentDepth: 2,
418
+ });
419
+
420
+ assert.equal(result.isError, true);
421
+ assert.match(result.content[0]?.text ?? "", /Failed to start async run/);
422
+ assert.match(result.content[0]?.text ?? "", /async runner did not produce a pid/);
423
+ } finally {
424
+ process.execPath = originalExecPath;
425
+ if (originalPath === undefined) {
426
+ delete process.env[pathKey];
427
+ } else {
428
+ process.env[pathKey] = originalPath;
429
+ }
430
+ }
431
+ });
432
+
433
+ it("returns a tool error when an async chain cannot write its detached runner config", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, () => {
434
+ const id = `async-chain-write-fail-${Date.now().toString(36)}`;
435
+ assert.ok(TEMP_ROOT_DIR, "TEMP_ROOT_DIR should be available for async tests");
436
+ fs.mkdirSync(TEMP_ROOT_DIR, { recursive: true });
437
+ fs.mkdirSync(path.join(TEMP_ROOT_DIR, `async-cfg-${id}.json`), { recursive: true });
438
+
439
+ const result = executeAsyncChain(id, {
440
+ chain: [{ agent: "worker", task: "Do work" }],
441
+ agents: [makeAgent("worker")],
442
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
443
+ artifactConfig: {
444
+ enabled: false,
445
+ includeInput: false,
446
+ includeOutput: false,
447
+ includeJsonl: false,
448
+ includeMetadata: false,
449
+ cleanupDays: 7,
450
+ },
451
+ shareEnabled: false,
452
+ sessionRoot: path.join(tempDir, "sessions"),
453
+ maxSubagentDepth: 2,
454
+ });
455
+
456
+ assert.equal(result.isError, true);
457
+ assert.match(result.content[0]?.text ?? "", /Failed to start async chain/);
458
+ assert.match(result.content[0]?.text ?? "", /async-cfg-/);
459
+ });
460
+
461
+ it("background ignores child watchdog status when child watchdogs are not configured", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
462
+ await withIsolatedWatchdogSettings(tempDir, async () => {
463
+ const id = `async-watchdog-unconfigured-${Date.now().toString(36)}`;
464
+ const terminalReleasePath = path.join(tempDir, `${id}-release`);
465
+ mockPi.onCall({
466
+ steps: [{ waitForPath: terminalReleasePath, jsonl: [events.assistantMessage("async-done-without-watchdog-config"), childWatchdogStatus(id, "reviewing", 1)] }],
467
+ keepAliveAfterFinalMessageMs: 10000,
468
+ });
469
+
470
+ executeAsyncSingle(id, {
471
+ agent: "worker",
472
+ task: "Do work",
473
+ agentConfig: makeAgent("worker"),
474
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
475
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
476
+ shareEnabled: false,
477
+ sessionRoot: path.join(tempDir, "sessions"),
478
+ maxSubagentDepth: 2,
479
+ });
480
+
481
+ await waitForMockPiCall(mockPi, 0);
482
+ // Measure final drain, not detached runner startup; terminal emission is still gated.
483
+ const start = Date.now();
484
+ fs.writeFileSync(terminalReleasePath, "release", "utf-8");
485
+ const resultPath = await waitForAsyncResultFile(id, 10_000);
486
+ const elapsed = Date.now() - start;
487
+ const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
488
+ assert.ok(elapsed < 6000, `unconfigured watchdog status should not delay async final drain, took ${elapsed}ms`);
489
+ assert.equal(payload.success, true);
490
+ assert.equal(payload.results[0]?.output, "async-done-without-watchdog-config");
491
+ assert.equal((payload.results[0] as { watchdog?: unknown }).watchdog, undefined);
492
+ });
493
+ });
494
+
495
+ it("background final-drain waits for child watchdog settlement", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
496
+ await withIsolatedWatchdogSettings(tempDir, async () => {
497
+ writeWatchdogSettings(tempDir);
498
+ const id = `async-watchdog-drain-${Date.now().toString(36)}`;
499
+ mockPi.onCall({
500
+ steps: [
501
+ { jsonl: [events.assistantMessage("async-done-before-watchdog"), childWatchdogStatus(id, "reviewing", 1)] },
502
+ { delay: 1400, jsonl: [childWatchdogStatus(id, "idle", 2)] },
503
+ ],
504
+ keepAliveAfterFinalMessageMs: 10000,
505
+ });
506
+
507
+ const start = Date.now();
508
+ executeAsyncSingle(id, {
509
+ agent: "worker",
510
+ task: "Do work",
511
+ agentConfig: makeAgent("worker"),
512
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
513
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
514
+ shareEnabled: false,
515
+ sessionRoot: path.join(tempDir, "sessions"),
516
+ maxSubagentDepth: 2,
517
+ });
518
+
519
+ const resultPath = await waitForAsyncResultFile(id, 10_000);
520
+ const elapsed = Date.now() - start;
521
+ const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
522
+ assert.ok(elapsed >= 1200, `watchdog settlement should delay async final drain, took ${elapsed}ms`);
523
+ assert.ok(elapsed < 9000, `settled watchdog should still allow async cleanup, took ${elapsed}ms`);
524
+ assert.equal(payload.success, true);
525
+ assert.equal(payload.results[0]?.output, "async-done-before-watchdog");
526
+ assert.equal((payload.results[0] as { watchdog?: { phase?: string } }).watchdog?.phase, "idle");
527
+ });
528
+ });
529
+
530
+ it("background child watchdog tail timeout still finalizes successful output", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
531
+ await withIsolatedWatchdogSettings(tempDir, async () => {
532
+ writeWatchdogSettings(tempDir, 150);
533
+ const id = `async-watchdog-timeout-${Date.now().toString(36)}`;
534
+ mockPi.onCall({
535
+ jsonl: [events.assistantMessage("async-done-before-watchdog-timeout"), childWatchdogStatus(id, "reviewing", 1)],
536
+ keepAliveAfterFinalMessageMs: 10000,
537
+ });
538
+
539
+ const start = Date.now();
540
+ executeAsyncSingle(id, {
541
+ agent: "worker",
542
+ task: "Do work",
543
+ agentConfig: makeAgent("worker"),
544
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
545
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
546
+ shareEnabled: false,
547
+ sessionRoot: path.join(tempDir, "sessions"),
548
+ maxSubagentDepth: 2,
549
+ });
550
+
551
+ const resultPath = await waitForAsyncResultFile(id, 10_000);
552
+ const elapsed = Date.now() - start;
553
+ const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
554
+ assert.ok(elapsed < 6000, `watchdog tail fallback should not hang async final drain, took ${elapsed}ms`);
555
+ assert.equal(payload.success, true);
556
+ assert.equal(payload.results[0]?.output, "async-done-before-watchdog-timeout");
557
+ const watchdog = (payload.results[0] as { watchdog?: { phase?: string; timedOut?: boolean } }).watchdog;
558
+ assert.equal(watchdog?.phase, "stale");
559
+ assert.equal(watchdog?.timedOut, true);
560
+ });
561
+ });
562
+
563
+ it("background runs carry unaddressed child watchdog blockers into the result payload and acceptance", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
564
+ await withIsolatedWatchdogSettings(tempDir, async () => {
565
+ writeWatchdogSettings(tempDir);
566
+ const id = `async-watchdog-blocker-${Date.now().toString(36)}`;
567
+ mockPi.onCall({ jsonl: [events.acceptanceReport(), events.watchdogWarning("blocker", "Claims tests passed without running them")] });
568
+
569
+ executeAsyncSingle(id, {
570
+ agent: "worker",
571
+ task: "Do work",
572
+ agentConfig: makeAgent("worker"),
573
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
574
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
575
+ shareEnabled: false,
576
+ sessionRoot: path.join(tempDir, "sessions"),
577
+ maxSubagentDepth: 2,
578
+ acceptance: { level: "checked", criteria: ["Ship it"] },
579
+ });
580
+
581
+ const resultPath = await waitForAsyncResultFile(id, 10_000);
582
+ const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
583
+ assert.equal(payload.success, false);
584
+ const child = payload.results[0] as AsyncResultPayload["results"][number] & { watchdog?: { warnings?: Array<{ severity: string; addressed: boolean; summary: string }> } };
585
+ assert.deepEqual(child.watchdog?.warnings?.map((warning) => [warning.severity, warning.addressed]), [["blocker", false]]);
586
+ const check = child.acceptance?.runtimeChecks?.find((entry) => entry.id === "watchdog-blocker");
587
+ assert.equal(check?.status, "failed");
588
+ assert.match(check?.message ?? "", /Unresolved watchdog blocker/);
589
+ });
590
+ });
591
+
592
+ it("background forced drain after final assistant output is cleanup success", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
593
+ mockPi.onCall({
594
+ jsonl: [events.assistantMessage("async-done-before-drain")],
595
+ stderr: "Done after 1 turn(s). Ready for input.\n",
596
+ keepAliveAfterFinalMessageMs: 10000,
597
+ });
598
+
599
+ const id = `async-final-drain-${Date.now().toString(36)}`;
600
+ const resultPath = path.join(RESULTS_DIR, `${id}.json`);
601
+ const sessionRoot = path.join(tempDir, "sessions");
602
+
603
+ const start = Date.now();
604
+ executeAsyncSingle(id, {
605
+ agent: "worker",
606
+ task: "Do work",
607
+ agentConfig: makeAgent("worker"),
608
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
609
+ artifactConfig: {
610
+ enabled: false,
611
+ includeInput: false,
612
+ includeOutput: false,
613
+ includeJsonl: false,
614
+ includeMetadata: false,
615
+ cleanupDays: 7,
616
+ },
617
+ shareEnabled: false,
618
+ sessionRoot,
619
+ maxSubagentDepth: 2,
620
+ });
621
+
622
+ const deadline = Date.now() + 10_000;
623
+ while (!fs.existsSync(resultPath)) {
624
+ if (Date.now() > deadline) {
625
+ assert.fail(`Timed out waiting for async result file: ${resultPath}`);
626
+ }
627
+ await new Promise((resolve) => setTimeout(resolve, 100));
628
+ }
629
+
630
+ const elapsed = Date.now() - start;
631
+ const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
632
+ assert.ok(elapsed < 9000, `should clean up async child before the mock's natural keepalive exit, took ${elapsed}ms`);
633
+ assert.equal(payload.success, true);
634
+ assert.equal(payload.exitCode, 0);
635
+ assert.equal(payload.results[0].success, true);
636
+ assert.equal(payload.results[0].output, "async-done-before-drain");
637
+ });
638
+
639
+ it("background forced drain after empty terminal assistant output is cleanup success", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
640
+ mockPi.onCall({
641
+ jsonl: [events.assistantMessage("")],
642
+ keepAliveAfterFinalMessageMs: 10000,
643
+ });
644
+
645
+ const id = `async-final-drain-empty-${Date.now().toString(36)}`;
646
+ const resultPath = path.join(RESULTS_DIR, `${id}.json`);
647
+
648
+ const start = Date.now();
649
+ executeAsyncSingle(id, {
650
+ agent: "scout",
651
+ task: "Inspect something",
652
+ agentConfig: makeAgent("scout"),
653
+ acceptance: { level: "attested", criteria: ["Finish cleanly"] },
654
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
655
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
656
+ shareEnabled: false,
657
+ sessionRoot: path.join(tempDir, "sessions"),
658
+ maxSubagentDepth: 2,
659
+ });
660
+
661
+ const deadline = Date.now() + 10_000;
662
+ while (!fs.existsSync(resultPath)) {
663
+ if (Date.now() > deadline) assert.fail(`Timed out waiting for async result file: ${resultPath}`);
664
+ await new Promise((resolve) => setTimeout(resolve, 100));
665
+ }
666
+
667
+ const elapsed = Date.now() - start;
668
+ const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
669
+ assert.ok(elapsed < 9000, `should clean up async child before the mock's natural keepalive exit, took ${elapsed}ms`);
670
+ assert.equal(payload.success, true);
671
+ assert.equal(payload.exitCode, 0);
672
+ assert.equal(payload.results[0].success, true);
673
+ assert.equal(payload.results[0].output, "");
674
+ });
675
+
676
+ it("background final-drain cleanup preserves explicit assistant errors", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
677
+ mockPi.onCall({
678
+ jsonl: [{
679
+ type: "message_end",
680
+ message: {
681
+ role: "assistant",
682
+ content: [{ type: "text", text: "failed" }],
683
+ model: "mock/test-model",
684
+ stopReason: "stop",
685
+ errorMessage: "provider exploded",
686
+ usage: { input: 100, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0.001 } },
687
+ },
688
+ }],
689
+ keepAliveAfterFinalMessageMs: 10000,
690
+ });
691
+
692
+ const id = `async-final-drain-error-${Date.now().toString(36)}`;
693
+ const resultPath = path.join(RESULTS_DIR, `${id}.json`);
694
+
695
+ executeAsyncSingle(id, {
696
+ agent: "worker",
697
+ task: "Do work",
698
+ agentConfig: makeAgent("worker"),
699
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
700
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
701
+ shareEnabled: false,
702
+ sessionRoot: path.join(tempDir, "sessions"),
703
+ maxSubagentDepth: 2,
704
+ });
705
+
706
+ const deadline = Date.now() + 10_000;
707
+ while (!fs.existsSync(resultPath)) {
708
+ if (Date.now() > deadline) assert.fail(`Timed out waiting for async result file: ${resultPath}`);
709
+ await new Promise((resolve) => setTimeout(resolve, 100));
710
+ }
711
+
712
+ const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
713
+ assert.equal(payload.success, false);
714
+ assert.equal(payload.exitCode, 1);
715
+ assert.equal(payload.results[0].success, false);
716
+ assert.equal(payload.results[0].error, "provider exploded");
717
+ });
718
+
719
+ it("reports terminal abort before a missing file-only handoff after mutation", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
720
+ const partialOutput = "I’ll inspect the retained candidate before changing it.";
721
+ const repo = createRepo("pi-subagents-missing-handoff-partial-");
722
+ const outputPath = path.join(repo, "missing-challenge-report.md");
723
+ mockPi.onCall({
724
+ jsonl: [
725
+ events.assistantMessage(partialOutput),
726
+ {
727
+ type: "message_end",
728
+ message: {
729
+ role: "assistant",
730
+ content: [],
731
+ model: "mock/test-model",
732
+ stopReason: "aborted",
733
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0 } },
734
+ },
735
+ },
736
+ ],
737
+ writeFiles: [{ path: "input.md", content: "changed by retained child\n" }],
738
+ });
739
+
740
+ const task = [
741
+ "You are reviving a previous subagent conversation.",
742
+ "",
743
+ "Original run: source-run",
744
+ "Original agent: worker",
745
+ "Original session file: /tmp/source-session.jsonl",
746
+ "",
747
+ "Use the stored session context as background. Answer the orchestrator's follow-up below. Do not assume the original child session is still running.",
748
+ "",
749
+ "Follow-up:",
750
+ "Implementation challenge pass 1 for the accepted candidate. Reconsider it and implement any better current-scope change.",
751
+ ].join("\n");
752
+ const id = `async-missing-handoff-guard-${Date.now().toString(36)}`;
753
+ try {
754
+ executeAsyncSingle(id, {
755
+ agent: "worker",
756
+ task,
757
+ agentConfig: makeAgent("worker"),
758
+ ctx: { pi: { events: { emit() {} } }, cwd: repo, currentSessionId: "session-1" },
759
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
760
+ shareEnabled: false,
761
+ sessionRoot: path.join(tempDir, "sessions"),
762
+ output: outputPath,
763
+ outputMode: "file-only",
764
+ maxSubagentDepth: 2,
765
+ });
766
+
767
+ const resultPath = await waitForAsyncResultFile(id);
768
+ const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
769
+ const child = payload.results[0];
770
+ const diagnostic = child?.error ?? "";
771
+ assert.equal(payload.success, false);
772
+ assert.equal(payload.state, "partial");
773
+ assert.equal(child?.success, false);
774
+ assert.match(diagnostic, /^Subagent produced no output after terminal assistant stopReason "aborted"\./);
775
+ assert.match(diagnostic, new RegExp(`Required file-only output was not produced: ${escapeRegExp(outputPath)}`));
776
+ assert.doesNotMatch(diagnostic, /completed without making edits/);
777
+ assert.doesNotMatch(child?.modelAttempts?.[0]?.error ?? "", /completed without making edits/);
778
+ assert.equal(child?.effects?.fileMutation?.status, "observed");
779
+ assert.equal(child?.effects?.fileMutation?.attempted, true);
780
+ assert.deepEqual(child?.effects?.fileMutation?.evidence?.changedFiles, ["input.md"]);
781
+ assert.equal(child?.effects?.settlementDiagnostic?.requiredOutput?.missing, true);
782
+ assert.equal(fs.existsSync(outputPath), false);
783
+
784
+ const status = await waitForAsyncState(id, (candidate) => candidate.state === "partial");
785
+ assert.equal(status.activityState, "needs_attention");
786
+ assert.equal(status.steps?.[0]?.activityState, "needs_attention");
787
+ assert.equal(status.steps?.[0]?.error, diagnostic);
788
+ const eventsText = fs.readFileSync(path.join(ASYNC_DIR, id, "events.jsonl"), "utf-8");
789
+ assert.doesNotMatch(eventsText, /completed without making edits/);
790
+ } finally {
791
+ removeTempDir(repo);
792
+ }
793
+ });
794
+
795
+ it("preserves terminal empty-output diagnostics after useful child work", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
796
+ const partialOutput = "I’ll inspect the retained candidate before changing it.";
797
+ const outputPath = path.join(tempDir, "missing-aborted-report.md");
798
+ mockPi.onCall({
799
+ jsonl: [
800
+ events.toolStart("read", { path: "src/index.ts" }),
801
+ events.toolEnd("read"),
802
+ events.toolResult("read", "file contents"),
803
+ events.assistantMessage(partialOutput),
804
+ {
805
+ type: "message_end",
806
+ message: {
807
+ role: "assistant",
808
+ content: [],
809
+ model: "mock/test-model",
810
+ stopReason: "aborted",
811
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0 } },
812
+ },
813
+ },
814
+ ],
815
+ exitCode: 0,
816
+ });
817
+
818
+ const id = `async-aborted-empty-handoff-${Date.now().toString(36)}`;
819
+ executeAsyncSingle(id, {
820
+ agent: "worker",
821
+ task: "Implement the approved file changes",
822
+ agentConfig: makeAgent("worker"),
823
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
824
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
825
+ shareEnabled: false,
826
+ sessionRoot: path.join(tempDir, "sessions"),
827
+ output: outputPath,
828
+ outputMode: "file-only",
829
+ maxSubagentDepth: 2,
830
+ });
831
+
832
+ const resultPath = await waitForAsyncResultFile(id);
833
+ const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
834
+ const child = payload.results[0];
835
+ const diagnostic = child?.error ?? "";
836
+ assert.equal(payload.success, false);
837
+ assert.equal(child?.success, false);
838
+ assert.match(diagnostic, /^Subagent produced no output after terminal assistant stopReason "aborted"\./);
839
+ assert.match(diagnostic, /Required file-only output was not produced/);
840
+ assert.doesNotMatch(diagnostic, /completed without making edits/);
841
+ assert.doesNotMatch(child?.modelAttempts?.[0]?.error ?? "", /completed without making edits/);
842
+ assert.equal(child?.effects?.settlementDiagnostic?.requiredOutput?.missing, true);
843
+ assert.equal(child?.effects?.settlementDiagnostic?.finalTextPresent, true);
844
+ assert.equal(fs.existsSync(outputPath), false);
845
+
846
+ const eventsText = fs.readFileSync(path.join(ASYNC_DIR, id, "events.jsonl"), "utf-8");
847
+ assert.doesNotMatch(eventsText, /completed without making edits/);
848
+ });
849
+
850
+ it("reports bounded compaction failure context when file-only output is missing", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
851
+ const terminalError = `This operation was aborted\n${"x".repeat(12_000)}`;
852
+ mockPi.onCall({
853
+ jsonl: [
854
+ events.toolStart("read", { path: "src/index.ts" }),
855
+ events.toolEnd("read"),
856
+ events.toolResult("read", "file contents"),
857
+ { type: "compaction_start" },
858
+ {
859
+ type: "message_end",
860
+ message: {
861
+ role: "assistant",
862
+ content: [],
863
+ model: "mock/test-model",
864
+ stopReason: "error",
865
+ errorMessage: terminalError,
866
+ usage: { input: 100, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0.001 } },
867
+ },
868
+ },
869
+ { type: "agent_settled" },
870
+ ],
871
+ exitCode: 0,
872
+ });
873
+
874
+ const id = `async-compaction-file-only-error-${Date.now().toString(36)}`;
875
+ const asyncDir = path.join(ASYNC_DIR, id);
876
+ const outputPath = path.join(tempDir, "missing-oracle-report.md");
877
+ executeAsyncSingle(id, {
878
+ agent: "oracle",
879
+ task: "Write a report",
880
+ agentConfig: makeAgent("oracle"),
881
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
882
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
883
+ shareEnabled: false,
884
+ sessionRoot: path.join(tempDir, "sessions"),
885
+ output: outputPath,
886
+ outputMode: "file-only",
887
+ maxSubagentDepth: 2,
888
+ });
889
+
890
+ const resultPath = await waitForAsyncResultFile(id);
891
+ const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
892
+ const child = payload.results[0] as (AsyncResultPayload["results"][number] & Record<string, unknown>) | undefined;
893
+ const diagnostic = child?.error ?? "";
894
+ assert.equal(payload.success, false);
895
+ assert.equal(child?.success, false);
896
+ assert.match(diagnostic, /^This operation was aborted/);
897
+ assert.match(diagnostic, /Compaction-induced child abort could not be resumed safely: retained session unavailable\./);
898
+ assert.match(diagnostic, /failure followed session compaction and agent settlement/);
899
+ assert.match(diagnostic, /Required file-only output was not produced/);
900
+ assert.ok(diagnostic.length <= 8_192);
901
+ assert.equal(fs.existsSync(outputPath), false);
902
+ assert.equal(child?.output, "");
903
+ assert.equal("savedOutputPath" in (child ?? {}), false);
904
+ assert.equal("outputReference" in (child ?? {}), false);
905
+ assert.equal(payload.summary, `oracle:\n${diagnostic}`);
906
+ const status = await waitForAsyncState(id, (candidate) => candidate.state === "failed");
907
+ assert.equal(status.steps?.[0]?.exitCode, 1);
908
+ assert.equal(status.steps?.[0]?.error, diagnostic);
909
+ const logPath = path.join(asyncDir, `subagent-log-${id}.md`);
910
+ const deadline = Date.now() + 10_000;
911
+ while (!fs.existsSync(logPath)) {
912
+ if (Date.now() > deadline) assert.fail(`Timed out waiting for async run log: ${logPath}`);
913
+ await new Promise((resolve) => setTimeout(resolve, 50));
914
+ }
915
+ assert.ok(fs.readFileSync(logPath, "utf-8").includes(`## Summary\noracle:\n${diagnostic}`));
916
+ });
917
+
918
+ it("preserves partial imported async roots with mutation evidence", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
919
+ const sourceId = `partial-source-${Date.now().toString(36)}`;
920
+ const sourceDir = path.join(ASYNC_DIR, sourceId);
921
+ const message = "Required file-only output was not produced: report.md";
922
+ const effects = { fileMutation: { status: "observed", expected: true, attempted: true, evidence: { source: "tracked-files", trackedOnly: true, cwd: tempDir, changedFiles: ["input.md"], attemptedMutation: true } } };
923
+ fs.mkdirSync(sourceDir, { recursive: true });
924
+ fs.writeFileSync(path.join(sourceDir, "status.json"), JSON.stringify({
925
+ runId: sourceId,
926
+ mode: "single",
927
+ state: "partial",
928
+ activityState: "needs_attention",
929
+ startedAt: Date.now(),
930
+ error: message,
931
+ steps: [{ agent: "worker", status: "failed", activityState: "needs_attention", error: message, effects }],
932
+ }), "utf-8");
933
+
934
+ const id = `async-imported-partial-${Date.now().toString(36)}`;
935
+ executeAsyncChain(id, {
936
+ chain: [],
937
+ attachRoot: { runId: sourceId, asyncDir: sourceDir, resultPath: path.join(RESULTS_DIR, `${sourceId}.json`), index: 0, agent: "worker" },
938
+ agents: [makeAgent("worker")],
939
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
940
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
941
+ shareEnabled: false,
942
+ sessionRoot: path.join(tempDir, "sessions"),
943
+ maxSubagentDepth: 2,
944
+ });
945
+
946
+ const resultPath = await waitForAsyncResultFile(id);
947
+ const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
948
+ assert.equal(payload.success, false);
949
+ assert.equal(payload.state, "partial");
950
+ assert.equal(payload.summary, message);
951
+ assert.equal(payload.results[0]?.execution?.status, "partial");
952
+ assert.deepEqual(payload.results[0]?.effects?.fileMutation?.evidence?.changedFiles, ["input.md"]);
953
+
954
+ const status = await waitForAsyncState(id, (candidate) => candidate.state === "partial");
955
+ assert.equal(status.activityState, "needs_attention");
956
+ assert.equal(status.steps?.[0]?.activityState, "needs_attention");
957
+ });
958
+
959
+ it("keeps concrete sibling failures above partial mutation evidence", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
960
+ const repo = createRepo("pi-subagents-partial-sibling-failure-");
961
+ const outputPath = path.join(repo, "missing-report.md");
962
+ mockPi.onCall({
963
+ matchArgIncludes: "Write required report",
964
+ jsonl: [
965
+ events.assistantMessage("I changed the file but did not hand off the report."),
966
+ {
967
+ type: "message_end",
968
+ message: {
969
+ role: "assistant",
970
+ content: [],
971
+ model: "mock/test-model",
972
+ stopReason: "aborted",
973
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0 } },
974
+ },
975
+ },
976
+ ],
977
+ writeFiles: [{ path: "input.md", content: "changed before missing report\n" }],
978
+ });
979
+ mockPi.onCall({ matchArgIncludes: "Fail normally", stderr: "ordinary sibling failure", exitCode: 1 });
980
+
981
+ const id = `async-partial-sibling-failure-${Date.now().toString(36)}`;
982
+ try {
983
+ executeAsyncChain(id, {
984
+ chain: [{
985
+ parallel: [
986
+ { agent: "partial", task: "Write required report" },
987
+ { agent: "failure", task: "Fail normally" },
988
+ ],
989
+ concurrency: 2,
990
+ }],
991
+ resultMode: "parallel",
992
+ agents: [makeAgent("partial", { output: outputPath, outputMode: "file-only" }), makeAgent("failure", { completionGuard: false })],
993
+ ctx: { pi: { events: { emit() {} } }, cwd: repo, currentSessionId: "session-1" },
994
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
995
+ shareEnabled: false,
996
+ maxSubagentDepth: 2,
997
+ });
998
+
999
+ const payload = await readAsyncPayload(id);
1000
+ assert.equal(payload.success, false);
1001
+ assert.equal(payload.state, "failed");
1002
+ assert.match(payload.summary, /ordinary sibling failure/);
1003
+ assert.equal(payload.results[0]?.effects?.fileMutation?.status, "observed");
1004
+ assert.equal(payload.results[0]?.effects?.settlementDiagnostic?.requiredOutput?.missing, true);
1005
+ assert.match(payload.results[1]?.error ?? "", /ordinary sibling failure/);
1006
+
1007
+ const status = await waitForAsyncState(id, (candidate) => candidate.state === "failed");
1008
+ assert.equal(status.activityState, undefined);
1009
+ assert.match(status.error ?? "", /ordinary sibling failure/);
1010
+ } finally {
1011
+ removeTempDir(repo);
1012
+ }
1013
+ });
1014
+
1015
+ it("background runs emit active-long-running control events from child turns", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
1016
+ mockPi.onCall({
1017
+ steps: [
1018
+ { jsonl: [events.assistantMessage("still working")] },
1019
+ { delay: 2_000, jsonl: [events.assistantMessage("done")] },
1020
+ ],
1021
+ });
1022
+
1023
+ const id = `async-active-long-${Date.now().toString(36)}`;
1024
+ const asyncDir = path.join(ASYNC_DIR, id);
1025
+ const eventsPath = path.join(asyncDir, "events.jsonl");
1026
+ const resultPath = path.join(RESULTS_DIR, `${id}.json`);
1027
+
1028
+ executeAsyncSingle(id, {
1029
+ agent: "scout",
1030
+ task: "Investigate behavior",
1031
+ agentConfig: makeAgent("scout"),
1032
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
1033
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
1034
+ shareEnabled: false,
1035
+ sessionRoot: path.join(tempDir, "sessions"),
1036
+ maxSubagentDepth: 2,
1037
+ controlConfig: {
1038
+ enabled: true,
1039
+ needsAttentionAfterMs: 999_999,
1040
+ activeNoticeAfterTurns: 1,
1041
+ activeNoticeAfterMs: 999_999,
1042
+ activeNoticeAfterTokens: 999_999,
1043
+ failedToolAttemptsBeforeAttention: 3,
1044
+ notifyOn: ["active_long_running", "needs_attention"],
1045
+ notifyChannels: ["event", "async", "intercom"],
1046
+ },
1047
+ });
1048
+
1049
+ const statusPath = path.join(asyncDir, "status.json");
1050
+ const deadline = Date.now() + 10_000;
1051
+ let eventText = "";
1052
+ let statusDuringEvent: AsyncStatusPayload | undefined;
1053
+ while (Date.now() < deadline) {
1054
+ if (fs.existsSync(eventsPath)) {
1055
+ eventText = fs.readFileSync(eventsPath, "utf-8");
1056
+ }
1057
+ if (eventText.includes('"type":"active_long_running"') && fs.existsSync(statusPath)) {
1058
+ const status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatusPayload;
1059
+ if (status.activityState === "active_long_running" && status.steps?.[0]?.activityState === "active_long_running") {
1060
+ statusDuringEvent = status;
1061
+ break;
1062
+ }
1063
+ }
1064
+ if (eventText.includes('"type":"active_long_running"') && fs.existsSync(resultPath)) {
1065
+ assert.fail("run completed before status.json exposed active_long_running");
1066
+ }
1067
+ await new Promise((resolve) => setTimeout(resolve, 100));
1068
+ }
1069
+
1070
+ assert.match(eventText, /"type":"active_long_running"/);
1071
+ assert.match(eventText, /"reason":"turn_threshold"/);
1072
+ assert.ok(statusDuringEvent, "expected status.json to expose active_long_running while the run is still active");
1073
+ assert.equal(statusDuringEvent.activityState, "active_long_running");
1074
+ assert.equal(statusDuringEvent.steps?.[0]?.activityState, "active_long_running");
1075
+
1076
+ const doneDeadline = Date.now() + 10_000;
1077
+ while (!fs.existsSync(resultPath)) {
1078
+ if (Date.now() > doneDeadline) assert.fail(`Timed out waiting for async result file: ${resultPath}`);
1079
+ await new Promise((resolve) => setTimeout(resolve, 100));
1080
+ }
1081
+ });
1082
+
1083
+ it("does not flag a delayed active tool as idle attention", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
1084
+ mockPi.onCall({
1085
+ steps: [
1086
+ { jsonl: [events.toolStart("bash", { command: "sleep 2" })] },
1087
+ { delay: 2_500, jsonl: [events.toolEnd("bash"), events.toolResult("bash", "done")] },
1088
+ { jsonl: [events.assistantMessage("Done")] },
1089
+ ],
1090
+ });
1091
+
1092
+ const id = `async-delayed-tool-attention-${Date.now().toString(36)}`;
1093
+ const asyncDir = path.join(ASYNC_DIR, id);
1094
+ const eventsPath = path.join(asyncDir, "events.jsonl");
1095
+ const resultPath = path.join(RESULTS_DIR, `${id}.json`);
1096
+ executeAsyncSingle(id, {
1097
+ agent: "worker",
1098
+ task: "Run the command",
1099
+ agentConfig: makeAgent("worker"),
1100
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
1101
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
1102
+ shareEnabled: false,
1103
+ sessionRoot: path.join(tempDir, "sessions"),
1104
+ maxSubagentDepth: 2,
1105
+ controlConfig: {
1106
+ enabled: true,
1107
+ needsAttentionAfterMs: 200,
1108
+ activeNoticeAfterMs: 999_999,
1109
+ failedToolAttemptsBeforeAttention: 3,
1110
+ notifyOn: ["active_long_running", "needs_attention"],
1111
+ notifyChannels: ["event", "async", "intercom"],
1112
+ },
1113
+ });
1114
+
1115
+ const deadline = Date.now() + 10_000;
1116
+ let statusDuringTool: AsyncStatusPayload | undefined;
1117
+ while (Date.now() < deadline && !fs.existsSync(resultPath)) {
1118
+ if (fs.existsSync(asyncDir) && fs.existsSync(path.join(asyncDir, "status.json"))) {
1119
+ const status = JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8")) as AsyncStatusPayload;
1120
+ const toolStartedAt = status.steps?.[0]?.currentToolStartedAt;
1121
+ if (status.currentTool === "bash" && status.steps?.[0]?.currentTool === "bash" && toolStartedAt && Date.now() - toolStartedAt >= 1_500) {
1122
+ statusDuringTool = status;
1123
+ break;
1124
+ }
1125
+ }
1126
+ await new Promise((resolve) => setTimeout(resolve, 100));
1127
+ }
1128
+ assert.ok(statusDuringTool, "expected status.json to expose the active tool");
1129
+ assert.equal(statusDuringTool?.activityState, undefined);
1130
+ assert.equal(statusDuringTool?.steps?.[0]?.activityState, undefined);
1131
+ const eventText = fs.existsSync(eventsPath) ? fs.readFileSync(eventsPath, "utf-8") : "";
1132
+ assert.doesNotMatch(eventText, /"type":"needs_attention"/);
1133
+ await waitForAsyncResultFile(id);
1134
+ const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
1135
+ assert.equal(payload.success, true);
1136
+ });
1137
+
1138
+ it("background open-tool attention survives an overlapping quick tool", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
1139
+ mockPi.onCall({
1140
+ steps: [
1141
+ { jsonl: [{ type: "tool_execution_start", toolCallId: "bash-1", toolName: "bash", args: { command: "sleep 2" } }] },
1142
+ { delay: 50, jsonl: [
1143
+ { type: "tool_execution_start", toolCallId: "read-1", toolName: "read", args: { path: "README.md" } },
1144
+ { type: "tool_execution_end", toolCallId: "read-1", toolName: "read" },
1145
+ ] },
1146
+ { delay: 2_000, jsonl: [
1147
+ { type: "tool_execution_end", toolCallId: "bash-1", toolName: "bash" },
1148
+ events.toolResult("bash", "done"),
1149
+ events.assistantMessage("Done"),
1150
+ ] },
1151
+ ],
1152
+ });
1153
+
1154
+ const id = `async-overlap-tool-attention-${Date.now().toString(36)}`;
1155
+ const asyncDir = path.join(ASYNC_DIR, id);
1156
+ const eventsPath = path.join(asyncDir, "events.jsonl");
1157
+ const statusPath = path.join(asyncDir, "status.json");
1158
+ const resultPath = path.join(RESULTS_DIR, `${id}.json`);
1159
+ executeAsyncSingle(id, {
1160
+ agent: "worker",
1161
+ task: "Run the command",
1162
+ agentConfig: makeAgent("worker"),
1163
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
1164
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
1165
+ shareEnabled: false,
1166
+ sessionRoot: path.join(tempDir, "sessions"),
1167
+ maxSubagentDepth: 2,
1168
+ controlConfig: {
1169
+ enabled: true,
1170
+ needsAttentionAfterMs: 999_999,
1171
+ activeNoticeAfterMs: 100,
1172
+ failedToolAttemptsBeforeAttention: 3,
1173
+ notifyOn: ["needs_attention"],
1174
+ notifyChannels: ["event", "async", "intercom"],
1175
+ },
1176
+ });
1177
+
1178
+ const deadline = Date.now() + 10_000;
1179
+ let eventText = "";
1180
+ let statusDuringEvent: AsyncStatusPayload | undefined;
1181
+ while (Date.now() < deadline) {
1182
+ if (fs.existsSync(eventsPath)) eventText = fs.readFileSync(eventsPath, "utf-8");
1183
+ if (eventText.includes('"reason":"tool_open_threshold"') && fs.existsSync(statusPath)) {
1184
+ const status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatusPayload;
1185
+ if (status.activityState === "needs_attention" && status.steps?.[0]?.activityState === "needs_attention") {
1186
+ statusDuringEvent = status;
1187
+ break;
1188
+ }
1189
+ }
1190
+ if (eventText.includes('"reason":"tool_open_threshold"') && fs.existsSync(resultPath)) {
1191
+ assert.fail("run completed before status.json exposed overlapping tool attention");
1192
+ }
1193
+ await new Promise((resolve) => setTimeout(resolve, 100));
1194
+ }
1195
+
1196
+ assert.match(eventText, /"type":"needs_attention"/);
1197
+ assert.match(eventText, /"reason":"tool_open_threshold"/);
1198
+ assert.match(eventText, /"currentTool":"bash"/);
1199
+ assert.ok(statusDuringEvent, "expected status.json to expose overlapping tool attention while the run is active");
1200
+ assert.equal(statusDuringEvent.currentTool, "bash");
1201
+ assert.equal(statusDuringEvent.steps?.[0]?.currentTool, "bash");
1202
+ await waitForAsyncResultFile(id);
1203
+ });
1204
+
1205
+ it("bg_wait wakes when an async child is waiting on contact_supervisor", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
1206
+ const id = `async-supervisor-attention-${Date.now().toString(36)}`;
1207
+ const replyReleasePath = path.join(tempDir, `${id}.reply`);
1208
+ const finalReleasePath = path.join(tempDir, `${id}.final`);
1209
+ mockPi.onCall({
1210
+ steps: [
1211
+ { jsonl: [events.toolStart("contact_supervisor", { reason: "need_decision", message: "Need a decision" })] },
1212
+ { waitForPath: replyReleasePath, jsonl: [events.toolEnd("contact_supervisor"), events.toolResult("contact_supervisor", "**Reply from supervisor:**\nProceed")] },
1213
+ { waitForPath: finalReleasePath, jsonl: [events.assistantMessage("Done")] },
1214
+ ],
1215
+ });
1216
+
1217
+ const asyncDir = path.join(ASYNC_DIR, id);
1218
+ const eventsPath = path.join(asyncDir, "events.jsonl");
1219
+ const resultPath = path.join(RESULTS_DIR, `${id}.json`);
1220
+ const statusPath = path.join(asyncDir, "status.json");
1221
+ executeAsyncSingle(id, {
1222
+ agent: "worker",
1223
+ task: "Ask the supervisor for a blocking decision",
1224
+ agentConfig: makeAgent("worker"),
1225
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
1226
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
1227
+ shareEnabled: false,
1228
+ sessionRoot: path.join(tempDir, "sessions"),
1229
+ maxSubagentDepth: 2,
1230
+ controlConfig: {
1231
+ enabled: true,
1232
+ needsAttentionAfterMs: 999_999,
1233
+ activeNoticeAfterMs: 999_999,
1234
+ failedToolAttemptsBeforeAttention: 3,
1235
+ notifyOn: ["active_long_running", "needs_attention"],
1236
+ notifyChannels: ["event", "async", "intercom"],
1237
+ },
1238
+ });
1239
+
1240
+ const releaseMockChild = () => {
1241
+ if (!fs.existsSync(replyReleasePath)) fs.writeFileSync(replyReleasePath, "release", "utf-8");
1242
+ if (!fs.existsSync(finalReleasePath)) fs.writeFileSync(finalReleasePath, "release", "utf-8");
1243
+ };
1244
+ const releaseSupervisorReply = () => {
1245
+ if (!fs.existsSync(replyReleasePath)) fs.writeFileSync(replyReleasePath, "release", "utf-8");
1246
+ };
1247
+ try {
1248
+ const attentionDeadline = Date.now() + 10_000;
1249
+ let statusDuringAttention: AsyncStatusPayload | undefined;
1250
+ while (Date.now() < attentionDeadline && !fs.existsSync(resultPath)) {
1251
+ if (fs.existsSync(statusPath)) {
1252
+ const nextStatus = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatusPayload;
1253
+ if (nextStatus.currentTool === "contact_supervisor" && nextStatus.activityState === "needs_attention") {
1254
+ statusDuringAttention = nextStatus;
1255
+ break;
1256
+ }
1257
+ }
1258
+ await new Promise((resolve) => setTimeout(resolve, 100));
1259
+ }
1260
+ assert.ok(statusDuringAttention, "expected status.json to expose the blocking supervisor request");
1261
+
1262
+ try {
1263
+ const waitResult = await waitForSubagents({ id, timeoutMs: 3_500 }, undefined, {
1264
+ state: { currentSessionId: "session-1", foregroundRuns: new Map(), asyncJobs: new Map(), cleanupTimers: new Map(), resultFileCoalescer: new Map() },
1265
+ pollIntervalMs: 100,
1266
+ events: createEventBus(),
1267
+ });
1268
+ const waitText = waitResult.content[0]?.text ?? "";
1269
+ assert.equal(waitResult.isError, undefined);
1270
+ assert.match(waitText, /attention required/i);
1271
+ assert.match(waitText, new RegExp(id));
1272
+ assert.match(waitText, /intercom\(\{ action: "pending" \}\)/);
1273
+ assert.equal(fs.existsSync(resultPath), false, "wait should return before the child completes");
1274
+ } finally {
1275
+ releaseSupervisorReply();
1276
+ }
1277
+
1278
+ const eventText = fs.existsSync(eventsPath) ? fs.readFileSync(eventsPath, "utf-8") : "";
1279
+ assert.match(eventText, /"type":"needs_attention"/);
1280
+ assert.match(eventText, /"reason":"supervisor_request"/);
1281
+ assert.equal(statusDuringAttention.activityState, "needs_attention");
1282
+ assert.equal(statusDuringAttention.steps?.[0]?.activityState, "needs_attention");
1283
+ assert.equal(statusDuringAttention.currentTool, "contact_supervisor");
1284
+ assert.equal(statusDuringAttention.steps?.[0]?.currentTool, "contact_supervisor");
1285
+
1286
+ const clearDeadline = Date.now() + 10_000;
1287
+ let statusAfterReply: AsyncStatusPayload | undefined;
1288
+ while (Date.now() < clearDeadline && !fs.existsSync(resultPath)) {
1289
+ const nextStatus = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatusPayload;
1290
+ if (nextStatus.state === "running" && !nextStatus.currentTool && !nextStatus.steps?.[0]?.currentTool) {
1291
+ statusAfterReply = nextStatus;
1292
+ break;
1293
+ }
1294
+ await new Promise((resolve) => setTimeout(resolve, 100));
1295
+ }
1296
+ assert.ok(statusAfterReply, "expected the child to keep running after the supervisor reply");
1297
+ assert.equal(statusAfterReply.activityState, undefined);
1298
+ assert.equal(statusAfterReply.steps?.[0]?.activityState, undefined);
1299
+
1300
+ fs.writeFileSync(finalReleasePath, "release", "utf-8");
1301
+ await waitForAsyncResultFile(id);
1302
+ } finally {
1303
+ releaseMockChild();
1304
+ }
1305
+ });
1306
+
1307
+ it("background runs escalate repeated mutating tool failures", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
1308
+ mockPi.onCall({
1309
+ steps: [
1310
+ { jsonl: [events.toolStart("edit", { path: "src/runs/background/subagent-runner.ts" }), events.toolEnd("edit"), events.toolResult("edit", "No exact match found for subagent-runner.ts", true)] },
1311
+ { jsonl: [events.toolStart("edit", { path: "src/runs/background/subagent-runner.ts" }), events.toolEnd("edit"), events.toolResult("edit", "No exact match found for subagent-runner.ts", true)] },
1312
+ { jsonl: [events.toolStart("edit", { path: "src/runs/background/subagent-runner.ts" }), events.toolEnd("edit"), events.toolResult("edit", "No exact match found for subagent-runner.ts", true)] },
1313
+ { delay: 2_000, jsonl: [events.assistantMessage("I need another attempt.")] },
1314
+ ],
1315
+ });
1316
+
1317
+ const id = `async-tool-failures-${Date.now().toString(36)}`;
1318
+ const asyncDir = path.join(ASYNC_DIR, id);
1319
+ const eventsPath = path.join(asyncDir, "events.jsonl");
1320
+ const resultPath = path.join(RESULTS_DIR, `${id}.json`);
1321
+
1322
+ executeAsyncSingle(id, {
1323
+ agent: "worker",
1324
+ task: "Implement the approved fixes",
1325
+ agentConfig: makeAgent("worker"),
1326
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
1327
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
1328
+ shareEnabled: false,
1329
+ sessionRoot: path.join(tempDir, "sessions"),
1330
+ maxSubagentDepth: 2,
1331
+ controlConfig: {
1332
+ enabled: true,
1333
+ needsAttentionAfterMs: 999_999,
1334
+ activeNoticeAfterTurns: 999_999,
1335
+ activeNoticeAfterMs: 999_999,
1336
+ activeNoticeAfterTokens: 999_999,
1337
+ failedToolAttemptsBeforeAttention: 3,
1338
+ notifyOn: ["active_long_running", "needs_attention"],
1339
+ notifyChannels: ["event", "async", "intercom"],
1340
+ },
1341
+ });
1342
+
1343
+ const statusPath = path.join(asyncDir, "status.json");
1344
+ const deadline = Date.now() + 10_000;
1345
+ let eventText = "";
1346
+ let statusDuringEvent: AsyncStatusPayload | undefined;
1347
+ while (Date.now() < deadline) {
1348
+ if (fs.existsSync(eventsPath)) {
1349
+ eventText = fs.readFileSync(eventsPath, "utf-8");
1350
+ }
1351
+ if (eventText.includes('"reason":"tool_failures"') && fs.existsSync(statusPath)) {
1352
+ const status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatusPayload;
1353
+ if (status.activityState === "needs_attention" && status.steps?.[0]?.activityState === "needs_attention") {
1354
+ statusDuringEvent = status;
1355
+ break;
1356
+ }
1357
+ }
1358
+ if (eventText.includes('"reason":"tool_failures"') && fs.existsSync(resultPath)) {
1359
+ assert.fail("run completed before status.json exposed needs_attention");
1360
+ }
1361
+ await new Promise((resolve) => setTimeout(resolve, 100));
1362
+ }
1363
+
1364
+ assert.match(eventText, /"type":"needs_attention"/);
1365
+ assert.match(eventText, /"reason":"tool_failures"/);
1366
+ assert.match(eventText, /subagent-runner\.ts/);
1367
+ assert.ok(statusDuringEvent, "expected status.json to expose needs_attention while the run is still active");
1368
+ assert.equal(statusDuringEvent.activityState, "needs_attention");
1369
+ assert.equal(statusDuringEvent.steps?.[0]?.activityState, "needs_attention");
1370
+
1371
+ const doneDeadline = Date.now() + 10_000;
1372
+ while (!fs.existsSync(resultPath)) {
1373
+ if (Date.now() > doneDeadline) assert.fail(`Timed out waiting for async result file: ${resultPath}`);
1374
+ await new Promise((resolve) => setTimeout(resolve, 100));
1375
+ }
1376
+ });
1377
+
1378
+ it("background event logs drop noisy message updates and cap child diagnostics", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
1379
+ const previousMaxBytes = process.env.SELESAI_SUBAGENT_ASYNC_EVENTS_MAX_BYTES;
1380
+ process.env.SELESAI_SUBAGENT_ASYNC_EVENTS_MAX_BYTES = "1100";
1381
+ try {
1382
+ mockPi.onCall({
1383
+ steps: [
1384
+ {
1385
+ jsonl: [
1386
+ {
1387
+ type: "message_update",
1388
+ assistantMessageEvent: {
1389
+ type: "thinking_delta",
1390
+ delta: "NOISY_PARTIAL_DELTA",
1391
+ partial: { role: "assistant", content: [{ type: "text", text: "NOISY_PARTIAL_SNAPSHOT".repeat(200) }] },
1392
+ },
1393
+ message: { role: "assistant", content: [{ type: "text", text: "NOISY_PARTIAL_MESSAGE".repeat(200) }] },
1394
+ },
1395
+ events.toolStart("bash", { command: `echo ${"BIG_COMMAND_PAYLOAD".repeat(200)}` }),
1396
+ events.assistantMessage("Done after noisy stream"),
1397
+ ],
1398
+ },
1399
+ ],
1400
+ });
1401
+
1402
+ const id = `async-noisy-events-${Date.now().toString(36)}`;
1403
+ const asyncDir = path.join(ASYNC_DIR, id);
1404
+ const sessionRoot = path.join(tempDir, "sessions");
1405
+
1406
+ executeAsyncSingle(id, {
1407
+ agent: "worker",
1408
+ task: "Stream noisy diagnostics",
1409
+ agentConfig: makeAgent("worker"),
1410
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
1411
+ artifactConfig: {
1412
+ enabled: false,
1413
+ includeInput: false,
1414
+ includeOutput: false,
1415
+ includeJsonl: false,
1416
+ includeMetadata: false,
1417
+ cleanupDays: 7,
1418
+ },
1419
+ shareEnabled: false,
1420
+ sessionRoot,
1421
+ maxSubagentDepth: 2,
1422
+ });
1423
+
1424
+ const resultPath = await waitForAsyncResultFile(id, 10_000);
1425
+ const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
1426
+ assert.equal(payload.success, true);
1427
+ assert.equal(payload.results[0]?.output, "Done after noisy stream");
1428
+
1429
+ const eventsText = fs.readFileSync(path.join(asyncDir, "events.jsonl"), "utf-8");
1430
+ assert.doesNotMatch(eventsText, /"type":"message_update"/);
1431
+ assert.doesNotMatch(eventsText, /NOISY_PARTIAL_/);
1432
+ assert.doesNotMatch(eventsText, /BIG_COMMAND_PAYLOAD/);
1433
+ assert.match(eventsText, /"type":"subagent\.events\.truncated"/);
1434
+ assert.match(eventsText, /"droppedEventType":"tool_execution_start"/);
1435
+ } finally {
1436
+ if (previousMaxBytes === undefined) delete process.env.SELESAI_SUBAGENT_ASYNC_EVENTS_MAX_BYTES;
1437
+ else process.env.SELESAI_SUBAGENT_ASYNC_EVENTS_MAX_BYTES = previousMaxBytes;
1438
+ }
1439
+ });
1440
+
1441
+ it("background runs stream child events and live output while active", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
1442
+ mockPi.onCall({
1443
+ steps: [
1444
+ { delay: 200, jsonl: [events.toolStart("bash", { command: "ls" })] },
1445
+ { delay: 600, jsonl: [events.toolEnd("bash"), events.toolResult("bash", "file-a\nfile-b")] },
1446
+ { delay: 600, jsonl: [events.assistantMessage("Done streaming")], stderr: "warning: mock stderr\n" },
1447
+ ],
1448
+ });
1449
+
1450
+ const id = `async-stream-${Date.now().toString(36)}`;
1451
+ const asyncDir = path.join(ASYNC_DIR, id);
1452
+ const eventsPath = path.join(asyncDir, "events.jsonl");
1453
+ const outputPath = path.join(asyncDir, "output-0.log");
1454
+ const resultPath = path.join(RESULTS_DIR, `${id}.json`);
1455
+ const sessionRoot = path.join(tempDir, "sessions");
1456
+
1457
+ executeAsyncSingle(id, {
1458
+ agent: "worker",
1459
+ task: "Stream detailed progress",
1460
+ agentConfig: makeAgent("worker"),
1461
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
1462
+ artifactConfig: {
1463
+ enabled: false,
1464
+ includeInput: false,
1465
+ includeOutput: false,
1466
+ includeJsonl: false,
1467
+ includeMetadata: false,
1468
+ cleanupDays: 7,
1469
+ },
1470
+ shareEnabled: false,
1471
+ sessionRoot,
1472
+ maxSubagentDepth: 2,
1473
+ });
1474
+
1475
+ const liveDeadline = Date.now() + 10_000;
1476
+ let sawChildEvent = false;
1477
+ let sawLiveOutput = false;
1478
+ while (Date.now() < liveDeadline && (!sawChildEvent || !sawLiveOutput)) {
1479
+ if (fs.existsSync(eventsPath)) {
1480
+ const content = fs.readFileSync(eventsPath, "utf-8");
1481
+ sawChildEvent = content.includes('"type":"tool_execution_start"')
1482
+ && content.includes('"subagentSource":"child"');
1483
+ }
1484
+ if (fs.existsSync(outputPath)) {
1485
+ const content = fs.readFileSync(outputPath, "utf-8");
1486
+ sawLiveOutput = content.includes("bash: ls") || content.includes("file-a") || content.includes("warning: mock stderr");
1487
+ }
1488
+ if (sawChildEvent && sawLiveOutput) break;
1489
+ assert.equal(fs.existsSync(resultPath), false, "run finished before live observability was written");
1490
+ await new Promise((resolve) => setTimeout(resolve, 100));
1491
+ }
1492
+
1493
+ assert.equal(sawChildEvent, true, "expected child JSON events to be streamed into events.jsonl");
1494
+ assert.equal(sawLiveOutput, true, "expected output-0.log to receive live child output");
1495
+
1496
+ const doneDeadline = Date.now() + 10_000;
1497
+ while (!fs.existsSync(resultPath)) {
1498
+ if (Date.now() > doneDeadline) {
1499
+ assert.fail(`Timed out waiting for async result file: ${resultPath}`);
1500
+ }
1501
+ await new Promise((resolve) => setTimeout(resolve, 100));
1502
+ }
1503
+
1504
+ const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
1505
+ assert.equal(payload.success, true);
1506
+ assert.equal(payload.results[0].output, "Done streaming");
1507
+
1508
+ const status = JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8"));
1509
+ assert.deepEqual(status.steps[0].recentTools.map((tool: { tool: string; args: string }) => ({ tool: tool.tool, args: tool.args })), [{ tool: "bash", args: "ls" }]);
1510
+ assert.deepEqual(status.steps[0].recentOutput, ["file-a", "file-b", "Done streaming"]);
1511
+ });
1512
+ });