@selesai/code 0.13.0 → 0.13.2

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 (463) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/core/agent-session.d.ts +1 -1
  3. package/dist/core/agent-session.js +1 -1
  4. package/dist/core/sdk.d.ts +2 -2
  5. package/dist/core/sdk.js +1 -1
  6. package/dist/core/system-prompt.d.ts +1 -1
  7. package/dist/core/system-prompt.js +1 -1
  8. package/dist/extensions/package.json +1 -2
  9. package/dist/extensions/pi-hermes-memory/CHANGELOG.md +371 -0
  10. package/dist/extensions/pi-hermes-memory/LICENSE +21 -0
  11. package/dist/extensions/pi-hermes-memory/README.md +643 -0
  12. package/dist/extensions/pi-hermes-memory/docs/0.1/TASKS.md +197 -0
  13. package/dist/extensions/pi-hermes-memory/docs/0.2/PLAN.md +290 -0
  14. package/dist/extensions/pi-hermes-memory/docs/0.2/TASKS.md +134 -0
  15. package/dist/extensions/pi-hermes-memory/docs/0.2/TEST-PLAN.md +216 -0
  16. package/dist/extensions/pi-hermes-memory/docs/0.3/PLAN.md +330 -0
  17. package/dist/extensions/pi-hermes-memory/docs/0.3/TASKS.md +125 -0
  18. package/dist/extensions/pi-hermes-memory/docs/0.4/PLAN.md +160 -0
  19. package/dist/extensions/pi-hermes-memory/docs/0.4/TASKS.md +113 -0
  20. package/dist/extensions/pi-hermes-memory/docs/0.6/CHANGELOG.md +57 -0
  21. package/dist/extensions/pi-hermes-memory/docs/0.6/PLAN.md +202 -0
  22. package/dist/extensions/pi-hermes-memory/docs/0.6/TASKS.md +144 -0
  23. package/dist/extensions/pi-hermes-memory/docs/0.7/PLAN.md +349 -0
  24. package/dist/extensions/pi-hermes-memory/docs/0.7/TASKS.md +110 -0
  25. package/dist/extensions/pi-hermes-memory/docs/PUBLISHING.md +149 -0
  26. package/dist/extensions/pi-hermes-memory/docs/ROADMAP.md +468 -0
  27. package/dist/extensions/pi-hermes-memory/docs/images/memory-architecture.svg +1 -0
  28. package/dist/extensions/pi-hermes-memory/docs/images/pi-logo.svg +22 -0
  29. package/dist/extensions/pi-hermes-memory/docs/images/pi_memory.png +0 -0
  30. package/dist/extensions/pi-hermes-memory/docs/images/pi_memory.svg +19 -0
  31. package/dist/extensions/pi-hermes-memory/docs/images/security-flow.svg +1 -0
  32. package/dist/extensions/pi-hermes-memory/docs/images/session-lifecycle.svg +1 -0
  33. package/dist/extensions/pi-hermes-memory/docs/images/source-architecture.svg +1 -0
  34. package/dist/extensions/pi-hermes-memory/docs/mermaid/memory-architecture.mmd +49 -0
  35. package/dist/extensions/pi-hermes-memory/docs/mermaid/security-flow.mmd +40 -0
  36. package/dist/extensions/pi-hermes-memory/docs/mermaid/session-lifecycle.mmd +56 -0
  37. package/dist/extensions/pi-hermes-memory/docs/mermaid/source-architecture.mmd +54 -0
  38. package/dist/extensions/pi-hermes-memory/package-lock.json +3799 -0
  39. package/dist/extensions/pi-hermes-memory/package.json +66 -0
  40. package/dist/extensions/pi-hermes-memory/scripts/check-min-sdk.mjs +99 -0
  41. package/dist/extensions/pi-hermes-memory/scripts/ensure-dev.mjs +57 -0
  42. package/dist/extensions/pi-hermes-memory/src/auto-consolidation-warning.ts +6 -0
  43. package/dist/extensions/pi-hermes-memory/src/config.ts +178 -0
  44. package/dist/extensions/pi-hermes-memory/src/constants.ts +424 -0
  45. package/dist/extensions/pi-hermes-memory/src/extension-root-migration.ts +639 -0
  46. package/dist/extensions/pi-hermes-memory/src/handlers/auto-consolidate.ts +401 -0
  47. package/dist/extensions/pi-hermes-memory/src/handlers/background-review.ts +297 -0
  48. package/dist/extensions/pi-hermes-memory/src/handlers/child-process-watchdog.mjs +90 -0
  49. package/dist/extensions/pi-hermes-memory/src/handlers/correction-detector.ts +300 -0
  50. package/dist/extensions/pi-hermes-memory/src/handlers/index-sessions.ts +86 -0
  51. package/dist/extensions/pi-hermes-memory/src/handlers/insights.ts +78 -0
  52. package/dist/extensions/pi-hermes-memory/src/handlers/interview.ts +37 -0
  53. package/dist/extensions/pi-hermes-memory/src/handlers/learn-memory.ts +186 -0
  54. package/dist/extensions/pi-hermes-memory/src/handlers/message-parts.ts +27 -0
  55. package/dist/extensions/pi-hermes-memory/src/handlers/pi-child-process.ts +485 -0
  56. package/dist/extensions/pi-hermes-memory/src/handlers/preview-context.ts +105 -0
  57. package/dist/extensions/pi-hermes-memory/src/handlers/review-memory-ops.ts +506 -0
  58. package/dist/extensions/pi-hermes-memory/src/handlers/session-backfill.ts +145 -0
  59. package/dist/extensions/pi-hermes-memory/src/handlers/session-flush.ts +151 -0
  60. package/dist/extensions/pi-hermes-memory/src/handlers/session-live-index.ts +94 -0
  61. package/dist/extensions/pi-hermes-memory/src/handlers/skills-command.ts +1334 -0
  62. package/dist/extensions/pi-hermes-memory/src/handlers/standing-pin.ts +111 -0
  63. package/dist/extensions/pi-hermes-memory/src/handlers/switch-project.ts +75 -0
  64. package/dist/extensions/pi-hermes-memory/src/handlers/sync-markdown-memories.ts +266 -0
  65. package/dist/extensions/pi-hermes-memory/src/index.ts +378 -0
  66. package/dist/extensions/pi-hermes-memory/src/lifecycle-timing.ts +54 -0
  67. package/dist/extensions/pi-hermes-memory/src/paths.ts +61 -0
  68. package/dist/extensions/pi-hermes-memory/src/project-context.ts +13 -0
  69. package/dist/extensions/pi-hermes-memory/src/project-memory-migration.ts +93 -0
  70. package/dist/extensions/pi-hermes-memory/src/project.ts +157 -0
  71. package/dist/extensions/pi-hermes-memory/src/prompt-context.ts +55 -0
  72. package/dist/extensions/pi-hermes-memory/src/store/atomic-lock-coordinator.ts +375 -0
  73. package/dist/extensions/pi-hermes-memory/src/store/canonical-storage-path.ts +54 -0
  74. package/dist/extensions/pi-hermes-memory/src/store/content-scanner.ts +101 -0
  75. package/dist/extensions/pi-hermes-memory/src/store/db.ts +1180 -0
  76. package/dist/extensions/pi-hermes-memory/src/store/fts-query.ts +101 -0
  77. package/dist/extensions/pi-hermes-memory/src/store/markdown-mutation-lock.ts +38 -0
  78. package/dist/extensions/pi-hermes-memory/src/store/memory-lookup.ts +24 -0
  79. package/dist/extensions/pi-hermes-memory/src/store/memory-store.ts +1191 -0
  80. package/dist/extensions/pi-hermes-memory/src/store/recovery-maintenance.ts +66 -0
  81. package/dist/extensions/pi-hermes-memory/src/store/schema.ts +118 -0
  82. package/dist/extensions/pi-hermes-memory/src/store/session-anchor-search.ts +472 -0
  83. package/dist/extensions/pi-hermes-memory/src/store/session-indexer.ts +511 -0
  84. package/dist/extensions/pi-hermes-memory/src/store/session-parser.ts +213 -0
  85. package/dist/extensions/pi-hermes-memory/src/store/session-search.ts +234 -0
  86. package/dist/extensions/pi-hermes-memory/src/store/skill-store.ts +950 -0
  87. package/dist/extensions/pi-hermes-memory/src/store/skill-utils.ts +133 -0
  88. package/dist/extensions/pi-hermes-memory/src/store/sqlite-memory-store.ts +1002 -0
  89. package/dist/extensions/pi-hermes-memory/src/store/sqlite-native.ts +241 -0
  90. package/dist/extensions/pi-hermes-memory/src/store/standing-instructions.ts +247 -0
  91. package/dist/extensions/pi-hermes-memory/src/tools/memory-search-tool.ts +95 -0
  92. package/dist/extensions/pi-hermes-memory/src/tools/memory-tool.ts +469 -0
  93. package/dist/extensions/pi-hermes-memory/src/tools/session-search-tool.ts +246 -0
  94. package/dist/extensions/pi-hermes-memory/src/tools/shared-output-view.ts +170 -0
  95. package/dist/extensions/pi-hermes-memory/src/tools/skill-tool.ts +333 -0
  96. package/dist/extensions/pi-hermes-memory/src/tools/tool-result-views.ts +105 -0
  97. package/dist/extensions/pi-hermes-memory/src/types.ts +214 -0
  98. package/dist/extensions/pi-hermes-memory/tests/config.test.ts +434 -0
  99. package/dist/extensions/pi-hermes-memory/tests/extension-root-migration.test.ts +368 -0
  100. package/dist/extensions/pi-hermes-memory/tests/handlers/auto-consolidate.test.ts +984 -0
  101. package/dist/extensions/pi-hermes-memory/tests/handlers/background-review.test.ts +950 -0
  102. package/dist/extensions/pi-hermes-memory/tests/handlers/correction-detector.test.ts +671 -0
  103. package/dist/extensions/pi-hermes-memory/tests/handlers/insights.test.ts +157 -0
  104. package/dist/extensions/pi-hermes-memory/tests/handlers/interview.test.ts +127 -0
  105. package/dist/extensions/pi-hermes-memory/tests/handlers/pi-child-process.test.ts +905 -0
  106. package/dist/extensions/pi-hermes-memory/tests/handlers/preview-context.test.ts +152 -0
  107. package/dist/extensions/pi-hermes-memory/tests/handlers/prompt-context.test.ts +166 -0
  108. package/dist/extensions/pi-hermes-memory/tests/handlers/resources-discover.test.ts +71 -0
  109. package/dist/extensions/pi-hermes-memory/tests/handlers/review-memory-ops.test.ts +614 -0
  110. package/dist/extensions/pi-hermes-memory/tests/handlers/session-backfill.test.ts +192 -0
  111. package/dist/extensions/pi-hermes-memory/tests/handlers/session-flush.test.ts +677 -0
  112. package/dist/extensions/pi-hermes-memory/tests/handlers/session-live-index.test.ts +225 -0
  113. package/dist/extensions/pi-hermes-memory/tests/handlers/skills-command.test.ts +827 -0
  114. package/dist/extensions/pi-hermes-memory/tests/handlers/standing-pin.test.ts +134 -0
  115. package/dist/extensions/pi-hermes-memory/tests/handlers/sync-markdown-memories.test.ts +491 -0
  116. package/dist/extensions/pi-hermes-memory/tests/handlers/system-prompt.test.ts +199 -0
  117. package/dist/extensions/pi-hermes-memory/tests/handlers/thinking-max.test.ts +64 -0
  118. package/dist/extensions/pi-hermes-memory/tests/index.test.ts +30 -0
  119. package/dist/extensions/pi-hermes-memory/tests/integration/flow.test.ts +126 -0
  120. package/dist/extensions/pi-hermes-memory/tests/lifecycle-timing.test.ts +76 -0
  121. package/dist/extensions/pi-hermes-memory/tests/paths.test.ts +23 -0
  122. package/dist/extensions/pi-hermes-memory/tests/project-memory-migration.test.ts +80 -0
  123. package/dist/extensions/pi-hermes-memory/tests/project-rebinding.test.ts +115 -0
  124. package/dist/extensions/pi-hermes-memory/tests/project.test.ts +120 -0
  125. package/dist/extensions/pi-hermes-memory/tests/run-all-timeout.test.ts +107 -0
  126. package/dist/extensions/pi-hermes-memory/tests/run-all.sh +45 -0
  127. package/dist/extensions/pi-hermes-memory/tests/store/atomic-lock-coordinator.test.ts +381 -0
  128. package/dist/extensions/pi-hermes-memory/tests/store/content-scanner.test.ts +388 -0
  129. package/dist/extensions/pi-hermes-memory/tests/store/db.test.ts +1046 -0
  130. package/dist/extensions/pi-hermes-memory/tests/store/markdown-mutation-lock.test.ts +34 -0
  131. package/dist/extensions/pi-hermes-memory/tests/store/memory-store.test.ts +2150 -0
  132. package/dist/extensions/pi-hermes-memory/tests/store/recovery-maintenance.test.ts +170 -0
  133. package/dist/extensions/pi-hermes-memory/tests/store/session-anchor-search.test.ts +210 -0
  134. package/dist/extensions/pi-hermes-memory/tests/store/session-indexer.test.ts +643 -0
  135. package/dist/extensions/pi-hermes-memory/tests/store/session-parser.test.ts +282 -0
  136. package/dist/extensions/pi-hermes-memory/tests/store/session-search.test.ts +287 -0
  137. package/dist/extensions/pi-hermes-memory/tests/store/skill-store.test.ts +678 -0
  138. package/dist/extensions/pi-hermes-memory/tests/store/skill-utils.test.ts +21 -0
  139. package/dist/extensions/pi-hermes-memory/tests/store/sqlite-lazy-load.test.ts +78 -0
  140. package/dist/extensions/pi-hermes-memory/tests/store/sqlite-memory-store.test.ts +543 -0
  141. package/dist/extensions/pi-hermes-memory/tests/store/sqlite-native.test.ts +164 -0
  142. package/dist/extensions/pi-hermes-memory/tests/store/standing-instructions.test.ts +171 -0
  143. package/dist/extensions/pi-hermes-memory/tests/tools/memory-search-tool.test.ts +84 -0
  144. package/dist/extensions/pi-hermes-memory/tests/tools/memory-tool.test.ts +709 -0
  145. package/dist/extensions/pi-hermes-memory/tests/tools/session-search-tool.test.ts +345 -0
  146. package/dist/extensions/pi-hermes-memory/tests/tools/shared-output-view.test.ts +362 -0
  147. package/dist/extensions/pi-hermes-memory/tests/tools/skill-tool.test.ts +478 -0
  148. package/dist/extensions/pi-hermes-memory/tests/tools/tool-result-renderer-wiring.test.ts +40 -0
  149. package/dist/extensions/pi-hermes-memory/tsconfig.json +19 -0
  150. package/dist/extensions/pi-rewind-hook/README.md +4 -1
  151. package/dist/extensions/pi-rewind-hook/index.ts +151 -21
  152. package/dist/extensions/pi-rewind-hook/package.json +5 -3
  153. package/dist/extensions/pi-subagents/.oxlintrc.json +43 -0
  154. package/dist/extensions/pi-subagents/AGENTS.md +5 -0
  155. package/dist/extensions/pi-subagents/CHANGELOG.md +157 -0
  156. package/dist/extensions/pi-subagents/README.md +12 -6
  157. package/dist/extensions/pi-subagents/VISION.md +81 -0
  158. package/dist/extensions/pi-subagents/agents/claude-code-writer.md +15 -0
  159. package/dist/extensions/pi-subagents/agents/claude-code.md +15 -0
  160. package/dist/extensions/pi-subagents/agents/codex-exec-writer.md +15 -0
  161. package/dist/extensions/pi-subagents/agents/codex-exec.md +15 -0
  162. package/dist/extensions/pi-subagents/agents/cursor-agent-writer.md +14 -0
  163. package/dist/extensions/pi-subagents/agents/cursor-agent.md +14 -0
  164. package/dist/extensions/pi-subagents/agents/reviewer.md +1 -3
  165. package/dist/extensions/pi-subagents/docs/agents.md +488 -0
  166. package/dist/extensions/pi-subagents/docs/configuration.md +46 -9
  167. package/dist/extensions/pi-subagents/docs/extension-api.md +42 -3
  168. package/dist/extensions/pi-subagents/docs/models.md +27 -4
  169. package/dist/extensions/pi-subagents/docs/observability.md +10 -9
  170. package/dist/extensions/pi-subagents/docs/tool-reference.md +102 -6
  171. package/dist/extensions/pi-subagents/docs/workflows.md +135 -0
  172. package/dist/extensions/pi-subagents/package-lock.json +389 -2
  173. package/dist/extensions/pi-subagents/package.json +3 -1
  174. package/dist/extensions/pi-subagents/prompts/review-loop.md +2 -2
  175. package/dist/extensions/pi-subagents/skills/council-mode/SKILL.md +1 -1
  176. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +3 -1
  177. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +7 -3
  178. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  179. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +26 -5
  180. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +41 -22
  181. package/dist/extensions/pi-subagents/src/agents/agent-refinements.ts +4 -4
  182. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +9 -2
  183. package/dist/extensions/pi-subagents/src/agents/agents.ts +322 -75
  184. package/dist/extensions/pi-subagents/src/agents/builtin-names.ts +6 -0
  185. package/dist/extensions/pi-subagents/src/agents/runtime-agent-events.ts +70 -0
  186. package/dist/extensions/pi-subagents/src/agents/runtime-agent-registry.ts +27 -20
  187. package/dist/extensions/pi-subagents/src/api/agents.ts +10 -5
  188. package/dist/extensions/pi-subagents/src/api/background-work.ts +5 -1
  189. package/dist/extensions/pi-subagents/src/api/delegation.ts +0 -7
  190. package/dist/extensions/pi-subagents/src/api/preflight.ts +34 -12
  191. package/dist/extensions/pi-subagents/src/extension/config.ts +70 -0
  192. package/dist/extensions/pi-subagents/src/extension/doctor.ts +3 -3
  193. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +5 -3
  194. package/dist/extensions/pi-subagents/src/extension/index.ts +83 -30
  195. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +44 -15
  196. package/dist/extensions/pi-subagents/src/extension/rpc.ts +55 -19
  197. package/dist/extensions/pi-subagents/src/extension/schemas.ts +41 -15
  198. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +28 -31
  199. package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +2 -1
  200. package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +2 -10
  201. package/dist/extensions/pi-subagents/src/inspectors/herdr/session-roots-codec.ts +42 -0
  202. package/dist/extensions/pi-subagents/src/integrations/herdr-status.ts +51 -7
  203. package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +2 -0
  204. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +5 -6
  205. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +77 -10
  206. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +176 -53
  207. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +19 -12
  208. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +29 -27
  209. package/dist/extensions/pi-subagents/src/runs/background/async-retention.ts +20 -3
  210. package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +23 -261
  211. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +73 -7
  212. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +8 -3
  213. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +74 -8
  214. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +37 -21
  215. package/dist/extensions/pi-subagents/src/runs/background/inspect-rpc.ts +8 -8
  216. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +184 -9
  217. package/dist/extensions/pi-subagents/src/runs/background/result-delivery-ownership.ts +45 -0
  218. package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +28 -14
  219. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +38 -15
  220. package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +1 -1
  221. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +1 -1
  222. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +50 -13
  223. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +93 -8
  224. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +10 -4
  225. package/dist/extensions/pi-subagents/src/runs/background/steering.ts +4 -14
  226. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +549 -377
  227. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +58 -10
  228. package/dist/extensions/pi-subagents/src/runs/background/terminal-run-index.ts +1 -1
  229. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +26 -1
  230. package/dist/extensions/pi-subagents/src/runs/background/wait-config.ts +23 -9
  231. package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +9 -2
  232. package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +2 -2
  233. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +204 -129
  234. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +9 -0
  235. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +23 -1
  236. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +773 -311
  237. package/dist/extensions/pi-subagents/src/runs/foreground/workflow-detach-reconcile.ts +140 -115
  238. package/dist/extensions/pi-subagents/src/runs/shared/abort-recovery.ts +119 -0
  239. package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +463 -0
  240. package/dist/extensions/pi-subagents/src/runs/shared/child-identity.ts +19 -4
  241. package/dist/extensions/pi-subagents/src/runs/shared/child-launch-plan.ts +151 -0
  242. package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +21 -7
  243. package/dist/extensions/pi-subagents/src/runs/shared/claude-code-adapter.ts +129 -0
  244. package/dist/extensions/pi-subagents/src/runs/shared/codex-exec-adapter.ts +129 -0
  245. package/dist/extensions/pi-subagents/src/runs/shared/completion-evidence.ts +89 -0
  246. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +5 -4
  247. package/dist/extensions/pi-subagents/src/runs/shared/cursor-agent-adapter.ts +114 -0
  248. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +3 -3
  249. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-contract.ts +167 -0
  250. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-preflight.ts +122 -0
  251. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +348 -55
  252. package/dist/extensions/pi-subagents/src/runs/shared/fast-mode-extension.ts +5 -5
  253. package/dist/extensions/pi-subagents/src/runs/shared/host-step-status.ts +230 -0
  254. package/dist/extensions/pi-subagents/src/runs/shared/lane-metadata.ts +105 -0
  255. package/dist/extensions/pi-subagents/src/runs/shared/launch-cwd.ts +16 -0
  256. package/dist/extensions/pi-subagents/src/runs/shared/long-running-guard.ts +2 -1
  257. package/dist/extensions/pi-subagents/src/runs/shared/mcp-config-sources.ts +422 -0
  258. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +221 -158
  259. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-grant.ts +197 -0
  260. package/dist/extensions/pi-subagents/src/runs/shared/model-exclusions.ts +69 -7
  261. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +47 -6
  262. package/dist/extensions/pi-subagents/src/runs/shared/mutation-evidence.ts +7 -2
  263. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +9 -3
  264. package/dist/extensions/pi-subagents/src/runs/shared/nested-render.ts +9 -5
  265. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +419 -7
  266. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +15 -1
  267. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +81 -8
  268. package/dist/extensions/pi-subagents/src/runs/shared/process-signal.ts +13 -0
  269. package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +21 -1
  270. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +44 -4
  271. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +98 -13
  272. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +1 -1
  273. package/dist/extensions/pi-subagents/src/runs/shared/worktree-cleanup-plan.ts +847 -0
  274. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +18 -0
  275. package/dist/extensions/pi-subagents/src/shared/child-session-name.ts +46 -0
  276. package/dist/extensions/pi-subagents/src/shared/extension-context.ts +24 -0
  277. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +21 -0
  278. package/dist/extensions/pi-subagents/src/shared/formatters.ts +18 -3
  279. package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +5 -1
  280. package/dist/extensions/pi-subagents/src/shared/pruned-fork.ts +450 -0
  281. package/dist/extensions/pi-subagents/src/shared/session-file-trust.ts +19 -0
  282. package/dist/extensions/pi-subagents/src/shared/session-tokens.ts +14 -3
  283. package/dist/extensions/pi-subagents/src/shared/settings.ts +19 -105
  284. package/dist/extensions/pi-subagents/src/shared/shortcuts.ts +17 -0
  285. package/dist/extensions/pi-subagents/src/shared/types.ts +359 -40
  286. package/dist/extensions/pi-subagents/src/shared/utils.ts +41 -84
  287. package/dist/extensions/pi-subagents/src/shared/workflow-child-permit.ts +116 -0
  288. package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +1 -9
  289. package/dist/extensions/pi-subagents/src/slash/delegation-request.ts +0 -4
  290. package/dist/extensions/pi-subagents/src/slash/slash-bridge.ts +1 -2
  291. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +378 -88
  292. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +22 -11
  293. package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +3 -0
  294. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +147 -79
  295. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +11 -5
  296. package/dist/extensions/pi-subagents/src/tui/fleet.ts +39 -18
  297. package/dist/extensions/pi-subagents/src/tui/render.ts +369 -46
  298. package/dist/extensions/pi-subagents/src/watchdog/turn-delta.ts +1 -1
  299. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +6 -3
  300. package/dist/extensions/pi-subagents/src/workflows/host-command.ts +230 -0
  301. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +740 -58
  302. package/dist/extensions/pi-subagents/src/workflows/workflow-child-summary.ts +121 -0
  303. package/dist/extensions/pi-subagents/src/workflows/workflow-preflight.ts +270 -0
  304. package/dist/extensions/pi-subagents/src/workflows/workflow-receipt.ts +195 -6
  305. package/dist/extensions/pi-subagents/src/workflows/workflow-settlement.ts +246 -0
  306. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +2 -2
  307. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1114 -260
  308. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +34 -0
  309. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +150 -3
  310. package/dist/extensions/pi-subagents/test/integration/claude-code-smoke.test.ts +52 -0
  311. package/dist/extensions/pi-subagents/test/integration/claude-code-writer-smoke.test.ts +55 -0
  312. package/dist/extensions/pi-subagents/test/integration/codex-exec-smoke.test.ts +53 -0
  313. package/dist/extensions/pi-subagents/test/integration/codex-exec-writer-smoke.test.ts +57 -0
  314. package/dist/extensions/pi-subagents/test/integration/cursor-agent-smoke.test.ts +59 -0
  315. package/dist/extensions/pi-subagents/test/integration/cursor-agent-writer-smoke.test.ts +62 -0
  316. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +57 -1
  317. package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
  318. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +20 -0
  319. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +4 -3
  320. package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +2 -2
  321. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +54 -4
  322. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +482 -4
  323. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +157 -9
  324. package/dist/extensions/pi-subagents/test/integration/session-tokens.test.ts +5 -5
  325. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1508 -291
  326. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +423 -1
  327. package/dist/extensions/pi-subagents/test/integration/template-resolution.test.ts +8 -0
  328. package/dist/extensions/pi-subagents/test/support/cursor-smoke-inputs.ts +51 -0
  329. package/dist/extensions/pi-subagents/test/support/helpers.ts +2 -0
  330. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +5 -5
  331. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +6 -6
  332. package/dist/extensions/pi-subagents/test/unit/abort-recovery.test.ts +152 -0
  333. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +48 -0
  334. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +184 -6
  335. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +66 -6
  336. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +77 -0
  337. package/dist/extensions/pi-subagents/test/unit/agent-refinements.test.ts +18 -0
  338. package/dist/extensions/pi-subagents/test/unit/anti-slop-oxlint.test.ts +86 -0
  339. package/dist/extensions/pi-subagents/test/unit/artifacts.test.ts +1 -1
  340. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +101 -1
  341. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +121 -0
  342. package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +28 -0
  343. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +14 -7
  344. package/dist/extensions/pi-subagents/test/unit/async-retention.test.ts +53 -0
  345. package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +190 -0
  346. package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +42 -0
  347. package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +8 -4
  348. package/dist/extensions/pi-subagents/test/unit/background-work.test.ts +23 -0
  349. package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +78 -1
  350. package/dist/extensions/pi-subagents/test/unit/child-launch-plan.test.ts +94 -0
  351. package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +21 -0
  352. package/dist/extensions/pi-subagents/test/unit/child-session-name.test.ts +50 -0
  353. package/dist/extensions/pi-subagents/test/unit/claude-code-adapter.test.ts +245 -0
  354. package/dist/extensions/pi-subagents/test/unit/codex-exec-adapter.test.ts +192 -0
  355. package/dist/extensions/pi-subagents/test/unit/compaction-resume.test.ts +27 -0
  356. package/dist/extensions/pi-subagents/test/unit/completion-evidence.test.ts +150 -0
  357. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +21 -0
  358. package/dist/extensions/pi-subagents/test/unit/cursor-agent-adapter.test.ts +259 -0
  359. package/dist/extensions/pi-subagents/test/unit/cursor-smoke-inputs.test.ts +77 -0
  360. package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +1 -4
  361. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +1 -1
  362. package/dist/extensions/pi-subagents/test/unit/extension-context.test.ts +27 -0
  363. package/dist/extensions/pi-subagents/test/unit/external-cli-runner.test.ts +223 -0
  364. package/dist/extensions/pi-subagents/test/unit/fast-mode-extension.test.ts +27 -0
  365. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +208 -2
  366. package/dist/extensions/pi-subagents/test/unit/fleet-transcript.test.ts +29 -1
  367. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +110 -1
  368. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +10 -0
  369. package/dist/extensions/pi-subagents/test/unit/formatters.test.ts +14 -0
  370. package/dist/extensions/pi-subagents/test/unit/herdr-inspector.test.ts +18 -9
  371. package/dist/extensions/pi-subagents/test/unit/herdr-session-roots-codec.test.ts +63 -0
  372. package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +2 -2
  373. package/dist/extensions/pi-subagents/test/unit/herdr-status-bridge.test.ts +136 -6
  374. package/dist/extensions/pi-subagents/test/unit/host-command.test.ts +156 -0
  375. package/dist/extensions/pi-subagents/test/unit/host-step-status.test.ts +98 -0
  376. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +178 -26
  377. package/dist/extensions/pi-subagents/test/unit/index-segment.test.ts +2 -1
  378. package/dist/extensions/pi-subagents/test/unit/inspect-rpc.test.ts +21 -0
  379. package/dist/extensions/pi-subagents/test/unit/mcp-direct-tool-grant.test.ts +138 -0
  380. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
  381. package/dist/extensions/pi-subagents/test/unit/model-exclusions.test.ts +91 -0
  382. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +72 -7
  383. package/dist/extensions/pi-subagents/test/unit/mutation-evidence.test.ts +21 -0
  384. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +1 -0
  385. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +20 -2
  386. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +129 -1
  387. package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +2 -2
  388. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +4 -0
  389. package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +358 -0
  390. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +574 -21
  391. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +109 -1
  392. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +71 -1
  393. package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +7 -7
  394. package/dist/extensions/pi-subagents/test/unit/pruned-fork.test.ts +214 -0
  395. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +29 -8
  396. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +53 -0
  397. package/dist/extensions/pi-subagents/test/unit/result-delivery-ownership.test.ts +34 -0
  398. package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +101 -3
  399. package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +1 -0
  400. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +10 -8
  401. package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +71 -1
  402. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +41 -7
  403. package/dist/extensions/pi-subagents/test/unit/runtime-agent-registration.test.ts +136 -2
  404. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +137 -0
  405. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +33 -12
  406. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +504 -1
  407. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +8 -0
  408. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +1 -1
  409. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +12 -1
  410. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +3 -5
  411. package/dist/extensions/pi-subagents/test/unit/steering.test.ts +5 -6
  412. package/dist/extensions/pi-subagents/test/unit/subagent-action-recovery.test.ts +0 -8
  413. package/dist/extensions/pi-subagents/test/unit/subagent-guide.test.ts +5 -0
  414. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +157 -2
  415. package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +1 -1
  416. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +63 -4
  417. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +27 -23
  418. package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +44 -0
  419. package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +1 -0
  420. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +7 -5
  421. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +106 -0
  422. package/dist/extensions/pi-subagents/test/unit/workflow-detach-reconcile.test.ts +484 -9
  423. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +38 -4
  424. package/dist/extensions/pi-subagents/test/unit/workflow-preflight.test.ts +155 -0
  425. package/dist/extensions/pi-subagents/test/unit/workflow-receipt.test.ts +318 -0
  426. package/dist/extensions/pi-subagents/test/unit/workflow-resume-hint.test.ts +365 -0
  427. package/dist/extensions/pi-subagents/test/unit/worktree-cleanup-plan.test.ts +349 -0
  428. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +21 -0
  429. package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +2 -7
  430. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/effect/index.ts +13 -0
  431. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/effect/rules/no-service-constructor-imports.ts +52 -0
  432. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/index.ts +41 -0
  433. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-chained-type-assertions.ts +77 -0
  434. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-conditional-empty-object-spread.ts +49 -0
  435. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-known-value-widening.ts +247 -0
  436. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-module-mocking.ts +91 -0
  437. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-object-parameters.ts +126 -0
  438. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-reflect-apply.ts +28 -0
  439. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-reflect-get.ts +28 -0
  440. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-runtime-typeof.ts +67 -0
  441. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-shape-in-symbol-names.ts +39 -0
  442. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-unknown-parameters.ts +83 -0
  443. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-unknown-returns.ts +115 -0
  444. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-unknown-type-aliases.ts +72 -0
  445. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-unsafe-dictionary-type.ts +134 -0
  446. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-widen-then-assert.ts +366 -0
  447. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/require-safety-comment-for-type-assertion.ts +62 -0
  448. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/shared/dictionary-types.ts +502 -0
  449. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/shared/lexical-type-parameters.ts +60 -0
  450. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/shared/reflect-method.ts +35 -0
  451. package/dist/extensions/pi-tool-display/tests/capabilities.test.ts +1 -1
  452. package/dist/extensions/pi-tool-display/tests/settings-inspector.test.ts +2 -2
  453. package/dist/extensions/pi-tool-display/tests/tool-overrides-mcp.test.ts +1 -1
  454. package/dist/modes/interactive/interactive-mode.js +7 -7
  455. package/dist/skills/workflow/SKILL.md +65 -50
  456. package/package.json +112 -110
  457. package/dist/extensions/enable-readonly-tools.test.ts +0 -63
  458. package/dist/extensions/enable-readonly-tools.ts +0 -24
  459. package/dist/extensions/model-prompt-injector/config.json +0 -16
  460. package/dist/extensions/model-prompt-injector/index.test.ts +0 -102
  461. package/dist/extensions/model-prompt-injector/index.ts +0 -148
  462. package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +0 -98
  463. package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +0 -250
@@ -80,7 +80,7 @@ interface AsyncResultPayload {
80
80
  totalTokens?: { input: number; output: number; total: number };
81
81
  totalCost?: { inputTokens: number; outputTokens: number; costUsd: number };
82
82
  usageBudget?: UsageBudgetState;
83
- results: Array<{ agent?: string; launchContractDigest?: string; launchResolvedExtensions?: LaunchResolvedExtensions; runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions; output?: string; outputState?: "present" | "absent" | "unknown"; success?: boolean; error?: string; protocolError?: { code?: string; stream?: string; limitBytes?: number; observedBytes?: number }; timedOut?: boolean; timeoutRecovery?: { changedFiles?: string[]; message?: string; warning?: string }; stopped?: boolean; turnBudget?: { maxTurns: number; graceTurns: number; outcome: string; turnCount: number; wrapUpRequestedAtTurn?: number; terminationDeferredAtTurn?: number; exceededAtTurn?: number }; turnBudgetExceeded?: boolean; wrapUpRequested?: boolean; model?: string; attemptedModels?: string[]; modelAttempts?: Array<{ success?: boolean; error?: string }>; totalCost?: { inputTokens: number; outputTokens: number; costUsd: number }; structuredOutput?: unknown; agentContract?: { version: 1 }; execution?: { status?: string; success?: boolean; exitCode?: number }; effects?: { fileMutation?: { status?: string; expected?: boolean; attempted?: boolean; message?: string } }; intercomTarget?: string; acceptance?: { status?: string; effectiveAcceptance?: { level?: string }; childReport?: unknown; runtimeChecks?: Array<{ id?: string; status?: string; message?: string }> }; artifactPaths?: { outputPath?: string; inputPath?: string; metadataPath?: string; transcriptPath?: string }; outputSaveError?: string; metadataSaveError?: string; capabilityCeiling?: { version?: number; allowedTools?: string[]; denyExtensions?: boolean; sources?: string[] }; capabilityAudit?: { effectiveTools?: string[]; removedTools?: string[]; extensionsDenied?: boolean } }>;
83
+ results: Array<{ agent?: string; sessionName?: string; launchContractDigest?: string; launchResolvedExtensions?: LaunchResolvedExtensions; runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions; output?: string; outputState?: "present" | "absent" | "unknown"; success?: boolean; error?: string; protocolError?: { code?: string; stream?: string; limitBytes?: number; observedBytes?: number }; timedOut?: boolean; timeoutRecovery?: { changedFiles?: string[]; message?: string; warning?: string }; stopped?: boolean; turnBudget?: { maxTurns: number; graceTurns: number; outcome: string; turnCount: number; wrapUpRequestedAtTurn?: number; terminationDeferredAtTurn?: number; exceededAtTurn?: number }; turnBudgetExceeded?: boolean; wrapUpRequested?: boolean; model?: string; attemptedModels?: string[]; modelAttempts?: Array<{ success?: boolean; error?: string }>; totalCost?: { inputTokens: number; outputTokens: number; costUsd: number }; usage?: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number; turns: number }; structuredOutput?: unknown; agentContract?: { version: 1 }; execution?: { status?: string; success?: boolean; exitCode?: number }; effects?: { fileMutation?: { status?: string; expected?: boolean; attempted?: boolean; message?: string }; settlementDiagnostic?: { finalTextPresent?: boolean; mutation?: { expected?: boolean; attempted?: boolean; observed?: boolean }; requiredOutput?: { kind?: string; path?: string; missing?: boolean }; afterCompactionSettlement?: boolean } }; intercomTarget?: string; acceptance?: { status?: string; effectiveAcceptance?: { level?: string }; childReport?: unknown; runtimeChecks?: Array<{ id?: string; status?: string; message?: string }> }; artifactPaths?: { outputPath?: string; inputPath?: string; metadataPath?: string; transcriptPath?: string }; outputSaveError?: string; metadataSaveError?: string; capabilityCeiling?: { version?: number; allowedTools?: string[]; denyExtensions?: boolean; sources?: string[] }; capabilityAudit?: { effectiveTools?: string[]; removedTools?: string[]; extensionsDenied?: boolean } }>;
84
84
  outputs?: Record<string, { text?: string; structured?: unknown }>;
85
85
  workflowGraph?: { nodes?: Array<{ kind?: string; label?: string; phase?: string; status?: string; acceptanceStatus?: string; error?: string; outputName?: string; structured?: boolean; children?: Array<{ label?: string; outputName?: string; itemKey?: string; status?: string; acceptanceStatus?: string; error?: string }> }> };
86
86
  parallelHandoff?: { version?: number; path?: string; groupCount?: number; childCount?: number; changedPatches?: number; cleanupState?: string };
@@ -116,6 +116,8 @@ interface AsyncStatusPayload {
116
116
  capabilityCeiling?: { version?: number; allowedTools?: string[]; denyExtensions?: boolean; sources?: string[] };
117
117
  capabilityAudit?: { effectiveTools?: string[]; removedTools?: string[]; extensionsDenied?: boolean };
118
118
  steps?: Array<{
119
+ agent?: string;
120
+ sessionName?: string;
119
121
  label?: string;
120
122
  phase?: string;
121
123
  outputName?: string;
@@ -137,8 +139,9 @@ interface AsyncStatusPayload {
137
139
  launchResolvedExtensions?: LaunchResolvedExtensions;
138
140
  runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions;
139
141
  execution?: { status?: string; success?: boolean; exitCode?: number };
140
- effects?: { fileMutation?: { status?: string; expected?: boolean; attempted?: boolean } };
142
+ effects?: { fileMutation?: { status?: string; expected?: boolean; attempted?: boolean }; settlementDiagnostic?: { finalTextPresent?: boolean; mutation?: { expected?: boolean; attempted?: boolean; observed?: boolean }; requiredOutput?: { kind?: string; path?: string; missing?: boolean }; afterCompactionSettlement?: boolean } };
141
143
  acceptance?: { status?: string };
144
+ contextLimit?: number;
142
145
  turnBudget?: { maxTurns: number; graceTurns: number; outcome: string; turnCount: number; wrapUpRequestedAtTurn?: number; terminationDeferredAtTurn?: number; exceededAtTurn?: number };
143
146
  turnBudgetExceeded?: boolean;
144
147
  wrapUpRequested?: boolean;
@@ -230,6 +233,10 @@ interface AsyncExecutionModule {
230
233
  executeAsyncChain(id: string, params: Record<string, unknown>): AsyncExecutionResult;
231
234
  }
232
235
 
236
+ interface AsyncStatusModule {
237
+ resolveTargetedAsyncRun(root: string, id: string, sessionId?: string): { kind: string };
238
+ }
239
+
233
240
  interface UtilsModule {
234
241
  readStatus(dir: string): { runId: string; state: string; mode: string } | null;
235
242
  pruneStatusCacheForAsyncRoot(root: string, runIds: Iterable<string>): number;
@@ -248,6 +255,7 @@ interface ExecutorModule {
248
255
  }
249
256
 
250
257
  const asyncMod = await tryImport<AsyncExecutionModule>("./src/runs/background/async-execution.ts");
258
+ const asyncStatusMod = await tryImport<AsyncStatusModule>("./src/runs/background/async-status.ts");
251
259
  const utils = await tryImport<UtilsModule>("./src/shared/utils.ts");
252
260
  const typesMod = await tryImport<TypesModule>("./src/shared/types.ts");
253
261
  const executorMod = await tryImport<ExecutorModule>("./src/runs/foreground/subagent-executor.ts");
@@ -256,6 +264,7 @@ const available = !!(asyncMod && utils && typesMod);
256
264
  const isAsyncAvailable = asyncMod?.isAsyncAvailable;
257
265
  const executeAsyncSingle = asyncMod?.executeAsyncSingle;
258
266
  const executeAsyncChain = asyncMod?.executeAsyncChain;
267
+ const resolveTargetedAsyncRun = asyncStatusMod?.resolveTargetedAsyncRun;
259
268
  const readStatus = utils?.readStatus;
260
269
  const pruneStatusCacheForAsyncRoot = utils?.pruneStatusCacheForAsyncRoot;
261
270
  const ASYNC_DIR = typesMod?.ASYNC_DIR;
@@ -358,24 +367,6 @@ async function waitForAsyncState(id: string, predicate: (status: AsyncStatusPayl
358
367
  assert.fail(`Timed out waiting for async status: ${statusPath}`);
359
368
  }
360
369
 
361
- async function waitForDeferredTurnBudget(id: string, timeoutMs = 10_000): Promise<AsyncStatusPayload> {
362
- const statusPath = path.join(ASYNC_DIR, id, "status.json");
363
- const deadline = Date.now() + timeoutMs;
364
- while (Date.now() <= deadline) {
365
- if (fs.existsSync(statusPath)) {
366
- const status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatusPayload;
367
- if (status.state === "running"
368
- && status.turnBudget?.outcome === "termination-deferred"
369
- && status.steps?.[0]?.turnBudget?.outcome === "termination-deferred"
370
- && status.steps[0].currentTool === "bash") {
371
- return status;
372
- }
373
- }
374
- await new Promise((resolve) => setTimeout(resolve, 50));
375
- }
376
- assert.fail(`Timed out waiting for deferred turn-budget status: ${statusPath}`);
377
- }
378
-
379
370
  async function waitForMockPiCall(mockPi: MockPi, index: number, timeoutMs = 30_000): Promise<{ args: string[]; systemPrompts: NonNullable<MockPiCallRecord["systemPrompts"]> }> {
380
371
  const deadline = Date.now() + timeoutMs;
381
372
  for (;;) {
@@ -464,7 +455,11 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
464
455
  removeTempDir(tempDir);
465
456
  });
466
457
 
467
- function makeAsyncExecutor(agents: ReturnType<typeof makeAgent>[], config: Record<string, unknown> = {}) {
458
+ function makeAsyncExecutor(
459
+ agents: ReturnType<typeof makeAgent>[],
460
+ config: Record<string, unknown> = {},
461
+ discoverOverride?: (cwd: string) => { agents: ReturnType<typeof makeAgent>[]; cwd?: string; scope?: "user" | "project" | "both"; directories?: Array<{ source: "builtin" | "package" | "user" | "project" | "runtime"; path: string; state: "absent" | "empty" | "candidates" | "unreadable" | "not-directory"; candidateCount?: number }> },
462
+ ) {
468
463
  return createSubagentExecutor!({
469
464
  pi: { events: createEventBus(), getSessionName: () => undefined },
470
465
  state: { baseCwd: tempDir, currentSessionId: null, asyncJobs: new Map(), foregroundControls: new Map(), lastForegroundControlId: null },
@@ -473,7 +468,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
473
468
  tempArtifactsDir: tempDir,
474
469
  getSubagentSessionRoot: () => tempDir,
475
470
  expandTilde: (p: string) => p,
476
- discoverAgents: () => ({ agents }),
471
+ discoverAgents: (cwd: string) => discoverOverride ? discoverOverride(cwd) : ({ agents }),
477
472
  });
478
473
  }
479
474
 
@@ -568,6 +563,54 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
568
563
  assert.equal(status.endedAt !== undefined, true);
569
564
  });
570
565
 
566
+ it("persists the bounded usage projection in async results and metadata", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
567
+ mockPi.onCall({ output: "accounted output" });
568
+ const id = `async-usage-artifact-${Date.now().toString(36)}`;
569
+ const artifactsDir = path.join(tempDir, ".pi-subagents", "artifacts");
570
+ executeAsyncSingle(id, {
571
+ agent: "worker",
572
+ task: "Persist usage",
573
+ agentConfig: makeAgent("worker", { completionGuard: false }),
574
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-usage-artifact" },
575
+ artifactConfig: { enabled: true, includeInput: false, includeOutput: true, includeJsonl: false, includeMetadata: true, cleanupDays: 7 },
576
+ artifactsDir,
577
+ shareEnabled: false,
578
+ maxSubagentDepth: 2,
579
+ acceptance: false,
580
+ });
581
+
582
+ const payload = await readAsyncPayload(id);
583
+ const expectedUsage = { input: 100, output: 50, cacheRead: 0, cacheWrite: 0, cost: 0.001, turns: 1 };
584
+ assert.deepEqual(payload.results[0]?.usage, expectedUsage);
585
+ const metadataPath = payload.results[0]?.artifactPaths?.metadataPath;
586
+ assert.ok(metadataPath);
587
+ const metadata = JSON.parse(fs.readFileSync(metadataPath, "utf-8")) as { usage?: unknown };
588
+ assert.deepEqual(metadata.usage, expectedUsage);
589
+ });
590
+
591
+ it("makes a launched async run immediately visible to exact status lookup", { skip: !isAsyncAvailable() || !resolveTargetedAsyncRun ? "jiti not available" : undefined }, async () => {
592
+ mockPi.onCall({ delay: 500, output: "visible async done" });
593
+ const id = `async-initial-status-${Date.now().toString(36)}`;
594
+ const launch = executeAsyncSingle(id, {
595
+ agent: "worker",
596
+ task: "Remain visible while starting",
597
+ agentConfig: makeAgent("worker", { completionGuard: false, model: "mock/test-model" }),
598
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-initial-status" },
599
+ availableModels: [{ provider: "mock", id: "test-model", fullId: "mock/test-model", contextWindow: 128_000 }],
600
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
601
+ shareEnabled: false,
602
+ maxSubagentDepth: 2,
603
+ });
604
+
605
+ assert.equal(launch.isError, undefined);
606
+ assert.equal(resolveTargetedAsyncRun(ASYNC_DIR, id, "session-initial-status").kind, "exact");
607
+ const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload;
608
+ assert.equal(status.sessionId, "session-initial-status");
609
+ assert.equal(status.pid !== undefined, true);
610
+ assert.equal(status.steps?.[0]?.contextLimit, 128_000);
611
+ await waitForAsyncResultFile(id);
612
+ });
613
+
571
614
  it("persists intercom detach receipts on failed async steps", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
572
615
  mockPi.onCall({ output: "Detached for intercom coordination before task completion.", exitCode: -2 });
573
616
  const id = `async-intercom-detach-${Date.now().toString(36)}`;
@@ -575,7 +618,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
575
618
  await waitForAsyncResultFile(id);
576
619
  const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload;
577
620
  assert.equal(status.state, "failed");
578
- assert.equal(status.error, "Step failed: worker");
621
+ assert.equal(status.error, "Detached for intercom coordination before task completion.");
579
622
  assert.equal(status.steps?.[0]?.error, "Detached for intercom coordination before task completion.");
580
623
  });
581
624
 
@@ -615,7 +658,6 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
615
658
  it("matches preflight launch digest in equivalent foreground and async execution", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
616
659
  const agentName = `contract-worker-${Date.now().toString(36)}`;
617
660
  const task = "Compare the resolved launch inputs.";
618
- const turnBudget = { maxTurns: 2, graceTurns: 1 } as const;
619
661
  const previousAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
620
662
  const agentDir = path.join(tempDir, "agent-home");
621
663
  process.env.SELESAI_CODING_AGENT_DIR = agentDir;
@@ -629,12 +671,12 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
629
671
  try {
630
672
  const discovered = discoverAgents(tempDir).agents.find((agent) => agent.name === agentName);
631
673
  assert.ok(discovered, "expected temporary agent definition to be discovered");
632
- const preflight = await resolveSubagentLaunchContract({ agent: agentName, cwd: tempDir, task, turnBudget, runId: "contract-preflight" });
674
+ const preflight = await resolveSubagentLaunchContract({ agent: agentName, cwd: tempDir, task, runId: "contract-preflight" });
633
675
  assert.equal(preflight.ok, true);
634
676
  assert.ok(preflight.contract.tools.extensionArgs.some((entry) => entry.endsWith(path.join("pi-permission-system", "src", "index.ts"))));
635
677
 
636
678
  mockPi.onCall({ output: "foreground contract comparison" });
637
- const foreground = await runSync(tempDir, [discovered], agentName, task, { runId: "contract-foreground", acceptance: false, turnBudget });
679
+ const foreground = await runSync(tempDir, [discovered], agentName, task, { runId: "contract-foreground", acceptance: false });
638
680
  assert.equal(foreground.exitCode, 0);
639
681
  assert.equal(foreground.launchContractDigest, preflight.contract.launchContractDigest);
640
682
 
@@ -650,7 +692,6 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
650
692
  sessionRoot: path.join(tempDir, "sessions"),
651
693
  maxSubagentDepth: 2,
652
694
  acceptance: false,
653
- turnBudget,
654
695
  });
655
696
  const payload = await readAsyncPayload(asyncId);
656
697
  assert.equal(launch.details.launchContractDigest, preflight.contract.launchContractDigest);
@@ -1170,6 +1211,20 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
1170
1211
  assert.ok(Date.now() - startedAt >= 1200, "background runner must not terminate during the retry delay");
1171
1212
  });
1172
1213
 
1214
+ it("background does not drain on settlement from a compaction attempt that will retry", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
1215
+ mockPi.onCall({ steps: [
1216
+ { jsonl: [{ type: "compaction_end", willRetry: true }, { type: "agent_settled" }] },
1217
+ { delay: 1400, jsonl: [events.assistantMessage("settled after compaction retry"), { type: "agent_start" }, { type: "agent_end", willRetry: false }, { type: "agent_settled" }] },
1218
+ ] });
1219
+ const id = `async-lifecycle-compaction-retry-${Date.now().toString(36)}`;
1220
+ const startedAt = Date.now();
1221
+ launchProtocolTest(id);
1222
+ const payload = await readAsyncPayload(id);
1223
+ assert.equal(payload.success, true);
1224
+ assert.equal(payload.results[0]?.output, "settled after compaction retry");
1225
+ assert.ok(Date.now() - startedAt >= 1200, "background runner must not terminate during compaction retry");
1226
+ });
1227
+
1173
1228
  it("background treats agent_settled as a clean terminal watermark", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
1174
1229
  mockPi.onCall({ jsonl: [mockAssistantMessage("settled async without a terminal assistant stop", "tool_use"), { type: "agent_settled" }], keepAliveAfterFinalMessageMs: 15_000 });
1175
1230
  const id = `async-lifecycle-settled-${Date.now().toString(36)}`;
@@ -1182,6 +1237,23 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
1182
1237
  assert.ok(Date.now() - startedAt < 10_000, "agent_settled should trigger bounded child cleanup");
1183
1238
  });
1184
1239
 
1240
+ it("does not report successful compaction settlement as a failure", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
1241
+ mockPi.onCall({
1242
+ jsonl: [{ type: "compaction_start" }, mockAssistantMessage("settled after compaction"), { type: "agent_settled" }],
1243
+ keepAliveAfterFinalMessageMs: 15_000,
1244
+ });
1245
+ const id = `async-lifecycle-compaction-success-${Date.now().toString(36)}`;
1246
+ launchProtocolTest(id);
1247
+ const payload = await readAsyncPayload(id);
1248
+ assert.equal(payload.success, true);
1249
+ assert.equal(payload.results[0]?.success, true);
1250
+ assert.equal(payload.results[0]?.error, undefined);
1251
+ assert.equal(payload.results[0]?.output, "settled after compaction");
1252
+ assert.equal(payload.results[0]?.effects?.settlementDiagnostic, undefined);
1253
+ const status = await waitForAsyncState(id, (candidate) => candidate.state === "complete");
1254
+ assert.equal(status.steps?.[0]?.effects?.settlementDiagnostic, undefined);
1255
+ });
1256
+
1185
1257
  it("keeps named output references literal in async single tasks", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
1186
1258
  const task = "Reply with OK. You may reference {outputs.name} if it helps.";
1187
1259
  mockPi.onCall({ output: "OK" });
@@ -1556,6 +1628,36 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
1556
1628
  }
1557
1629
  });
1558
1630
 
1631
+ it("background keeps a terminal answer authoritative over an earlier tool timeout", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
1632
+ mockPi.onCall({
1633
+ steps: [
1634
+ { jsonl: [events.toolStart("bash")] },
1635
+ { delay: 50, jsonl: [events.assistantMessage("Done")] },
1636
+ ],
1637
+ keepAliveAfterFinalMessageMs: 1_500,
1638
+ });
1639
+ const id = `async-terminal-tool-timeout-${Date.now().toString(36)}`;
1640
+ process.env.SELESAI_SUBAGENT_TOOL_TIMEOUT_MS = "600";
1641
+ try {
1642
+ executeAsyncChain(id, {
1643
+ chain: [{ agent: "one", task: "Do work" }],
1644
+ agents: [makeAgent("one")],
1645
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
1646
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
1647
+ shareEnabled: false,
1648
+ maxSubagentDepth: 2,
1649
+ timeoutMs: 5_000,
1650
+ });
1651
+
1652
+ const payload = await readAsyncPayload(id);
1653
+ assert.equal(payload.success, true);
1654
+ assert.equal(payload.results[0]?.timedOut, undefined);
1655
+ assert.equal(payload.results[0]?.output, "Done");
1656
+ } finally {
1657
+ delete process.env.SELESAI_SUBAGENT_TOOL_TIMEOUT_MS;
1658
+ }
1659
+ });
1660
+
1559
1661
  it("keeps an earlier wedged tool armed when another tool starts and ends", { skip: !isAsyncAvailable() ? "jiti not available" : process.platform === "win32" ? "timeout signal delivery intermittent on Windows CI" : undefined }, async () => {
1560
1662
  mockPi.onCall({
1561
1663
  steps: [
@@ -1835,224 +1937,6 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
1835
1937
  assert.ok(elapsedMs < timeoutMs + 4_000, `timeout should cancel acceptance verification well before the verify command completes, elapsed ${elapsedMs}ms`);
1836
1938
  });
1837
1939
 
1838
- it("async turn budget allows a terminal final grace turn", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
1839
- mockPi.onCall({
1840
- jsonl: [
1841
- mockAssistantMessage("working before wrap-up", "tool_use"),
1842
- mockAssistantMessage("final wrapped output", "stop"),
1843
- ],
1844
- });
1845
- const id = `async-turn-budget-soft-${Date.now().toString(36)}`;
1846
- executeAsyncSingle(id, {
1847
- agent: "worker",
1848
- task: "Use the final grace turn to wrap up.",
1849
- agentConfig: makeAgent("worker"),
1850
- ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
1851
- artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
1852
- shareEnabled: false,
1853
- maxSubagentDepth: 2,
1854
- turnBudget: { maxTurns: 1, graceTurns: 1 },
1855
- });
1856
-
1857
- const resultPath = await waitForAsyncResultFile(id, 10_000);
1858
- const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
1859
- const status = await waitForAsyncState(id, (candidate) => candidate.state === "complete");
1860
- assert.equal(payload.success, true);
1861
- assert.equal(payload.state, "complete");
1862
- assert.equal(payload.turnBudgetExceeded, undefined);
1863
- assert.equal(payload.wrapUpRequested, true);
1864
- assert.equal(payload.turnBudget?.outcome, "wrap-up-requested");
1865
- assert.equal(payload.turnBudget?.turnCount, 2);
1866
- assert.equal(payload.results[0]?.wrapUpRequested, true);
1867
- assert.equal(payload.results[0]?.turnBudget?.turnCount, 2);
1868
- assert.match(payload.results[0]?.output ?? "", /Turn budget wrap-up was requested after 1 assistant turn/);
1869
- assert.match(payload.results[0]?.output ?? "", /final wrapped output/);
1870
- assert.equal(status.wrapUpRequested, true);
1871
- assert.equal(status.turnBudgetExceeded, undefined);
1872
- assert.equal(status.steps?.[0]?.wrapUpRequested, true);
1873
- assert.equal(status.steps?.[0]?.turnBudget?.turnCount, 2);
1874
- });
1875
-
1876
- it("async turn budget preserves a clean terminal response beyond the final grace turn", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
1877
- mockPi.onCall({
1878
- jsonl: [
1879
- mockAssistantMessage("working before wrap-up", "tool_use"),
1880
- mockAssistantMessage("starting final grace tool work", "tool_use"),
1881
- mockAssistantMessage("safe assistant boundary after tool work", "stop"),
1882
- ],
1883
- });
1884
- const id = `async-turn-budget-hard-${Date.now().toString(36)}`;
1885
- executeAsyncSingle(id, {
1886
- agent: "worker",
1887
- task: "Exceed the turn budget.",
1888
- agentConfig: makeAgent("worker"),
1889
- ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
1890
- artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
1891
- shareEnabled: false,
1892
- maxSubagentDepth: 2,
1893
- turnBudget: { maxTurns: 1, graceTurns: 1 },
1894
- });
1895
-
1896
- const resultPath = await waitForAsyncResultFile(id, 10_000);
1897
- const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
1898
- const status = await waitForAsyncState(id, (candidate) => candidate.state === "complete");
1899
- assert.equal(payload.success, true);
1900
- assert.equal(payload.state, "complete");
1901
- assert.equal(payload.exitCode, 0);
1902
- assert.equal(payload.turnBudgetExceeded, undefined);
1903
- assert.equal(payload.wrapUpRequested, true);
1904
- assert.equal(payload.turnBudget?.outcome, "wrap-up-requested");
1905
- assert.equal(payload.turnBudget?.turnCount, 3);
1906
- assert.equal(payload.results[0]?.turnBudgetExceeded, undefined);
1907
- assert.match(payload.results[0]?.output ?? "", /safe assistant boundary after tool work/);
1908
- assert.equal(status.state, "complete");
1909
- assert.equal(status.turnBudgetExceeded, undefined);
1910
- assert.equal(status.steps?.[0]?.turnBudgetExceeded, undefined);
1911
- assert.equal(status.steps?.[0]?.turnBudget?.outcome, "wrap-up-requested");
1912
- });
1913
-
1914
- it("preserves an async clean completion after turn-budget work defers", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
1915
- const id = `async-turn-budget-deferred-${Date.now().toString(36)}`;
1916
- const releasePath = path.join(tempDir, `${id}.release`);
1917
- mockPi.onCall({
1918
- steps: [
1919
- {
1920
- jsonl: [
1921
- mockAssistantMessage("starting required tool work", "tool_use"),
1922
- events.toolStart("bash", { command: "node build.mjs" }),
1923
- ],
1924
- },
1925
- {
1926
- waitForPath: releasePath,
1927
- jsonl: [
1928
- events.toolResult("bash", "build completed"),
1929
- events.toolEnd("bash"),
1930
- mockAssistantMessage("safe assistant boundary reached", "stop"),
1931
- ],
1932
- },
1933
- ],
1934
- });
1935
- executeAsyncSingle(id, {
1936
- agent: "worker",
1937
- task: "Finish active tool work before enforcing the hard limit.",
1938
- agentConfig: makeAgent("worker"),
1939
- ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
1940
- artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
1941
- shareEnabled: false,
1942
- maxSubagentDepth: 2,
1943
- turnBudget: { maxTurns: 1, graceTurns: 0 },
1944
- });
1945
-
1946
- let pending: AsyncStatusPayload;
1947
- try {
1948
- pending = await waitForDeferredTurnBudget(id);
1949
- } finally {
1950
- fs.writeFileSync(releasePath, "release", "utf-8");
1951
- }
1952
- assert.equal(pending.error, undefined);
1953
- assert.equal(pending.turnBudgetExceeded, undefined);
1954
- assert.equal(pending.turnBudget?.terminationDeferredAtTurn, 1);
1955
- assert.equal(pending.steps?.[0]?.status, "running");
1956
- assert.equal(pending.steps?.[0]?.turnBudgetExceeded, undefined);
1957
-
1958
- const resultPath = await waitForAsyncResultFile(id, 10_000);
1959
- const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
1960
- const status = await waitForAsyncState(id, (candidate) => candidate.state === "complete");
1961
- assert.equal(payload.state, "complete");
1962
- assert.equal(payload.turnBudgetExceeded, undefined);
1963
- assert.equal(payload.turnBudget?.outcome, "wrap-up-requested");
1964
- assert.equal(payload.turnBudget?.turnCount, 2);
1965
- assert.equal(payload.results[0]?.turnBudgetExceeded, undefined);
1966
- assert.match(payload.results[0]?.output ?? "", /safe assistant boundary reached/);
1967
- assert.equal(status.state, "complete");
1968
- assert.equal(status.turnBudgetExceeded, undefined);
1969
- assert.equal(status.steps?.[0]?.turnBudget?.outcome, "wrap-up-requested");
1970
- });
1971
-
1972
- it("lets timeout delivery preempt deferred turn-budget termination", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
1973
- mockPi.onCall({
1974
- steps: [
1975
- {
1976
- jsonl: [
1977
- mockAssistantMessage("starting long tool work", "tool_use"),
1978
- events.toolStart("bash", { command: "node build.mjs" }),
1979
- ],
1980
- },
1981
- { delay: 5_000, jsonl: [events.toolEnd("bash")] },
1982
- ],
1983
- });
1984
- const id = `async-turn-budget-timeout-${Date.now().toString(36)}`;
1985
- executeAsyncSingle(id, {
1986
- agent: "worker",
1987
- task: "Timeout while hard termination is deferred.",
1988
- agentConfig: makeAgent("worker"),
1989
- ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
1990
- artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
1991
- shareEnabled: false,
1992
- maxSubagentDepth: 2,
1993
- turnBudget: { maxTurns: 1, graceTurns: 0 },
1994
- timeoutMs: 30_000,
1995
- });
1996
-
1997
- const pending = await waitForDeferredTurnBudget(id);
1998
- const asyncDir = path.join(ASYNC_DIR, id);
1999
- deliverTimeoutRequest({ asyncDir, pid: pending.pid, source: "test" });
2000
- const resultPath = await waitForAsyncResultFile(id, 10_000);
2001
- const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
2002
- const status = await waitForAsyncState(id, (candidate) => candidate.state === "failed");
2003
- assert.equal(payload.state, "failed");
2004
- assert.equal(payload.timedOut, true);
2005
- assert.equal(payload.turnBudgetExceeded, undefined);
2006
- assert.match(payload.error ?? payload.summary ?? "", /timed out after 30000ms/);
2007
- assert.equal(payload.results[0]?.timedOut, true);
2008
- assert.equal(payload.results[0]?.turnBudgetExceeded, undefined);
2009
- assert.equal(status.timedOut, true);
2010
- assert.equal(status.turnBudgetExceeded, undefined);
2011
- assert.equal(status.steps?.[0]?.timedOut, true);
2012
- });
2013
-
2014
- it("lets explicit stop preempt deferred turn-budget termination", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
2015
- mockPi.onCall({
2016
- steps: [
2017
- {
2018
- jsonl: [
2019
- mockAssistantMessage("starting long tool work", "tool_use"),
2020
- events.toolStart("bash", { command: "node build.mjs" }),
2021
- ],
2022
- },
2023
- { delay: 5_000, jsonl: [events.toolEnd("bash")] },
2024
- ],
2025
- });
2026
- const id = `async-turn-budget-stop-${Date.now().toString(36)}`;
2027
- executeAsyncSingle(id, {
2028
- agent: "worker",
2029
- task: "Stop while hard termination is deferred.",
2030
- agentConfig: makeAgent("worker"),
2031
- ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
2032
- artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
2033
- shareEnabled: false,
2034
- maxSubagentDepth: 2,
2035
- turnBudget: { maxTurns: 1, graceTurns: 0 },
2036
- });
2037
-
2038
- const pending = await waitForDeferredTurnBudget(id);
2039
- const asyncDir = path.join(ASYNC_DIR, id);
2040
- deliverStopRequest({ asyncDir, pid: pending.pid, source: "test" });
2041
- const resultPath = await waitForAsyncResultFile(id, 10_000);
2042
- const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
2043
- const status = await waitForAsyncState(id, (candidate) => candidate.state === "stopped");
2044
- assert.equal(payload.state, "stopped");
2045
- assert.equal(payload.stopped, true);
2046
- assert.equal(payload.turnBudgetExceeded, undefined);
2047
- assert.match(payload.error ?? payload.summary ?? "", /stopped by user/i);
2048
- assert.equal(payload.results[0]?.stopped, true);
2049
- assert.equal(payload.results[0]?.turnBudgetExceeded, undefined);
2050
- assert.equal(status.state, "stopped");
2051
- assert.equal(status.stopped, true);
2052
- assert.equal(status.turnBudgetExceeded, undefined);
2053
- assert.equal(status.steps?.[0]?.status, "stopped");
2054
- });
2055
-
2056
1940
  it("async launch messages tell the parent not to sleep-poll", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
2057
1941
  const artifactConfig = {
2058
1942
  enabled: false,
@@ -2530,7 +2414,6 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
2530
2414
  parallel: {
2531
2415
  agent: "reviewer",
2532
2416
  task: "Review {target.path}",
2533
- label: "Review {target.path}",
2534
2417
  outputSchema: { type: "object" },
2535
2418
  },
2536
2419
  collect: { as: "reviews" },
@@ -2555,6 +2438,8 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
2555
2438
  assert.match(readMockPiArgs(mockPi, 1).at(-1) ?? "", /Review src\/a\.ts/);
2556
2439
  assert.match(readMockPiArgs(mockPi, 2).at(-1) ?? "", /Review src\/b\.ts/);
2557
2440
  assert.match(readMockPiArgs(mockPi, 3).at(-1) ?? "", /"key":"src\/a\.ts"/);
2441
+ assert.deepEqual(status.steps?.slice(1, 3).map((step) => step.sessionName), ["reviewer: Review src/a.ts", "reviewer: Review src/b.ts"]);
2442
+ assert.deepEqual(payload.results.slice(1, 3).map((result) => result.sessionName), ["reviewer: Review src/a.ts", "reviewer: Review src/b.ts"]);
2558
2443
  const collected = payload.outputs?.reviews?.structured as Array<{ key: string; structured: unknown }>;
2559
2444
  assert.deepEqual(collected.map((item) => item.key), ["src/a.ts", "src/b.ts"]);
2560
2445
  assert.deepEqual(collected.map((item) => item.structured), [{ ok: "a" }, { ok: "b" }]);
@@ -2984,7 +2869,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
2984
2869
  content: [{ type: "text", text: "primary failed" }],
2985
2870
  model: "openai/gpt-5-mini",
2986
2871
  errorMessage: "rate limit exceeded",
2987
- usage: { input: 10, output: 5, cacheRead: 0, cacheWrite: 0, cost: { total: 0.01 } },
2872
+ usage: { input: 10, output: 5, cacheRead: 300, cacheWrite: 0, cost: { total: 0.01 } },
2988
2873
  },
2989
2874
  }],
2990
2875
  exitCode: 1,
@@ -2998,7 +2883,6 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
2998
2883
  agent: "worker",
2999
2884
  task: "Do work",
3000
2885
  acceptance: { level: "none", reason: "descriptor persistence coverage" },
3001
- turnBudget: { maxTurns: 8, graceTurns: 2 },
3002
2886
  agentConfig: makeAgent("worker", {
3003
2887
  model: "openai/gpt-5-mini:high",
3004
2888
  fallbackModels: ["anthropic/claude-sonnet-4:low"],
@@ -3040,7 +2924,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
3040
2924
  assert.equal(descriptor.cwd, tempDir);
3041
2925
  assert.equal(descriptor.sessionDir, path.join(sessionRoot, `async-${id}`));
3042
2926
  assert.deepEqual(descriptor.acceptance, { level: "none", reason: "descriptor persistence coverage" });
3043
- assert.deepEqual(descriptor.initialTurnBudget, { maxTurns: 8, graceTurns: 2 });
2927
+ assert.equal(descriptor.initialTurnBudget, undefined);
3044
2928
  assert.equal(Object.hasOwn(descriptor.acceptance, "explicit"), false);
3045
2929
  assert.equal(Object.hasOwn(descriptor.acceptance, "inferredReason"), false);
3046
2930
  assert.equal(Object.hasOwn(descriptor, "task"), false);
@@ -3067,6 +2951,10 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
3067
2951
  assert.equal(statusPayload.steps[0]?.thinking, "low");
3068
2952
  assert.ok(statusPayload.totalTokens!.total > 0);
3069
2953
  assert.ok(statusPayload.steps[0]?.tokens!.total > 0);
2954
+ assert.equal(statusPayload.totalTokens!.window, 100);
2955
+ assert.equal(statusPayload.totalTokens!.windowPeak, 310);
2956
+ assert.equal(statusPayload.steps[0]?.tokens!.window, 100);
2957
+ assert.equal(statusPayload.steps[0]?.tokens!.windowPeak, 310);
3070
2958
  assert.deepEqual(statusPayload.steps[0]?.totalCost, { inputTokens: 110, outputTokens: 55, costUsd: 0.011 });
3071
2959
  assert.deepEqual(statusPayload.totalCost, { inputTokens: 110, outputTokens: 55, costUsd: 0.011 });
3072
2960
  const events = fs.readFileSync(path.join(asyncDir, "events.jsonl"), "utf-8").trim().split("\n").map((line) => JSON.parse(line));
@@ -3200,6 +3088,54 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
3200
3088
  assert.equal(mockPi.callCount(), 2);
3201
3089
  });
3202
3090
 
3091
+ it("background runs retry the fallback model after a provider connection error", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
3092
+ mockPi.onCall({
3093
+ jsonl: [{
3094
+ type: "message_end",
3095
+ message: {
3096
+ role: "assistant",
3097
+ content: [],
3098
+ model: "openai/gpt-5-mini",
3099
+ errorMessage: "Connection error.",
3100
+ usage: { input: 10, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0.001 } },
3101
+ },
3102
+ }],
3103
+ exitCode: 1,
3104
+ });
3105
+ mockPi.onCall({ output: "Recovered after connection error" });
3106
+ const id = `async-fallback-connection-error-${Date.now().toString(36)}`;
3107
+ executeAsyncSingle(id, {
3108
+ agent: "worker",
3109
+ task: "Do work",
3110
+ agentConfig: makeAgent("worker", {
3111
+ model: "openai/gpt-5-mini:high",
3112
+ fallbackModels: ["anthropic/claude-sonnet-4:low"],
3113
+ }),
3114
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
3115
+ availableModels: [
3116
+ { provider: "openai", id: "gpt-5-mini", fullId: "openai/gpt-5-mini" },
3117
+ { provider: "anthropic", id: "claude-sonnet-4", fullId: "anthropic/claude-sonnet-4" },
3118
+ ],
3119
+ artifactConfig: {
3120
+ enabled: false,
3121
+ includeInput: false,
3122
+ includeOutput: false,
3123
+ includeJsonl: false,
3124
+ includeMetadata: false,
3125
+ cleanupDays: 7,
3126
+ },
3127
+ shareEnabled: false,
3128
+ maxSubagentDepth: 2,
3129
+ });
3130
+
3131
+ const payload = JSON.parse(fs.readFileSync(await waitForAsyncResultFile(id), "utf-8"));
3132
+ assert.equal(payload.success, true);
3133
+ assert.deepEqual(payload.results[0].attemptedModels, ["openai/gpt-5-mini:high", "anthropic/claude-sonnet-4:low"]);
3134
+ assert.match(payload.results[0].modelAttempts[0].error ?? "", /Connection error/u);
3135
+ assert.match(payload.results[0].output ?? "", /Recovered after connection error/u);
3136
+ assert.equal(mockPi.callCount(), 2);
3137
+ });
3138
+
3203
3139
  it("background runs do not retry the fallback model for a trailing tool failure", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
3204
3140
  mockPi.onCall({
3205
3141
  jsonl: [
@@ -3242,29 +3178,297 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
3242
3178
  assert.equal(mockPi.callCount(), 1);
3243
3179
  });
3244
3180
 
3245
- it("background single thinking override replaces primary and fallback suffixes", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
3181
+ it("background runs do not retry raw connection stderr after child activity", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
3246
3182
  mockPi.onCall({
3247
3183
  jsonl: [{
3248
3184
  type: "message_end",
3249
3185
  message: {
3250
3186
  role: "assistant",
3251
- content: [{ type: "text", text: "primary failed" }],
3187
+ content: [{ type: "text", text: "completed side effect" }],
3252
3188
  model: "openai/gpt-5-mini",
3253
- errorMessage: "rate limit exceeded",
3254
- usage: { input: 10, output: 5, cacheRead: 0, cacheWrite: 0, cost: { total: 0.01 } },
3189
+ stopReason: "stop",
3190
+ usage: { input: 10, output: 5, cacheRead: 0, cacheWrite: 0, cost: { total: 0.001 } },
3255
3191
  },
3256
3192
  }],
3193
+ stderr: "APIConnectionError: Connection closed.",
3257
3194
  exitCode: 1,
3258
3195
  });
3259
- mockPi.onCall({ output: "Recovered asynchronously" });
3260
- const id = `async-fallback-thinking-off-${Date.now().toString(36)}`;
3261
- const run = executeAsyncSingle(id, {
3196
+ mockPi.onCall({ output: "fallback must not run" });
3197
+ const id = `async-fallback-raw-stderr-${Date.now().toString(36)}`;
3198
+ executeAsyncSingle(id, {
3262
3199
  agent: "worker",
3263
3200
  task: "Do work",
3264
3201
  agentConfig: makeAgent("worker", {
3265
3202
  model: "openai/gpt-5-mini:high",
3266
3203
  fallbackModels: ["anthropic/claude-sonnet-4:low"],
3267
- thinking: "high",
3204
+ }),
3205
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
3206
+ availableModels: [
3207
+ { provider: "openai", id: "gpt-5-mini", fullId: "openai/gpt-5-mini" },
3208
+ { provider: "anthropic", id: "claude-sonnet-4", fullId: "anthropic/claude-sonnet-4" },
3209
+ ],
3210
+ artifactConfig: {
3211
+ enabled: false,
3212
+ includeInput: false,
3213
+ includeOutput: false,
3214
+ includeJsonl: false,
3215
+ includeMetadata: false,
3216
+ cleanupDays: 7,
3217
+ },
3218
+ shareEnabled: false,
3219
+ maxSubagentDepth: 2,
3220
+ });
3221
+
3222
+ const payload = JSON.parse(fs.readFileSync(await waitForAsyncResultFile(id), "utf-8"));
3223
+ assert.equal(payload.success, false);
3224
+ assert.equal(payload.results[0].modelAttempts.length, 1);
3225
+ assert.match(payload.results[0].error ?? "", /Connection closed/u);
3226
+ assert.equal(mockPi.callCount(), 1);
3227
+ });
3228
+
3229
+ it("background runs resume the retained session once after a compaction-induced abort following completed tool work", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
3230
+ const sessionFile = path.join(tempDir, "async-abort-recovery-session.jsonl");
3231
+ mockPi.onCall({
3232
+ jsonl: [
3233
+ events.toolStart("write", { path: "side-effect.txt", content: "done" }),
3234
+ events.toolEnd("write"),
3235
+ events.toolResult("write", "Wrote side-effect.txt"),
3236
+ { type: "compaction_start" },
3237
+ {
3238
+ type: "message_end",
3239
+ message: {
3240
+ role: "assistant",
3241
+ content: [],
3242
+ model: "openai/gpt-5-mini",
3243
+ stopReason: "aborted",
3244
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0 } },
3245
+ },
3246
+ },
3247
+ { type: "agent_settled" },
3248
+ ],
3249
+ writeFiles: [{ path: "side-effect.txt", content: "done" }, { path: sessionFile, content: "{}\n" }],
3250
+ keepAliveAfterFinalMessageMs: 5_000,
3251
+ exitCode: 0,
3252
+ });
3253
+ mockPi.onCall({ output: "Recovered asynchronously from retained session" });
3254
+ const id = `async-fallback-provider-after-tool-${Date.now().toString(36)}`;
3255
+ executeAsyncSingle(id, {
3256
+ agent: "worker",
3257
+ task: "Do work",
3258
+ sessionFile,
3259
+ agentConfig: makeAgent("worker", {
3260
+ model: "openai/gpt-5-mini:high",
3261
+ fallbackModels: ["anthropic/claude-sonnet-4:low"],
3262
+ }),
3263
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
3264
+ availableModels: [
3265
+ { provider: "openai", id: "gpt-5-mini", fullId: "openai/gpt-5-mini" },
3266
+ { provider: "anthropic", id: "claude-sonnet-4", fullId: "anthropic/claude-sonnet-4" },
3267
+ ],
3268
+ artifactConfig: {
3269
+ enabled: false,
3270
+ includeInput: false,
3271
+ includeOutput: false,
3272
+ includeJsonl: false,
3273
+ includeMetadata: false,
3274
+ cleanupDays: 7,
3275
+ },
3276
+ shareEnabled: false,
3277
+ maxSubagentDepth: 2,
3278
+ });
3279
+
3280
+ const payload = JSON.parse(fs.readFileSync(await waitForAsyncResultFile(id), "utf-8"));
3281
+ assert.equal(payload.success, true);
3282
+ assert.deepEqual(payload.results[0].attemptedModels, ["openai/gpt-5-mini:high"]);
3283
+ assert.deepEqual(payload.results[0].modelAttempts.map((attempt: { success: boolean }) => attempt.success), [false, true]);
3284
+ assert.equal(mockPi.callCount(), 2);
3285
+ const firstArgs = readMockPiArgs(mockPi, 0);
3286
+ const resumedArgs = readMockPiArgs(mockPi, 1);
3287
+ assert.equal(firstArgs[firstArgs.indexOf("--session") + 1], sessionFile);
3288
+ assert.equal(resumedArgs[resumedArgs.indexOf("--session") + 1], sessionFile);
3289
+ assert.match(resumedArgs.at(-1) ?? "", /Continue from the current files and transcript/);
3290
+ assert.equal(fs.readFileSync(path.join(tempDir, "side-effect.txt"), "utf-8"), "done");
3291
+ });
3292
+
3293
+ it("background compaction abort without a retained session does not fall back", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
3294
+ mockPi.onCall({
3295
+ jsonl: [
3296
+ events.assistantMessage("I inspected the source."),
3297
+ { type: "compaction_start" },
3298
+ {
3299
+ type: "message_end",
3300
+ message: {
3301
+ role: "assistant",
3302
+ content: [],
3303
+ model: "openai/gpt-5-mini",
3304
+ stopReason: "aborted",
3305
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0 } },
3306
+ },
3307
+ },
3308
+ { type: "agent_settled" },
3309
+ ],
3310
+ keepAliveAfterFinalMessageMs: 5_000,
3311
+ exitCode: 0,
3312
+ });
3313
+ mockPi.onCall({ output: "Fallback must not run" });
3314
+ const id = `async-compaction-abort-no-session-${Date.now().toString(36)}`;
3315
+ executeAsyncSingle(id, {
3316
+ agent: "worker",
3317
+ task: "Do work",
3318
+ agentConfig: makeAgent("worker", {
3319
+ model: "openai/gpt-5-mini:high",
3320
+ fallbackModels: ["anthropic/claude-sonnet-4:low"],
3321
+ }),
3322
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
3323
+ availableModels: [
3324
+ { provider: "openai", id: "gpt-5-mini", fullId: "openai/gpt-5-mini" },
3325
+ { provider: "anthropic", id: "claude-sonnet-4", fullId: "anthropic/claude-sonnet-4" },
3326
+ ],
3327
+ artifactConfig: {
3328
+ enabled: false,
3329
+ includeInput: false,
3330
+ includeOutput: false,
3331
+ includeJsonl: false,
3332
+ includeMetadata: false,
3333
+ cleanupDays: 7,
3334
+ },
3335
+ shareEnabled: false,
3336
+ maxSubagentDepth: 2,
3337
+ });
3338
+
3339
+ const payload = JSON.parse(fs.readFileSync(await waitForAsyncResultFile(id), "utf-8"));
3340
+ assert.equal(payload.success, false);
3341
+ assert.match(payload.results[0]?.error ?? "", /Compaction-induced child abort could not be resumed safely: retained session unavailable\./);
3342
+ assert.doesNotMatch(payload.results[0]?.output ?? "", /Fallback must not run/);
3343
+ assert.equal(mockPi.callCount(), 1);
3344
+ });
3345
+
3346
+ it("background does not use compaction recovery for a generic empty assistant abort after a compaction retry", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
3347
+ const sessionFile = path.join(tempDir, "async-generic-empty-after-compaction-retry-session.jsonl");
3348
+ mockPi.onCall({
3349
+ jsonl: [
3350
+ { type: "compaction_start" },
3351
+ { type: "compaction_end", willRetry: true },
3352
+ { type: "agent_settled" },
3353
+ { type: "agent_start" },
3354
+ events.assistantMessage("The compaction retry produced useful output.", "openai/gpt-5-mini"),
3355
+ { type: "agent_settled" },
3356
+ {
3357
+ type: "message_end",
3358
+ message: {
3359
+ role: "assistant",
3360
+ content: [],
3361
+ model: "openai/gpt-5-mini",
3362
+ stopReason: "aborted",
3363
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0 } },
3364
+ },
3365
+ },
3366
+ { type: "agent_settled" },
3367
+ ],
3368
+ writeFiles: [{ path: sessionFile, content: "{}\n" }],
3369
+ exitCode: 0,
3370
+ });
3371
+ mockPi.onCall({ output: "Compaction recovery must not run" });
3372
+ const id = `async-no-compaction-recovery-after-retry-${Date.now().toString(36)}`;
3373
+ executeAsyncSingle(id, {
3374
+ agent: "worker",
3375
+ task: "Do work",
3376
+ sessionFile,
3377
+ agentConfig: makeAgent("worker", { model: "openai/gpt-5-mini" }),
3378
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
3379
+ availableModels: [{ provider: "openai", id: "gpt-5-mini", fullId: "openai/gpt-5-mini" }],
3380
+ artifactConfig: {
3381
+ enabled: false,
3382
+ includeInput: false,
3383
+ includeOutput: false,
3384
+ includeJsonl: false,
3385
+ includeMetadata: false,
3386
+ cleanupDays: 7,
3387
+ },
3388
+ shareEnabled: false,
3389
+ maxSubagentDepth: 2,
3390
+ });
3391
+
3392
+ const payload = JSON.parse(fs.readFileSync(await waitForAsyncResultFile(id), "utf-8"));
3393
+ assert.equal(payload.success, false);
3394
+ assert.match(payload.results[0]?.error ?? "", /Subagent produced no output after terminal assistant stopReason "aborted"\./u);
3395
+ assert.equal(payload.results[0]?.modelAttempts?.length, 1);
3396
+ assert.equal(mockPi.callCount(), 1);
3397
+ });
3398
+
3399
+ it("background does not use compaction recovery after compaction_end willRetry false and a continued agent turn", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
3400
+ const sessionFile = path.join(tempDir, "async-generic-empty-after-successful-compaction-session.jsonl");
3401
+ mockPi.onCall({
3402
+ jsonl: [
3403
+ { type: "compaction_start" },
3404
+ { type: "compaction_end", willRetry: false },
3405
+ { type: "agent_start" },
3406
+ {
3407
+ type: "message_end",
3408
+ message: {
3409
+ role: "assistant",
3410
+ content: [],
3411
+ model: "openai/gpt-5-mini",
3412
+ stopReason: "aborted",
3413
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0 } },
3414
+ },
3415
+ },
3416
+ { type: "agent_settled" },
3417
+ ],
3418
+ writeFiles: [{ path: sessionFile, content: "{}\n" }],
3419
+ exitCode: 0,
3420
+ });
3421
+ mockPi.onCall({ output: "Compaction recovery must not run" });
3422
+ const id = `async-no-compaction-recovery-after-successful-compaction-${Date.now().toString(36)}`;
3423
+ executeAsyncSingle(id, {
3424
+ agent: "worker",
3425
+ task: "Do work",
3426
+ sessionFile,
3427
+ agentConfig: makeAgent("worker", { model: "openai/gpt-5-mini" }),
3428
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
3429
+ availableModels: [{ provider: "openai", id: "gpt-5-mini", fullId: "openai/gpt-5-mini" }],
3430
+ artifactConfig: {
3431
+ enabled: false,
3432
+ includeInput: false,
3433
+ includeOutput: false,
3434
+ includeJsonl: false,
3435
+ includeMetadata: false,
3436
+ cleanupDays: 7,
3437
+ },
3438
+ shareEnabled: false,
3439
+ maxSubagentDepth: 2,
3440
+ });
3441
+
3442
+ const payload = JSON.parse(fs.readFileSync(await waitForAsyncResultFile(id), "utf-8"));
3443
+ assert.equal(payload.success, false);
3444
+ assert.match(payload.results[0]?.error ?? "", /Subagent produced no output after terminal assistant stopReason "aborted"\./u);
3445
+ assert.equal(payload.results[0]?.modelAttempts?.length, 1);
3446
+ assert.equal(mockPi.callCount(), 1);
3447
+ });
3448
+
3449
+ it("background single thinking override replaces primary and fallback suffixes", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
3450
+ mockPi.onCall({
3451
+ jsonl: [{
3452
+ type: "message_end",
3453
+ message: {
3454
+ role: "assistant",
3455
+ content: [{ type: "text", text: "primary failed" }],
3456
+ model: "openai/gpt-5-mini",
3457
+ errorMessage: "rate limit exceeded",
3458
+ usage: { input: 10, output: 5, cacheRead: 0, cacheWrite: 0, cost: { total: 0.01 } },
3459
+ },
3460
+ }],
3461
+ exitCode: 1,
3462
+ });
3463
+ mockPi.onCall({ output: "Recovered asynchronously" });
3464
+ const id = `async-fallback-thinking-off-${Date.now().toString(36)}`;
3465
+ const run = executeAsyncSingle(id, {
3466
+ agent: "worker",
3467
+ task: "Do work",
3468
+ agentConfig: makeAgent("worker", {
3469
+ model: "openai/gpt-5-mini:high",
3470
+ fallbackModels: ["anthropic/claude-sonnet-4:low"],
3471
+ thinking: "high",
3268
3472
  }),
3269
3473
  ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
3270
3474
  availableModels: [
@@ -3343,6 +3547,68 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
3343
3547
  assert.equal(mockPi.callCount(), 2);
3344
3548
  });
3345
3549
 
3550
+ it("background fails a zero-exit child that stops during a tool after earlier assistant output", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
3551
+ mockPi.onCall({
3552
+ jsonl: [
3553
+ events.assistantMessage("Work is in progress"),
3554
+ events.toolStart("bash", { command: "write files" }),
3555
+ ],
3556
+ exitCode: 0,
3557
+ });
3558
+ const id = `async-mid-tool-exit-${Date.now().toString(36)}`;
3559
+ executeAsyncSingle(id, {
3560
+ agent: "worker",
3561
+ task: "Do work",
3562
+ agentConfig: makeAgent("worker", { model: "openai/gpt-5-mini" }),
3563
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
3564
+ artifactConfig: {
3565
+ enabled: false,
3566
+ includeInput: false,
3567
+ includeOutput: false,
3568
+ includeJsonl: false,
3569
+ includeMetadata: false,
3570
+ cleanupDays: 7,
3571
+ },
3572
+ shareEnabled: false,
3573
+ maxSubagentDepth: 2,
3574
+ });
3575
+
3576
+ const resultPath = await waitForAsyncResultFile(id);
3577
+ const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
3578
+ assert.equal(payload.success, false);
3579
+ assert.equal(payload.results[0]?.success, false);
3580
+ assert.match(payload.results[0]?.error ?? "", /exited during 'bash' tool execution \(exit 0\)/);
3581
+ assert.match(payload.results[0]?.error ?? "", /Earlier assistant output is not a terminal result/);
3582
+ assert.doesNotMatch(payload.results[0]?.error ?? "", /cold-start/);
3583
+ assert.equal(mockPi.callCount(), 1);
3584
+ });
3585
+
3586
+ it("background retains an earlier open tool when a later overlapping tool completes", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
3587
+ mockPi.onCall({
3588
+ jsonl: [
3589
+ { type: "tool_execution_start", toolCallId: "bash-1", toolName: "bash", args: { command: "wait" } },
3590
+ { type: "tool_execution_start", toolCallId: "read-1", toolName: "read", args: { path: "README.md" } },
3591
+ { type: "tool_execution_end", toolCallId: "read-1", toolName: "read" },
3592
+ ],
3593
+ exitCode: 0,
3594
+ });
3595
+ const id = `async-overlap-mid-tool-exit-${Date.now().toString(36)}`;
3596
+ executeAsyncSingle(id, {
3597
+ agent: "worker",
3598
+ task: "Do work",
3599
+ agentConfig: makeAgent("worker", { model: "openai/gpt-5-mini" }),
3600
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
3601
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
3602
+ shareEnabled: false,
3603
+ maxSubagentDepth: 2,
3604
+ });
3605
+
3606
+ const resultPath = await waitForAsyncResultFile(id);
3607
+ const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
3608
+ assert.equal(payload.success, false);
3609
+ assert.match(payload.results[0]?.error ?? "", /exited during 'bash' tool execution \(exit 0\)/);
3610
+ });
3611
+
3346
3612
  it("background runs prefer empty-output fallback over an earlier tool error", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
3347
3613
  mockPi.onCall({
3348
3614
  jsonl: [
@@ -3770,6 +4036,12 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
3770
4036
  assert.equal(payload.results[0].success, false);
3771
4037
  assert.match(String(payload.results[0].error ?? ""), /completed without making edits/);
3772
4038
  assert.match(String(payload.results[0].modelAttempts?.[0]?.error ?? ""), /completed without making edits/);
4039
+ assert.deepEqual(payload.results[0].effects?.settlementDiagnostic?.mutation, {
4040
+ expected: true,
4041
+ attempted: false,
4042
+ observed: false,
4043
+ });
4044
+ assert.equal(payload.results[0].effects?.settlementDiagnostic?.finalTextPresent, true);
3773
4045
 
3774
4046
  const eventsPath = path.join(ASYNC_DIR, id, "events.jsonl");
3775
4047
  const eventsText = fs.readFileSync(eventsPath, "utf-8");
@@ -4080,7 +4352,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
4080
4352
  assert.match(result.content[0]?.type === "text" ? result.content[0].text : "", /Active async run capacity exhausted: 1\/1 used/);
4081
4353
  assert.equal(fs.existsSync(rejectedAsyncDir), false);
4082
4354
  assert.equal(fs.existsSync(rejectedResultPath), false);
4083
- assert.equal(fs.existsSync(path.join(tempDir, ".selesai", "subagents")), false);
4355
+ assert.equal(fs.existsSync(path.join(tempDir, ".pi-subagents")), false);
4084
4356
  fs.rmSync(path.join(ACTIVE_ASYNC_CAPACITY_DIR, activeAsyncCapacitySessionKey("session-cap")), { recursive: true, force: true });
4085
4357
  });
4086
4358
 
@@ -4136,6 +4408,50 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
4136
4408
  await readAsyncPayload(launch.details.asyncId);
4137
4409
  });
4138
4410
 
4411
+ it("scheduled owners without a model do not inherit the live session model", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
4412
+ mockPi.onCall({ output: "Scheduled owner work completed" });
4413
+ const liveCwd = path.join(tempDir, "live-model-project");
4414
+ fs.mkdirSync(liveCwd);
4415
+ const state = {
4416
+ baseCwd: liveCwd,
4417
+ currentSessionId: "session-live",
4418
+ lastParentModel: { provider: "router", id: "live-model" },
4419
+ asyncJobs: new Map(),
4420
+ fleetJobs: new Map(),
4421
+ foregroundControls: new Map(),
4422
+ lastForegroundControlId: null,
4423
+ };
4424
+ const executor = createSubagentExecutor!({
4425
+ pi: { events: createEventBus(), getSessionName: () => undefined },
4426
+ state,
4427
+ config: {},
4428
+ asyncByDefault: false,
4429
+ tempArtifactsDir: tempDir,
4430
+ getSubagentSessionRoot: () => path.join(tempDir, "sessions"),
4431
+ expandTilde: (p: string) => p,
4432
+ discoverAgents: () => ({ agents: [makeAgent("worker")] }),
4433
+ });
4434
+ const retainedCtx = makeMinimalCtx(tempDir);
4435
+ retainedCtx.sessionManager.getSessionId = () => "session-scheduled-owner";
4436
+
4437
+ const launch = await executor.executeScheduled(
4438
+ `scheduled-owner-no-model-${Date.now().toString(36)}`,
4439
+ { agent: "worker", task: "Run owner without model", async: true, acceptance: false },
4440
+ new AbortController().signal,
4441
+ retainedCtx,
4442
+ ) as AsyncExecutionResult;
4443
+ assert.equal(launch.isError, undefined);
4444
+ assert.ok(launch.details.asyncId);
4445
+
4446
+ const payload = await readAsyncPayload(launch.details.asyncId);
4447
+ assert.equal(payload.success, true);
4448
+ assert.equal(state.currentSessionId, "session-live");
4449
+ assert.deepEqual(state.lastParentModel, { provider: "router", id: "live-model" });
4450
+ const args = readMockPiArgsMatching(mockPi, "Run owner without model");
4451
+ assert.equal(args.includes("router/live-model"), false);
4452
+ assert.equal(args.includes("--model"), false);
4453
+ });
4454
+
4139
4455
  it("scheduled workflows keep owner status and registries isolated from the live session", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
4140
4456
  const liveCwd = path.join(tempDir, "live-workflow-project");
4141
4457
  fs.mkdirSync(liveCwd);
@@ -4222,8 +4538,52 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
4222
4538
  assert.equal(args[args.indexOf("--model") + 1], "deepseek/deepseek-v4-flash");
4223
4539
  });
4224
4540
 
4225
-
4226
-
4541
+ it("async workflows snapshot the parent model before workflow setup reads session data", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
4542
+ mockPi.onCall({ output: "Inherited model work" });
4543
+ mockPi.onCall({ output: "Explicit model work" });
4544
+ const state = {
4545
+ baseCwd: tempDir,
4546
+ currentSessionId: null,
4547
+ asyncJobs: new Map(),
4548
+ foregroundControls: new Map(),
4549
+ lastForegroundControlId: null,
4550
+ };
4551
+ const executor = createSubagentExecutor!({
4552
+ pi: { events: createEventBus(), getSessionName: () => undefined },
4553
+ state,
4554
+ config: {},
4555
+ asyncByDefault: false,
4556
+ tempArtifactsDir: tempDir,
4557
+ getSubagentSessionRoot: () => path.join(tempDir, "sessions"),
4558
+ expandTilde: (p: string) => p,
4559
+ discoverAgents: () => ({ agents: [makeAgent("worker")] }),
4560
+ });
4561
+ const context = makeMinimalCtx(tempDir);
4562
+ context.sessionManager.getSessionId = () => "session-workflow-parent-model";
4563
+ context.model = { provider: "router", id: "openai-personal" };
4564
+ context.sessionManager.getSessionFile = () => {
4565
+ context.model = undefined;
4566
+ return null;
4567
+ };
4568
+
4569
+ const launch = await executor.execute(
4570
+ "workflow-parent-model",
4571
+ { workflowScript: `await runs.run("inherited", { agent: "worker", task: "Do inherited work" }); return runs.run("explicit", { agent: "worker", task: "Do explicit work", model: "openai/gpt-5-mini" });`, async: true },
4572
+ new AbortController().signal,
4573
+ undefined,
4574
+ context,
4575
+ ) as AsyncExecutionResult;
4576
+ assert.equal(launch.isError, undefined);
4577
+ assert.ok(launch.details.asyncId);
4578
+
4579
+ const payload = await readAsyncPayload(launch.details.asyncId);
4580
+ assert.equal(payload.success, true);
4581
+ const inheritedArgs = readMockPiArgsMatching(mockPi, "Do inherited work");
4582
+ const explicitArgs = readMockPiArgsMatching(mockPi, "Do explicit work");
4583
+ assert.equal(inheritedArgs[inheritedArgs.indexOf("--model") + 1], "router/openai-personal");
4584
+ assert.equal(explicitArgs[explicitArgs.indexOf("--model") + 1], "openai/gpt-5-mini");
4585
+ });
4586
+
4227
4587
  it("background single runs inherit the parent session model when no model is set", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
4228
4588
  mockPi.onCall({ output: "Done asynchronously" });
4229
4589
 
@@ -4329,6 +4689,91 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
4329
4689
  assert.equal(payload.results[0]?.model, "gateway/parent-model");
4330
4690
  });
4331
4691
 
4692
+ it("reports the retained discovery context when a resumed target agent is missing", { skip: !createSubagentExecutor ? "executor not available" : undefined }, async () => {
4693
+ const runId = `resume-missing-agent-${Date.now().toString(36)}`;
4694
+ const asyncDir = path.join(ASYNC_DIR, runId);
4695
+ const sessionFile = path.join(tempDir, "missing-agent-session.jsonl");
4696
+ const visible = makeAgent("visible");
4697
+ visible.source = "project";
4698
+ const evidenceDir = path.join(tempDir, "request-discovery", "agents");
4699
+ try {
4700
+ fs.mkdirSync(asyncDir, { recursive: true });
4701
+ fs.writeFileSync(sessionFile, "", "utf-8");
4702
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
4703
+ runId,
4704
+ sessionId: "session-123",
4705
+ mode: "single",
4706
+ state: "complete",
4707
+ startedAt: 100,
4708
+ lastUpdate: 200,
4709
+ cwd: tempDir,
4710
+ sessionFile,
4711
+ steps: [{ agent: "vanished", status: "complete" }],
4712
+ }, null, 2), "utf-8");
4713
+ const executor = makeAsyncExecutor([visible], {}, (cwd) => ({
4714
+ agents: [visible],
4715
+ cwd: path.resolve(cwd),
4716
+ scope: "both",
4717
+ directories: [{ source: "project", path: evidenceDir, state: "empty" }],
4718
+ }));
4719
+ const result = await executor.execute(
4720
+ "resume-missing-agent",
4721
+ { action: "resume", id: runId, message: "Continue" },
4722
+ new AbortController().signal,
4723
+ undefined,
4724
+ makeMinimalCtx(tempDir),
4725
+ ) as AsyncExecutionResult;
4726
+ assert.equal(result.isError, true);
4727
+ assert.match(result.content[0]?.text ?? "", /^Unknown agent for resume: vanished\nEffective cwd: /);
4728
+ assert.match(result.content[0]?.text ?? "", new RegExp(evidenceDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
4729
+ assert.match(result.content[0]?.text ?? "", /visible \(project\)/);
4730
+ } finally {
4731
+ fs.rmSync(asyncDir, { recursive: true, force: true });
4732
+ }
4733
+ });
4734
+
4735
+ it("uses the append request discovery context for a missing appended agent", { skip: !createSubagentExecutor ? "executor not available" : undefined }, async () => {
4736
+ const runId = `append-missing-agent-${Date.now().toString(36)}`;
4737
+ const asyncDir = path.join(ASYNC_DIR, runId);
4738
+ const visible = makeAgent("visible");
4739
+ visible.source = "project";
4740
+ const evidenceDir = path.join(tempDir, "append-request", "agents");
4741
+ const storedCwd = path.join(tempDir, "stored-run-cwd");
4742
+ try {
4743
+ fs.mkdirSync(asyncDir, { recursive: true });
4744
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
4745
+ runId,
4746
+ sessionId: "session-123",
4747
+ mode: "chain",
4748
+ state: "running",
4749
+ startedAt: 100,
4750
+ lastUpdate: 200,
4751
+ cwd: storedCwd,
4752
+ steps: [{ agent: "visible", status: "running" }],
4753
+ }, null, 2), "utf-8");
4754
+ const executor = makeAsyncExecutor([visible], {}, (cwd) => ({
4755
+ agents: [visible],
4756
+ cwd: path.resolve(cwd),
4757
+ scope: "both",
4758
+ directories: [{ source: "project", path: evidenceDir, state: "empty" }],
4759
+ }));
4760
+ const result = await executor.execute(
4761
+ "append-missing-agent",
4762
+ { action: "append-step", id: runId, step: { agent: "vanished", task: "Review" } },
4763
+ new AbortController().signal,
4764
+ undefined,
4765
+ makeMinimalCtx(tempDir),
4766
+ ) as AsyncExecutionResult;
4767
+ assert.equal(result.isError, true);
4768
+ assert.match(result.content[0]?.text ?? "", /^Unknown agent: vanished\nEffective cwd: /);
4769
+ assert.match(result.content[0]?.text ?? "", new RegExp(evidenceDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
4770
+ assert.match(result.content[0]?.text ?? "", /visible \(project\)/);
4771
+ assert.doesNotMatch(result.content[0]?.text ?? "", new RegExp(storedCwd.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
4772
+ } finally {
4773
+ fs.rmSync(asyncDir, { recursive: true, force: true });
4774
+ }
4775
+ });
4776
+
4332
4777
  it("background chains inherit the parent session model when no step or agent model is set", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
4333
4778
  mockPi.onCall({ output: "Done asynchronously" });
4334
4779
 
@@ -4735,6 +5180,35 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
4735
5180
  assert.match(result.content[0]?.text ?? "", /async-cfg-/);
4736
5181
  });
4737
5182
 
5183
+ it("does not start child work when initial async status cannot be written", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
5184
+ const id = `async-status-write-fail-${Date.now().toString(36)}`;
5185
+ fs.mkdirSync(path.join(ASYNC_DIR, id, "status.json"), { recursive: true });
5186
+ mockPi.onCall({ output: "must not run" });
5187
+
5188
+ const result = executeAsyncSingle(id, {
5189
+ agent: "worker",
5190
+ task: "Do not start",
5191
+ agentConfig: makeAgent("worker"),
5192
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
5193
+ artifactConfig: {
5194
+ enabled: false,
5195
+ includeInput: false,
5196
+ includeOutput: false,
5197
+ includeJsonl: false,
5198
+ includeMetadata: false,
5199
+ cleanupDays: 7,
5200
+ },
5201
+ shareEnabled: false,
5202
+ sessionRoot: path.join(tempDir, "sessions"),
5203
+ maxSubagentDepth: 2,
5204
+ });
5205
+
5206
+ assert.equal(result.isError, true);
5207
+ assert.match(result.content[0]?.text ?? "", /Failed to persist initial async status/);
5208
+ await new Promise((resolve) => setTimeout(resolve, 300));
5209
+ assert.equal(mockPi.callCount(), 0);
5210
+ });
5211
+
4738
5212
  it("returns a tool error when an async run uses a missing cwd", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, () => {
4739
5213
  const id = `async-missing-cwd-${Date.now().toString(36)}`;
4740
5214
  const missingCwd = path.join(tempDir, "missing-cwd");
@@ -5077,6 +5551,386 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
5077
5551
  assert.equal(payload.results[0].error, "provider exploded");
5078
5552
  });
5079
5553
 
5554
+ it("reports terminal abort before a missing file-only handoff after mutation", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
5555
+ const partialOutput = "I’ll inspect the retained candidate before changing it.";
5556
+ const repo = createRepo("pi-subagents-missing-handoff-partial-");
5557
+ const outputPath = path.join(repo, "missing-challenge-report.md");
5558
+ mockPi.onCall({
5559
+ jsonl: [
5560
+ events.assistantMessage(partialOutput),
5561
+ {
5562
+ type: "message_end",
5563
+ message: {
5564
+ role: "assistant",
5565
+ content: [],
5566
+ model: "mock/test-model",
5567
+ stopReason: "aborted",
5568
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0 } },
5569
+ },
5570
+ },
5571
+ ],
5572
+ writeFiles: [{ path: "input.md", content: "changed by retained child\n" }],
5573
+ });
5574
+
5575
+ const task = [
5576
+ "You are reviving a previous subagent conversation.",
5577
+ "",
5578
+ "Original run: source-run",
5579
+ "Original agent: worker",
5580
+ "Original session file: /tmp/source-session.jsonl",
5581
+ "",
5582
+ "Use the stored session context as background. Answer the orchestrator's follow-up below. Do not assume the original child process is still alive.",
5583
+ "",
5584
+ "Follow-up:",
5585
+ "Implementation challenge pass 1 for the accepted candidate. Reconsider it and implement any better current-scope change.",
5586
+ ].join("\n");
5587
+ const id = `async-missing-handoff-guard-${Date.now().toString(36)}`;
5588
+ try {
5589
+ executeAsyncSingle(id, {
5590
+ agent: "worker",
5591
+ task,
5592
+ agentConfig: makeAgent("worker"),
5593
+ ctx: { pi: { events: { emit() {} } }, cwd: repo, currentSessionId: "session-1" },
5594
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
5595
+ shareEnabled: false,
5596
+ sessionRoot: path.join(tempDir, "sessions"),
5597
+ output: outputPath,
5598
+ outputMode: "file-only",
5599
+ maxSubagentDepth: 2,
5600
+ });
5601
+
5602
+ const resultPath = await waitForAsyncResultFile(id);
5603
+ const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
5604
+ const child = payload.results[0];
5605
+ const diagnostic = child?.error ?? "";
5606
+ assert.equal(payload.success, false);
5607
+ assert.equal(payload.state, "partial");
5608
+ assert.equal(child?.success, false);
5609
+ assert.match(diagnostic, /^Subagent produced no output after terminal assistant stopReason "aborted"\./);
5610
+ assert.match(diagnostic, new RegExp(`Required file-only output was not produced: ${escapeRegExp(outputPath)}`));
5611
+ assert.doesNotMatch(diagnostic, /completed without making edits/);
5612
+ assert.doesNotMatch(child?.modelAttempts?.[0]?.error ?? "", /completed without making edits/);
5613
+ assert.equal(child?.effects?.fileMutation?.status, "observed");
5614
+ assert.equal(child?.effects?.fileMutation?.attempted, true);
5615
+ assert.deepEqual(child?.effects?.fileMutation?.evidence?.changedFiles, ["input.md"]);
5616
+ assert.equal(child?.effects?.settlementDiagnostic?.requiredOutput?.missing, true);
5617
+ assert.equal(fs.existsSync(outputPath), false);
5618
+
5619
+ const status = await waitForAsyncState(id, (candidate) => candidate.state === "partial");
5620
+ assert.equal(status.activityState, "needs_attention");
5621
+ assert.equal(status.steps?.[0]?.activityState, "needs_attention");
5622
+ assert.equal(status.steps?.[0]?.error, diagnostic);
5623
+ const eventsText = fs.readFileSync(path.join(ASYNC_DIR, id, "events.jsonl"), "utf-8");
5624
+ assert.doesNotMatch(eventsText, /completed without making edits/);
5625
+ } finally {
5626
+ removeTempDir(repo);
5627
+ }
5628
+ });
5629
+
5630
+ it("preserves terminal empty-output diagnostics after useful child work", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
5631
+ const partialOutput = "I’ll inspect the retained candidate before changing it.";
5632
+ const outputPath = path.join(tempDir, "missing-aborted-report.md");
5633
+ mockPi.onCall({
5634
+ jsonl: [
5635
+ events.toolStart("read", { path: "src/index.ts" }),
5636
+ events.toolEnd("read"),
5637
+ events.toolResult("read", "file contents"),
5638
+ events.assistantMessage(partialOutput),
5639
+ {
5640
+ type: "message_end",
5641
+ message: {
5642
+ role: "assistant",
5643
+ content: [],
5644
+ model: "mock/test-model",
5645
+ stopReason: "aborted",
5646
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0 } },
5647
+ },
5648
+ },
5649
+ ],
5650
+ exitCode: 0,
5651
+ });
5652
+
5653
+ const id = `async-aborted-empty-handoff-${Date.now().toString(36)}`;
5654
+ executeAsyncSingle(id, {
5655
+ agent: "worker",
5656
+ task: "Implement the approved file changes",
5657
+ agentConfig: makeAgent("worker"),
5658
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
5659
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
5660
+ shareEnabled: false,
5661
+ sessionRoot: path.join(tempDir, "sessions"),
5662
+ output: outputPath,
5663
+ outputMode: "file-only",
5664
+ maxSubagentDepth: 2,
5665
+ });
5666
+
5667
+ const resultPath = await waitForAsyncResultFile(id);
5668
+ const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
5669
+ const child = payload.results[0];
5670
+ const diagnostic = child?.error ?? "";
5671
+ assert.equal(payload.success, false);
5672
+ assert.equal(child?.success, false);
5673
+ assert.match(diagnostic, /^Subagent produced no output after terminal assistant stopReason "aborted"\./);
5674
+ assert.match(diagnostic, /Required file-only output was not produced/);
5675
+ assert.doesNotMatch(diagnostic, /completed without making edits/);
5676
+ assert.doesNotMatch(child?.modelAttempts?.[0]?.error ?? "", /completed without making edits/);
5677
+ assert.equal(child?.effects?.settlementDiagnostic?.requiredOutput?.missing, true);
5678
+ assert.equal(child?.effects?.settlementDiagnostic?.finalTextPresent, true);
5679
+ assert.equal(fs.existsSync(outputPath), false);
5680
+
5681
+ const eventsText = fs.readFileSync(path.join(ASYNC_DIR, id, "events.jsonl"), "utf-8");
5682
+ assert.doesNotMatch(eventsText, /completed without making edits/);
5683
+ });
5684
+
5685
+ it("reports zero-exit terminal stderr before missing file-only output after settlement", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
5686
+ const outputPath = path.join(tempDir, "missing-stderr-report.md");
5687
+ const terminalStderr = "Extension error: stale ctx after session replacement";
5688
+ mockPi.onCall({
5689
+ jsonl: [
5690
+ events.toolStart("read", { path: "src/index.ts" }),
5691
+ events.toolEnd("read"),
5692
+ events.toolResult("read", "file contents"),
5693
+ events.assistantMessage("I found the candidate seam."),
5694
+ {
5695
+ type: "message_end",
5696
+ message: {
5697
+ role: "assistant",
5698
+ content: [],
5699
+ model: "mock/test-model",
5700
+ stopReason: "aborted",
5701
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0 } },
5702
+ },
5703
+ },
5704
+ { type: "agent_settled" },
5705
+ ],
5706
+ stderr: terminalStderr,
5707
+ });
5708
+
5709
+ const id = `async-stderr-empty-handoff-${Date.now().toString(36)}`;
5710
+ executeAsyncSingle(id, {
5711
+ agent: "worker",
5712
+ task: "Implement the approved file changes",
5713
+ agentConfig: makeAgent("worker"),
5714
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
5715
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
5716
+ shareEnabled: false,
5717
+ sessionRoot: path.join(tempDir, "sessions"),
5718
+ output: outputPath,
5719
+ outputMode: "file-only",
5720
+ maxSubagentDepth: 2,
5721
+ });
5722
+
5723
+ const resultPath = await waitForAsyncResultFile(id);
5724
+ const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
5725
+ const child = payload.results[0];
5726
+ const diagnostic = child?.error ?? "";
5727
+ assert.equal(payload.success, false);
5728
+ assert.equal(child?.success, false);
5729
+ assert.match(diagnostic, new RegExp(`^${escapeRegExp(terminalStderr)}`));
5730
+ assert.match(diagnostic, /Required file-only output was not produced/);
5731
+ assert.doesNotMatch(diagnostic, /^Subagent produced no output \(possible model cold-start or empty response\)\./);
5732
+ assert.equal(fs.existsSync(outputPath), false);
5733
+ });
5734
+
5735
+ it("reports bounded compaction failure context when file-only output is missing", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
5736
+ const terminalError = `This operation was aborted\n${"x".repeat(12_000)}`;
5737
+ mockPi.onCall({
5738
+ jsonl: [
5739
+ events.toolStart("read", { path: "src/index.ts" }),
5740
+ events.toolEnd("read"),
5741
+ events.toolResult("read", "file contents"),
5742
+ { type: "compaction_start" },
5743
+ {
5744
+ type: "message_end",
5745
+ message: {
5746
+ role: "assistant",
5747
+ content: [],
5748
+ model: "mock/test-model",
5749
+ stopReason: "error",
5750
+ errorMessage: terminalError,
5751
+ usage: { input: 100, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0.001 } },
5752
+ },
5753
+ },
5754
+ { type: "agent_settled" },
5755
+ ],
5756
+ exitCode: 0,
5757
+ });
5758
+
5759
+ const id = `async-compaction-file-only-error-${Date.now().toString(36)}`;
5760
+ const asyncDir = path.join(ASYNC_DIR, id);
5761
+ const outputPath = path.join(tempDir, "missing-oracle-report.md");
5762
+ executeAsyncSingle(id, {
5763
+ agent: "oracle",
5764
+ task: "Write a report",
5765
+ agentConfig: makeAgent("oracle"),
5766
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
5767
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
5768
+ shareEnabled: false,
5769
+ sessionRoot: path.join(tempDir, "sessions"),
5770
+ output: outputPath,
5771
+ outputMode: "file-only",
5772
+ maxSubagentDepth: 2,
5773
+ });
5774
+
5775
+ const resultPath = await waitForAsyncResultFile(id);
5776
+ const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
5777
+ const child = payload.results[0] as (AsyncResultPayload["results"][number] & Record<string, unknown>) | undefined;
5778
+ const diagnostic = child?.error ?? "";
5779
+ assert.equal(payload.success, false);
5780
+ assert.equal(child?.success, false);
5781
+ assert.match(diagnostic, /^This operation was aborted/);
5782
+ assert.match(diagnostic, /Compaction-induced child abort could not be resumed safely: retained session unavailable\./);
5783
+ assert.match(diagnostic, /failure followed session compaction and agent settlement/);
5784
+ assert.match(diagnostic, /Required file-only output was not produced/);
5785
+ assert.ok(diagnostic.length <= 8_192);
5786
+ assert.equal(fs.existsSync(outputPath), false);
5787
+ assert.equal(child?.output, "");
5788
+ assert.equal("savedOutputPath" in (child ?? {}), false);
5789
+ assert.equal("outputReference" in (child ?? {}), false);
5790
+ assert.equal(payload.summary, `oracle:\n${diagnostic}`);
5791
+ const status = await waitForAsyncState(id, (candidate) => candidate.state === "failed");
5792
+ assert.equal(status.steps?.[0]?.exitCode, 1);
5793
+ assert.equal(status.steps?.[0]?.error, diagnostic);
5794
+ const logPath = path.join(asyncDir, `subagent-log-${id}.md`);
5795
+ const deadline = Date.now() + 10_000;
5796
+ while (!fs.existsSync(logPath)) {
5797
+ if (Date.now() > deadline) assert.fail(`Timed out waiting for async run log: ${logPath}`);
5798
+ await new Promise((resolve) => setTimeout(resolve, 50));
5799
+ }
5800
+ assert.ok(fs.readFileSync(logPath, "utf-8").includes(`## Summary\noracle:\n${diagnostic}`));
5801
+ });
5802
+
5803
+ it("preserves partial imported async roots with mutation evidence", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
5804
+ const sourceId = `partial-source-${Date.now().toString(36)}`;
5805
+ const sourceDir = path.join(ASYNC_DIR, sourceId);
5806
+ const message = "Required file-only output was not produced: report.md";
5807
+ const effects = { fileMutation: { status: "observed", expected: true, attempted: true, evidence: { source: "tracked-files", trackedOnly: true, cwd: tempDir, changedFiles: ["input.md"], attemptedMutation: true } } };
5808
+ fs.mkdirSync(sourceDir, { recursive: true });
5809
+ fs.writeFileSync(path.join(sourceDir, "status.json"), JSON.stringify({
5810
+ runId: sourceId,
5811
+ mode: "single",
5812
+ state: "partial",
5813
+ activityState: "needs_attention",
5814
+ startedAt: Date.now(),
5815
+ error: message,
5816
+ steps: [{ agent: "worker", status: "failed", activityState: "needs_attention", error: message, effects }],
5817
+ }), "utf-8");
5818
+
5819
+ const id = `async-imported-partial-${Date.now().toString(36)}`;
5820
+ executeAsyncChain(id, {
5821
+ chain: [],
5822
+ attachRoot: { runId: sourceId, asyncDir: sourceDir, resultPath: path.join(RESULTS_DIR, `${sourceId}.json`), index: 0, agent: "worker" },
5823
+ agents: [makeAgent("worker")],
5824
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
5825
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
5826
+ shareEnabled: false,
5827
+ sessionRoot: path.join(tempDir, "sessions"),
5828
+ maxSubagentDepth: 2,
5829
+ });
5830
+
5831
+ const resultPath = await waitForAsyncResultFile(id);
5832
+ const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
5833
+ assert.equal(payload.success, false);
5834
+ assert.equal(payload.state, "partial");
5835
+ assert.equal(payload.summary, message);
5836
+ assert.equal(payload.results[0]?.execution?.status, "partial");
5837
+ assert.deepEqual(payload.results[0]?.effects?.fileMutation?.evidence?.changedFiles, ["input.md"]);
5838
+
5839
+ const status = await waitForAsyncState(id, (candidate) => candidate.state === "partial");
5840
+ assert.equal(status.activityState, "needs_attention");
5841
+ assert.equal(status.steps?.[0]?.activityState, "needs_attention");
5842
+ });
5843
+
5844
+ it("keeps concrete sibling failures above partial mutation evidence", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
5845
+ const repo = createRepo("pi-subagents-partial-sibling-failure-");
5846
+ const outputPath = path.join(repo, "missing-report.md");
5847
+ mockPi.onCall({
5848
+ matchArgIncludes: "Write required report",
5849
+ jsonl: [
5850
+ events.assistantMessage("I changed the file but did not hand off the report."),
5851
+ {
5852
+ type: "message_end",
5853
+ message: {
5854
+ role: "assistant",
5855
+ content: [],
5856
+ model: "mock/test-model",
5857
+ stopReason: "aborted",
5858
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0 } },
5859
+ },
5860
+ },
5861
+ ],
5862
+ writeFiles: [{ path: "input.md", content: "changed before missing report\n" }],
5863
+ });
5864
+ mockPi.onCall({ matchArgIncludes: "Fail normally", stderr: "ordinary sibling failure", exitCode: 1 });
5865
+
5866
+ const id = `async-partial-sibling-failure-${Date.now().toString(36)}`;
5867
+ try {
5868
+ executeAsyncChain(id, {
5869
+ chain: [{
5870
+ parallel: [
5871
+ { agent: "partial", task: "Write required report" },
5872
+ { agent: "failure", task: "Fail normally" },
5873
+ ],
5874
+ concurrency: 2,
5875
+ }],
5876
+ resultMode: "parallel",
5877
+ agents: [makeAgent("partial", { output: outputPath, outputMode: "file-only" }), makeAgent("failure", { completionGuard: false })],
5878
+ ctx: { pi: { events: { emit() {} } }, cwd: repo, currentSessionId: "session-1" },
5879
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
5880
+ shareEnabled: false,
5881
+ maxSubagentDepth: 2,
5882
+ });
5883
+
5884
+ const payload = await readAsyncPayload(id);
5885
+ assert.equal(payload.success, false);
5886
+ assert.equal(payload.state, "failed");
5887
+ assert.match(payload.summary, /ordinary sibling failure/);
5888
+ assert.equal(payload.results[0]?.effects?.fileMutation?.status, "observed");
5889
+ assert.equal(payload.results[0]?.effects?.settlementDiagnostic?.requiredOutput?.missing, true);
5890
+ assert.match(payload.results[1]?.error ?? "", /ordinary sibling failure/);
5891
+
5892
+ const status = await waitForAsyncState(id, (candidate) => candidate.state === "failed");
5893
+ assert.equal(status.activityState, undefined);
5894
+ assert.match(status.error ?? "", /ordinary sibling failure/);
5895
+ } finally {
5896
+ removeTempDir(repo);
5897
+ }
5898
+ });
5899
+
5900
+ it("reports missing file-only output when a child exits without an error", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
5901
+ mockPi.onCall({ delay: 2_100, exitCode: 1 });
5902
+
5903
+ const id = `async-file-only-exit-without-error-${Date.now().toString(36)}`;
5904
+ const outputPath = path.join(tempDir, "missing-exit-report.md");
5905
+ executeAsyncSingle(id, {
5906
+ agent: "worker",
5907
+ task: "Write a report",
5908
+ agentConfig: makeAgent("worker", { completionGuard: false }),
5909
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
5910
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
5911
+ shareEnabled: false,
5912
+ sessionRoot: path.join(tempDir, "sessions"),
5913
+ output: outputPath,
5914
+ outputMode: "file-only",
5915
+ maxSubagentDepth: 2,
5916
+ });
5917
+
5918
+ const payload = await readAsyncPayload(id);
5919
+ const child = payload.results[0];
5920
+ const diagnostic = child?.error ?? "";
5921
+ assert.equal(payload.success, false);
5922
+ assert.equal(payload.exitCode, 1);
5923
+ assert.equal(child?.success, false);
5924
+ assert.match(diagnostic, /^Required file-only output was not produced:/);
5925
+ assert.equal(fs.existsSync(outputPath), false);
5926
+ assert.equal(child?.output, "");
5927
+ assert.equal(child?.effects?.settlementDiagnostic?.requiredOutput?.kind, "file-only");
5928
+ assert.equal(child?.effects?.settlementDiagnostic?.requiredOutput?.path, outputPath);
5929
+ assert.equal(child?.effects?.settlementDiagnostic?.requiredOutput?.missing, true);
5930
+ const status = await waitForAsyncState(id, (candidate) => candidate.state === "failed");
5931
+ assert.equal(status.steps?.[0]?.error, diagnostic);
5932
+ });
5933
+
5080
5934
  it("background runs emit active-long-running control events from child turns", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
5081
5935
  mockPi.onCall({
5082
5936
  steps: [