@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,682 @@
1
+ import assert from "node:assert/strict";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { randomUUID } from "node:crypto";
6
+ import { execFileSync } from "node:child_process";
7
+ import { afterEach, describe, it } from "node:test";
8
+ import {
9
+ NATIVE_SUPERVISOR_TOOL_NAME,
10
+ createNativeSupervisorChannel,
11
+ ensureSupervisorChannelDir,
12
+ registerNativeSupervisorClient,
13
+ resolveSupervisorChannelDir,
14
+ } from "../../src/intercom/native-supervisor-channel.ts";
15
+ import { steerWorkflowForegroundTarget } from "../../src/runs/foreground/workflow-foreground-steering.ts";
16
+ import { createSubagentExecutor } from "../../src/runs/foreground/subagent-executor.ts";
17
+ import type { ForegroundRunControl, ForegroundSteerInput, SubagentState } from "../../src/shared/types.ts";
18
+ import { DIRS } from "../../src/shared/types.ts";
19
+
20
+ const createdChannels: string[] = [];
21
+
22
+ interface SupervisorTool {
23
+ execute: (id: string, params: { action: string; replyTo?: string; to?: string; message?: string }) => Promise<{ content: Array<{ type: string; text?: string }>; isError?: boolean }>;
24
+ }
25
+
26
+ function makeState(sessionId: string | null, ctx: unknown): SubagentState {
27
+ return {
28
+ baseCwd: process.cwd(),
29
+ currentSessionId: sessionId,
30
+ supervisorOwnerSessionId: (ctx as SubagentState["lastUiContext"])?.sessionManager.getSessionId() ?? null,
31
+ asyncJobs: new Map(),
32
+ foregroundControls: new Map(),
33
+ lastForegroundControlId: null,
34
+ cleanupTimers: new Map(),
35
+ lastUiContext: ctx as SubagentState["lastUiContext"],
36
+ poller: null,
37
+ completionSeen: new Map(),
38
+ watcher: null,
39
+ watcherRestartTimer: null,
40
+ resultFileCoalescer: { schedule: () => false, clear: () => {} },
41
+ };
42
+ }
43
+
44
+ function makeCtx(sessionId: string, sessionFile: string | null = null): { cwd: string; hasUI: boolean; sessionManager: { getSessionId: () => string; getSessionFile: () => string | null; getEntries: () => [] } } {
45
+ return {
46
+ cwd: process.cwd(),
47
+ hasUI: false,
48
+ sessionManager: {
49
+ getSessionId: () => sessionId,
50
+ getSessionFile: () => sessionFile,
51
+ getEntries: () => [],
52
+ },
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Write an ask directly to the channel the way a child's `contact_supervisor` call does, without
58
+ * going through a live poller. This is the state the wave-11 incident was in: the request file was
59
+ * on disk and no scan had happened yet.
60
+ */
61
+ function writeRequest(input: { sessionId: string; runId: string; agent?: string; index?: number; message?: string; reason?: "need_decision" | "progress_update"; expiresAt?: number }): string {
62
+ const agent = input.agent ?? "worker";
63
+ const index = input.index ?? 0;
64
+ const channelDir = resolveSupervisorChannelDir(input.runId, agent, index);
65
+ createdChannels.push(channelDir);
66
+ ensureSupervisorChannelDir(channelDir);
67
+ const requestId = randomUUID();
68
+ const reason = input.reason ?? "need_decision";
69
+ fs.writeFileSync(path.join(channelDir, "requests", `${requestId}.json`), JSON.stringify({
70
+ type: "subagent.supervisor.request",
71
+ id: requestId,
72
+ createdAt: Date.now(),
73
+ ...(input.expiresAt !== undefined ? { expiresAt: input.expiresAt } : {}),
74
+ reason,
75
+ message: input.message ?? "Need a decision",
76
+ expectsReply: reason !== "progress_update",
77
+ orchestratorSessionId: input.sessionId,
78
+ orchestratorTarget: "shared-name",
79
+ runId: input.runId,
80
+ agent,
81
+ childIndex: index,
82
+ }, null, "\t"));
83
+ return requestId;
84
+ }
85
+
86
+ function makePi(options: { tools: Map<string, SupervisorTool>; onSend?: (message: { content: string }) => void }): Record<string, unknown> {
87
+ return {
88
+ getAllTools: () => [...options.tools.keys()].map((name) => ({ name })),
89
+ registerTool: (tool: { name: string } & SupervisorTool) => { options.tools.set(tool.name, tool); },
90
+ sendMessage: (message: { content: string }) => { options.onSend?.(message); },
91
+ getSessionName: () => "shared-name",
92
+ };
93
+ }
94
+
95
+ function text(result: { content: Array<{ type: string; text?: string }> }): string {
96
+ return result.content[0]?.type === "text" ? result.content[0].text ?? "" : "";
97
+ }
98
+
99
+ afterEach(() => {
100
+ for (const channel of createdChannels.splice(0)) fs.rmSync(channel, { recursive: true, force: true });
101
+ });
102
+
103
+ describe("supervisor ask registration", () => {
104
+ it("public single async steering reports exact pending asks without reply, queue or recovery", async () => {
105
+ const owner = randomUUID();
106
+ fs.mkdirSync(DIRS.async, { recursive: true });
107
+ const root = fs.mkdtempSync(path.join(DIRS.async, "blocked-steer-"));
108
+ const ctx = { ...makeCtx(owner, path.join(root, "parent.jsonl")), cwd: root };
109
+ const state = makeState(ctx.sessionManager.getSessionFile(), ctx);
110
+ state.lastUiContext = null;
111
+ const runId = randomUUID();
112
+ const channel = createNativeSupervisorChannel(makePi({ tools: new Map(), onSend: () => { throw new Error("lookup must not notify"); } }) as never, state);
113
+ let probes = 0;
114
+ let race = false;
115
+ let racedAsk: string | undefined;
116
+ const executor = createSubagentExecutor({
117
+ pi: {} as never, state, config: { maxSubagentDepth: 2, control: {}, intercomBridge: {} } as never,
118
+ asyncByDefault: false, tempArtifactsDir: root, getSubagentSessionRoot: () => root,
119
+ expandTilde: value => value, discoverAgents: () => ({ agents: [] }),
120
+ findPendingAsks: target => {
121
+ const asks = channel.findPendingAsks(target);
122
+ if (race) racedAsk = writeRequest({ sessionId: owner, runId });
123
+ return asks;
124
+ },
125
+ kill: () => { probes++; return true; },
126
+ });
127
+ const status = { runId, sessionId: ctx.sessionManager.getSessionFile(), state: "running", mode: "single", pid: process.pid, startedAt: Date.now(), updatedAt: Date.now(), steps: [{ agent: "worker", status: "running" }] };
128
+ fs.writeFileSync(path.join(root, "status.json"), JSON.stringify(status));
129
+ const invoke = (mode: "steer" | "follow_up") => executor.executePublic(randomUUID(), { action: "steer", dir: root, message: "After this is resolved, update docs.", mode }, new AbortController().signal, undefined, ctx as never);
130
+ try {
131
+ const first = writeRequest({ sessionId: owner, runId });
132
+ const second = writeRequest({ sessionId: owner, runId });
133
+ for (const mode of ["steer", "follow_up"] as const) {
134
+ const result = await invoke(mode);
135
+ assert.equal(result.isError, true);
136
+ assert.match(text(result), /not delivered or queued/);
137
+ assert.match(text(result), /ambiguous/);
138
+ for (const id of [first, second]) assert.ok(text(result).includes(`"replyTo":"${id}"`));
139
+ assert.equal(result.details?.steering, undefined);
140
+ }
141
+ assert.deepEqual(fs.readdirSync(root), ["status.json"]);
142
+ assert.equal(fs.readFileSync(path.join(root, "status.json"), "utf8"), JSON.stringify(status));
143
+ assert.equal(channel.pending.size, 0);
144
+ const dir = resolveSupervisorChannelDir(runId, "worker", 0);
145
+ assert.deepEqual(fs.readdirSync(path.join(dir, "replies")), []);
146
+ fs.rmSync(path.join(dir, "requests", `${second}.json`));
147
+ assert.doesNotMatch(text(await invoke("follow_up")), /ambiguous/);
148
+ assert.equal(probes, 0, "blocked branch precedes reconciliation/control");
149
+ fs.rmSync(path.join(dir, "requests", `${first}.json`));
150
+ status.steps[0]!.status = "pending";
151
+ fs.writeFileSync(path.join(root, "status.json"), JSON.stringify(status));
152
+ race = true;
153
+ const raced = await invoke("follow_up");
154
+ assert.equal(raced.isError, undefined);
155
+ assert.equal(raced.details?.steering?.state, "scheduled");
156
+ assert.equal(raced.details?.steering?.deliveryStatus, "queued");
157
+ assert.match(text(raced), /Steering scheduled/);
158
+ assert.doesNotMatch(text(raced), /unblocked|consumed/);
159
+ assert.ok(fs.existsSync(path.join(dir, "requests", `${racedAsk}.json`)));
160
+ assert.deepEqual(fs.readdirSync(path.join(dir, "replies")), []);
161
+ } finally { channel.dispose(); fs.rmSync(root, { recursive: true, force: true }); }
162
+ });
163
+
164
+ it("receipt lookup is read-only, fresh and exact for runtime owner, run, agent and index", () => {
165
+ const owner = randomUUID(), runId = randomUUID();
166
+ const state = makeState("/sessions/parent.jsonl", makeCtx(owner));
167
+ state.lastUiContext = null;
168
+ const channel = createNativeSupervisorChannel({} as never, state);
169
+ const target = { runId, agent: "worker", childIndex: 0 };
170
+ const live = writeRequest({ sessionId: owner, runId });
171
+ const excluded = [
172
+ writeRequest({ sessionId: "foreign", runId }),
173
+ writeRequest({ sessionId: owner, runId, expiresAt: Date.now() - 1 }),
174
+ writeRequest({ sessionId: owner, runId, reason: "progress_update" }),
175
+ ];
176
+ const resolved = writeRequest({ sessionId: owner, runId });
177
+ const dir = resolveSupervisorChannelDir(runId, "worker", 0);
178
+ fs.writeFileSync(path.join(dir, "replies", `${resolved}.json`), "{}");
179
+ // Put mismatched metadata in the exact target directory, not just another directory.
180
+ for (const mismatch of [{ runId: "other" }, { agent: "other" }, { childIndex: 1 }]) {
181
+ const id = writeRequest({ sessionId: owner, runId });
182
+ const file = path.join(dir, "requests", `${id}.json`);
183
+ fs.writeFileSync(file, JSON.stringify({ ...JSON.parse(fs.readFileSync(file, "utf8")), ...mismatch }));
184
+ excluded.push(id);
185
+ }
186
+ assert.deepEqual(channel.findPendingAsks(target), [live]);
187
+ assert.equal(channel.pending.size, 0);
188
+ for (const id of [...excluded, resolved]) assert.ok(fs.existsSync(path.join(dir, "requests", `${id}.json`)));
189
+ fs.rmSync(path.join(dir, "requests", `${live}.json`));
190
+ assert.deepEqual(channel.findPendingAsks(target), []);
191
+ state.supervisorOwnerSessionId = null;
192
+ writeRequest({ sessionId: owner, runId });
193
+ assert.deepEqual(channel.findPendingAsks(target), []);
194
+ state.supervisorOwnerSessionId = owner;
195
+ state.asyncJobs.set(runId, { status: "complete" } as never);
196
+ assert.deepEqual(channel.findPendingAsks(target), [], "terminal child asks are not live");
197
+ channel.dispose();
198
+ });
199
+
200
+ it("discovers delayed asks through Darwin workflow registration, control release, terminal and rearm", async () => {
201
+ const sessionId = randomUUID();
202
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "supervisor-demand-"));
203
+ const ctx = { ...makeCtx(sessionId), cwd: root };
204
+ const state = makeState(sessionId, ctx);
205
+ const intervals = new Map<object, () => void>();
206
+ let starts = 0;
207
+ let unrefs = 0;
208
+ const sent: string[] = [];
209
+ const pi = {
210
+ getAllTools: () => [], registerTool() {}, getSessionName: () => "parent",
211
+ events: { emit() {}, on() { return () => {}; } },
212
+ sendMessage(message: { details?: { id?: string } }) { if (message.details?.id) sent.push(message.details.id); },
213
+ };
214
+ const channel = createNativeSupervisorChannel(pi as never, state, {
215
+ platform: "darwin",
216
+ watch: (() => { throw new Error("Darwin must not call fs.watch"); }) as never,
217
+ timers: {
218
+ setInterval: ((handler: () => void, delay: number) => {
219
+ assert.equal(delay, 250);
220
+ starts += 1;
221
+ const token = { unref() { unrefs += 1; } };
222
+ intervals.set(token, handler);
223
+ return token;
224
+ }) as typeof setInterval,
225
+ clearInterval: ((token: object) => { intervals.delete(token); }) as typeof clearInterval,
226
+ setImmediate, clearImmediate,
227
+ },
228
+ });
229
+ const tick = () => { for (const handler of [...intervals.values()]) handler(); };
230
+ let terminal: () => void = () => {};
231
+ const executor = createSubagentExecutor({
232
+ pi: pi as never, state, config: { maxSubagentDepth: 2, control: {}, intercomBridge: {} } as never,
233
+ asyncByDefault: false, tempArtifactsDir: root, getSubagentSessionRoot: () => root,
234
+ expandTilde: (value) => value, discoverAgents: () => ({ agents: [] }),
235
+ activateSupervisorTransport: () => channel.activateTransport(),
236
+ refreshResultDelivery: () => { if ([...state.asyncJobs.values()].every(job => job.status !== "running" && job.status !== "queued")) terminal(); },
237
+ });
238
+ const dirs: string[] = [];
239
+ try {
240
+ channel.start();
241
+ channel.activateTransport();
242
+ assert.equal(intervals.size, 0, "idle registration has no timer");
243
+ for (let cycle = 0; cycle < 2; cycle += 1) {
244
+ const done = new Promise<void>(resolve => { terminal = resolve; });
245
+ const result = await executor.executePublic(randomUUID(), { workflowScript: "return [];", async: true }, new AbortController().signal, undefined, ctx as never);
246
+ assert.equal(result.isError, undefined, JSON.stringify(result));
247
+ const runId = result.details!.asyncId!;
248
+ const job = state.asyncJobs.get(runId)!;
249
+ dirs.push(job.asyncDir);
250
+ assert.equal(job.status, "running");
251
+ assert.equal(state.foregroundControls.size, 0);
252
+ const delayedAsk = writeRequest({ sessionId, runId });
253
+ tick();
254
+ assert.ok(sent.includes(delayedAsk), "lone registered workflow must discover a delayed ask without a query or foreground activation");
255
+ assert.equal(intervals.size, 1);
256
+ assert.equal(starts, cycle + 1);
257
+ const controlId = randomUUID();
258
+ // Model the unchanged foreground insertion/activation and last-control removal.
259
+ state.foregroundControls.set(controlId, { runId: controlId, activeChildren: new Map(), schedulingOwners: 1 } as never);
260
+ channel.activateTransport();
261
+ assert.equal(starts, cycle + 1, "foreground activation reuses the poller");
262
+ state.foregroundControls.delete(controlId);
263
+ fs.rmSync(channel.pending.get(delayedAsk)!.requestFile);
264
+ tick();
265
+ assert.equal(channel.pending.size, 0);
266
+ assert.equal(intervals.size, 1, "workflow demand survives last control and ask removal");
267
+ const laterAsk = writeRequest({ sessionId, runId });
268
+ tick();
269
+ assert.ok(sent.includes(laterAsk));
270
+ fs.rmSync(channel.pending.get(laterAsk)!.requestFile);
271
+ await done;
272
+ assert.equal(job.status, "complete");
273
+ tick();
274
+ assert.equal(intervals.size, 0, "terminal work and empty channel directories do not keep polling");
275
+ }
276
+ assert.equal(unrefs, 2);
277
+ } finally {
278
+ channel.dispose();
279
+ for (const controller of state.workflowControllers?.values() ?? []) controller.abort();
280
+ for (const dir of dirs) fs.rmSync(dir, { recursive: true, force: true });
281
+ fs.rmSync(root, { recursive: true, force: true });
282
+ }
283
+ });
284
+
285
+ it("caches runtime ownership at extension session_start and clears it at shutdown despite stale UI", () => {
286
+ const script = String.raw`
287
+ import assert from "node:assert/strict";
288
+ import fs from "node:fs";
289
+ import os from "node:os";
290
+ import path from "node:path";
291
+ import { randomUUID } from "node:crypto";
292
+ import registerExtension from "./index.ts";
293
+ import { resolveSupervisorChannelDir, ensureSupervisorChannelDir } from "./src/intercom/native-supervisor-channel.ts";
294
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "supervisor-owner-"));
295
+ const channels = [];
296
+ async function start(owner) {
297
+ const handlers = new Map();
298
+ const tools = new Map();
299
+ const pi = new Proxy({
300
+ events: { on() { return () => {}; }, emit() {} },
301
+ on(name, handler) { handlers.set(name, [...(handlers.get(name) ?? []), handler]); },
302
+ getAllTools() { return [...tools.keys()].map(name => ({ name })); },
303
+ registerTool(tool) { tools.set(tool.name, tool); },
304
+ registerCommand() {}, registerShortcut() {}, registerMessageRenderer() {}, sendMessage() {}, getSessionName() {},
305
+ }, { get(target, key) { return key in target ? target[key] : () => undefined; } });
306
+ const sessionFile = path.join(root, owner + ".jsonl");
307
+ fs.writeFileSync(sessionFile, JSON.stringify({ type: "session", version: 3, id: owner, timestamp: new Date().toISOString(), cwd: root }) + "\n");
308
+ let stale = false;
309
+ const ctx = {
310
+ cwd: root,
311
+ get hasUI() { if (stale) throw new Error("This extension ctx is stale after session replacement or reload."); return false; },
312
+ ui: { setWidget() {}, requestRender() {}, onTerminalInput() { return () => {}; }, notify() {}, theme: { fg(_name, text) { return text; }, bold(text) { return text; } } },
313
+ sessionManager: {
314
+ getSessionId() { if (stale) throw new Error("stale session manager"); return owner; },
315
+ getSessionFile() { return sessionFile; }, getEntries() { return []; },
316
+ },
317
+ modelRegistry: { getAvailable() { return []; } },
318
+ };
319
+ registerExtension(pi);
320
+ for (const handler of handlers.get("session_start") ?? []) await handler({ reason: "startup" }, ctx);
321
+ stale = true;
322
+ // Exercise the production stale-context clearing path, not direct state assignment.
323
+ for (const handler of handlers.get("session_before_compact") ?? []) await handler({ reason: "threshold", signal: new AbortController().signal });
324
+ return { tool: tools.get("subagent_supervisor"), shutdown: async () => { for (const handler of handlers.get("session_shutdown") ?? []) await handler(); } };
325
+ }
326
+ function ask(owner) {
327
+ const id = randomUUID();
328
+ const runId = randomUUID();
329
+ const dir = resolveSupervisorChannelDir(runId, "worker", 0);
330
+ channels.push(dir);
331
+ ensureSupervisorChannelDir(dir);
332
+ fs.writeFileSync(path.join(dir, "requests", id + ".json"), JSON.stringify({ type: "subagent.supervisor.request", id, createdAt: Date.now(), reason: "need_decision", message: "Decision?", expectsReply: true, orchestratorSessionId: owner, runId, agent: "worker", childIndex: 0 }));
333
+ return id;
334
+ }
335
+ let runtime;
336
+ try {
337
+ const owner = randomUUID();
338
+ runtime = await start(owner);
339
+ const first = ask(owner);
340
+ assert.match(JSON.stringify(await runtime.tool.execute("pending", { action: "pending" })), new RegExp(first));
341
+ await runtime.shutdown();
342
+ const afterShutdown = ask(owner);
343
+ await assert.rejects(runtime.tool.execute("reply", { action: "reply", replyTo: afterShutdown, message: "No authority" }), /No pending supervisor request found/);
344
+ runtime = undefined;
345
+ const replacement = randomUUID();
346
+ runtime = await start(replacement);
347
+ await assert.rejects(runtime.tool.execute("reply", { action: "reply", replyTo: first, message: "Foreign" }), /No pending supervisor request found/);
348
+ const next = ask(replacement);
349
+ assert.match(JSON.stringify(await runtime.tool.execute("reply", { action: "reply", replyTo: next, message: "Approved" })), new RegExp(next));
350
+ } finally {
351
+ await runtime?.shutdown();
352
+ for (const dir of channels) fs.rmSync(dir, { recursive: true, force: true });
353
+ fs.rmSync(root, { recursive: true, force: true });
354
+ }
355
+ `;
356
+ const env = { ...process.env };
357
+ delete env.SELESAI_SUBAGENT_CHILD;
358
+ execFileSync(process.execPath, ["--experimental-strip-types", "--import", "./test/support/register-loader.mjs", "--input-type=module", "--eval", script], { cwd: process.cwd(), env, stdio: "pipe", timeout: 30_000 });
359
+ });
360
+
361
+ it("fails closed for explicit mistargets and ambiguity without UI, then answers only the exact ask", async () => {
362
+ const sessionId = `session-${randomUUID()}`;
363
+ const runId = `run-${randomUUID()}`;
364
+ const tools = new Map<string, SupervisorTool>();
365
+ const state = makeState(`/sessions/${sessionId}.jsonl`, makeCtx(sessionId));
366
+ state.lastUiContext = null;
367
+ const channel = createNativeSupervisorChannel(makePi({ tools }) as never, state, { platform: "darwin" });
368
+ try {
369
+ channel.start();
370
+ const first = writeRequest({ sessionId, runId });
371
+ const tool = tools.get(NATIVE_SUPERVISOR_TOOL_NAME)!;
372
+ await assert.rejects(tool.execute("wrong", { action: "reply", to: "different-agent", message: "Approved" }), /No pending supervisor request matches/);
373
+ await assert.rejects(tool.execute("unknown", { action: "reply", replyTo: "unknown", message: "Approved" }), /No pending supervisor request found/);
374
+ assert.equal(channel.pending.has(first), true);
375
+ const second = writeRequest({ sessionId, runId });
376
+ await assert.rejects(tool.execute("ambiguous", { action: "reply", message: "Approved" }), /Multiple pending supervisor requests/);
377
+ await assert.rejects(tool.execute("agent", { action: "reply", to: "worker", message: "Approved" }), /Multiple pending supervisor requests match/);
378
+ const replies = path.join(resolveSupervisorChannelDir(runId, "worker", 0), "replies");
379
+ assert.deepEqual(fs.readdirSync(replies), []);
380
+ await tool.execute("exact", { action: "reply", replyTo: second, message: "Only second" });
381
+ assert.deepEqual(fs.readdirSync(replies), [`${second}.json`]);
382
+ assert.equal(JSON.parse(fs.readFileSync(path.join(replies, `${second}.json`), "utf8")).message, "Only second");
383
+ assert.equal(channel.pending.has(first), true);
384
+ assert.equal(channel.pending.has(second), false);
385
+ } finally { channel.dispose(); }
386
+ });
387
+
388
+ it("rejects stale discovered and cached asks with no UI", async () => {
389
+ const sessionId = `session-${randomUUID()}`;
390
+ const runId = `run-${randomUUID()}`;
391
+ const tools = new Map<string, SupervisorTool>();
392
+ const state = makeState(`/sessions/${sessionId}.jsonl`, makeCtx(sessionId));
393
+ state.lastUiContext = null;
394
+ const channel = createNativeSupervisorChannel(makePi({ tools }) as never, state, { platform: "darwin" });
395
+ try {
396
+ channel.start();
397
+ const tool = tools.get(NATIVE_SUPERVISOR_TOOL_NAME)!;
398
+ const expired = writeRequest({ sessionId, runId, expiresAt: Date.now() - 1 });
399
+ await assert.rejects(tool.execute("expired", { action: "reply", replyTo: expired, message: "Too late" }), /No pending supervisor request found/);
400
+ const missing = writeRequest({ sessionId, runId });
401
+ const resolved = writeRequest({ sessionId, runId });
402
+ await tool.execute("pending", { action: "pending" });
403
+ assert.equal(channel.pending.size, 2);
404
+ const dir = resolveSupervisorChannelDir(runId, "worker", 0);
405
+ fs.rmSync(path.join(dir, "requests", `${missing}.json`));
406
+ const replyFile = path.join(dir, "replies", `${resolved}.json`);
407
+ fs.writeFileSync(replyFile, JSON.stringify({ message: "Already answered" }));
408
+ for (const replyTo of [missing, resolved]) {
409
+ await assert.rejects(tool.execute("stale", { action: "reply", replyTo, message: "Must not overwrite" }), /No pending supervisor request found/);
410
+ }
411
+ assert.equal(channel.pending.size, 0);
412
+ assert.deepEqual(fs.readdirSync(path.join(dir, "replies")), [`${resolved}.json`]);
413
+ assert.equal(JSON.parse(fs.readFileSync(replyFile, "utf8")).message, "Already answered");
414
+ } finally { channel.dispose(); }
415
+ });
416
+
417
+ it("rechecks cached ownership without UI and never uses the persisted identity as authority", async () => {
418
+ const sessionId = `session-${randomUUID()}`;
419
+ const runId = `run-${randomUUID()}`;
420
+ const tools = new Map<string, SupervisorTool>();
421
+ const state = makeState(sessionId, makeCtx(sessionId));
422
+ const channel = createNativeSupervisorChannel(makePi({ tools }) as never, state, { platform: "darwin" });
423
+ try {
424
+ channel.start();
425
+ const requestId = writeRequest({ sessionId, runId });
426
+ const tool = tools.get(NATIVE_SUPERVISOR_TOOL_NAME)!;
427
+ await tool.execute("pending", { action: "pending" });
428
+ assert.equal(channel.pending.has(requestId), true);
429
+ state.lastUiContext = null;
430
+ state.supervisorOwnerSessionId = `replacement-${randomUUID()}`;
431
+ await assert.rejects(tool.execute("foreign", { action: "reply", replyTo: requestId, message: "Wrong owner" }), /No pending supervisor request found/);
432
+ assert.equal(channel.pending.size, 0);
433
+ state.supervisorOwnerSessionId = null;
434
+ const uncached = writeRequest({ sessionId, runId });
435
+ await assert.rejects(tool.execute("no-owner", { action: "reply", replyTo: uncached, message: "No authority" }), /No pending supervisor request found/);
436
+ const dir = resolveSupervisorChannelDir(runId, "worker", 0);
437
+ assert.equal(fs.existsSync(path.join(dir, "requests", `${requestId}.json`)), true, "foreign requests are not ours to delete");
438
+ assert.deepEqual(fs.readdirSync(path.join(dir, "replies")), []);
439
+ } finally { channel.dispose(); }
440
+ });
441
+
442
+ // D1: `refreshPendingRequests` only re-evaluates asks already in `pending`; it never reads the
443
+ // filesystem. With no watcher installed and the demand-gated poller stopped, an ask written by
444
+ // a child was unfindable by ANY number of `action:"pending"` calls.
445
+ it("discovers an ask written while nothing was polling", async () => {
446
+ const sessionId = `session-${randomUUID()}`;
447
+ const runId = `run-${randomUUID()}`;
448
+ const tools = new Map<string, SupervisorTool>();
449
+ const state = makeState(sessionId, makeCtx(sessionId));
450
+ const channel = createNativeSupervisorChannel(makePi({ tools }) as never, state, {
451
+ platform: "darwin",
452
+ watch: (() => { throw new Error("darwin must not install a supervisor fs watcher"); }) as never,
453
+ timers: {
454
+ setInterval: (() => ({ unref() {} }) as NodeJS.Timeout) as typeof setInterval,
455
+ clearInterval: (() => {}) as typeof clearInterval,
456
+ setImmediate,
457
+ clearImmediate,
458
+ },
459
+ });
460
+
461
+ try {
462
+ channel.start();
463
+ // The ask lands AFTER start(), so the start-time poll cannot have seen it.
464
+ const requestId = writeRequest({ sessionId, runId });
465
+ assert.equal(channel.pending.has(requestId), false, "precondition: nothing has scanned the ask yet");
466
+
467
+ const result = await tools.get(NATIVE_SUPERVISOR_TOOL_NAME)!.execute("pending", { action: "pending" });
468
+
469
+ assert.equal(channel.pending.has(requestId), true, "an explicit supervisor query must be authoritative against disk");
470
+ assert.match(text(result), new RegExp(requestId), "the ask must be listed with its reply id");
471
+ } finally {
472
+ channel.dispose();
473
+ }
474
+ });
475
+
476
+ it("accepts a reply to an ask that was never seen by a poller", async () => {
477
+ const sessionId = `session-${randomUUID()}`;
478
+ const runId = `run-${randomUUID()}`;
479
+ const tools = new Map<string, SupervisorTool>();
480
+ const channel = createNativeSupervisorChannel(makePi({ tools }) as never, makeState(sessionId, makeCtx(sessionId)), { platform: "darwin" });
481
+
482
+ try {
483
+ channel.start();
484
+ const requestId = writeRequest({ sessionId, runId });
485
+
486
+ const result = await tools.get(NATIVE_SUPERVISOR_TOOL_NAME)!.execute("reply", {
487
+ action: "reply",
488
+ replyTo: requestId,
489
+ message: "Approved",
490
+ });
491
+
492
+ assert.notEqual(result.isError, true, text(result));
493
+ const reply = path.join(resolveSupervisorChannelDir(runId, "worker", 0), "replies", `${requestId}.json`);
494
+ assert.equal(fs.existsSync(reply), true, "the child's reply file must be written");
495
+ assert.equal(JSON.parse(fs.readFileSync(reply, "utf-8")).message, "Approved");
496
+ } finally {
497
+ channel.dispose();
498
+ }
499
+ });
500
+
501
+ it("discovers a runtime-owned ask with a persisted session path and null UI context", async () => {
502
+ const sessionId = `session-${randomUUID()}`;
503
+ const runId = `run-${randomUUID()}`;
504
+ const tools = new Map<string, SupervisorTool>();
505
+ const sessionFile = path.join(process.cwd(), `${sessionId}.jsonl`);
506
+ const state = makeState(sessionFile, makeCtx(sessionId, sessionFile));
507
+ const channel = createNativeSupervisorChannel(makePi({ tools }) as never, state, { platform: "darwin" });
508
+
509
+ try {
510
+ channel.start();
511
+ state.lastUiContext = null;
512
+ const requestId = writeRequest({ sessionId, runId });
513
+ await tools.get(NATIVE_SUPERVISOR_TOOL_NAME)!.execute("pending", { action: "pending" });
514
+ assert.equal(channel.pending.has(requestId), true, "a null lastUiContext must not drop the ask");
515
+ assert.equal(state.currentSessionId, sessionFile, "global session identity must remain unchanged");
516
+ } finally {
517
+ channel.dispose();
518
+ }
519
+ });
520
+
521
+ it("still refuses an ask that belongs to another session when the UI context is null", () => {
522
+ const sessionId = `session-${randomUUID()}`;
523
+ const foreignSessionId = `session-${randomUUID()}`;
524
+ const runId = `run-${randomUUID()}`;
525
+ const foreignId = writeRequest({ sessionId: foreignSessionId, runId });
526
+ const tools = new Map<string, SupervisorTool>();
527
+ const state = makeState(`/sessions/${sessionId}.jsonl`, makeCtx(sessionId));
528
+ state.lastUiContext = null;
529
+ const channel = createNativeSupervisorChannel(makePi({ tools }) as never, state, { platform: "darwin" });
530
+
531
+ try {
532
+ channel.start();
533
+ assert.equal(channel.pending.has(foreignId), false, "session-ownership filtering must survive the null-context path");
534
+ } finally {
535
+ channel.dispose();
536
+ }
537
+ });
538
+
539
+ it("retains a failed progress update until a later query accepts it without duplicate notifications", async () => {
540
+ const sessionId = `session-${randomUUID()}`;
541
+ const runId = `run-${randomUUID()}`;
542
+ const tools = new Map<string, SupervisorTool>();
543
+ const state = makeState(`/sessions/${sessionId}.jsonl`, makeCtx(sessionId));
544
+ state.lastUiContext = null;
545
+ let attempts = 0;
546
+ let accepted = 0;
547
+ let requestFile = "";
548
+ let visibleText = "";
549
+ const pi = makePi({ tools, onSend: (message) => {
550
+ visibleText = message.content;
551
+ attempts++;
552
+ assert.equal(fs.existsSync(requestFile), true, "retain the update until sendMessage returns");
553
+ if (attempts === 1) throw new Error("notification not accepted");
554
+ accepted++;
555
+ } });
556
+ const channel = createNativeSupervisorChannel(pi as never, state, { platform: "darwin" });
557
+ try {
558
+ channel.start();
559
+ const requestId = writeRequest({ sessionId, runId, reason: "progress_update" });
560
+ requestFile = path.join(resolveSupervisorChannelDir(runId, "worker", 0), "requests", `${requestId}.json`);
561
+ const tool = tools.get(NATIVE_SUPERVISOR_TOOL_NAME)!;
562
+ await tool.execute("failed", { action: "pending" });
563
+ assert.equal(attempts, 1);
564
+ assert.equal(accepted, 0);
565
+ assert.equal(fs.existsSync(requestFile), true);
566
+ assert.equal(channel.pending.size, 0, "a progress update must not become a blocking ask");
567
+ await tool.execute("retry", { action: "pending" });
568
+ assert.equal(attempts, 2);
569
+ assert.equal(accepted, 1);
570
+ assert.equal(fs.existsSync(requestFile), false);
571
+ assert.ok(visibleText.includes(`Live guidance: subagent({ action: "steer", id: "${runId}", index: 0, message: "..." })`));
572
+ assert.doesNotMatch(visibleText, /Reply with:|replyTo:|Child intercom target:/);
573
+ await tool.execute("after-acceptance", { action: "pending" });
574
+ assert.equal(attempts, 2);
575
+ assert.equal(accepted, 1);
576
+ assert.equal(channel.pending.size, 0);
577
+ } finally { channel.dispose(); }
578
+ });
579
+
580
+ it("registers both asks even when every user-turn notification throws", () => {
581
+ const sessionId = `session-${randomUUID()}`;
582
+ const runId = `run-${randomUUID()}`;
583
+ const requestId = writeRequest({ sessionId, runId });
584
+ const tools = new Map<string, SupervisorTool>();
585
+ const pi = makePi({ tools, onSend: () => { throw new Error("no UI attached"); } });
586
+ const secondId = writeRequest({ sessionId, runId });
587
+ const channel = createNativeSupervisorChannel(pi as never, makeState(sessionId, makeCtx(sessionId)), { platform: "darwin" });
588
+
589
+ try {
590
+ channel.start();
591
+ assert.equal(channel.pending.has(requestId), true, "a display failure must not lose an already-queued ask");
592
+ assert.equal(channel.pending.has(secondId), true, "a display failure must not abort discovery of remaining asks");
593
+ } finally {
594
+ channel.dispose();
595
+ }
596
+ });
597
+
598
+ // Drive the real child-side disk protocol: steering cannot resolve asks, explicit reply can.
599
+ it("only unblocks the real contact_supervisor tool through an explicit reply, not steer or follow_up", async () => {
600
+ const sessionId = `session-${randomUUID()}`;
601
+ const workflowRunId = `workflow-${randomUUID()}`;
602
+ const channelDir = resolveSupervisorChannelDir(workflowRunId, "worker", 0);
603
+ createdChannels.push(channelDir);
604
+ const supervisorTools = new Map<string, SupervisorTool>();
605
+ const childTools = new Map<string, SupervisorTool>();
606
+ const state = makeState(sessionId, makeCtx(sessionId));
607
+ const queued: ForegroundSteerInput[] = [];
608
+ const control: ForegroundRunControl = {
609
+ runId: workflowRunId, mode: "single", startedAt: 100, updatedAt: 100,
610
+ activeChildren: new Map([[0, {
611
+ index: 0, agent: "worker", startedAt: 100, updatedAt: 100,
612
+ steer: async (input) => { queued.push(input); return { state: "queued" }; },
613
+ }]]),
614
+ };
615
+ state.foregroundControls.set(workflowRunId, control);
616
+ const channel = createNativeSupervisorChannel(makePi({ tools: supervisorTools }) as never, state, { platform: "darwin" });
617
+
618
+ try {
619
+ channel.start();
620
+ registerNativeSupervisorClient(makePi({ tools: childTools }) as never, {
621
+ channelDir,
622
+ runId: workflowRunId,
623
+ agent: "worker",
624
+ childIndex: 0,
625
+ orchestratorSessionId: sessionId,
626
+ });
627
+ const target = { control, workflowRunId, sourceRunId: workflowRunId };
628
+ const ordinary = await steerWorkflowForegroundTarget({ target, message: "No ask is open here." });
629
+ assert.equal(ordinary.details.steering?.state, "pending");
630
+ assert.equal(ordinary.details.steering?.targets[0]?.state, "queued");
631
+ assert.deepEqual(queued.splice(0), [{ message: "No ask is open here." }]);
632
+
633
+ // The child blocks here exactly as it does in production: inside an open tool call,
634
+ // polling its reply file.
635
+ const blocked = childTools.get("contact_supervisor")!.execute("ask", {
636
+ action: "ask",
637
+ reason: "need_decision",
638
+ message: "Which option should I take?",
639
+ } as never);
640
+
641
+ await waitForCondition(() => fs.readdirSync(path.join(channelDir, "requests")).length > 0, "the child's ask to reach disk");
642
+ await supervisorTools.get(NATIVE_SUPERVISOR_TOOL_NAME)!.execute("pending", { action: "pending" });
643
+
644
+ const requestId = [...channel.pending.keys()][0]!;
645
+ assert.ok(requestId);
646
+ const otherIndexAsk = writeRequest({ sessionId, runId: workflowRunId, index: 3 });
647
+ await supervisorTools.get(NATIVE_SUPERVISOR_TOOL_NAME)!.execute("pending", { action: "pending" });
648
+ for (const mode of ["steer", "follow_up"] as const) {
649
+ const result = await steerWorkflowForegroundTarget({ target, message: "After this is resolved, update the docs.", mode });
650
+ assert.equal(result.details.steering?.state, "pending");
651
+ assert.equal(result.details.steering?.targets[0]?.state, "queued");
652
+ assert.doesNotMatch(text(result), /the child is unblocked/);
653
+ assert.equal(channel.pending.has(requestId), true);
654
+ assert.equal(channel.pending.has(otherIndexAsk), true);
655
+ assert.equal(fs.existsSync(path.join(channelDir, "replies", `${requestId}.json`)), false);
656
+ assert.equal(fs.existsSync(path.join(channelDir, "requests", `${requestId}.json`)), true);
657
+ }
658
+ assert.deepEqual(queued, [
659
+ { message: "After this is resolved, update the docs." },
660
+ { message: "After this is resolved, update the docs.", mode: "follow_up" },
661
+ ]);
662
+ await supervisorTools.get(NATIVE_SUPERVISOR_TOOL_NAME)!.execute("reply", {
663
+ action: "reply", replyTo: requestId, message: "Take option A.",
664
+ });
665
+
666
+ const childResult = await blocked;
667
+ assert.notEqual(childResult.isError, true, text(childResult));
668
+ assert.match(text(childResult), /Take option A\./);
669
+ assert.doesNotMatch(text(childResult), /update the docs/);
670
+ } finally {
671
+ channel.dispose();
672
+ }
673
+ });
674
+ });
675
+
676
+ async function waitForCondition(condition: () => boolean, description: string): Promise<void> {
677
+ const deadline = Date.now() + 2000;
678
+ while (!condition()) {
679
+ if (Date.now() > deadline) assert.fail(`Timed out waiting for ${description}`);
680
+ await new Promise((resolve) => setTimeout(resolve, 10));
681
+ }
682
+ }