@selesai/code 0.5.20 → 0.5.21

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 (263) hide show
  1. package/dist/extensions/handoff-new.test.ts +8 -9
  2. package/dist/extensions/handoff-new.ts +6 -8
  3. package/dist/extensions/pi-subagents/CHANGELOG.md +1367 -0
  4. package/dist/extensions/pi-subagents/README.md +602 -220
  5. package/dist/extensions/pi-subagents/banner.png +0 -0
  6. package/dist/extensions/pi-subagents/index.ts +1 -0
  7. package/dist/extensions/pi-subagents/install.mjs +3 -3
  8. package/dist/extensions/pi-subagents/package-lock.json +3405 -0
  9. package/dist/extensions/pi-subagents/package.json +26 -14
  10. package/dist/extensions/pi-subagents/prompts/gather-context-and-clarify.md +1 -1
  11. package/dist/extensions/pi-subagents/prompts/parallel-cleanup.md +9 -9
  12. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +3 -3
  13. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +7 -7
  14. package/dist/extensions/pi-subagents/prompts/parallel-research.md +6 -6
  15. package/dist/extensions/pi-subagents/prompts/parallel-review.md +8 -8
  16. package/dist/extensions/pi-subagents/prompts/review-loop.md +13 -11
  17. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +267 -176
  18. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +89 -16
  19. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +19 -0
  20. package/dist/extensions/pi-subagents/src/agents/agents.ts +218 -120
  21. package/dist/extensions/pi-subagents/src/agents/frontmatter.ts +67 -13
  22. package/dist/extensions/pi-subagents/src/agents/proactive-skills.ts +2 -2
  23. package/dist/extensions/pi-subagents/src/agents/skills.ts +25 -12
  24. package/dist/extensions/pi-subagents/src/api/background-work.ts +197 -0
  25. package/dist/extensions/pi-subagents/src/api/capability-ceiling.ts +17 -0
  26. package/dist/extensions/pi-subagents/src/api/delegation.ts +285 -0
  27. package/dist/extensions/pi-subagents/src/api/preflight.ts +399 -0
  28. package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +165 -0
  29. package/dist/extensions/pi-subagents/src/extension/config.ts +7 -1
  30. package/dist/extensions/pi-subagents/src/extension/doctor.ts +15 -0
  31. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -1
  32. package/dist/extensions/pi-subagents/src/extension/index.ts +113 -157
  33. package/dist/extensions/pi-subagents/src/extension/rpc.ts +41 -4
  34. package/dist/extensions/pi-subagents/src/extension/schemas.ts +45 -15
  35. package/dist/extensions/pi-subagents/src/extension/steering-notices.ts +35 -0
  36. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +28 -15
  37. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -3
  38. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +225 -29
  39. package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +11 -0
  40. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +8 -10
  41. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +452 -62
  42. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +60 -9
  43. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +198 -52
  44. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +119 -21
  45. package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +67 -0
  46. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +2 -0
  47. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +16 -8
  48. package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +6 -4
  49. package/dist/extensions/pi-subagents/src/runs/background/completion-dedupe.ts +2 -11
  50. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +260 -13
  51. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +32 -6
  52. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +171 -90
  53. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +280 -0
  54. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +149 -88
  55. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +14 -2
  56. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +33 -17
  57. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +9 -1
  58. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +38 -10
  59. package/dist/extensions/pi-subagents/src/runs/background/steering.ts +237 -0
  60. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +1297 -307
  61. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +610 -0
  62. package/dist/extensions/pi-subagents/src/runs/background/top-level-async.ts +2 -1
  63. package/dist/extensions/pi-subagents/src/runs/background/wait-config.ts +36 -0
  64. package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +26 -0
  65. package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +230 -0
  66. package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +22 -6
  67. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +228 -140
  68. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +452 -138
  69. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +90 -0
  70. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +1001 -416
  71. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +443 -132
  72. package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +38 -0
  73. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +178 -0
  74. package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +121 -0
  75. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +41 -93
  76. package/dist/extensions/pi-subagents/src/runs/shared/context-mode.ts +44 -0
  77. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +11 -9
  78. package/dist/extensions/pi-subagents/src/runs/shared/long-running-guard.ts +4 -0
  79. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
  80. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +36 -0
  81. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +44 -7
  82. package/dist/extensions/pi-subagents/src/runs/shared/nested-render.ts +4 -1
  83. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +154 -0
  84. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +18 -0
  85. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +174 -54
  86. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +38 -31
  87. package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +90 -5
  88. package/dist/extensions/pi-subagents/src/runs/shared/session-lease.ts +299 -0
  89. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +61 -6
  90. package/dist/extensions/pi-subagents/src/runs/shared/spawn-budget.ts +128 -0
  91. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +112 -7
  92. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +14 -6
  93. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +149 -36
  94. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +181 -0
  95. package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +83 -0
  96. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +11 -5
  97. package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +50 -4
  98. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +63 -14
  99. package/dist/extensions/pi-subagents/src/shared/accessible-dir.ts +25 -0
  100. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +37 -7
  101. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +18 -43
  102. package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +52 -0
  103. package/dist/extensions/pi-subagents/src/shared/env.ts +16 -0
  104. package/dist/extensions/pi-subagents/src/shared/file-system-retry.ts +47 -0
  105. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +28 -3
  106. package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +123 -0
  107. package/dist/extensions/pi-subagents/src/shared/model-info.ts +8 -5
  108. package/dist/extensions/pi-subagents/src/shared/settings.ts +9 -1
  109. package/dist/extensions/pi-subagents/src/shared/status-format.ts +7 -1
  110. package/dist/extensions/pi-subagents/src/shared/types.ts +522 -51
  111. package/dist/extensions/pi-subagents/src/shared/utils.ts +59 -53
  112. package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +604 -0
  113. package/dist/extensions/pi-subagents/src/slash/delegation-json.ts +108 -0
  114. package/dist/extensions/pi-subagents/src/slash/delegation-request.ts +249 -0
  115. package/dist/extensions/pi-subagents/src/slash/prompt-template-bridge.ts +353 -345
  116. package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +2 -2
  117. package/dist/extensions/pi-subagents/src/slash/selector.ts +147 -0
  118. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +265 -23
  119. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +2 -2
  120. package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +428 -0
  121. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +362 -0
  122. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +472 -0
  123. package/dist/extensions/pi-subagents/src/tui/fleet.ts +664 -0
  124. package/dist/extensions/pi-subagents/src/tui/render.ts +115 -31
  125. package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +220 -0
  126. package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +205 -0
  127. package/dist/extensions/pi-subagents/src/watchdog/emission-guard.ts +123 -0
  128. package/dist/extensions/pi-subagents/src/watchdog/lsp-diagnostics.ts +532 -0
  129. package/dist/extensions/pi-subagents/src/watchdog/model-selection.ts +167 -0
  130. package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +117 -0
  131. package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +433 -0
  132. package/dist/extensions/pi-subagents/src/watchdog/render.ts +54 -0
  133. package/dist/extensions/pi-subagents/src/watchdog/review.ts +298 -0
  134. package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +714 -0
  135. package/dist/extensions/pi-subagents/src/watchdog/settings.ts +528 -0
  136. package/dist/extensions/pi-subagents/src/watchdog/tool-actions.ts +155 -0
  137. package/dist/extensions/pi-subagents/src/watchdog/turn-delta.ts +161 -0
  138. package/dist/extensions/pi-subagents/src/watchdog/types.ts +188 -0
  139. package/dist/extensions/pi-subagents/src/watchdog/warning-format.ts +73 -0
  140. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +130 -2
  141. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +439 -0
  142. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1632 -61
  143. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +54 -1
  144. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +112 -0
  145. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +98 -1
  146. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +179 -9
  147. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +16 -9
  148. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +6 -5
  149. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +589 -9
  150. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +528 -21
  151. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +227 -3
  152. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +235 -7
  153. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +27 -0
  154. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +172 -2
  155. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1502 -18
  156. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +993 -4
  157. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +27 -0
  158. package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +7 -1
  159. package/dist/extensions/pi-subagents/test/support/helpers.ts +32 -0
  160. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +59 -2
  161. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +32 -6
  162. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +25 -24
  163. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +59 -35
  164. package/dist/extensions/pi-subagents/test/support/session-lease-child.mjs +42 -0
  165. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +703 -69
  166. package/dist/extensions/pi-subagents/test/unit/accessible-dir.test.ts +82 -0
  167. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +15 -15
  168. package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +66 -66
  169. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +478 -19
  170. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +228 -7
  171. package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +1 -1
  172. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +198 -78
  173. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +15 -1
  174. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +102 -7
  175. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +439 -6
  176. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +41 -1
  177. package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +79 -0
  178. package/dist/extensions/pi-subagents/test/unit/background-work.test.ts +285 -0
  179. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-pi-args.test.ts +84 -0
  180. package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +68 -0
  181. package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +326 -0
  182. package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +82 -0
  183. package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +46 -4
  184. package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +3 -1
  185. package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +9 -17
  186. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +175 -3
  187. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +10 -5
  188. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +126 -6
  189. package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +219 -0
  190. package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +913 -0
  191. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +13 -2
  192. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +3 -3
  193. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +351 -0
  194. package/dist/extensions/pi-subagents/test/unit/fleet-transcript.test.ts +280 -0
  195. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +575 -0
  196. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +65 -0
  197. package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +94 -1
  198. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +23 -0
  199. package/dist/extensions/pi-subagents/test/unit/host-peer-runtime-imports.test.ts +157 -0
  200. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +367 -5
  201. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +11 -0
  202. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +36 -0
  203. package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +25 -3
  204. package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +1 -1
  205. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +341 -10
  206. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +16 -0
  207. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +52 -0
  208. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +168 -7
  209. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +85 -7
  210. package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +175 -0
  211. package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +1 -1
  212. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +243 -16
  213. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +79 -36
  214. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +174 -56
  215. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +322 -0
  216. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +204 -0
  217. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +20 -12
  218. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +117 -4
  219. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +40 -7
  220. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +48 -1
  221. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +21 -3
  222. package/dist/extensions/pi-subagents/test/unit/selector.test.ts +57 -0
  223. package/dist/extensions/pi-subagents/test/unit/session-lease.test.ts +326 -0
  224. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +101 -2
  225. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +111 -1
  226. package/dist/extensions/pi-subagents/test/unit/spawn-budget.test.ts +124 -0
  227. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +6 -4
  228. package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +7 -0
  229. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +397 -0
  230. package/dist/extensions/pi-subagents/test/unit/steering-notices.test.ts +58 -0
  231. package/dist/extensions/pi-subagents/test/unit/steering.test.ts +173 -0
  232. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +25 -4
  233. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +390 -25
  234. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +837 -0
  235. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +86 -0
  236. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +15 -0
  237. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +47 -1
  238. package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +56 -10
  239. package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +387 -0
  240. package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +110 -0
  241. package/dist/extensions/pi-subagents/test/unit/watchdog-emission-guard.test.ts +62 -0
  242. package/dist/extensions/pi-subagents/test/unit/watchdog-lsp-diagnostics.test.ts +144 -0
  243. package/dist/extensions/pi-subagents/test/unit/watchdog-model-selection.test.ts +121 -0
  244. package/dist/extensions/pi-subagents/test/unit/watchdog-render.test.ts +76 -0
  245. package/dist/extensions/pi-subagents/test/unit/watchdog-review.test.ts +327 -0
  246. package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +863 -0
  247. package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +290 -0
  248. package/dist/extensions/pi-subagents/test/unit/watchdog-tool-actions.test.ts +91 -0
  249. package/dist/extensions/pi-subagents/test/unit/watchdog-turn-delta.test.ts +130 -0
  250. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +32 -4
  251. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +5 -1
  252. package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +30 -0
  253. package/dist/extensions/question/batch.ts +39 -0
  254. package/dist/extensions/question/dialog-adapter.ts +23 -1
  255. package/dist/extensions/question/index.ts +237 -7
  256. package/dist/extensions/question/schemas.ts +28 -15
  257. package/dist/extensions/question/tests/batch.test.ts +26 -0
  258. package/dist/extensions/question/tests/ui-protocol.test.ts +23 -3
  259. package/dist/extensions/question/tui-adapter.ts +2 -3
  260. package/dist/extensions/question/types.ts +37 -0
  261. package/dist/extensions/question/ui-protocol.ts +3 -3
  262. package/dist/skills/batch-grill-me/SKILL.md +3 -1
  263. package/package.json +1 -1
@@ -1,57 +1,54 @@
1
1
  /**
2
- * Subagent completion notifications.
2
+ * Completion notification delivery.
3
3
  *
4
- * Successful (completed) async results are held briefly and emitted as a
5
- * single grouped message when sibling jobs finish within a short window (see
6
- * `completion-batcher.ts`). Failed and paused results bypass grouping and fire
7
- * immediately, flushing any held successes first, so failure and attention
8
- * signals are never delayed.
4
+ * Async result files call this notifier directly and are deleted only after
5
+ * `sendMessage()` accepts the notification. The event bus remains an
6
+ * observation channel, not a delivery acknowledgement.
9
7
  */
10
8
 
11
9
  import type { ExtensionAPI } from "@selesai/code";
12
- import { buildCompletionKey, getGlobalSeenMap, markSeenWithTtl } from "./completion-dedupe.ts";
10
+ import { buildCompletionKey, markSeenWithTtl } from "./completion-dedupe.ts";
13
11
  import {
14
12
  type CompletionBatchConfig,
15
13
  type CompletionBatcher,
16
14
  createCompletionBatcher,
17
15
  resolveCompletionBatchConfig,
18
16
  } from "./completion-batcher.ts";
19
- import { SUBAGENT_ASYNC_COMPLETE_EVENT, type SubagentState } from "../../shared/types.ts";
20
-
21
- interface ChainStepResult {
22
- agent: string;
23
- output: string;
24
- success: boolean;
25
- }
17
+ import { SUBAGENT_ASYNC_COMPLETE_EVENT, SUBAGENT_FOREGROUND_COMPLETE_EVENT, type ParallelHandoffReference, type SubagentState } from "../../shared/types.ts";
26
18
 
27
19
  export interface SubagentNotifyDetails {
28
20
  agent: string;
29
21
  status: "completed" | "failed" | "paused";
22
+ source?: "async" | "foreground";
30
23
  taskInfo?: string;
31
24
  resultPreview: string;
32
25
  durationMs?: number;
33
26
  sessionLabel?: string;
34
27
  sessionValue?: string;
28
+ handoffPath?: string;
35
29
  }
36
30
 
37
- interface SubagentResult {
38
- id: string | null;
39
- agent: string | null;
40
- success: boolean;
41
- summary: string;
31
+ export interface CompletionNotification {
32
+ [key: string]: unknown;
33
+ id?: string | null;
34
+ source?: "async" | "foreground";
35
+ agent?: string | null;
36
+ success?: boolean;
37
+ summary?: string;
42
38
  exitCode?: number;
43
39
  state?: string;
44
- timestamp: number;
40
+ timestamp?: number;
45
41
  durationMs?: number;
46
42
  cwd?: string;
47
43
  sessionFile?: string;
48
44
  shareUrl?: string;
49
45
  gistUrl?: string;
50
46
  shareError?: string;
51
- results?: ChainStepResult[];
52
47
  taskIndex?: number;
53
48
  totalTasks?: number;
54
49
  sessionId?: string | null;
50
+ triggerTurn?: boolean;
51
+ parallelHandoff?: ParallelHandoffReference;
55
52
  }
56
53
 
57
54
  interface NotifyTimerApi {
@@ -65,6 +62,11 @@ export interface RegisterSubagentNotifyOptions {
65
62
  now?: () => number;
66
63
  }
67
64
 
65
+ export interface CompletionNotifier {
66
+ deliver(result: CompletionNotification): Promise<boolean>;
67
+ dispose(): void;
68
+ }
69
+
68
70
  function formatSessionLine(details: SubagentNotifyDetails): string | undefined {
69
71
  if (!details.sessionValue) return undefined;
70
72
  return details.sessionLabel ? `${details.sessionLabel}: ${details.sessionValue}` : details.sessionValue;
@@ -72,10 +74,13 @@ function formatSessionLine(details: SubagentNotifyDetails): string | undefined {
72
74
 
73
75
  export function formatSingleCompletion(details: SubagentNotifyDetails): string {
74
76
  const sessionLine = formatSessionLine(details);
77
+ const taskKind = details.source === "foreground" ? "Detached foreground task" : "Background task";
75
78
  return [
76
- `Background task ${details.status}: **${details.agent}**${details.taskInfo ?? ""}`,
79
+ `${taskKind} ${details.status}: **${details.agent}**${details.taskInfo ?? ""}`,
77
80
  "",
78
81
  details.resultPreview.trim() ? details.resultPreview : "(no output)",
82
+ details.handoffPath ? "" : undefined,
83
+ details.handoffPath ? `Parallel handoff: ${details.handoffPath}` : undefined,
79
84
  sessionLine ? "" : undefined,
80
85
  sessionLine,
81
86
  ]
@@ -83,6 +88,43 @@ export function formatSingleCompletion(details: SubagentNotifyDetails): string {
83
88
  .join("\n");
84
89
  }
85
90
 
91
+ export function parseSubagentNotifyContent(content: string): SubagentNotifyDetails | undefined {
92
+ const lines = content.split("\n");
93
+ const match = (lines[0] ?? "").match(/^(Background task|Detached foreground task) (completed|failed|paused): \*\*(.+?)\*\*(?:\s+(\([^)]*\)))?$/);
94
+ if (!match) return undefined;
95
+ const body = lines.slice(2);
96
+ let sessionIndex = -1;
97
+ for (let i = body.length - 1; i >= 1; i--) {
98
+ if (body[i - 1]?.trim() === "" && /^(Session|Session file|Session share error):\s+/.test(body[i]!)) {
99
+ sessionIndex = i;
100
+ break;
101
+ }
102
+ }
103
+ const sessionLine = sessionIndex >= 0 ? body[sessionIndex] : undefined;
104
+ const handoffIndex = body.findIndex((line) => line.startsWith("Parallel handoff: "));
105
+ const metadataIndexes = [sessionIndex, handoffIndex].filter((index) => index >= 0);
106
+ const firstMetadataIndex = metadataIndexes.length ? Math.min(...metadataIndexes) : body.length;
107
+ const resultEnd = firstMetadataIndex > 0 && body[firstMetadataIndex - 1]?.trim() === "" ? firstMetadataIndex - 1 : firstMetadataIndex;
108
+ const resultPreview = body.slice(0, resultEnd).join("\n").trim() || "(no output)";
109
+ const handoffPath = handoffIndex >= 0 ? body[handoffIndex]!.slice("Parallel handoff: ".length).trim() : undefined;
110
+ let sessionLabel: string | undefined;
111
+ let sessionValue: string | undefined;
112
+ if (sessionLine) {
113
+ const separator = sessionLine.indexOf(":");
114
+ sessionLabel = sessionLine.slice(0, separator).toLowerCase();
115
+ sessionValue = sessionLine.slice(separator + 1).trim();
116
+ }
117
+ return {
118
+ agent: match[3]!,
119
+ status: match[2] as SubagentNotifyDetails["status"],
120
+ ...(match[1] === "Detached foreground task" ? { source: "foreground" as const } : {}),
121
+ ...(match[4] ? { taskInfo: match[4] } : {}),
122
+ resultPreview,
123
+ ...(handoffPath ? { handoffPath } : {}),
124
+ ...(sessionLabel && sessionValue ? { sessionLabel, sessionValue } : {}),
125
+ };
126
+ }
127
+
86
128
  export function formatGroupedCompletion(details: SubagentNotifyDetails[]): string {
87
129
  const header = `Background tasks completed (${details.length}): ${details.map((d) => `**${d.agent}**${d.taskInfo ?? ""}`).join(", ")}`;
88
130
  const blocks: string[] = [header, ""];
@@ -92,35 +134,47 @@ export function formatGroupedCompletion(details: SubagentNotifyDetails[]): strin
92
134
  const sessionLine = formatSessionLine(detail);
93
135
  blocks.push(`${index + 1}. ${detail.agent}${detail.taskInfo ?? ""}`);
94
136
  blocks.push(detail.resultPreview.trim() ? detail.resultPreview : "(no output)");
137
+ if (detail.handoffPath) blocks.push(`Parallel handoff: ${detail.handoffPath}`);
95
138
  if (sessionLine) blocks.push(sessionLine);
96
139
  blocks.push("");
97
140
  }
98
141
  return blocks.join("\n").trimEnd();
99
142
  }
100
143
 
101
- function sendCompletion(pi: Pick<ExtensionAPI, "sendMessage">, details: SubagentNotifyDetails[]): void {
102
- if (details.length === 0) return;
103
- const content = details.length === 1
104
- ? formatSingleCompletion(details[0]!)
105
- : formatGroupedCompletion(details);
106
- pi.sendMessage(
107
- {
108
- customType: "subagent-notify",
109
- content,
110
- display: true,
111
- },
112
- { triggerTurn: true },
113
- );
144
+ interface PendingCompletion {
145
+ key: string;
146
+ details: SubagentNotifyDetails;
147
+ triggerTurn: boolean;
148
+ resolve(accepted: boolean): void;
149
+ }
150
+
151
+ function sendCompletion(pi: Pick<ExtensionAPI, "sendMessage">, items: PendingCompletion[]): boolean {
152
+ if (items.length === 0) return true;
153
+ const details = items.map((item) => item.details);
154
+ const content = details.length === 1 ? formatSingleCompletion(details[0]!) : formatGroupedCompletion(details);
155
+ try {
156
+ pi.sendMessage(
157
+ {
158
+ customType: "subagent-notify",
159
+ content,
160
+ display: true,
161
+ },
162
+ { triggerTurn: items.some((item) => item.triggerTurn) },
163
+ );
164
+ return true;
165
+ } catch {
166
+ return false;
167
+ }
114
168
  }
115
169
 
116
- function completionBatchKey(result: SubagentResult): string {
170
+ function completionBatchKey(result: CompletionNotification): string {
117
171
  const sessionId = typeof result.sessionId === "string" ? result.sessionId.trim() : "";
118
172
  if (sessionId) return `session:${sessionId}`;
119
173
  const cwd = typeof result.cwd === "string" ? result.cwd.trim() : "";
120
174
  return cwd ? `cwd:${cwd}` : "unknown";
121
175
  }
122
176
 
123
- export function buildCompletionDetails(result: SubagentResult): SubagentNotifyDetails {
177
+ export function buildCompletionDetails(result: CompletionNotification): SubagentNotifyDetails {
124
178
  const agent = result.agent ?? "unknown";
125
179
  const summary = typeof result.summary === "string" ? result.summary : "";
126
180
  const paused = !result.success && (
@@ -129,12 +183,15 @@ export function buildCompletionDetails(result: SubagentResult): SubagentNotifyDe
129
183
  || summary.startsWith("Paused after interrupt.")
130
184
  );
131
185
  const status = paused ? "paused" : result.success ? "completed" : "failed";
132
-
133
186
  const taskInfo =
134
187
  result.taskIndex !== undefined && result.totalTasks !== undefined
135
188
  ? ` (${result.taskIndex + 1}/${result.totalTasks})`
136
189
  : undefined;
137
190
 
191
+ const parallelHandoff = result.parallelHandoff && typeof result.parallelHandoff === "object"
192
+ ? result.parallelHandoff as { path?: unknown }
193
+ : undefined;
194
+ const handoffPath = typeof parallelHandoff?.path === "string" ? parallelHandoff.path : undefined;
138
195
  const session =
139
196
  result.shareUrl
140
197
  ? { label: "Session", value: result.shareUrl }
@@ -143,13 +200,14 @@ export function buildCompletionDetails(result: SubagentResult): SubagentNotifyDe
143
200
  : result.sessionFile
144
201
  ? { label: "Session file", value: result.sessionFile }
145
202
  : undefined;
146
-
147
203
  return {
148
204
  agent,
149
205
  status,
206
+ ...(result.source ? { source: result.source } : {}),
150
207
  ...(taskInfo ? { taskInfo } : {}),
151
208
  resultPreview: summary,
152
209
  ...(typeof result.durationMs === "number" ? { durationMs: result.durationMs } : {}),
210
+ ...(handoffPath ? { handoffPath } : {}),
153
211
  ...(session ? { sessionLabel: session.label, sessionValue: session.value } : {}),
154
212
  };
155
213
  }
@@ -158,68 +216,91 @@ export default function registerSubagentNotify(
158
216
  pi: ExtensionAPI,
159
217
  state: Pick<SubagentState, "currentSessionId">,
160
218
  options: RegisterSubagentNotifyOptions = {},
161
- ): void {
162
- const unsubscribeStoreKey = "__pi_subagents_notify_unsubscribe__";
163
- const batcherStoreKey = "__pi_subagents_notify_batcher__";
164
- const globalStore = globalThis as Record<string, unknown>;
165
- const previousUnsubscribe = globalStore[unsubscribeStoreKey];
166
- if (typeof previousUnsubscribe === "function") {
167
- try {
168
- previousUnsubscribe();
169
- } catch {
170
- // Best effort cleanup for stale handlers from an older reload.
171
- }
172
- }
173
- const previousBatcher = globalStore[batcherStoreKey];
174
- if (previousBatcher && typeof (previousBatcher as { dispose?: () => void }).dispose === "function") {
175
- try {
176
- (previousBatcher as { dispose: () => void }).dispose();
177
- } catch {
178
- // Best effort cleanup for a stale batcher from an older reload.
179
- }
180
- }
181
-
182
- const seen = getGlobalSeenMap("__pi_subagents_notify_seen__");
219
+ ): CompletionNotifier {
220
+ const seen = new Map<string, number>();
221
+ const pending = new Map<string, Promise<boolean>>();
183
222
  const ttlMs = 10 * 60 * 1000;
184
- const nowFn = options.now ?? Date.now;
223
+ const now = options.now ?? Date.now;
185
224
  const batchConfig = resolveCompletionBatchConfig(options.batchConfig);
186
- const batchers = new Map<string, CompletionBatcher<SubagentNotifyDetails>>();
187
- globalStore[batcherStoreKey] = {
188
- dispose() {
189
- for (const batcher of batchers.values()) batcher.dispose();
190
- batchers.clear();
191
- },
192
- };
225
+ const batchers = new Map<string, CompletionBatcher<PendingCompletion>>();
226
+ let disposed = false;
193
227
 
194
- const handleComplete = (data: unknown) => {
195
- const result = data as SubagentResult;
196
- if (typeof result.sessionId !== "string" || result.sessionId !== state.currentSessionId) return;
197
- const now = nowFn();
198
- const key = buildCompletionKey(result, "notify");
199
- if (markSeenWithTtl(seen, key, now, ttlMs)) return;
200
-
201
- const details = buildCompletionDetails(result);
202
- const batchKey = completionBatchKey(result);
203
- let batcher = batchers.get(batchKey);
228
+ const settle = (items: PendingCompletion[], accepted: boolean) => {
229
+ for (const item of items) {
230
+ pending.delete(item.key);
231
+ if (accepted) markSeenWithTtl(seen, item.key, now(), ttlMs);
232
+ item.resolve(accepted);
233
+ }
234
+ };
235
+ const emit = (items: PendingCompletion[]) => settle(items, sendCompletion(pi, items));
236
+ const getBatcher = (result: CompletionNotification) => {
237
+ const key = completionBatchKey(result);
238
+ let batcher = batchers.get(key);
204
239
  if (!batcher) {
205
- batcher = createCompletionBatcher<SubagentNotifyDetails>({
240
+ batcher = createCompletionBatcher<PendingCompletion>({
206
241
  config: batchConfig,
207
- emit: (items) => sendCompletion(pi, items),
242
+ emit,
208
243
  ...(options.timers ? { timers: options.timers } : {}),
209
- now: nowFn,
244
+ now,
210
245
  });
211
- batchers.set(batchKey, batcher);
246
+ batchers.set(key, batcher);
212
247
  }
248
+ return batcher;
249
+ };
250
+
251
+ const deliver = (result: CompletionNotification): Promise<boolean> => {
252
+ if (disposed || typeof result.sessionId !== "string" || result.sessionId !== state.currentSessionId) return Promise.resolve(false);
253
+ const key = buildCompletionKey(result, "notify");
254
+ const seenAt = seen.get(key);
255
+ if (seenAt !== undefined && now() - seenAt <= ttlMs) return Promise.resolve(true);
256
+ if (seenAt !== undefined) seen.delete(key);
257
+ const inFlight = pending.get(key);
258
+ if (inFlight) return inFlight;
259
+ const details = buildCompletionDetails(result);
260
+ let resolve!: (accepted: boolean) => void;
261
+ const completion = new Promise<boolean>((settleCompletion) => { resolve = settleCompletion; });
262
+ pending.set(key, completion);
263
+ const item: PendingCompletion = {
264
+ key,
265
+ details,
266
+ triggerTurn: result.triggerTurn !== false,
267
+ resolve,
268
+ };
269
+ if (details.source === "foreground") {
270
+ emit([item]);
271
+ return completion;
272
+ }
273
+ const batcher = getBatcher(result);
213
274
  if (details.status !== "completed") {
214
- // Failures and paused runs bypass grouping. Flush any held
215
- // successes for the same owner first so they are not stranded
216
- // behind this signal, then emit the non-completion result immediately.
217
275
  batcher.flush();
218
- sendCompletion(pi, [details]);
219
- return;
276
+ emit([item]);
277
+ return completion;
220
278
  }
221
- batcher.push(details);
279
+ batcher.push(item);
280
+ return completion;
222
281
  };
223
282
 
224
- globalStore[unsubscribeStoreKey] = pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, handleComplete);
283
+ const unsubscribeAsync = pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, (data) => {
284
+ void deliver(data as CompletionNotification);
285
+ });
286
+ const unsubscribeForeground = pi.events.on(SUBAGENT_FOREGROUND_COMPLETE_EVENT, (data) => {
287
+ void deliver(data as CompletionNotification);
288
+ });
289
+
290
+ return {
291
+ deliver,
292
+ dispose() {
293
+ if (disposed) return;
294
+ disposed = true;
295
+ for (const batcher of batchers.values()) settle(batcher.dispose(), false);
296
+ batchers.clear();
297
+ for (const unsubscribe of [unsubscribeAsync, unsubscribeForeground]) {
298
+ try {
299
+ unsubscribe?.();
300
+ } catch {
301
+ // The runtime is already shutting down; pending records stay on disk.
302
+ }
303
+ }
304
+ },
305
+ };
225
306
  }
@@ -0,0 +1,280 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { writeAtomicJson, writePrivateAtomicJson } from "../../shared/atomic-json.ts";
4
+ import {
5
+ SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
6
+ type AsyncStatus,
7
+ type CanonicalSessionTerminalV1,
8
+ type ProcessInstanceExitV1,
9
+ type ProcessTerminalReason,
10
+ type ProcessTerminalV1,
11
+ } from "../../shared/types.ts";
12
+ import { canonicalSessionId, inspectSessionLease } from "../shared/session-lease.ts";
13
+
14
+ export interface ProcessTerminalCandidate {
15
+ version: 1;
16
+ runId: string;
17
+ runnerProcessInstanceId: string;
18
+ writers: Record<string, ProcessInstanceExitV1[]>;
19
+ expectedWriters?: Record<string, number>;
20
+ sessionFile?: string;
21
+ revivalLeaseToken?: string;
22
+ revivalLeaseReleaseAcknowledged?: boolean;
23
+ }
24
+
25
+ export interface RunnerCloseObservation {
26
+ processInstanceId: string;
27
+ closeObservedAt: number;
28
+ exitCode: number | null;
29
+ signal: string | null;
30
+ }
31
+
32
+ function isRecord(value: unknown): value is Record<string, unknown> {
33
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
34
+ }
35
+
36
+ function validProcessInstance(value: unknown, kind?: "runner" | "pi-writer"): value is ProcessInstanceExitV1 {
37
+ if (!isRecord(value)) return false;
38
+ if (typeof value.processInstanceId !== "string" || value.processInstanceId.length === 0) return false;
39
+ if (kind ? value.kind !== kind : (value.kind !== "runner" && value.kind !== "pi-writer")) return false;
40
+ if (typeof value.closeObservedAt !== "number" || !Number.isFinite(value.closeObservedAt)) return false;
41
+ if (typeof value.exitCode !== "number" && value.exitCode !== null) return false;
42
+ if (typeof value.signal !== "string" && value.signal !== null) return false;
43
+ return value.kind === "runner"
44
+ ? value.attempt === undefined
45
+ : typeof value.attempt === "number" && Number.isInteger(value.attempt) && value.attempt >= 0;
46
+ }
47
+
48
+ function validInstance(value: unknown): value is ProcessInstanceExitV1 {
49
+ return validProcessInstance(value, "pi-writer");
50
+ }
51
+
52
+ export function processTerminalCandidatePath(asyncDir: string): string {
53
+ return path.join(asyncDir, "process-terminal-candidate.json");
54
+ }
55
+
56
+ export function processTerminalPath(asyncDir: string): string {
57
+ return path.join(asyncDir, "process-terminal.json");
58
+ }
59
+
60
+ function errorMessage(error: unknown): string {
61
+ return error instanceof Error ? error.message : String(error);
62
+ }
63
+
64
+ export function readProcessTerminalCandidate(asyncDir: string): ProcessTerminalCandidate | undefined {
65
+ try {
66
+ const raw = JSON.parse(fs.readFileSync(processTerminalCandidatePath(asyncDir), "utf-8")) as unknown;
67
+ if (!isRecord(raw) || raw.version !== 1 || typeof raw.runId !== "string" || typeof raw.runnerProcessInstanceId !== "string" || !isRecord(raw.writers)) {
68
+ throw new Error(`Invalid process-terminal candidate in '${asyncDir}'.`);
69
+ }
70
+ const writers: Record<string, ProcessInstanceExitV1[]> = {};
71
+ for (const [index, entries] of Object.entries(raw.writers)) {
72
+ if (!Array.isArray(entries) || !entries.every(validInstance)) throw new Error(`Invalid writer process records for child '${index}'.`);
73
+ writers[index] = entries;
74
+ }
75
+ let expectedWriters: Record<string, number> | undefined;
76
+ if (raw.expectedWriters !== undefined) {
77
+ if (!isRecord(raw.expectedWriters)) throw new Error("Invalid expected writer process records.");
78
+ expectedWriters = {};
79
+ for (const [index, count] of Object.entries(raw.expectedWriters)) {
80
+ if (typeof count !== "number" || !Number.isInteger(count) || count < 0) throw new Error(`Invalid expected writer count for child '${index}'.`);
81
+ expectedWriters[index] = count;
82
+ }
83
+ }
84
+ if (raw.sessionFile !== undefined && typeof raw.sessionFile !== "string") throw new Error("Invalid process-terminal candidate sessionFile.");
85
+ if (raw.revivalLeaseToken !== undefined && typeof raw.revivalLeaseToken !== "string") throw new Error("Invalid process-terminal candidate lease token.");
86
+ if (raw.revivalLeaseReleaseAcknowledged !== undefined && typeof raw.revivalLeaseReleaseAcknowledged !== "boolean") throw new Error("Invalid process-terminal lease release acknowledgement.");
87
+ return {
88
+ version: 1,
89
+ runId: raw.runId,
90
+ runnerProcessInstanceId: raw.runnerProcessInstanceId,
91
+ writers,
92
+ ...(expectedWriters ? { expectedWriters } : {}),
93
+ ...(raw.sessionFile ? { sessionFile: raw.sessionFile } : {}),
94
+ ...(raw.revivalLeaseToken ? { revivalLeaseToken: raw.revivalLeaseToken } : {}),
95
+ ...(raw.revivalLeaseReleaseAcknowledged !== undefined ? { revivalLeaseReleaseAcknowledged: raw.revivalLeaseReleaseAcknowledged } : {}),
96
+ };
97
+ } catch (error) {
98
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
99
+ throw error;
100
+ }
101
+ }
102
+
103
+ export function writeProcessTerminalCandidate(asyncDir: string, candidate: ProcessTerminalCandidate): void {
104
+ writePrivateAtomicJson(processTerminalCandidatePath(asyncDir), candidate);
105
+ }
106
+
107
+ export function markProcessTerminalCandidateLeaseRelease(asyncDir: string, token: string, acknowledged: boolean): void {
108
+ const candidate = readProcessTerminalCandidate(asyncDir);
109
+ if (!candidate || candidate.revivalLeaseToken !== token) return;
110
+ writeProcessTerminalCandidate(asyncDir, { ...candidate, revivalLeaseReleaseAcknowledged: acknowledged });
111
+ }
112
+
113
+ function unknownProof(runId: string, runnerProcessInstanceId: string, reason: ProcessTerminalReason, diagnostic?: string): ProcessTerminalV1 {
114
+ return { version: 1, state: "unknown", runId, runnerProcessInstanceId, reason, ...(diagnostic ? { diagnostic } : {}) };
115
+ }
116
+
117
+ function resumeDisposition(state: string | undefined, sessionFile: string | undefined): "resumable" | "non-resumable" | "unavailable" {
118
+ if (state === "stopped") return "non-resumable";
119
+ if (state !== "complete" && state !== "completed" && state !== "failed" && state !== "paused") return "unavailable";
120
+ return sessionFile && fs.existsSync(sessionFile) ? "resumable" : "unavailable";
121
+ }
122
+
123
+ function sessionProjection(candidate: ProcessTerminalCandidate, lease: ReturnType<typeof inspectSessionLease>): CanonicalSessionTerminalV1 | undefined {
124
+ if (!candidate.sessionFile || lease.state !== "free") return undefined;
125
+ if (candidate.revivalLeaseToken && candidate.revivalLeaseReleaseAcknowledged !== true) return undefined;
126
+ return {
127
+ canonicalSessionId: canonicalSessionId(candidate.sessionFile),
128
+ leaseDisposition: candidate.revivalLeaseToken ? "released" : "not-held",
129
+ freeAtObservation: true,
130
+ ...(candidate.revivalLeaseToken ? { canonicalSessionLeaseReleased: true } : {}),
131
+ };
132
+ }
133
+
134
+ function validateProof(raw: unknown, asyncDir: string, fallback?: { runId?: string; runnerProcessInstanceId?: string }): raw is ProcessTerminalV1 {
135
+ if (!isRecord(raw) || raw.version !== 1 || !["pending", "observed", "unknown", "not-started"].includes(String(raw.state)) || typeof raw.runId !== "string" || !raw.runId || typeof raw.runnerProcessInstanceId !== "string" || !raw.runnerProcessInstanceId) {
136
+ throw new Error(`Invalid process-terminal proof in '${asyncDir}'.`);
137
+ }
138
+ if (fallback?.runId && raw.runId !== fallback.runId) throw new Error(`Process-terminal proof in '${asyncDir}' belongs to run '${raw.runId}', expected '${fallback.runId}'.`);
139
+ if (fallback?.runnerProcessInstanceId && raw.runnerProcessInstanceId !== fallback.runnerProcessInstanceId) throw new Error(`Process-terminal proof in '${asyncDir}' belongs to runner '${raw.runnerProcessInstanceId}', expected '${fallback.runnerProcessInstanceId}'.`);
140
+ if (raw.instances !== undefined && (!Array.isArray(raw.instances) || !raw.instances.every((entry) => validProcessInstance(entry)))) {
141
+ throw new Error(`Invalid process-terminal instances in '${asyncDir}'.`);
142
+ }
143
+ if (raw.state === "observed") {
144
+ if (typeof raw.observedAt !== "number" || !Number.isFinite(raw.observedAt)) throw new Error(`Observed process-terminal proof in '${asyncDir}' is missing observedAt.`);
145
+ if (!Array.isArray(raw.instances)) throw new Error(`Observed process-terminal proof in '${asyncDir}' is missing instances.`);
146
+ const runner = raw.instances.find((entry) => isRecord(entry) && entry.kind === "runner");
147
+ if (!validProcessInstance(runner, "runner") || runner.processInstanceId !== raw.runnerProcessInstanceId) throw new Error(`Observed process-terminal proof in '${asyncDir}' has no matching runner instance.`);
148
+ }
149
+ if (raw.resumeDisposition !== undefined && !["resumable", "non-resumable", "unavailable"].includes(String(raw.resumeDisposition))) throw new Error(`Invalid process-terminal resume disposition in '${asyncDir}'.`);
150
+ return true;
151
+ }
152
+
153
+ export function sanitizeProcessTerminal(value: unknown, fallback: { runId?: string; runnerProcessInstanceId?: string }, label = "status"): ProcessTerminalV1 | undefined {
154
+ if (value === undefined) return undefined;
155
+ try {
156
+ validateProof(value, label, fallback);
157
+ return value;
158
+ } catch (error) {
159
+ return unknownProof(fallback.runId ?? label, fallback.runnerProcessInstanceId ?? "unknown", "proof-write-failed", errorMessage(error));
160
+ }
161
+ }
162
+
163
+ export function readProcessTerminal(asyncDir: string, fallback?: { runId?: string; runnerProcessInstanceId?: string }): ProcessTerminalV1 | undefined {
164
+ try {
165
+ const raw = JSON.parse(fs.readFileSync(processTerminalPath(asyncDir), "utf-8")) as unknown;
166
+ validateProof(raw, asyncDir, fallback);
167
+ return raw;
168
+ } catch (error) {
169
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
170
+ return unknownProof(fallback?.runId ?? path.basename(asyncDir), fallback?.runnerProcessInstanceId ?? "unknown", "proof-write-failed", errorMessage(error));
171
+ }
172
+ }
173
+
174
+ function stepProcessTerminalProof(
175
+ proof: ProcessTerminalV1,
176
+ childIndex: number,
177
+ state: ProcessTerminalV1["state"],
178
+ records: ProcessInstanceExitV1[],
179
+ resumeDispositionValue: ProcessTerminalV1["resumeDisposition"],
180
+ ): ProcessTerminalV1 {
181
+ const base = {
182
+ version: 1 as const,
183
+ runId: proof.runId,
184
+ childIndex,
185
+ runnerProcessInstanceId: proof.runnerProcessInstanceId,
186
+ ...(resumeDispositionValue ? { resumeDisposition: resumeDispositionValue } : {}),
187
+ };
188
+ if (state === "observed") {
189
+ return { ...base, state, observedAt: proof.state === "observed" ? proof.observedAt : Date.now(), instances: records };
190
+ }
191
+ if (state === "unknown") {
192
+ return { ...base, state, reason: proof.state === "unknown" ? proof.reason : "writer-close-unverified" };
193
+ }
194
+ return { ...base, state };
195
+ }
196
+
197
+ function overlayStatus(asyncDir: string, proof: ProcessTerminalV1, candidate?: ProcessTerminalCandidate): void {
198
+ const statusPath = path.join(asyncDir, "status.json");
199
+ try {
200
+ const status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
201
+ status.processTerminal = proof;
202
+ if (status.steps) {
203
+ for (const [index, step] of status.steps.entries()) {
204
+ const records = candidate?.writers[String(index)] ?? [];
205
+ const expected = candidate?.expectedWriters?.[String(index)] ?? (records.length > 0 ? records.length : 0);
206
+ const stepState = expected === 0 ? "not-started" : proof.state === "observed" && records.length === expected ? "observed" : proof.state === "pending" ? "pending" : "unknown";
207
+ step.processTerminal = stepProcessTerminalProof(proof, index, stepState, records, resumeDisposition(step.status, step.sessionFile ?? candidate?.sessionFile));
208
+ }
209
+ }
210
+ writeAtomicJson(statusPath, status);
211
+ } catch {
212
+ // The proof sidecar remains authoritative when terminal status is unavailable.
213
+ }
214
+ }
215
+
216
+ export function finalizeProcessTerminal(
217
+ asyncDir: string,
218
+ runId: string,
219
+ runnerClose: RunnerCloseObservation,
220
+ ): ProcessTerminalV1 {
221
+ const existing = readProcessTerminal(asyncDir, { runId, runnerProcessInstanceId: runnerClose.processInstanceId });
222
+ if (existing && fs.existsSync(processTerminalPath(asyncDir))) {
223
+ if (existing.state === "observed" && existing.runId === runId && existing.runnerProcessInstanceId === runnerClose.processInstanceId) return existing;
224
+ if (existing.state === "unknown") return existing;
225
+ }
226
+ let proof: ProcessTerminalV1;
227
+ let candidateForOverlay: ProcessTerminalCandidate | undefined;
228
+ try {
229
+ const candidate = readProcessTerminalCandidate(asyncDir);
230
+ candidateForOverlay = candidate;
231
+ if (!candidate) proof = unknownProof(runId, runnerClose.processInstanceId, "runner-candidate-missing");
232
+ else if (candidate.runId !== runId || candidate.runnerProcessInstanceId !== runnerClose.processInstanceId) proof = unknownProof(runId, runnerClose.processInstanceId, "runner-instance-mismatch");
233
+ else {
234
+ const allWriters = Object.values(candidate.writers).flat();
235
+ const status = (() => {
236
+ try { return JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8")) as AsyncStatus; } catch { return undefined; }
237
+ })();
238
+ const session = candidate.sessionFile ? inspectSessionLease(candidate.sessionFile) : undefined;
239
+ const writerEntries = Object.entries(candidate.writers);
240
+ const expectedWriters = candidate.expectedWriters ?? Object.fromEntries(writerEntries.map(([index, records]) => [index, records.length]));
241
+ const expectedEntries = Object.entries(expectedWriters);
242
+ const expectedIndexes = new Set(expectedEntries.map(([index]) => index));
243
+ const writerIndexes = new Set(writerEntries.map(([index]) => index));
244
+ const inconsistentWriters = writerEntries.some(([index, records]) => !expectedIndexes.has(index) || records.length !== expectedWriters[index])
245
+ || expectedEntries.some(([index, expected]) => !writerIndexes.has(index) && expected !== 0);
246
+ if (session && session.state !== "free") {
247
+ proof = unknownProof(runId, runnerClose.processInstanceId, session.state === "owned" ? "canonical-session-lease-active" : "canonical-session-unavailable");
248
+ } else if (candidate.revivalLeaseToken && candidate.revivalLeaseReleaseAcknowledged !== true) {
249
+ proof = unknownProof(runId, runnerClose.processInstanceId, "canonical-session-release-unverified");
250
+ } else if (inconsistentWriters || (allWriters.length === 0 && expectedEntries.length === 0)) {
251
+ proof = unknownProof(runId, runnerClose.processInstanceId, "writer-close-unverified");
252
+ } else {
253
+ const runner: ProcessInstanceExitV1 = { kind: "runner", ...runnerClose };
254
+ const canonicalSession = session && sessionProjection(candidate, session);
255
+ proof = {
256
+ version: 1,
257
+ state: "observed",
258
+ runId,
259
+ runnerProcessInstanceId: runnerClose.processInstanceId,
260
+ observedAt: runnerClose.closeObservedAt,
261
+ instances: [runner, ...allWriters],
262
+ resumeDisposition: resumeDisposition(status?.state, candidate.sessionFile ?? status?.sessionFile),
263
+ ...(canonicalSession ? { canonicalSession } : {}),
264
+ };
265
+ }
266
+ }
267
+ } catch (error) {
268
+ proof = unknownProof(runId, runnerClose.processInstanceId, "proof-write-failed", errorMessage(error));
269
+ }
270
+ let durable = false;
271
+ try {
272
+ writeAtomicJson(processTerminalPath(asyncDir), proof);
273
+ durable = true;
274
+ overlayStatus(asyncDir, proof, candidateForOverlay);
275
+ fs.appendFileSync(path.join(asyncDir, "events.jsonl"), `${JSON.stringify({ type: "subagent.run.process_terminal", lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION, ts: Date.now(), runId, processTerminal: proof })}\n`, "utf-8");
276
+ } catch {
277
+ // Do not emit a process-terminal event when the proof sidecar was not durable.
278
+ }
279
+ return durable ? proof : unknownProof(runId, runnerClose.processInstanceId, "proof-write-failed", "Failed to persist process-terminal proof.");
280
+ }