@oh-my-pi/pi-coding-agent 17.0.8 → 17.1.0

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 (340) hide show
  1. package/CHANGELOG.md +87 -1
  2. package/README.md +1 -1
  3. package/dist/cli.js +5495 -5021
  4. package/dist/types/async/job-manager.d.ts +37 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/bench-cli.d.ts +41 -0
  7. package/dist/types/commands/bench.d.ts +15 -3
  8. package/dist/types/commands/launch.d.ts +4 -0
  9. package/dist/types/config/__tests__/model-registry.test.d.ts +1 -0
  10. package/dist/types/config/model-registry.d.ts +30 -1
  11. package/dist/types/config/models-config-schema.d.ts +18 -6
  12. package/dist/types/config/models-config.d.ts +14 -5
  13. package/dist/types/config/provider-globals.d.ts +2 -2
  14. package/dist/types/config/service-tier.d.ts +5 -1
  15. package/dist/types/config/settings-schema.d.ts +350 -50
  16. package/dist/types/config/settings.d.ts +7 -0
  17. package/dist/types/dap/client.d.ts +20 -0
  18. package/dist/types/exec/bash-executor.d.ts +38 -0
  19. package/dist/types/exec/direnv.d.ts +33 -0
  20. package/dist/types/export/html/args.d.ts +15 -0
  21. package/dist/types/export/html/index.d.ts +11 -26
  22. package/dist/types/export/html/web-palette.d.ts +55 -116
  23. package/dist/types/extensibility/extensions/loader.d.ts +3 -0
  24. package/dist/types/extensibility/extensions/types.d.ts +17 -2
  25. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -0
  26. package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +10 -0
  27. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +14 -1
  28. package/dist/types/internal-urls/registry-helpers.d.ts +11 -0
  29. package/dist/types/live/controller.d.ts +40 -0
  30. package/dist/types/live/protocol.d.ts +85 -0
  31. package/dist/types/live/protocol.test.d.ts +1 -0
  32. package/dist/types/live/transport.d.ts +48 -0
  33. package/dist/types/live/visualizer.d.ts +33 -0
  34. package/dist/types/lsp/client.d.ts +18 -1
  35. package/dist/types/mcp/config-writer.test.d.ts +1 -0
  36. package/dist/types/mcp/manager.d.ts +6 -2
  37. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  38. package/dist/types/mcp/render.d.ts +2 -2
  39. package/dist/types/mcp/smithery-auth.d.ts +1 -1
  40. package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
  41. package/dist/types/mcp/tool-bridge.d.ts +7 -3
  42. package/dist/types/mcp/types.d.ts +6 -0
  43. package/dist/types/memory-backend/tool-names.d.ts +2 -0
  44. package/dist/types/modes/components/login-dialog.d.ts +2 -2
  45. package/dist/types/modes/components/oauth-selector.d.ts +8 -0
  46. package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
  47. package/dist/types/modes/components/settings-defs.d.ts +8 -1
  48. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
  49. package/dist/types/modes/components/tool-execution.d.ts +9 -1
  50. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  51. package/dist/types/modes/controllers/event-controller.d.ts +6 -1
  52. package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
  53. package/dist/types/modes/controllers/mcp-command-controller.d.ts +3 -0
  54. package/dist/types/modes/interactive-mode.d.ts +5 -0
  55. package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
  56. package/dist/types/modes/rpc/rpc-frame.d.ts +16 -0
  57. package/dist/types/modes/rpc/rpc-messages.d.ts +26 -0
  58. package/dist/types/modes/rpc/rpc-types.d.ts +40 -0
  59. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
  60. package/dist/types/modes/setup-wizard/scenes/types.d.ts +9 -1
  61. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
  62. package/dist/types/modes/theme/theme.d.ts +1 -1
  63. package/dist/types/modes/types.d.ts +3 -1
  64. package/dist/types/modes/utils/context-usage.d.ts +12 -2
  65. package/dist/types/plan-mode/plan-files.d.ts +10 -0
  66. package/dist/types/registry/agent-lifecycle.d.ts +21 -9
  67. package/dist/types/registry/agent-registry.d.ts +11 -4
  68. package/dist/types/sdk.d.ts +15 -1
  69. package/dist/types/secrets/index.d.ts +15 -2
  70. package/dist/types/secrets/obfuscator.d.ts +113 -15
  71. package/dist/types/session/acp-permission-gate.d.ts +19 -0
  72. package/dist/types/session/agent-session-events.d.ts +84 -0
  73. package/dist/types/session/agent-session-types.d.ts +314 -0
  74. package/dist/types/session/agent-session.d.ts +120 -750
  75. package/dist/types/session/async-job-delivery.d.ts +28 -0
  76. package/dist/types/session/bash-runner.d.ts +75 -0
  77. package/dist/types/session/blob-store.d.ts +11 -1
  78. package/dist/types/session/blob-store.test.d.ts +1 -0
  79. package/dist/types/session/checkpoint-entries.d.ts +28 -0
  80. package/dist/types/session/eval-runner.d.ts +49 -0
  81. package/dist/types/session/irc-bridge.d.ts +46 -0
  82. package/dist/types/session/messages.d.ts +22 -0
  83. package/dist/types/session/model-controls.d.ts +180 -0
  84. package/dist/types/session/prewalk.d.ts +46 -0
  85. package/dist/types/session/queued-messages.d.ts +22 -0
  86. package/dist/types/session/redis-session-storage.d.ts +1 -0
  87. package/dist/types/session/retry-fallback-chains.d.ts +74 -0
  88. package/dist/types/session/role-models.d.ts +15 -0
  89. package/dist/types/session/session-advisors.d.ts +228 -0
  90. package/dist/types/session/session-entries.d.ts +8 -0
  91. package/dist/types/session/session-handoff.d.ts +70 -0
  92. package/dist/types/session/session-history-format.d.ts +6 -1
  93. package/dist/types/session/session-loader.d.ts +6 -4
  94. package/dist/types/session/session-maintenance.d.ts +246 -0
  95. package/dist/types/session/session-manager.d.ts +41 -0
  96. package/dist/types/session/session-memory.d.ts +56 -0
  97. package/dist/types/session/session-provider-boundary.d.ts +62 -0
  98. package/dist/types/session/session-stats.d.ts +48 -0
  99. package/dist/types/session/session-tools.d.ts +143 -0
  100. package/dist/types/session/session-workspace.d.ts +32 -0
  101. package/dist/types/session/stream-guards.d.ts +44 -0
  102. package/dist/types/session/todo-tracker.d.ts +56 -0
  103. package/dist/types/session/ttsr-coordinator.d.ts +51 -0
  104. package/dist/types/session/turn-recovery.d.ts +225 -0
  105. package/dist/types/system-prompt.d.ts +2 -0
  106. package/dist/types/task/executor.d.ts +4 -1
  107. package/dist/types/task/output-manager.d.ts +3 -1
  108. package/dist/types/task/types.d.ts +14 -0
  109. package/dist/types/tools/approval.d.ts +1 -0
  110. package/dist/types/tools/browser/launch.d.ts +5 -0
  111. package/dist/types/tools/hub/jobs.d.ts +1 -1
  112. package/dist/types/tools/image-gen.d.ts +4 -4
  113. package/dist/types/tools/image-providers.d.ts +38 -0
  114. package/dist/types/tools/index.d.ts +8 -0
  115. package/dist/types/tools/path-utils.d.ts +8 -0
  116. package/dist/types/tools/report-tool-issue.d.ts +24 -9
  117. package/dist/types/tools/todo.d.ts +10 -4
  118. package/dist/types/tools/xdev.d.ts +5 -1
  119. package/dist/types/tts/streaming-player.d.ts +14 -0
  120. package/dist/types/tts/vocalizer.d.ts +5 -0
  121. package/dist/types/utils/jj.d.ts +29 -0
  122. package/dist/types/utils/lang-from-path.d.ts +1 -0
  123. package/dist/types/utils/title-generator.d.ts +30 -0
  124. package/dist/types/vibe/runtime.d.ts +36 -8
  125. package/dist/types/web/search/index.d.ts +1 -1
  126. package/dist/types/web/search/provider.d.ts +15 -5
  127. package/dist/types/web/search/providers/firecrawl.d.ts +9 -0
  128. package/dist/types/web/search/types.d.ts +95 -1
  129. package/package.json +13 -12
  130. package/scripts/generate-share-viewer.ts +4 -6
  131. package/scripts/legacy-pi-virtual-module.ts +1 -1
  132. package/src/advisor/__tests__/advisor.test.ts +643 -0
  133. package/src/advisor/runtime.ts +144 -43
  134. package/src/async/job-manager.ts +90 -2
  135. package/src/cli/args.ts +2 -0
  136. package/src/cli/auth-gateway-cli.ts +18 -3
  137. package/src/cli/bench-cli.ts +395 -29
  138. package/src/cli/flag-tables.ts +3 -0
  139. package/src/cli/gc-cli.ts +2 -2
  140. package/src/cli/grievances-cli.ts +2 -2
  141. package/src/cli/usage-cli.ts +11 -2
  142. package/src/collab/host.ts +3 -2
  143. package/src/commands/bench.ts +18 -4
  144. package/src/commands/launch.ts +4 -0
  145. package/src/config/__tests__/model-registry.test.ts +147 -0
  146. package/src/config/model-registry.ts +50 -2
  147. package/src/config/model-resolver.ts +0 -1
  148. package/src/config/models-config-schema.ts +15 -4
  149. package/src/config/provider-globals.ts +9 -9
  150. package/src/config/service-tier.ts +26 -1
  151. package/src/config/settings-schema.ts +250 -54
  152. package/src/config/settings.ts +81 -2
  153. package/src/dap/client.ts +37 -4
  154. package/src/edit/index.ts +49 -12
  155. package/src/eval/py/executor.ts +103 -33
  156. package/src/exec/bash-executor.ts +89 -6
  157. package/src/exec/direnv.ts +145 -0
  158. package/src/export/html/args.ts +20 -0
  159. package/src/export/html/index.ts +45 -47
  160. package/src/export/html/template.css +19 -1
  161. package/src/export/html/template.html +6 -0
  162. package/src/export/html/template.js +21 -0
  163. package/src/export/html/tool-views.generated.js +31 -31
  164. package/src/export/html/web-palette.ts +116 -132
  165. package/src/export/share.ts +249 -49
  166. package/src/extensibility/extensions/loader.ts +29 -1
  167. package/src/extensibility/extensions/runner.ts +6 -0
  168. package/src/extensibility/extensions/types.ts +33 -2
  169. package/src/extensibility/legacy-pi-coding-agent-shim.ts +1 -0
  170. package/src/extensibility/legacy-pi-tui-shim.ts +10 -0
  171. package/src/extensibility/plugins/legacy-pi-compat.ts +870 -311
  172. package/src/hindsight/state.ts +64 -6
  173. package/src/internal-urls/local-protocol.ts +2 -1
  174. package/src/internal-urls/memory-protocol.ts +2 -2
  175. package/src/internal-urls/registry-helpers.ts +40 -0
  176. package/src/internal-urls/skill-protocol.ts +2 -2
  177. package/src/internal-urls/ssh-protocol.ts +2 -1
  178. package/src/internal-urls/vault-protocol.ts +2 -1
  179. package/src/live/audio-worklet.txt +59 -0
  180. package/src/live/browser-runtime.txt +221 -0
  181. package/src/live/controller.ts +464 -0
  182. package/src/live/prompts/agent-final-message.md +3 -0
  183. package/src/live/prompts/live-instructions.md +23 -0
  184. package/src/live/protocol.test.ts +140 -0
  185. package/src/live/protocol.ts +233 -0
  186. package/src/live/transport.ts +490 -0
  187. package/src/live/visualizer.ts +234 -0
  188. package/src/lsp/client.ts +52 -9
  189. package/src/lsp/clients/biome-client.ts +3 -4
  190. package/src/lsp/index.ts +44 -11
  191. package/src/main.ts +9 -1
  192. package/src/mcp/config-writer.test.ts +43 -0
  193. package/src/mcp/config-writer.ts +109 -82
  194. package/src/mcp/manager.ts +39 -8
  195. package/src/mcp/oauth-discovery.ts +10 -2
  196. package/src/mcp/render.ts +94 -35
  197. package/src/mcp/smithery-auth.test.ts +29 -0
  198. package/src/mcp/smithery-auth.ts +3 -2
  199. package/src/mcp/tool-bridge.ts +107 -11
  200. package/src/mcp/types.ts +7 -0
  201. package/src/memories/index.ts +40 -20
  202. package/src/memory-backend/tool-names.ts +2 -0
  203. package/src/mnemopi/backend.ts +24 -7
  204. package/src/modes/acp/acp-agent.ts +56 -10
  205. package/src/modes/acp/acp-event-mapper.ts +8 -1
  206. package/src/modes/components/agent-hub.ts +27 -10
  207. package/src/modes/components/login-dialog.ts +14 -4
  208. package/src/modes/components/oauth-selector.ts +24 -7
  209. package/src/modes/components/plan-review-overlay.ts +350 -35
  210. package/src/modes/components/settings-defs.ts +28 -3
  211. package/src/modes/components/settings-selector.ts +178 -6
  212. package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
  213. package/src/modes/components/status-line/component.ts +123 -2
  214. package/src/modes/components/tool-execution.test.ts +63 -2
  215. package/src/modes/components/tool-execution.ts +11 -2
  216. package/src/modes/controllers/command-controller.ts +70 -22
  217. package/src/modes/controllers/event-controller.ts +111 -16
  218. package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
  219. package/src/modes/controllers/extension-ui-controller.ts +20 -4
  220. package/src/modes/controllers/live-command-controller.ts +178 -0
  221. package/src/modes/controllers/mcp-command-controller.ts +80 -41
  222. package/src/modes/controllers/omfg-controller.ts +44 -40
  223. package/src/modes/controllers/selector-controller.ts +18 -10
  224. package/src/modes/controllers/session-focus-controller.ts +6 -1
  225. package/src/modes/interactive-mode.ts +160 -38
  226. package/src/modes/rpc/rpc-client.ts +94 -3
  227. package/src/modes/rpc/rpc-frame.ts +164 -4
  228. package/src/modes/rpc/rpc-messages.ts +127 -0
  229. package/src/modes/rpc/rpc-mode.ts +79 -7
  230. package/src/modes/rpc/rpc-types.ts +34 -2
  231. package/src/modes/runtime-init.ts +2 -0
  232. package/src/modes/setup-wizard/scenes/model.ts +5 -7
  233. package/src/modes/setup-wizard/scenes/providers.ts +3 -2
  234. package/src/modes/setup-wizard/scenes/sign-in.ts +8 -2
  235. package/src/modes/setup-wizard/scenes/theme.ts +13 -3
  236. package/src/modes/setup-wizard/scenes/types.ts +9 -1
  237. package/src/modes/setup-wizard/scenes/web-search.ts +23 -10
  238. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  239. package/src/modes/theme/theme.ts +1 -1
  240. package/src/modes/types.ts +3 -1
  241. package/src/modes/utils/context-usage.ts +23 -7
  242. package/src/modes/utils/ui-helpers.ts +10 -5
  243. package/src/plan-mode/plan-files.ts +40 -0
  244. package/src/prompts/bench/cache-prefix-chunk.md +1 -0
  245. package/src/prompts/bench/cache-prefix.md +3 -0
  246. package/src/prompts/bench/cache-suffix.md +1 -0
  247. package/src/prompts/goals/guided-goal-system.md +24 -3
  248. package/src/prompts/system/custom-system-prompt.md +1 -1
  249. package/src/prompts/system/project-prompt.md +8 -1
  250. package/src/prompts/system/subagent-async-pending.md +6 -0
  251. package/src/prompts/system/system-prompt.md +1 -1
  252. package/src/prompts/system/workflow-notice.md +48 -40
  253. package/src/prompts/system/xdev-mount-notice.md +4 -0
  254. package/src/prompts/tools/task.md +12 -2
  255. package/src/prompts/tools/todo.md +3 -1
  256. package/src/registry/agent-lifecycle.ts +116 -46
  257. package/src/registry/agent-registry.ts +35 -8
  258. package/src/registry/persisted-agents.ts +26 -2
  259. package/src/sdk.ts +255 -139
  260. package/src/secrets/index.ts +127 -4
  261. package/src/secrets/obfuscator.ts +2328 -105
  262. package/src/session/acp-permission-gate.ts +165 -0
  263. package/src/session/agent-session-error-log.test.ts +1 -1
  264. package/src/session/agent-session-events.ts +66 -0
  265. package/src/session/agent-session-types.ts +334 -0
  266. package/src/session/agent-session.ts +1910 -12213
  267. package/src/session/artifacts.ts +6 -5
  268. package/src/session/async-job-delivery.ts +74 -0
  269. package/src/session/bash-runner.ts +326 -0
  270. package/src/session/blob-store.test.ts +56 -0
  271. package/src/session/blob-store.ts +18 -2
  272. package/src/session/checkpoint-entries.ts +81 -0
  273. package/src/session/eval-runner.ts +212 -0
  274. package/src/session/indexed-session-storage.ts +9 -2
  275. package/src/session/irc-bridge.ts +203 -0
  276. package/src/session/messages.ts +263 -1
  277. package/src/session/model-controls.ts +714 -0
  278. package/src/session/prewalk.ts +252 -0
  279. package/src/session/queued-messages.ts +93 -0
  280. package/src/session/redis-session-storage.ts +51 -11
  281. package/src/session/retry-fallback-chains.ts +455 -0
  282. package/src/session/role-models.ts +85 -0
  283. package/src/session/session-advisors.ts +1679 -0
  284. package/src/session/session-context.test.ts +224 -1
  285. package/src/session/session-context.ts +47 -5
  286. package/src/session/session-entries.ts +8 -0
  287. package/src/session/session-handoff.ts +308 -0
  288. package/src/session/session-history-format.ts +20 -9
  289. package/src/session/session-loader.ts +10 -51
  290. package/src/session/session-maintenance.ts +2983 -0
  291. package/src/session/session-manager.ts +390 -30
  292. package/src/session/session-memory.ts +222 -0
  293. package/src/session/session-provider-boundary.ts +307 -0
  294. package/src/session/session-stats.ts +293 -0
  295. package/src/session/session-tools.ts +943 -0
  296. package/src/session/session-workspace.ts +53 -0
  297. package/src/session/stream-guards.ts +417 -0
  298. package/src/session/todo-tracker.ts +380 -0
  299. package/src/session/ttsr-coordinator.ts +496 -0
  300. package/src/session/turn-recovery.ts +1629 -0
  301. package/src/slash-commands/builtin-registry.ts +92 -11
  302. package/src/slash-commands/helpers/usage-report.ts +25 -4
  303. package/src/system-prompt.ts +41 -17
  304. package/src/task/executor.ts +288 -70
  305. package/src/task/index.ts +148 -111
  306. package/src/task/output-manager.ts +25 -3
  307. package/src/task/persisted-revive.ts +4 -3
  308. package/src/task/structured-subagent.ts +4 -1
  309. package/src/task/types.ts +16 -0
  310. package/src/tools/approval.ts +55 -10
  311. package/src/tools/bash-interactive.ts +26 -0
  312. package/src/tools/bash-skill-urls.ts +28 -2
  313. package/src/tools/bash.ts +339 -123
  314. package/src/tools/browser/launch.ts +9 -1
  315. package/src/tools/browser/tab-supervisor.ts +8 -5
  316. package/src/tools/browser.ts +94 -47
  317. package/src/tools/hub/index.ts +1 -1
  318. package/src/tools/hub/jobs.ts +67 -8
  319. package/src/tools/image-gen.ts +20 -28
  320. package/src/tools/image-providers.ts +50 -0
  321. package/src/tools/index.ts +12 -0
  322. package/src/tools/path-utils.ts +49 -2
  323. package/src/tools/read.ts +156 -88
  324. package/src/tools/renderers.ts +7 -1
  325. package/src/tools/report-tool-issue.ts +79 -28
  326. package/src/tools/todo.ts +101 -11
  327. package/src/tools/vibe.ts +1 -2
  328. package/src/tools/write.ts +51 -1
  329. package/src/tools/xdev.ts +67 -3
  330. package/src/tts/streaming-player.ts +62 -4
  331. package/src/tts/vocalizer.ts +18 -1
  332. package/src/utils/jj.ts +125 -4
  333. package/src/utils/lang-from-path.ts +7 -0
  334. package/src/utils/title-generator.ts +137 -1
  335. package/src/vibe/runtime.ts +852 -72
  336. package/src/web/search/index.ts +3 -6
  337. package/src/web/search/provider.ts +31 -16
  338. package/src/web/search/providers/firecrawl.ts +46 -13
  339. package/src/web/search/providers/xai.ts +7 -1
  340. package/src/web/search/types.ts +8 -1
@@ -5,10 +5,11 @@
5
5
  */
6
6
 
7
7
  import path from "node:path";
8
- import type { AgentEvent, AgentIdentity, AgentTelemetryConfig } from "@oh-my-pi/pi-agent-core";
8
+ import type { AgentEvent, AgentIdentity, AgentMessage, AgentTelemetryConfig } from "@oh-my-pi/pi-agent-core";
9
9
  import { recordHandoff, resolveTelemetry } from "@oh-my-pi/pi-agent-core";
10
10
  import type { Api, Model, ServiceTierByFamily, Usage } from "@oh-my-pi/pi-ai";
11
11
  import { logger, popLoopPhase, prompt, pushLoopPhase, untilAborted } from "@oh-my-pi/pi-utils";
12
+ import { AsyncJobManager } from "../async";
12
13
  import type { Rule } from "../capability/rule";
13
14
  import { ModelRegistry } from "../config/model-registry";
14
15
  import {
@@ -32,13 +33,15 @@ import type { HindsightSessionState } from "../hindsight/state";
32
33
  import type { LocalProtocolOptions } from "../internal-urls";
33
34
  import type { MCPManager } from "../mcp/manager";
34
35
  import type { MnemopiSessionState } from "../mnemopi/state";
36
+ import subagentAsyncPendingTemplate from "../prompts/system/subagent-async-pending.md" with { type: "text" };
35
37
  import subagentSystemPromptTemplate from "../prompts/system/subagent-system-prompt.md" with { type: "text" };
36
38
  import submitReminderTemplate from "../prompts/system/subagent-yield-reminder.md" with { type: "text" };
37
- import { AgentLifecycleManager } from "../registry/agent-lifecycle";
39
+ import { AgentLifecycleManager, type AgentReviver } from "../registry/agent-lifecycle";
38
40
  import { AgentRegistry } from "../registry/agent-registry";
39
41
  import { type CreateAgentSessionOptions, createAgentSession, discoverAuthStorage } from "../sdk";
40
42
  import type { AgentSession, AgentSessionEvent, Prewalk } from "../session/agent-session";
41
43
  import type { ArtifactManager } from "../session/artifacts";
44
+ import { ASYNC_RESULT_MESSAGE_TYPE } from "../session/async-job-delivery";
42
45
  import type { AuthStorage } from "../session/auth-storage";
43
46
  import { SKILL_PROMPT_MESSAGE_TYPE, USER_INTERRUPT_LABEL } from "../session/messages";
44
47
  import { SessionManager } from "../session/session-manager";
@@ -287,6 +290,8 @@ function isRecord(value: unknown): value is Record<string, unknown> {
287
290
  /** Options for subagent execution */
288
291
  export interface ExecutorOptions {
289
292
  cwd: string;
293
+ /** Additional workspace directories to seed on the subagent session (multi-root). */
294
+ additionalDirectories?: string[];
290
295
  worktree?: string;
291
296
  agent: AgentDefinition;
292
297
  task: string;
@@ -823,8 +828,11 @@ export function createSubagentSettings(
823
828
  snapshot["tier.google"] = subagentTiers.google ?? "none";
824
829
  return Settings.isolated({
825
830
  ...snapshot,
826
- "async.enabled": false,
827
- "bash.autoBackground.enabled": false,
831
+ // Async jobs and bash auto-backgrounding are inherited from the parent:
832
+ // background jobs are owner-routed to the subagent's own session, and
833
+ // the run driver's quiescence barrier + teardown reap guarantee no
834
+ // owner job outlives the run, so worktree capture/cleanup stays
835
+ // race-free (previously both were force-disabled here).
828
836
 
829
837
  // Subagents run headless — there is no UI to confirm prompts against, so
830
838
  // the parent task approval is the authorization boundary. Use yolo mode
@@ -882,6 +890,20 @@ interface SubagentRunMonitor {
882
890
  budgetStopRequested(): boolean;
883
891
  /** Resolves when the budget-stop session abort has settled (immediately when no stop fired). */
884
892
  waitForBudgetStop(): Promise<void>;
893
+ /**
894
+ * True when a recorded yield was invalidated by a later async-result
895
+ * injection and no fresh yield has landed since: the yield payload
896
+ * predates background job outcomes the model was shown.
897
+ */
898
+ yieldInvalidatedByAsync(): boolean;
899
+ /**
900
+ * True once a terminal yield with pending owner async work stopped the
901
+ * free-running turn (recoverable, like a budget stop) instead of
902
+ * terminating the run. Cleared when {@link waitForYieldTurnStop} settles.
903
+ */
904
+ yieldTurnStopRequested(): boolean;
905
+ /** Resolves when the yield turn-stop session abort has settled (immediately when none fired). */
906
+ waitForYieldTurnStop(): Promise<void>;
885
907
  /** The abort kind for this run, when an abort was requested. */
886
908
  abortKind(): AbortReason | undefined;
887
909
  terminalError(): string | undefined;
@@ -910,6 +932,15 @@ interface SubagentRunMonitor {
910
932
  finish(): void;
911
933
  }
912
934
 
935
+ /**
936
+ * True when `message` is the session-injected async-result follow-up
937
+ * ({@link ASYNC_RESULT_MESSAGE_TYPE}): the transcript-ordered signal that a
938
+ * background job outcome landed after whatever the model said before it.
939
+ */
940
+ function isAsyncResultInjection(message: AgentMessage | undefined): boolean {
941
+ return message?.role === "custom" && message.customType === ASYNC_RESULT_MESSAGE_TYPE;
942
+ }
943
+
913
944
  function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
914
945
  const {
915
946
  index,
@@ -961,6 +992,9 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
961
992
  let activeSession: AgentSession | null = null;
962
993
  let yieldCalled = false;
963
994
  let yieldCallPending = false;
995
+ let yieldInvalidatedByAsync = false;
996
+ let yieldTurnStopRequested = false;
997
+ let yieldTurnStopPromise: Promise<void> | null = null;
964
998
 
965
999
  // Accumulate usage incrementally from message_end events (no memory for streaming events)
966
1000
  const accumulatedUsage: Usage = {
@@ -1039,6 +1073,29 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
1039
1073
  terminalError ??= message;
1040
1074
  requestAbort("terminate");
1041
1075
  };
1076
+ // Yield turn-stop: a terminal yield recorded while owner async work is
1077
+ // still pending is a scheduling pause, not run completion. Stop the
1078
+ // free-running turn exactly like a budget stop (session abort, monitor
1079
+ // signal untouched) so driveSessionToYield's quiescence barrier can settle
1080
+ // the jobs, fold their results in, and demand a fresh yield. Terminating
1081
+ // here instead would abort the run signal and make the barrier
1082
+ // unreachable, completing the run with a payload that predates the job
1083
+ // outcomes.
1084
+ const requestYieldTurnStop = () => {
1085
+ if (yieldTurnStopRequested || abortSent || resolved) return;
1086
+ yieldTurnStopRequested = true;
1087
+ const session = activeSession;
1088
+ yieldTurnStopPromise = session
1089
+ ? session.abort().catch(error => {
1090
+ logger.debug("Subagent yield turn-stop abort failed", {
1091
+ error: error instanceof Error ? error.message : String(error),
1092
+ });
1093
+ })
1094
+ : Promise.resolve();
1095
+ };
1096
+
1097
+ /** Owner async work that can still re-wake the run (quiescence barrier predicate). */
1098
+ const sessionHasPendingAsyncWork = (): boolean => activeSession?.hasPendingAsyncWork?.() ?? false;
1042
1099
 
1043
1100
  // Handle abort signal
1044
1101
  if (signal) {
@@ -1243,6 +1300,7 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
1243
1300
  if (toolName === "yield") {
1244
1301
  yieldCalled = true;
1245
1302
  yieldCallPending = false;
1303
+ yieldInvalidatedByAsync = false;
1246
1304
  }
1247
1305
  };
1248
1306
 
@@ -1256,6 +1314,16 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
1256
1314
  if (event.message?.role === "assistant") {
1257
1315
  resetRecentOutput();
1258
1316
  }
1317
+ // An async-result follow-up injected after a recorded yield
1318
+ // supersedes that yield: its payload predates the job outcome the
1319
+ // model is now being shown. Un-latch so the quiescence barrier's
1320
+ // reminder ladder demands a fresh yield. Guarded on the run signal:
1321
+ // once the run is completing, late injections must not destabilize
1322
+ // the settled classification.
1323
+ if (yieldCalled && !abortSignal.aborted && isAsyncResultInjection(event.message)) {
1324
+ yieldCalled = false;
1325
+ yieldInvalidatedByAsync = true;
1326
+ }
1259
1327
  break;
1260
1328
 
1261
1329
  case "tool_execution_start": {
@@ -1339,7 +1407,14 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
1339
1407
  isError: event.isError,
1340
1408
  })
1341
1409
  ) {
1342
- requestAbort("terminate");
1410
+ if (event.toolName === "yield" && sessionHasPendingAsyncWork()) {
1411
+ // Terminal yield with owner jobs still pending: park the
1412
+ // run behind the quiescence barrier instead of completing
1413
+ // it (see requestYieldTurnStop).
1414
+ requestYieldTurnStop();
1415
+ } else {
1416
+ requestAbort("terminate");
1417
+ }
1343
1418
  }
1344
1419
  }
1345
1420
  if (event.toolName === "yield") {
@@ -1578,11 +1653,13 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
1578
1653
  }
1579
1654
  if (event.type === "retry_fallback_applied") {
1580
1655
  progress.resolvedModel = event.to;
1656
+ progress.resolvedModelIsFallback = true;
1581
1657
  scheduleProgress(true);
1582
1658
  return;
1583
1659
  }
1584
1660
  if (event.type === "retry_fallback_succeeded") {
1585
1661
  progress.resolvedModel = event.model;
1662
+ progress.resolvedModelIsFallback = true;
1586
1663
  scheduleProgress(true);
1587
1664
  return;
1588
1665
  }
@@ -1621,6 +1698,25 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
1621
1698
  abortReason === "signal" || runtimeLimitExceeded || budgetLimitExceeded || budgetStopRequested,
1622
1699
  budgetStopRequested: () => budgetStopRequested,
1623
1700
  waitForBudgetStop: () => budgetStopAbortPromise ?? Promise.resolve(),
1701
+ yieldInvalidatedByAsync: () => yieldInvalidatedByAsync,
1702
+ yieldTurnStopRequested: () => yieldTurnStopRequested,
1703
+ waitForYieldTurnStop: async () => {
1704
+ const pending = yieldTurnStopPromise;
1705
+ if (!pending) {
1706
+ yieldTurnStopRequested = false;
1707
+ return;
1708
+ }
1709
+ try {
1710
+ await pending;
1711
+ } finally {
1712
+ // Clear only after the abort settled so the idempotence gate in
1713
+ // requestYieldTurnStop stays closed while it is in flight.
1714
+ if (yieldTurnStopPromise === pending) {
1715
+ yieldTurnStopPromise = null;
1716
+ yieldTurnStopRequested = false;
1717
+ }
1718
+ }
1719
+ },
1624
1720
  // A soft stop that never escalated still identifies as a budget abort so
1625
1721
  // the lifecycle can park the agent as resumable instead of killing it.
1626
1722
  abortKind: () => abortReason ?? (budgetStopRequested ? "budget" : undefined),
@@ -1719,62 +1815,135 @@ async function driveSessionToYield(
1719
1815
  await awaitAbortable(session.prompt(task, { attribution: "agent" }));
1720
1816
  await awaitAbortable(session.waitForIdle());
1721
1817
  } catch (err) {
1722
- // A budget stop cancels the free-running turn by aborting the
1723
- // session, which can surface here as a rejected prompt. Swallow it
1724
- // and drive the forced final yield below; real caller/timeout
1725
- // aborts (monitor signal) and genuine failures keep the old path.
1726
- if (!monitor.budgetStopRequested() || abortSignal.aborted) throw err;
1818
+ // A budget stop or a yield turn-stop (terminal yield parked behind
1819
+ // the async quiescence barrier) cancels the free-running turn by
1820
+ // aborting the session, which can surface here as a rejected
1821
+ // prompt. Swallow it and drive the barrier/forced final yield
1822
+ // below; real caller/timeout aborts (monitor signal) and genuine
1823
+ // failures keep the old path.
1824
+ const recoverableStop = monitor.budgetStopRequested() || monitor.yieldTurnStopRequested();
1825
+ if (!recoverableStop || abortSignal.aborted) throw err;
1727
1826
  }
1728
1827
 
1729
1828
  const reminderToolChoice = buildNamedToolChoice("yield", session.model);
1730
1829
 
1731
- let retryCount = 0;
1732
- while (!monitor.yieldCalled() && retryCount < MAX_YIELD_RETRIES && !abortSignal.aborted) {
1733
- // A budget stop collapses the reminder ladder to a single forced
1734
- // final yield: wait for the stop's session abort to settle, then
1735
- // prompt once with the wrap-up reminder + named tool choice.
1736
- const budgetStop = monitor.budgetStopRequested();
1737
- if (budgetStop) {
1738
- retryCount = MAX_YIELD_RETRIES - 1;
1739
- await monitor.waitForBudgetStop();
1740
- if (monitor.yieldCalled() || abortSignal.aborted) break;
1830
+ const runYieldLadder = async (): Promise<void> => {
1831
+ let retryCount = 0;
1832
+ while (!monitor.yieldCalled() && retryCount < MAX_YIELD_RETRIES && !abortSignal.aborted) {
1833
+ // A budget stop collapses the reminder ladder to a single forced
1834
+ // final yield: wait for the stop's session abort to settle, then
1835
+ // prompt once with the wrap-up reminder + named tool choice.
1836
+ const budgetStop = monitor.budgetStopRequested();
1837
+ if (budgetStop) {
1838
+ retryCount = MAX_YIELD_RETRIES - 1;
1839
+ await monitor.waitForBudgetStop();
1840
+ if (monitor.yieldCalled() || abortSignal.aborted) break;
1841
+ }
1842
+ // Skip reminders when the model returned a terminal error (e.g.
1843
+ // rate-limit cap hit, auth failure). Re-prompting would just
1844
+ // hit the same wall, multiplying the failure noise without
1845
+ // any chance of producing a yield.
1846
+ const lastBeforeReminder = session.getLastAssistantMessage();
1847
+ if (lastBeforeReminder?.stopReason === "error") break;
1848
+ try {
1849
+ retryCount++;
1850
+ const reminder = prompt.render(submitReminderTemplate, {
1851
+ retryCount,
1852
+ maxRetries: MAX_YIELD_RETRIES,
1853
+ budgetStop,
1854
+ });
1855
+
1856
+ const isFinalRetry = retryCount >= MAX_YIELD_RETRIES;
1857
+ await awaitAbortable(
1858
+ session.prompt(reminder, {
1859
+ attribution: "agent",
1860
+ synthetic: true,
1861
+ ...(isFinalRetry && reminderToolChoice ? { toolChoice: reminderToolChoice } : {}),
1862
+ }),
1863
+ );
1864
+ await awaitAbortable(session.waitForIdle());
1865
+ } catch (err) {
1866
+ if (abortSignal.aborted || err instanceof ToolAbortError) {
1867
+ // Benign control-flow exit — user cancel (^C) or compaction aborting
1868
+ // pending operations both surface here as ToolAbortError. The outer
1869
+ // catch and finally already mark the run aborted; logging at ERROR
1870
+ // would spam operator dashboards with non-failures.
1871
+ logger.debug("Subagent prompt aborted");
1872
+ } else {
1873
+ logger.error("Subagent prompt failed", {
1874
+ error: err instanceof Error ? err.message : String(err),
1875
+ });
1876
+ }
1877
+ }
1741
1878
  }
1742
- // Skip reminders when the model returned a terminal error (e.g.
1743
- // rate-limit cap hit, auth failure). Re-prompting would just
1744
- // hit the same wall, multiplying the failure noise without
1745
- // any chance of producing a yield.
1746
- const lastBeforeReminder = session.getLastAssistantMessage();
1747
- if (lastBeforeReminder?.stopReason === "error") break;
1748
- try {
1749
- retryCount++;
1750
- const reminder = prompt.render(submitReminderTemplate, {
1751
- retryCount,
1752
- maxRetries: MAX_YIELD_RETRIES,
1753
- budgetStop,
1754
- });
1879
+ };
1755
1880
 
1756
- const isFinalRetry = retryCount >= MAX_YIELD_RETRIES;
1757
- await awaitAbortable(
1758
- session.prompt(reminder, {
1759
- attribution: "agent",
1760
- synthetic: true,
1761
- ...(isFinalRetry && reminderToolChoice ? { toolChoice: reminderToolChoice } : {}),
1762
- }),
1763
- );
1764
- await awaitAbortable(session.waitForIdle());
1765
- } catch (err) {
1766
- if (abortSignal.aborted || err instanceof ToolAbortError) {
1767
- // Benign control-flow exit user cancel (^C) or compaction aborting
1768
- // pending operations both surface here as ToolAbortError. The outer
1769
- // catch and finally already mark the run aborted; logging at ERROR
1770
- // would spam operator dashboards with non-failures.
1771
- logger.debug("Subagent prompt aborted");
1772
- } else {
1773
- logger.error("Subagent prompt failed", {
1774
- error: err instanceof Error ? err.message : String(err),
1881
+ // Yield ladder + quiescence barrier (structured concurrency), one
1882
+ // loop: each iteration first demands a yield — initially, and again
1883
+ // whenever an async-result delivery un-latched the previous one
1884
+ // (including during the notice turn) — then either completes on
1885
+ // quiescence or settles one generation of owner async work.
1886
+ //
1887
+ // A final yield with owner background jobs still running or
1888
+ // undelivered is a scheduling pause, not run completion — the monitor
1889
+ // parks such a yield with a recoverable turn-stop instead of
1890
+ // terminating the run. Jobs are settled and their results folded into
1891
+ // the run as async-result follow-up turns; each delivered result
1892
+ // supersedes the yield it postdates, so the reminder ladder re-runs
1893
+ // to demand a fresh yield that accounts for it. Only a yield with no
1894
+ // pending owner work left is terminal the isolation runner captures
1895
+ // and destroys the worktree right after this run resolves, so no
1896
+ // owner job that could still re-wake the session may outlive it.
1897
+ // Suppressed (acknowledged / hub-watched) jobs never re-wake the run
1898
+ // and are reaped at teardown.
1899
+ //
1900
+ // Before blocking on running jobs, tell the model ONCE what it is
1901
+ // waiting on so it can `hub` wait/cancel instead of sitting silent
1902
+ // until the jobs (or the runtime limit) expire. Runs that never yield
1903
+ // (ladder exhausted / terminal model error) skip the barrier — more
1904
+ // injected turns just multiply the failure noise; the teardown reap
1905
+ // still cancels and awaits their jobs before worktree capture.
1906
+ let asyncPendingNoticeSent = false;
1907
+ while (!abortSignal.aborted) {
1908
+ if (!monitor.yieldCalled()) {
1909
+ await runYieldLadder();
1910
+ // Ladder exhausted / terminal model error: classified below
1911
+ // (missing yield, or stale yield when one was invalidated).
1912
+ if (!monitor.yieldCalled()) break;
1913
+ }
1914
+ // Let the parked yield's turn-stop session abort settle before
1915
+ // prompting again (mirrors waitForBudgetStop).
1916
+ await awaitAbortable(monitor.waitForYieldTurnStop());
1917
+ if (!session.hasPendingAsyncWork()) break;
1918
+ if (!asyncPendingNoticeSent) {
1919
+ asyncPendingNoticeSent = true;
1920
+ const running = session.getAsyncJobSnapshot()?.running ?? [];
1921
+ if (running.length > 0) {
1922
+ const jobs = running.map(job => `${job.id}${job.label ? ` (${job.label})` : ""}`).join(", ");
1923
+ const notice = prompt.render(subagentAsyncPendingTemplate, {
1924
+ count: running.length,
1925
+ multiple: running.length > 1,
1926
+ jobs,
1775
1927
  });
1928
+ try {
1929
+ await awaitAbortable(session.prompt(notice, { attribution: "agent", synthetic: true }));
1930
+ await awaitAbortable(session.waitForIdle());
1931
+ } catch (err) {
1932
+ if (abortSignal.aborted || err instanceof ToolAbortError) throw err;
1933
+ // A failed notice turn must not kill the run — fall through
1934
+ // to the passive settle below.
1935
+ logger.warn("Subagent async-pending notice failed", {
1936
+ error: err instanceof Error ? err.message : String(err),
1937
+ });
1938
+ }
1939
+ // Re-evaluate: the notice turn may have cancelled, watched, or
1940
+ // absorbed the jobs — or already re-yielded.
1941
+ continue;
1776
1942
  }
1777
1943
  }
1944
+ await awaitAbortable(session.settleAsyncWork());
1945
+ // Results delivered during the settle invalidated the recorded
1946
+ // yield: the next iteration's ladder demands a fresh one.
1778
1947
  }
1779
1948
 
1780
1949
  if (monitor.yieldCalled()) {
@@ -1813,6 +1982,18 @@ async function driveSessionToYield(
1813
1982
  abortReasonText ??= monitor.resolveAbortReasonText();
1814
1983
  exitCode = 1;
1815
1984
  }
1985
+
1986
+ // A recorded yield that async-result deliveries superseded and the
1987
+ // model never refreshed is stale: fail the run instead of letting the
1988
+ // parent act on a payload that predates the background job outcomes
1989
+ // the model was shown. The stale payload still ships through
1990
+ // finalizeSubprocessOutput's failed-after-yield path (exit 1 + stderr,
1991
+ // output preserved as salvage).
1992
+ if (monitor.yieldInvalidatedByAsync() && !abortSignal.aborted) {
1993
+ exitCode = 1;
1994
+ error ??=
1995
+ "Background job results arrived after the subagent's last yield; it did not submit a refreshed yield covering them.";
1996
+ }
1816
1997
  } catch (err) {
1817
1998
  if (abortSignal.aborted && monitor.yieldCalled() && !monitor.runtimeLimitExceeded()) {
1818
1999
  exitCode = 0;
@@ -1989,6 +2170,7 @@ async function finalizeRunResult(args: FinalizeRunArgs): Promise<SingleResult> {
1989
2170
  contextWindow: progress.contextWindow,
1990
2171
  modelOverride,
1991
2172
  resolvedModel: progress.resolvedModel,
2173
+ resolvedModelIsFallback: progress.resolvedModelIsFallback,
1992
2174
  error: exitCode !== 0 && stderr ? stderr : undefined,
1993
2175
  aborted: wasAborted,
1994
2176
  abortReason: finalAbortReason,
@@ -2016,9 +2198,11 @@ export async function finalizeSubagentLifecycle(args: {
2016
2198
  keepAlive: boolean;
2017
2199
  isolated: boolean;
2018
2200
  agentIdleTtlMs: number;
2019
- reviveSession: (() => Promise<AgentSession>) | null;
2201
+ reviveSession: AgentReviver | null;
2020
2202
  }): Promise<void> {
2021
2203
  const registry = AgentRegistry.global();
2204
+ const ref = registry.get(args.id);
2205
+ const ownsRef = Boolean(ref && ref.session === args.session);
2022
2206
  const disposeSession = async (): Promise<void> => {
2023
2207
  try {
2024
2208
  await untilAborted(AbortSignal.timeout(5000), () => args.session.dispose());
@@ -2033,7 +2217,7 @@ export async function finalizeSubagentLifecycle(args: {
2033
2217
  const resumableAbort =
2034
2218
  args.abortKind === "budget" && args.keepAlive && !args.isolated && args.reviveSession !== null;
2035
2219
  if (args.aborted && !resumableAbort) {
2036
- registry.setStatus(args.id, "aborted");
2220
+ if (ref && ownsRef) registry.setStatus(args.id, "aborted", ref);
2037
2221
  await disposeSession();
2038
2222
  return;
2039
2223
  }
@@ -2041,7 +2225,7 @@ export async function finalizeSubagentLifecycle(args: {
2041
2225
  if (!args.keepAlive) {
2042
2226
  // One-shot helper: dispose and unregister. No IRC, no revival.
2043
2227
  await disposeSession();
2044
- registry.unregister(args.id);
2228
+ if (ref && ownsRef) registry.unregister(args.id, ref);
2045
2229
  return;
2046
2230
  }
2047
2231
 
@@ -2051,19 +2235,26 @@ export async function finalizeSubagentLifecycle(args: {
2051
2235
  // transcript stays reachable (history://), but ensureLive will throw.
2052
2236
  // Status must flip to "parked" before dispose so the sdk dispose
2053
2237
  // wrapper skips unregister.
2054
- registry.setStatus(args.id, "parked");
2238
+ if (ref && ownsRef) registry.setStatus(args.id, "parked", ref);
2055
2239
  await disposeSession();
2056
- registry.detachSession(args.id);
2240
+ if (ref && ownsRef) registry.detachSession(args.id, ref);
2057
2241
  return;
2058
2242
  }
2059
2243
 
2060
2244
  // Keep-alive: finished and failed subagents both stay interrogable.
2061
2245
  // The lifecycle manager owns idle-TTL parking + revival from here on.
2062
- registry.setStatus(args.id, "idle");
2063
- AgentLifecycleManager.global().adopt(args.id, {
2064
- idleTtlMs: args.agentIdleTtlMs,
2065
- revive: args.reviveSession ?? undefined,
2066
- });
2246
+ if (!ref || !ownsRef || !registry.setStatus(args.id, "idle", ref)) {
2247
+ await disposeSession();
2248
+ return;
2249
+ }
2250
+ AgentLifecycleManager.global().adopt(
2251
+ args.id,
2252
+ {
2253
+ idleTtlMs: args.agentIdleTtlMs,
2254
+ revive: args.reviveSession ?? undefined,
2255
+ },
2256
+ ref,
2257
+ );
2067
2258
  }
2068
2259
 
2069
2260
  /** Options for {@link runSubagentFollowUpTurn}. */
@@ -2234,7 +2425,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2234
2425
  const settings = options.settings ?? Settings.isolated();
2235
2426
  const subagentSettings = createSubagentSettings(
2236
2427
  settings,
2237
- agent.readSummarize === false ? { "read.summarize.enabled": false } : undefined,
2428
+ {
2429
+ ...(agent.readSummarize === false ? { "read.summarize.enabled": false } : undefined),
2430
+ // Isolated runs must not expose roots outside the worktree.
2431
+ ...(worktree !== undefined ? { "workspace.additionalDirectories": [] } : undefined),
2432
+ },
2238
2433
  options.parentServiceTier,
2239
2434
  );
2240
2435
  const maxRecursionDepth = settings.get("task.maxRecursionDepth") ?? 2;
@@ -2318,7 +2513,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2318
2513
  });
2319
2514
  const progress = monitor.progress;
2320
2515
  let unsubscribe: (() => void) | null = null;
2321
- let reviveSession: (() => Promise<AgentSession>) | null = null;
2516
+ let reviveSession: AgentReviver | null = null;
2322
2517
  // Adopted (kept-alive) subagents flip registry status from session events on
2323
2518
  // later turns: revive/wake → running, turn drained → idle. The subscription
2324
2519
  // intentionally survives this run; a disposed session emits nothing, so it
@@ -2326,9 +2521,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2326
2521
  const installRegistryStatusSync = (target: AgentSession): void => {
2327
2522
  target.subscribe(event => {
2328
2523
  if (event.type === "agent_start") {
2329
- AgentRegistry.global().setStatus(id, "running");
2524
+ AgentRegistry.global().setStatus(id, "running", target);
2330
2525
  } else if (event.type === "agent_end") {
2331
- AgentRegistry.global().setStatus(id, "idle");
2526
+ AgentRegistry.global().setStatus(id, "idle", target);
2332
2527
  }
2333
2528
  });
2334
2529
  };
@@ -2551,8 +2746,12 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2551
2746
  // the same JSONL file re-invokes createAgentSession with the exact options
2552
2747
  // of the original run (same agent id, tools, model, system prompt,
2553
2748
  // artifacts dir) — only the SessionManager differs.
2554
- const buildSubagentSessionOptions = (sessionManagerForRun: SessionManager): CreateAgentSessionOptions => ({
2749
+ const buildSubagentSessionOptions = (
2750
+ sessionManagerForRun: SessionManager,
2751
+ expectedAgentRef: CreateAgentSessionOptions["expectedAgentRef"],
2752
+ ): CreateAgentSessionOptions => ({
2555
2753
  cwd: worktree ?? cwd,
2754
+ additionalDirectories: worktree !== undefined ? undefined : options.additionalDirectories,
2556
2755
  authStorage,
2557
2756
  modelRegistry,
2558
2757
  settings: subagentSettings,
@@ -2604,6 +2803,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2604
2803
  parentAgentId: options.parentAgentId,
2605
2804
  agentId: id,
2606
2805
  agentDisplayName: agent.name,
2806
+ expectedAgentRef,
2607
2807
  enableLsp: lspEnabled,
2608
2808
  enableIrc: options.enableIrc,
2609
2809
  skipPythonPreflight,
@@ -2618,7 +2818,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2618
2818
  },
2619
2819
  });
2620
2820
 
2621
- const sessionPromise = createAgentSession(buildSubagentSessionOptions(sessionManager));
2821
+ const sessionPromise = createAgentSession(buildSubagentSessionOptions(sessionManager, null));
2622
2822
  let session: AgentSession;
2623
2823
  try {
2624
2824
  ({ session } = await awaitAbortable(sessionPromise));
@@ -2638,14 +2838,16 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2638
2838
  // the single-writer lock cleanly and restores the full message history
2639
2839
  // (createAgentSession → agent.replaceMessages). Isolated runs are not
2640
2840
  // resumable (worktree is merged + cleaned) and never get a reviver.
2641
- reviveSession = async () => {
2841
+ reviveSession = async expectedAgentRef => {
2642
2842
  const reopened = await SessionManager.open(sessionFile, undefined, undefined, {
2643
2843
  suppressBreadcrumb: true,
2644
2844
  });
2645
2845
  if (options.parentArtifactManager) {
2646
2846
  reopened.adoptArtifactManager(options.parentArtifactManager);
2647
2847
  }
2648
- const { session: revived } = await createAgentSession(buildSubagentSessionOptions(reopened));
2848
+ const { session: revived } = await createAgentSession(
2849
+ buildSubagentSessionOptions(reopened, expectedAgentRef),
2850
+ );
2649
2851
  installRegistryStatusSync(revived);
2650
2852
  return revived;
2651
2853
  };
@@ -2736,6 +2938,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2736
2938
  setModel: model => runExtensionSetModel(session, model),
2737
2939
  getThinkingLevel: () => session.thinkingLevel,
2738
2940
  setThinkingLevel: level => session.setThinkingLevel(level),
2941
+ getServiceTiers: () => session.serviceTierByFamily,
2942
+ setServiceTier: (family, tier) => session.setServiceTierFamily(family, tier),
2739
2943
  getSessionName: () => session.sessionManager.getSessionName(),
2740
2944
  setSessionName: async name => {
2741
2945
  await session.sessionManager.setSessionName(name, "user");
@@ -2827,6 +3031,20 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2827
3031
  reviveSession,
2828
3032
  });
2829
3033
  }
3034
+ // Structured-concurrency reap: cancel and await ALL surviving owner
3035
+ // jobs (abort paths; suppressed/watched jobs the model left behind)
3036
+ // so isolation capture/cleanup never races a live process writing
3037
+ // into the worktree. This never proceeds while an owner process is
3038
+ // live: cancellation SIGKILL-escalates, so settlement is expected
3039
+ // within one interval — an unkillable process blocks here visibly
3040
+ // (with periodic warnings) instead of silently racing teardown.
3041
+ const jobManager = AsyncJobManager.instance();
3042
+ if (jobManager) {
3043
+ jobManager.cancelAll({ ownerId: id });
3044
+ while (!(await jobManager.waitForOwnerJobs(id, { timeoutMs: 10_000 }))) {
3045
+ logger.warn("Subagent async jobs still settling; delaying teardown until process exit", { id });
3046
+ }
3047
+ }
2830
3048
  }
2831
3049
 
2832
3050
  // Launch-latency breakdown (subagent invocation → first chat dispatch).