@gajae-code/coding-agent 0.18.7 → 0.18.9

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 (344) hide show
  1. package/CHANGELOG.md +377 -1
  2. package/dist/types/async/endpoint-id.d.ts +16 -0
  3. package/dist/types/async/job-manager.d.ts +1 -1
  4. package/dist/types/async/support.d.ts +0 -14
  5. package/dist/types/capability/mcp.d.ts +2 -0
  6. package/dist/types/capability/skill.d.ts +2 -0
  7. package/dist/types/capability/types.d.ts +2 -0
  8. package/dist/types/cli/public-command-errors.d.ts +3 -1
  9. package/dist/types/cli/setup-cli.d.ts +1 -1
  10. package/dist/types/commands/deep-interview.d.ts +3 -0
  11. package/dist/types/commands/mcp-serve.d.ts +4 -0
  12. package/dist/types/config/autorouting-tier-map.d.ts +348 -9
  13. package/dist/types/config/file-lock.d.ts +1 -0
  14. package/dist/types/config/model-bindings-applier.d.ts +2 -0
  15. package/dist/types/config/model-profile-activation.d.ts +11 -3
  16. package/dist/types/config/model-registry.d.ts +4 -9
  17. package/dist/types/config/skill-settings-defaults.d.ts +2 -0
  18. package/dist/types/cursor-delete-path.d.ts +1 -0
  19. package/dist/types/cursor-mcp-name.d.ts +8 -0
  20. package/dist/types/customization/inventory.d.ts +2 -0
  21. package/dist/types/debug/report-bundle-parent.d.ts +2 -0
  22. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
  23. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
  24. package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
  25. package/dist/types/discovery/helpers.d.ts +6 -1
  26. package/dist/types/discovery/mcp-json.d.ts +1 -0
  27. package/dist/types/eval/py/kernel.d.ts +18 -0
  28. package/dist/types/export/html/css-color.d.ts +2 -0
  29. package/dist/types/export/html/html-export-destination.d.ts +2 -0
  30. package/dist/types/export/html/template.generated.d.ts +1 -1
  31. package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +5 -0
  32. package/dist/types/extensibility/extensions/types.d.ts +7 -0
  33. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
  34. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
  35. package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
  36. package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
  37. package/dist/types/extensibility/skill-management.d.ts +4 -1
  38. package/dist/types/extensibility/skills.d.ts +2 -0
  39. package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
  40. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
  41. package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
  42. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -3
  43. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
  44. package/dist/types/gjc-runtime/sidecar-bootstrap-env.d.ts +6 -0
  45. package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
  46. package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
  47. package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
  48. package/dist/types/gjc-runtime/state-writer.d.ts +14 -1
  49. package/dist/types/gjc-runtime/tmux-common.d.ts +8 -5
  50. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
  51. package/dist/types/internal-urls/skill-path.d.ts +1 -0
  52. package/dist/types/lsp/config.d.ts +8 -5
  53. package/dist/types/lsp/workspace-path.d.ts +1 -0
  54. package/dist/types/modes/acp/acp-agent.d.ts +7 -0
  55. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  56. package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
  57. package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
  58. package/dist/types/modes/controllers/input-controller.d.ts +9 -0
  59. package/dist/types/modes/interactive-mode.d.ts +1 -0
  60. package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
  61. package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
  62. package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
  63. package/dist/types/progress/collect-project-progress.d.ts +38 -0
  64. package/dist/types/progress/progress-contract.d.ts +154 -0
  65. package/dist/types/progress/progress-snapshot.d.ts +11 -0
  66. package/dist/types/progress/project-progress.d.ts +141 -0
  67. package/dist/types/progress/render-progress.d.ts +28 -0
  68. package/dist/types/runtime-mcp/manager.d.ts +2 -1
  69. package/dist/types/runtime-mcp/mcp-redirect.d.ts +9 -0
  70. package/dist/types/runtime-mcp/oauth-public-url.d.ts +3 -0
  71. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
  72. package/dist/types/runtime-mcp/project-host-values.d.ts +27 -0
  73. package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
  74. package/dist/types/runtime-mcp/types.d.ts +6 -0
  75. package/dist/types/sdk/acp/adapter.d.ts +1 -1
  76. package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
  77. package/dist/types/sdk/broker/broker.d.ts +24 -2
  78. package/dist/types/sdk/broker/doctor-restart.d.ts +5 -0
  79. package/dist/types/sdk/broker/ensure.d.ts +14 -1
  80. package/dist/types/sdk/broker/session-index.d.ts +12 -6
  81. package/dist/types/sdk/bus/index.d.ts +12 -1
  82. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
  83. package/dist/types/sdk/client/client.d.ts +2 -0
  84. package/dist/types/sdk/host/query/handlers.d.ts +3 -0
  85. package/dist/types/sdk/index.d.ts +1 -0
  86. package/dist/types/sdk/router/session-router.d.ts +2 -0
  87. package/dist/types/sdk/session-list.d.ts +1 -1
  88. package/dist/types/sdk/session.d.ts +8 -0
  89. package/dist/types/session/agent-session.d.ts +38 -9
  90. package/dist/types/session/artifacts.d.ts +2 -2
  91. package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
  92. package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
  93. package/dist/types/session/internal/managed-session-storage.d.ts +26 -1
  94. package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
  95. package/dist/types/session/internal/task-artifact-owner-access.d.ts +2 -0
  96. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +9 -1
  97. package/dist/types/session/session-manager.d.ts +31 -5
  98. package/dist/types/session/session-retirement.d.ts +31 -7
  99. package/dist/types/session/session-storage.d.ts +69 -2
  100. package/dist/types/session/task-artifact-owner-codec.d.ts +2 -0
  101. package/dist/types/setup/credential-auto-import.d.ts +1 -1
  102. package/dist/types/setup/credential-import.d.ts +6 -4
  103. package/dist/types/skill-state/active-state.d.ts +38 -0
  104. package/dist/types/skill-state/workflow-mutation-guard.d.ts +3 -0
  105. package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
  106. package/dist/types/task/executor.d.ts +3 -0
  107. package/dist/types/task/isolation-dir.d.ts +26 -0
  108. package/dist/types/tools/ast-edit-write-path.d.ts +13 -0
  109. package/dist/types/tools/bash-skill-urls.d.ts +1 -1
  110. package/dist/types/tools/browser/cdp-url.d.ts +2 -0
  111. package/dist/types/tools/github-side-effect.d.ts +22 -0
  112. package/dist/types/tools/index.d.ts +3 -0
  113. package/dist/types/tools/monitor-mutation.d.ts +9 -0
  114. package/dist/types/tools/python.d.ts +9 -1
  115. package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
  116. package/dist/types/tools/restricted-cwd.d.ts +1 -0
  117. package/dist/types/tools/todo-write.d.ts +6 -0
  118. package/dist/types/utils/tools-checksum.d.ts +7 -0
  119. package/package.json +7 -7
  120. package/scripts/generate-sdk-operation-inventory.ts +21 -0
  121. package/scripts/verify-managed-empty-stop.ts +28 -0
  122. package/src/async/endpoint-id.ts +26 -0
  123. package/src/async/job-manager.ts +247 -132
  124. package/src/async/support.ts +0 -25
  125. package/src/capability/index.ts +15 -0
  126. package/src/capability/mcp.ts +2 -0
  127. package/src/capability/skill.ts +2 -0
  128. package/src/capability/types.ts +2 -0
  129. package/src/cli/auth-broker-cli.ts +1 -1
  130. package/src/cli/customize-doctor.ts +15 -2
  131. package/src/cli/doctor/repairs.ts +4 -1
  132. package/src/cli/doctor-supervisor.ts +3 -0
  133. package/src/cli/public-command-errors.ts +21 -1
  134. package/src/cli/public-command-registry.ts +28 -6
  135. package/src/cli/setup-cli.ts +4 -4
  136. package/src/cli/skills-cli.ts +5 -0
  137. package/src/commands/deep-interview.ts +47 -8
  138. package/src/commands/mcp-serve.ts +7 -0
  139. package/src/commands/sdk.ts +455 -397
  140. package/src/config/autorouting-tier-map.ts +117 -7
  141. package/src/config/file-lock.ts +316 -79
  142. package/src/config/mcp-schema.json +4 -0
  143. package/src/config/model-bindings-applier.ts +20 -0
  144. package/src/config/model-profile-activation.ts +240 -46
  145. package/src/config/model-registry.ts +47 -153
  146. package/src/config/skill-settings-defaults.ts +7 -0
  147. package/src/coordinator-mcp/policy.ts +3 -2
  148. package/src/coordinator-mcp/server.ts +2 -0
  149. package/src/cursor-delete-path.ts +10 -0
  150. package/src/cursor-mcp-name.ts +15 -0
  151. package/src/cursor.ts +13 -2
  152. package/src/customization/inventory.ts +4 -13
  153. package/src/dap/config.ts +2 -2
  154. package/src/debug/report-bundle-parent.ts +22 -0
  155. package/src/debug/report-bundle.ts +3 -0
  156. package/src/debug/system-info.ts +13 -1
  157. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +10 -6
  158. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
  159. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
  160. package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
  161. package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
  162. package/src/defaults/gjc-defaults.test.ts +18 -1
  163. package/src/defaults/gjc-defaults.ts +3 -0
  164. package/src/defaults/gjc-skills.generated.ts +58 -36
  165. package/src/discovery/agents.ts +6 -1
  166. package/src/discovery/builtin.ts +13 -1
  167. package/src/discovery/helpers.ts +171 -29
  168. package/src/discovery/mcp-json.ts +26 -12
  169. package/src/discovery/ssh.ts +1 -1
  170. package/src/discovery/substitute-plugin-root.ts +3 -1
  171. package/src/edit/file-read-cache.ts +1 -1
  172. package/src/eval/py/executor.ts +796 -154
  173. package/src/eval/py/kernel.ts +79 -13
  174. package/src/eval/py/tool-bridge.ts +4 -0
  175. package/src/export/html/css-color.ts +9 -0
  176. package/src/export/html/html-export-destination.ts +16 -0
  177. package/src/export/html/index.ts +7 -10
  178. package/src/export/html/template.generated.ts +1 -1
  179. package/src/extensibility/extensions/function-hooks-internal.ts +27 -0
  180. package/src/extensibility/extensions/runner.ts +15 -1
  181. package/src/extensibility/extensions/types.ts +7 -0
  182. package/src/extensibility/gjc-plugins/mcp-policy.ts +2 -1
  183. package/src/extensibility/gjc-plugins/runtime-adapters.ts +85 -8
  184. package/src/extensibility/plugins/manager.ts +3 -2
  185. package/src/extensibility/plugins/marketplace/cache.ts +15 -0
  186. package/src/extensibility/plugins/marketplace/manager.ts +11 -3
  187. package/src/extensibility/plugins/plugin-link-path.ts +12 -0
  188. package/src/extensibility/runtime-skill-discovery.ts +20 -2
  189. package/src/extensibility/skill-management.ts +9 -2
  190. package/src/extensibility/skills.ts +6 -1
  191. package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
  192. package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
  193. package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
  194. package/src/gjc-runtime/deep-interview-stage.ts +98 -21
  195. package/src/gjc-runtime/gc-runtime.ts +206 -9
  196. package/src/gjc-runtime/launch-worktree.ts +70 -9
  197. package/src/gjc-runtime/psmux-detect.ts +28 -4
  198. package/src/gjc-runtime/ralplan-runtime.ts +447 -52
  199. package/src/gjc-runtime/session-state-sidecar.ts +3 -2
  200. package/src/gjc-runtime/sidecar-bootstrap-env.ts +19 -0
  201. package/src/gjc-runtime/state-argv.ts +2 -0
  202. package/src/gjc-runtime/state-migrations.ts +91 -29
  203. package/src/gjc-runtime/state-runtime.ts +3716 -135
  204. package/src/gjc-runtime/state-writer.ts +105 -16
  205. package/src/gjc-runtime/tmux-common.ts +8 -5
  206. package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
  207. package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
  208. package/src/gjc-runtime/workflow-manifest.ts +54 -21
  209. package/src/harness-control-plane/operate.ts +12 -4
  210. package/src/harness-control-plane/owner.ts +5 -1
  211. package/src/hashline/parser.ts +48 -9
  212. package/src/hashline/recovery.ts +13 -4
  213. package/src/internal-urls/docs-index.generated.ts +20 -19
  214. package/src/internal-urls/skill-path.ts +19 -0
  215. package/src/internal-urls/skill-protocol.ts +2 -0
  216. package/src/lsp/config.ts +12 -63
  217. package/src/lsp/edits.ts +6 -0
  218. package/src/lsp/workspace-path.ts +51 -0
  219. package/src/main.ts +195 -31
  220. package/src/memories/index.ts +88 -7
  221. package/src/modes/acp/acp-agent.ts +88 -4
  222. package/src/modes/components/custom-editor.ts +14 -0
  223. package/src/modes/components/customization/customization-dashboard.ts +12 -0
  224. package/src/modes/components/extensions/extension-dashboard.ts +2 -2
  225. package/src/modes/components/extensions/state-manager.ts +15 -5
  226. package/src/modes/components/hook-editor.ts +1 -1
  227. package/src/modes/components/hook-selector.ts +1 -1
  228. package/src/modes/components/model-selector.ts +1 -4
  229. package/src/modes/components/provider-onboarding-selector.ts +1 -1
  230. package/src/modes/components/status-line/segments.ts +4 -0
  231. package/src/modes/controllers/event-controller.ts +11 -4
  232. package/src/modes/controllers/input-controller.ts +76 -31
  233. package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -16
  234. package/src/modes/controllers/selector-controller.ts +24 -15
  235. package/src/modes/controllers/todo-command-controller.ts +1 -1
  236. package/src/modes/interactive-mode.ts +5 -1
  237. package/src/modes/runtime-init.ts +4 -0
  238. package/src/modes/utils/abort-message.ts +3 -1
  239. package/src/modes/utils/interrupt-ownership.ts +12 -0
  240. package/src/modes/utils/keybinding-matchers.ts +4 -0
  241. package/src/modes/utils/macos-option-forwarding.ts +80 -0
  242. package/src/progress/collect-project-progress.ts +153 -0
  243. package/src/progress/progress-contract.ts +172 -0
  244. package/src/progress/progress-snapshot.ts +140 -0
  245. package/src/progress/project-progress.ts +446 -0
  246. package/src/progress/render-progress.ts +209 -0
  247. package/src/prompts/system/master-mode.md +1 -0
  248. package/src/prompts/tools/skill-discovery.md +1 -1
  249. package/src/runtime-mcp/client.ts +4 -2
  250. package/src/runtime-mcp/config-writer.ts +10 -6
  251. package/src/runtime-mcp/config.ts +1 -0
  252. package/src/runtime-mcp/json-rpc.ts +2 -1
  253. package/src/runtime-mcp/manager.ts +29 -11
  254. package/src/runtime-mcp/mcp-redirect.ts +73 -0
  255. package/src/runtime-mcp/oauth-flow.ts +14 -2
  256. package/src/runtime-mcp/oauth-public-url.ts +14 -0
  257. package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
  258. package/src/runtime-mcp/pool.ts +6 -0
  259. package/src/runtime-mcp/project-host-values.ts +46 -0
  260. package/src/runtime-mcp/smithery-registry.ts +22 -3
  261. package/src/runtime-mcp/transports/http.ts +4 -3
  262. package/src/runtime-mcp/types.ts +6 -0
  263. package/src/sdk/acp/adapter.ts +95 -8
  264. package/src/sdk/broker/broker-exit.ts +52 -23
  265. package/src/sdk/broker/broker.ts +81 -6
  266. package/src/sdk/broker/doctor-restart.ts +4 -0
  267. package/src/sdk/broker/ensure.ts +31 -2
  268. package/src/sdk/broker/lifecycle.ts +711 -66
  269. package/src/sdk/broker/session-index.ts +191 -81
  270. package/src/sdk/bus/chat-daemon-cli.ts +38 -1
  271. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  272. package/src/sdk/bus/index.ts +238 -43
  273. package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
  274. package/src/sdk/cli/session-cli.ts +17 -3
  275. package/src/sdk/client/client.ts +28 -8
  276. package/src/sdk/host/query/handlers.ts +9 -1
  277. package/src/sdk/host/session-runtime.test.ts +2705 -303
  278. package/src/sdk/host/session-runtime.ts +490 -143
  279. package/src/sdk/host/surface-policy.ts +1 -0
  280. package/src/sdk/index.ts +8 -0
  281. package/src/sdk/lifecycle-session.ts +3 -1
  282. package/src/sdk/prompt-deadline-manager.ts +15 -0
  283. package/src/sdk/protocol/operation-inventory.generated.json +146 -0
  284. package/src/sdk/protocol/operation-registry.ts +9 -2
  285. package/src/sdk/router/session-router.ts +82 -18
  286. package/src/sdk/session-list.ts +1 -0
  287. package/src/sdk/session.ts +69 -45
  288. package/src/session/agent-session.ts +679 -238
  289. package/src/session/artifacts.ts +176 -71
  290. package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
  291. package/src/session/internal/managed-session-scope.ts +3779 -462
  292. package/src/session/internal/managed-session-storage.ts +501 -78
  293. package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
  294. package/src/session/internal/task-artifact-owner-access.ts +19 -3
  295. package/src/session/internal/task-artifact-owner-transcript.ts +547 -1
  296. package/src/session/session-manager.ts +951 -133
  297. package/src/session/session-retirement.ts +573 -14
  298. package/src/session/session-storage.ts +447 -15
  299. package/src/session/task-artifact-owner-codec.ts +71 -8
  300. package/src/session/task-artifact-owner-retirement.ts +21 -19
  301. package/src/setup/credential-auto-import.ts +12 -3
  302. package/src/setup/credential-import.ts +176 -5
  303. package/src/setup/paseo/announce.ts +6 -1
  304. package/src/skill-state/active-state.ts +145 -2
  305. package/src/skill-state/workflow-mutation-guard.ts +102 -10
  306. package/src/slash-commands/builtin-registry.ts +39 -9
  307. package/src/slash-commands/helpers/mcp.ts +8 -4
  308. package/src/slash-commands/helpers/progress-report.ts +23 -0
  309. package/src/stt/setup.ts +3 -5
  310. package/src/task/commands.ts +3 -1
  311. package/src/task/executor.ts +4 -0
  312. package/src/task/index.ts +6 -0
  313. package/src/task/isolation-dir.ts +143 -0
  314. package/src/task/worktree.ts +16 -7
  315. package/src/tools/ask.ts +205 -6
  316. package/src/tools/ast-edit-write-path.ts +68 -0
  317. package/src/tools/ast-edit.ts +10 -1
  318. package/src/tools/bash-allowed-prefixes.ts +15 -0
  319. package/src/tools/bash-skill-urls.ts +56 -3
  320. package/src/tools/bash.ts +48 -79
  321. package/src/tools/browser/cdp-url.ts +18 -0
  322. package/src/tools/browser.ts +8 -1
  323. package/src/tools/descriptors.ts +5 -1
  324. package/src/tools/eval.ts +56 -36
  325. package/src/tools/gh.ts +10 -0
  326. package/src/tools/github-side-effect.ts +112 -0
  327. package/src/tools/image-gen.ts +1 -1
  328. package/src/tools/index.ts +3 -0
  329. package/src/tools/monitor-mutation.ts +27 -0
  330. package/src/tools/monitor.ts +7 -0
  331. package/src/tools/python.ts +188 -66
  332. package/src/tools/recipe/index.ts +9 -0
  333. package/src/tools/recipe/recipe-guard.ts +19 -0
  334. package/src/tools/restricted-cwd.ts +10 -0
  335. package/src/tools/skill-discovery.ts +5 -0
  336. package/src/tools/skill.ts +28 -1
  337. package/src/tools/todo-write.ts +10 -1
  338. package/src/tools/tool-catalog.generated.ts +1 -1
  339. package/src/tools/write.ts +1 -0
  340. package/src/utils/external-editor.ts +3 -2
  341. package/src/utils/tools-checksum.ts +34 -0
  342. package/src/utils/tools-manager.ts +24 -6
  343. package/src/web/insane/url-guard.ts +2 -1
  344. package/src/web/search/providers/codex.ts +3 -2
@@ -39,7 +39,7 @@ export interface AsyncJob {
39
39
  endTime?: number;
40
40
  label: string;
41
41
  abortController: AbortController;
42
- promise: Promise<void>;
42
+ readonly promise: Promise<void>;
43
43
  resultText?: string;
44
44
  errorText?: string;
45
45
  /** Safe, bounded cause when session setup failed before the LLM began work. */
@@ -151,7 +151,8 @@ export interface OwnerSubagentShutdownProof {
151
151
 
152
152
  interface OwnerSubagentShutdownLeaseState {
153
153
  lease: OwnerSubagentShutdownLease;
154
- backingJobIds: ReadonlyMap<string, readonly string[]>;
154
+ backingExecutions: ReadonlyMap<string, readonly AsyncJobExecution[]>;
155
+ records: ReadonlyMap<string, OwnerSubagentShutdownRecordCapture>;
155
156
  phase: "active" | "proving" | "proved";
156
157
  proof?: OwnerSubagentShutdownProof;
157
158
  }
@@ -213,6 +214,21 @@ export interface SubagentRecord {
213
214
  currentJobGeneration?: string;
214
215
  }
215
216
 
217
+ /** Retains one exact job object until its runner and deferred eviction cleanup settle. */
218
+ interface AsyncJobExecution {
219
+ readonly job: AsyncJob;
220
+ readonly physicalCompletion: Promise<void>;
221
+ physicallySettled: boolean;
222
+ /** An evicted row's lifecycle cleanup must not race its still-running runner. */
223
+ pendingEvictionCleanup: boolean;
224
+ }
225
+
226
+ interface OwnerSubagentShutdownRecordCapture {
227
+ readonly record: SubagentRecord;
228
+ readonly currentJobId: string | null;
229
+ readonly currentJobGeneration?: string;
230
+ }
231
+
216
232
  /** Lightweight, manager-owned resume payload. The async layer treats `data` as opaque. */
217
233
  export interface ResumeDescriptor {
218
234
  subagentId: string;
@@ -676,8 +692,10 @@ export class AsyncJobManager {
676
692
  readonly #evictionTimers = new Map<string, NodeJS.Timeout>();
677
693
  readonly #outputState = new Map<string, AsyncJobOutputState>();
678
694
  readonly #ownerCleanups = new Map<string, Set<() => void>>();
679
- readonly #lifecycles = new Map<string, AsyncJobLifecycleCleanup>();
680
- readonly #lifecyclePhases = new Map<string, Set<"cancel" | "terminal" | "evict">>();
695
+ readonly #lifecycles = new WeakMap<AsyncJob, AsyncJobLifecycleCleanup>();
696
+ readonly #lifecyclePhases = new WeakMap<AsyncJob, Set<"cancel" | "terminal" | "evict">>();
697
+ readonly #executionsByJob = new WeakMap<AsyncJob, AsyncJobExecution>();
698
+ readonly #activeExecutions = new Set<AsyncJobExecution>();
681
699
  readonly #monitorTombstones = new Map<string, MonitorTombstone>();
682
700
  readonly #outputRetentionBytes = DEFAULT_JOB_OUTPUT_RETENTION_BYTES;
683
701
  readonly #onJobComplete: AsyncJobManagerOptions["onJobComplete"];
@@ -697,7 +715,7 @@ export class AsyncJobManager {
697
715
  readonly #terminalWaits = new Map<string, TerminalWaitState>();
698
716
  #waitSeq = 0;
699
717
  readonly #publishedTerminalGenerations = new Set<string>();
700
- readonly #settledJobIds = new Set<string>();
718
+ readonly #settledJobs = new Set<AsyncJob>();
701
719
  #jobGenerationSeq = 0;
702
720
  readonly #liveHandles = new Map<string, SubagentLiveHandle>();
703
721
  readonly #subagentProgress = new Map<string, AgentProgress>();
@@ -1074,9 +1092,9 @@ export class AsyncJobManager {
1074
1092
 
1075
1093
  this.#expireMonitorTombstones();
1076
1094
  const id = this.#resolveJobId(options?.id);
1077
- this.#settledJobIds.delete(id);
1078
1095
  const abortController = new AbortController();
1079
1096
  const startTime = Date.now();
1097
+ const completion = Promise.withResolvers<void>();
1080
1098
 
1081
1099
  const job: AsyncJob = {
1082
1100
  id,
@@ -1086,11 +1104,20 @@ export class AsyncJobManager {
1086
1104
  startTime,
1087
1105
  label,
1088
1106
  abortController,
1089
- promise: Promise.resolve(),
1107
+ promise: completion.promise,
1090
1108
  ownerId: options?.ownerId,
1091
1109
  metadata: options?.metadata,
1092
1110
  };
1093
- if (options?.lifecycle) this.#lifecycles.set(id, options.lifecycle);
1111
+ Object.defineProperty(job, "promise", { writable: false, configurable: false });
1112
+ if (options?.lifecycle) this.#lifecycles.set(job, options.lifecycle);
1113
+ const execution: AsyncJobExecution = {
1114
+ job,
1115
+ physicalCompletion: completion.promise,
1116
+ physicallySettled: false,
1117
+ pendingEvictionCleanup: false,
1118
+ };
1119
+ this.#executionsByJob.set(job, execution);
1120
+ this.#activeExecutions.add(execution);
1094
1121
 
1095
1122
  const reportProgress = async (text: string, details?: Record<string, unknown>): Promise<void> => {
1096
1123
  if (!options?.onProgress) return;
@@ -1103,7 +1130,8 @@ export class AsyncJobManager {
1103
1130
  });
1104
1131
  }
1105
1132
  };
1106
- job.promise = (async () => {
1133
+ this.#jobs.set(id, job);
1134
+ void (async () => {
1107
1135
  try {
1108
1136
  const result = await run({ jobId: id, signal: abortController.signal, reportProgress });
1109
1137
  const outcome: SubagentRunOutcome =
@@ -1120,8 +1148,8 @@ export class AsyncJobManager {
1120
1148
  job.resultText = outcome.kind === "paused" ? outcome.note : outcome.text;
1121
1149
  this.#markRecordTerminal(id, "cancelled", job.generation);
1122
1150
  this.#publishTerminal(job);
1123
- this.#runLifecycle(id, "terminal", job);
1124
- this.#scheduleEviction(id);
1151
+ this.#runLifecycle(job, "terminal");
1152
+ this.#scheduleEviction(job);
1125
1153
  this.#drainResumeQueue();
1126
1154
  // The run has now actually unwound: retire the owned tuple so a
1127
1155
  // later scope:"owned" abort of the same turn does not report
@@ -1151,8 +1179,8 @@ export class AsyncJobManager {
1151
1179
  this.#markRecordTerminal(id, "failed", job.generation);
1152
1180
  this.#publishTerminal(job);
1153
1181
  this.#enqueueDelivery(id, outcome.text);
1154
- this.#runLifecycle(id, "terminal", job);
1155
- this.#scheduleEviction(id);
1182
+ this.#runLifecycle(job, "terminal");
1183
+ this.#scheduleEviction(job);
1156
1184
  this.#drainResumeQueue();
1157
1185
  return;
1158
1186
  }
@@ -1163,8 +1191,8 @@ export class AsyncJobManager {
1163
1191
  this.#markRecordTerminal(id, "completed", job.generation);
1164
1192
  this.#publishTerminal(job);
1165
1193
  this.#enqueueDelivery(id, outcome.text);
1166
- this.#runLifecycle(id, "terminal", job);
1167
- this.#scheduleEviction(id);
1194
+ this.#runLifecycle(job, "terminal");
1195
+ this.#scheduleEviction(job);
1168
1196
  this.#drainResumeQueue();
1169
1197
  } catch (error) {
1170
1198
  if (this.#externallySettled.has(job.generation)) {
@@ -1175,8 +1203,8 @@ export class AsyncJobManager {
1175
1203
  job.errorText = error instanceof Error ? error.message : String(error);
1176
1204
  this.#markRecordTerminal(id, "cancelled", job.generation);
1177
1205
  this.#publishTerminal(job);
1178
- this.#runLifecycle(id, "terminal", job);
1179
- this.#scheduleEviction(id);
1206
+ this.#runLifecycle(job, "terminal");
1207
+ this.#scheduleEviction(job);
1180
1208
  this.#drainResumeQueue();
1181
1209
  this.#retireCancelledJobOwned(id, job.generation);
1182
1210
  return;
@@ -1188,20 +1216,43 @@ export class AsyncJobManager {
1188
1216
  this.#markRecordTerminal(id, "failed", job.generation);
1189
1217
  this.#publishTerminal(job);
1190
1218
  this.#enqueueDelivery(id, errorText);
1191
- this.#runLifecycle(id, "terminal", job);
1192
- this.#scheduleEviction(id);
1219
+ this.#runLifecycle(job, "terminal");
1220
+ this.#scheduleEviction(job);
1193
1221
  this.#drainResumeQueue();
1194
1222
  }
1195
- })().finally(() => {
1196
- this.#externallySettled.delete(job.generation);
1197
- this.#settledJobIds.add(id);
1198
- });
1223
+ })()
1224
+ .finally(() => {
1225
+ this.#externallySettled.delete(job.generation);
1226
+ if (this.#jobs.get(id) === job) this.#settledJobs.add(job);
1227
+ this.#settleExecution(execution);
1228
+ })
1229
+ .then(completion.resolve, completion.reject);
1199
1230
 
1200
- this.#jobs.set(id, job);
1201
1231
  this.#notifyChange();
1202
1232
  return id;
1203
1233
  }
1204
1234
 
1235
+ #settleExecution(execution: AsyncJobExecution): void {
1236
+ if (execution.physicallySettled) return;
1237
+ try {
1238
+ if (execution.pendingEvictionCleanup) this.#finishEvictionCleanup(execution.job);
1239
+ } finally {
1240
+ execution.physicallySettled = true;
1241
+ this.#activeExecutions.delete(execution);
1242
+ }
1243
+ }
1244
+
1245
+ #finishEvictionCleanup(job: AsyncJob): void {
1246
+ this.#runLifecycle(job, "evict");
1247
+ this.#purgeTerminalSubagentStateForJob(job.id, job.generation);
1248
+ }
1249
+
1250
+ #cancelExactExecution(execution: AsyncJobExecution): boolean {
1251
+ const job = execution.job;
1252
+ if (this.#jobs.get(job.id) !== job) return false;
1253
+ return this.cancel(job.id, job.ownerId ? { ownerId: job.ownerId } : undefined);
1254
+ }
1255
+
1205
1256
  /**
1206
1257
  * Cancel a single job by id. When `filter.ownerId` is set and does not
1207
1258
  * match the job's owner, the call is treated as not-found (returns false)
@@ -1229,8 +1280,8 @@ export class AsyncJobManager {
1229
1280
  this.#markRecordTerminal(id, "cancelled", job.generation);
1230
1281
  this.#freezeEndTime(job);
1231
1282
  this.#publishTerminal(job);
1232
- this.#runLifecycle(id, "cancel");
1233
- this.#scheduleEviction(id);
1283
+ this.#runLifecycle(job, "cancel");
1284
+ this.#scheduleEviction(job);
1234
1285
  this.#drainResumeQueue();
1235
1286
  // A PAUSED job's run has already unwound (it returned "paused"), so
1236
1287
  // its owned tuple is retired here; a RUNNING job's tuple is retired
@@ -1243,7 +1294,7 @@ export class AsyncJobManager {
1243
1294
  this.#freezeEndTime(job);
1244
1295
  this.#markRecordTerminal(id, "cancelled", job.generation);
1245
1296
  this.#publishTerminal(job);
1246
- this.#runLifecycle(id, "cancel");
1297
+ this.#runLifecycle(job, "cancel");
1247
1298
  job.abortController.abort();
1248
1299
  this.#notifyChange();
1249
1300
  return true;
@@ -1260,14 +1311,14 @@ export class AsyncJobManager {
1260
1311
  job.endTime ??= Date.now();
1261
1312
  }
1262
1313
 
1263
- #runLifecycle(jobId: string, phase: "cancel" | "terminal" | "evict", jobOverride?: AsyncJob): void {
1264
- const lifecycle = this.#lifecycles.get(jobId);
1265
- const job = jobOverride ?? this.#jobs.get(jobId);
1266
- if (!lifecycle || !job) return;
1267
- const fired = this.#lifecyclePhases.get(jobId) ?? new Set<"cancel" | "terminal" | "evict">();
1314
+ #runLifecycle(job: AsyncJob, phase: "cancel" | "terminal" | "evict"): void {
1315
+ const lifecycle = this.#lifecycles.get(job);
1316
+ if (!lifecycle) return;
1317
+ const jobId = job.id;
1318
+ const fired = this.#lifecyclePhases.get(job) ?? new Set<"cancel" | "terminal" | "evict">();
1268
1319
  if (fired.has(phase)) return;
1269
1320
  fired.add(phase);
1270
- this.#lifecyclePhases.set(jobId, fired);
1321
+ this.#lifecyclePhases.set(job, fired);
1271
1322
  try {
1272
1323
  if (phase === "cancel") lifecycle.onCancel?.(job);
1273
1324
  else if (phase === "terminal") lifecycle.onTerminal?.(job);
@@ -1288,16 +1339,16 @@ export class AsyncJobManager {
1288
1339
  }
1289
1340
  }
1290
1341
 
1291
- #recordMonitorTombstone(jobId: string): void {
1292
- const job = this.#jobs.get(jobId);
1293
- if (!job?.metadata?.monitor) return;
1294
- const lifecycle = this.#lifecycles.get(jobId);
1342
+ #recordMonitorTombstone(job: AsyncJob): void {
1343
+ const jobId = job.id;
1344
+ if (!job.metadata?.monitor) return;
1345
+ const lifecycle = this.#lifecycles.get(job);
1295
1346
  this.#monitorTombstones.set(jobId, {
1296
1347
  jobId,
1297
1348
  ownerId: job.ownerId,
1298
1349
  status: job.status,
1299
1350
  expiresAt: Date.now() + MONITOR_TOMBSTONE_TTL_MS,
1300
- purge: () => (lifecycle?.onTombstonePurge ?? lifecycle?.onEvict)?.(job),
1351
+ purge: () => lifecycle?.onTombstonePurge?.(job),
1301
1352
  });
1302
1353
  }
1303
1354
 
@@ -1484,37 +1535,63 @@ export class AsyncJobManager {
1484
1535
  beginOwnerSubagentShutdown(ownerId: string): OwnerSubagentShutdownLease | undefined {
1485
1536
  if (!ownerId || this.#ownerSubagentShutdownLeases.has(ownerId)) return undefined;
1486
1537
  const targets = new Map<string, OwnerSubagentShutdownTarget>();
1487
- const backingJobIds = new Map<string, Set<string>>();
1488
- const addBackingJob = (subagentId: string, jobId: string | null): void => {
1489
- if (!jobId) return;
1490
- const ids = backingJobIds.get(subagentId) ?? new Set<string>();
1491
- ids.add(jobId);
1492
- backingJobIds.set(subagentId, ids);
1538
+ const backingExecutions = new Map<string, Set<AsyncJobExecution>>();
1539
+ const records = new Map<string, OwnerSubagentShutdownRecordCapture>();
1540
+ const findExecution = (jobId: string, generation?: string): AsyncJobExecution | undefined => {
1541
+ if (generation === undefined) return undefined;
1542
+ const job = this.#jobs.get(jobId);
1543
+ if (job?.generation === generation) {
1544
+ return this.#executionsByJob.get(job);
1545
+ }
1546
+ for (const execution of this.#activeExecutions) {
1547
+ if (execution.job.id === jobId && execution.job.generation === generation) return execution;
1548
+ }
1549
+ return undefined;
1550
+ };
1551
+ const addBackingExecution = (subagentId: string, execution: AsyncJobExecution | undefined): void => {
1552
+ if (!execution || execution.job.ownerId !== ownerId) return;
1553
+ const executions = backingExecutions.get(subagentId) ?? new Set<AsyncJobExecution>();
1554
+ executions.add(execution);
1555
+ backingExecutions.set(subagentId, executions);
1493
1556
  };
1494
1557
  for (const record of this.#subagentRecords.values()) {
1495
- if (record.ownerId !== ownerId || this.#isTerminalSubagentStatus(record.status)) continue;
1558
+ if (record.ownerId !== ownerId) continue;
1559
+ const backingExecution =
1560
+ record.currentJobId && record.currentJobGeneration
1561
+ ? findExecution(record.currentJobId, record.currentJobGeneration)
1562
+ : undefined;
1563
+ records.set(record.subagentId, {
1564
+ record,
1565
+ currentJobId: record.currentJobId,
1566
+ currentJobGeneration: record.currentJobGeneration,
1567
+ });
1568
+ if (this.#isTerminalSubagentStatus(record.status) && backingExecution?.physicallySettled !== false) continue;
1496
1569
  targets.set(record.subagentId, {
1497
1570
  subagentId: record.subagentId,
1498
1571
  jobId: record.status === "queued" ? null : record.currentJobId,
1499
1572
  source: "record",
1500
1573
  });
1501
- if (record.status !== "queued") addBackingJob(record.subagentId, record.currentJobId);
1574
+ addBackingExecution(record.subagentId, backingExecution);
1502
1575
  }
1503
- for (const job of this.#jobs.values()) {
1576
+ const candidateJobs = new Set<AsyncJob>(this.#jobs.values());
1577
+ for (const execution of this.#activeExecutions) candidateJobs.add(execution.job);
1578
+ for (const job of candidateJobs) {
1504
1579
  const subagentId = job.metadata?.subagent?.id;
1580
+ const execution = this.#executionsByJob.get(job);
1581
+ const physicallyActive = execution !== undefined && !execution.physicallySettled;
1505
1582
  if (
1506
1583
  job.ownerId !== ownerId ||
1507
1584
  !subagentId ||
1508
- (job.status !== "running" && job.status !== "paused" && job.status !== "cancelled")
1585
+ (!physicallyActive && job.status !== "running" && job.status !== "paused" && job.status !== "cancelled")
1509
1586
  ) {
1510
1587
  continue;
1511
1588
  }
1512
- if (job.status === "cancelled" && this.#settledJobIds.has(job.id) && !this.#subagentRecords.has(subagentId))
1589
+ if (job.status === "cancelled" && execution?.physicallySettled && !this.#subagentRecords.has(subagentId))
1513
1590
  continue;
1514
1591
  if (!targets.has(subagentId)) {
1515
1592
  targets.set(subagentId, { subagentId, jobId: job.id, source: "metadata_job" });
1516
1593
  }
1517
- addBackingJob(subagentId, job.id);
1594
+ addBackingExecution(subagentId, execution);
1518
1595
  }
1519
1596
  const lease: OwnerSubagentShutdownLease = {
1520
1597
  ownerId,
@@ -1523,12 +1600,13 @@ export class AsyncJobManager {
1523
1600
  };
1524
1601
  this.#ownerSubagentShutdownLeases.set(ownerId, {
1525
1602
  lease,
1526
- backingJobIds: new Map<string, readonly string[]>(
1527
- Array.from(backingJobIds, ([subagentId, jobIds]): [string, readonly string[]] => [
1603
+ backingExecutions: new Map<string, readonly AsyncJobExecution[]>(
1604
+ Array.from(backingExecutions, ([subagentId, executions]): [string, readonly AsyncJobExecution[]] => [
1528
1605
  subagentId,
1529
- Array.from(jobIds),
1606
+ Array.from(executions),
1530
1607
  ]),
1531
1608
  ),
1609
+ records,
1532
1610
  phase: "active",
1533
1611
  });
1534
1612
  return lease;
@@ -1593,33 +1671,38 @@ export class AsyncJobManager {
1593
1671
  }
1594
1672
  if (state.phase === "proved" && state.proof?.confirmed) return state.proof;
1595
1673
  state.phase = "proving";
1596
- const settled = new Set<string>();
1597
- const promises: Promise<void>[] = [];
1674
+ const promises = new Set<Promise<void>>();
1598
1675
  for (const target of lease.targets) {
1599
- const backingJobs = (state.backingJobIds.get(target.subagentId) ?? []).map(jobId => ({
1600
- jobId,
1601
- job: this.#jobs.get(jobId),
1602
- }));
1603
- if (target.source === "record") this.cancelSubagent(target.subagentId, { ownerId: lease.ownerId });
1604
- for (const { jobId, job } of backingJobs) {
1605
- this.cancel(jobId, { ownerId: lease.ownerId });
1606
- if (!job || job.ownerId !== lease.ownerId) continue;
1607
- promises.push(
1608
- job.promise.then(
1609
- () => {
1610
- settled.add(jobId);
1611
- },
1612
- () => {
1613
- settled.add(jobId);
1614
- },
1615
- ),
1616
- );
1676
+ const executions = state.backingExecutions.get(target.subagentId) ?? [];
1677
+ const capture = state.records.get(target.subagentId);
1678
+ if (
1679
+ target.source === "record" &&
1680
+ capture &&
1681
+ this.#subagentRecords.get(target.subagentId) === capture.record &&
1682
+ capture.record.status === "queued"
1683
+ ) {
1684
+ this.cancelSubagent(target.subagentId, { ownerId: lease.ownerId });
1685
+ }
1686
+ for (const execution of executions) {
1687
+ this.#cancelExactExecution(execution);
1688
+ promises.add(execution.physicalCompletion);
1689
+ }
1690
+ if (target.source !== "record" || executions.length > 0) continue;
1691
+ const record = capture?.record;
1692
+ if (!capture || !record || this.#subagentRecords.get(target.subagentId) !== record) continue;
1693
+ if (record.status === "paused") {
1694
+ const row = record.currentJobId ? this.#jobs.get(record.currentJobId) : undefined;
1695
+ if (row && row.generation === capture.currentJobGeneration) continue;
1696
+ record.status = "cancelled";
1697
+ this.#liveHandles.delete(record.subagentId);
1698
+ this.#subagentProgress.delete(record.subagentId);
1699
+ this.#notifyChange();
1617
1700
  }
1618
1701
  }
1619
1702
  const timeoutMs = Math.max(0, options?.timeoutMs ?? OWNER_SUBAGENT_SHUTDOWN_TIMEOUT_MS);
1620
1703
  let deadlineExceeded = false;
1621
1704
  await Promise.race([
1622
- Promise.allSettled(promises),
1705
+ Promise.allSettled([...promises]),
1623
1706
  Bun.sleep(timeoutMs).then(() => {
1624
1707
  deadlineExceeded = true;
1625
1708
  }),
@@ -1633,7 +1716,7 @@ export class AsyncJobManager {
1633
1716
  );
1634
1717
  }
1635
1718
  const unresolvedIds = lease.targets
1636
- .filter(target => !this.#hasTerminalShutdownEvidence(target, lease.ownerId, current.backingJobIds, settled))
1719
+ .filter(target => !this.#hasTerminalShutdownEvidence(target, lease.ownerId, current))
1637
1720
  .map(target => target.subagentId);
1638
1721
  const reason =
1639
1722
  unresolvedIds.length === 0
@@ -1650,16 +1733,23 @@ export class AsyncJobManager {
1650
1733
  #hasTerminalShutdownEvidence(
1651
1734
  target: OwnerSubagentShutdownTarget,
1652
1735
  ownerId: string,
1653
- backingJobIds: ReadonlyMap<string, readonly string[]>,
1654
- settled: ReadonlySet<string>,
1736
+ state: OwnerSubagentShutdownLeaseState,
1655
1737
  ): boolean {
1656
- const record = this.#subagentRecords.get(target.subagentId);
1657
- if (!record || record.ownerId !== ownerId || !this.#isTerminalSubagentStatus(record.status)) return false;
1658
- return (backingJobIds.get(target.subagentId) ?? []).every(jobId => {
1659
- if (!settled.has(jobId)) return false;
1660
- const job = this.#jobs.get(jobId);
1661
- return job === undefined || job.ownerId === ownerId;
1662
- });
1738
+ const capture = state.records.get(target.subagentId);
1739
+ const record = capture?.record;
1740
+ if (
1741
+ !record ||
1742
+ this.#subagentRecords.get(target.subagentId) !== record ||
1743
+ record.ownerId !== ownerId ||
1744
+ record.currentJobId !== capture.currentJobId ||
1745
+ record.currentJobGeneration !== capture.currentJobGeneration ||
1746
+ !this.#isTerminalSubagentStatus(record.status)
1747
+ ) {
1748
+ return false;
1749
+ }
1750
+ return (state.backingExecutions.get(target.subagentId) ?? []).every(
1751
+ execution => execution.job.ownerId === ownerId && execution.physicallySettled,
1752
+ );
1663
1753
  }
1664
1754
 
1665
1755
  #ownerSubagentShutdownProof(
@@ -1962,7 +2052,7 @@ export class AsyncJobManager {
1962
2052
  rec.status = "cancelled";
1963
2053
  this.#liveHandles.delete(rec.subagentId);
1964
2054
  this.#subagentProgress.delete(rec.subagentId);
1965
- if (shouldScheduleEviction && currentJobId) this.#scheduleEviction(currentJobId);
2055
+ if (shouldScheduleEviction && job) this.#scheduleEviction(job);
1966
2056
  else this.#notifyChange();
1967
2057
  this.#drainResumeQueue();
1968
2058
  // A paused run has already unwound (it returned "paused") and this
@@ -2597,8 +2687,8 @@ export class AsyncJobManager {
2597
2687
  this.#markRecordTerminal(jobId, "failed", generation);
2598
2688
  this.#publishTerminal(job);
2599
2689
  this.#enqueueDelivery(jobId, errorText);
2600
- this.#runLifecycle(jobId, "terminal", job);
2601
- this.#scheduleEviction(jobId);
2690
+ this.#runLifecycle(job, "terminal");
2691
+ this.#scheduleEviction(job);
2602
2692
  this.#drainResumeQueue();
2603
2693
  this.#notifyChange();
2604
2694
  return true;
@@ -2606,7 +2696,7 @@ export class AsyncJobManager {
2606
2696
 
2607
2697
  cancelAll(filter?: AsyncJobFilter): void {
2608
2698
  for (const job of this.getRunningJobs(filter)) {
2609
- if (this.cancel(job.id, filter)) this.#scheduleEviction(job.id);
2699
+ if (this.cancel(job.id, filter)) this.#scheduleEviction(job);
2610
2700
  }
2611
2701
  }
2612
2702
 
@@ -2628,11 +2718,23 @@ export class AsyncJobManager {
2628
2718
  }
2629
2719
  async cancelAndSettleOwnerJobs(ownerId: string, options?: { timeoutMs?: number }): Promise<boolean> {
2630
2720
  const jobs = this.getAllJobs({ ownerId });
2631
- for (const job of jobs) this.cancel(job.id, { ownerId });
2721
+ const executions = new Set<AsyncJobExecution>();
2722
+ for (const job of jobs) {
2723
+ const execution = this.#executionsByJob.get(job);
2724
+ if (execution) {
2725
+ executions.add(execution);
2726
+ this.#cancelExactExecution(execution);
2727
+ }
2728
+ }
2729
+ for (const execution of this.#activeExecutions) {
2730
+ if (execution.job.ownerId !== ownerId) continue;
2731
+ executions.add(execution);
2732
+ this.#cancelExactExecution(execution);
2733
+ }
2632
2734
  const timeoutMs = Math.max(0, options?.timeoutMs ?? OWNER_SUBAGENT_SHUTDOWN_TIMEOUT_MS);
2633
2735
  let timedOut = false;
2634
2736
  await Promise.race([
2635
- Promise.allSettled(jobs.map(job => job.promise)),
2737
+ Promise.allSettled([...executions].map(execution => execution.physicalCompletion)),
2636
2738
  Bun.sleep(timeoutMs).then(() => {
2637
2739
  timedOut = true;
2638
2740
  }),
@@ -2657,11 +2759,11 @@ export class AsyncJobManager {
2657
2759
  }
2658
2760
 
2659
2761
  async #waitForAllWithDeadline(timeoutMs: number): Promise<{ completed: boolean; stuckJobIds: string[] }> {
2660
- const jobs = Array.from(this.#jobs.values());
2661
- if (jobs.length === 0) return { completed: true, stuckJobIds: [] };
2762
+ const executions = [...this.#activeExecutions];
2763
+ if (executions.length === 0) return { completed: true, stuckJobIds: [] };
2662
2764
  let timedOut = false;
2663
2765
  await Promise.race([
2664
- Promise.allSettled(jobs.map(job => job.promise)),
2766
+ Promise.allSettled(executions.map(execution => execution.physicalCompletion)),
2665
2767
  Bun.sleep(Math.max(0, timeoutMs)).then(() => {
2666
2768
  timedOut = true;
2667
2769
  }),
@@ -2669,9 +2771,7 @@ export class AsyncJobManager {
2669
2771
  if (!timedOut) return { completed: true, stuckJobIds: [] };
2670
2772
  return {
2671
2773
  completed: false,
2672
- stuckJobIds: Array.from(this.#jobs.values())
2673
- .filter(job => job.status === "running" || job.status === "cancelled")
2674
- .map(job => job.id),
2774
+ stuckJobIds: executions.filter(execution => !execution.physicallySettled).map(execution => execution.job.id),
2675
2775
  };
2676
2776
  }
2677
2777
 
@@ -2757,7 +2857,7 @@ export class AsyncJobManager {
2757
2857
  }
2758
2858
 
2759
2859
  async waitForAll(): Promise<void> {
2760
- await Promise.all(Array.from(this.#jobs.values()).map(job => job.promise));
2860
+ await Promise.all([...this.#activeExecutions].map(execution => execution.physicalCompletion));
2761
2861
  }
2762
2862
 
2763
2863
  async drainDeliveries(options?: { timeoutMs?: number; filter?: AsyncJobFilter }): Promise<boolean> {
@@ -2824,15 +2924,15 @@ export class AsyncJobManager {
2824
2924
  // registration keeps the original protection (cleanups cannot register fresh
2825
2925
  // work) without disabling delivery. Errors in cleanups are logged, never
2826
2926
  // escalated.
2827
- const unsettledJobPromises = new Set<Promise<void>>();
2927
+ const unsettledExecutionPromises = new Set<Promise<void>>();
2828
2928
  const jobIdsByPromise = new Map<Promise<void>, string>();
2829
- for (const job of this.#jobs.values()) {
2830
- const promise = job.promise;
2831
- unsettledJobPromises.add(promise);
2832
- jobIdsByPromise.set(promise, job.id);
2929
+ for (const execution of this.#activeExecutions) {
2930
+ const promise = execution.physicalCompletion;
2931
+ unsettledExecutionPromises.add(promise);
2932
+ jobIdsByPromise.set(promise, execution.job.id);
2833
2933
  void promise.then(
2834
- () => unsettledJobPromises.delete(promise),
2835
- () => unsettledJobPromises.delete(promise),
2934
+ () => unsettledExecutionPromises.delete(promise),
2935
+ () => unsettledExecutionPromises.delete(promise),
2836
2936
  );
2837
2937
  }
2838
2938
  this.#registrationClosed = true;
@@ -2861,12 +2961,12 @@ export class AsyncJobManager {
2861
2961
  const remainingDeliveryMs = Math.max(0, disposalDeadline - Date.now());
2862
2962
  const drained = await this.drainDeliveries({ timeoutMs: remainingDeliveryMs });
2863
2963
  if (!drained) this.#projectUndeliveredDisposalFailures();
2864
- const retainedAuthority = new Set<Promise<void>>(unsettledJobPromises);
2964
+ const retainedAuthority = new Set<Promise<void>>(unsettledExecutionPromises);
2865
2965
  for (const delivery of this.#inFlightDeliveries) {
2866
2966
  if (delivery.promise) retainedAuthority.add(delivery.promise);
2867
2967
  }
2868
2968
  this.#retainedDisposalCompletion = Promise.allSettled(retainedAuthority).then(() => {});
2869
- const physicalStuckJobIds = [...unsettledJobPromises]
2969
+ const physicalStuckJobIds = [...unsettledExecutionPromises]
2870
2970
  .map(promise => jobIdsByPromise.get(promise))
2871
2971
  .filter((id): id is string => id !== undefined);
2872
2972
  const stuckJobIds = [...new Set([...waitResult.stuckJobIds, ...physicalStuckJobIds])];
@@ -2881,6 +2981,7 @@ export class AsyncJobManager {
2881
2981
  this.#clearEvictionTimers();
2882
2982
  this.#disposed = true;
2883
2983
  this.#jobs.clear();
2984
+ this.#settledJobs.clear();
2884
2985
  this.#deliveries.length = 0;
2885
2986
  this.#inFlightDeliveries.length = 0;
2886
2987
  this.#deadLetteredDeliveries.clear();
@@ -2968,11 +3069,13 @@ export class AsyncJobManager {
2968
3069
  return true;
2969
3070
  }
2970
3071
 
2971
- #scheduleEviction(jobId: string): void {
2972
- if (this.#disposed) return;
3072
+ #scheduleEviction(job: AsyncJob): void {
3073
+ const jobId = job.id;
3074
+ if (this.#disposed || this.#jobs.get(jobId) !== job) return;
2973
3075
  this.#notifyChange();
3076
+ if (this.#disposed || this.#jobs.get(jobId) !== job) return;
2974
3077
  if (this.#retentionMs <= 0) {
2975
- this.#evictJob(jobId);
3078
+ this.#evictJob(job);
2976
3079
  // The terminal notification above precedes eviction so consumers can see
2977
3080
  // the terminal transition; publish once more after the record disappears
2978
3081
  // so observers can project any delivery that still owns the generation.
@@ -2984,28 +3087,32 @@ export class AsyncJobManager {
2984
3087
  clearTimeout(existing);
2985
3088
  }
2986
3089
  const timer = setTimeout(() => {
3090
+ if (this.#evictionTimers.get(jobId) !== timer) return;
2987
3091
  this.#evictionTimers.delete(jobId);
2988
- this.#evictJob(jobId);
3092
+ this.#evictJob(job);
2989
3093
  this.#notifyChange();
2990
3094
  }, this.#retentionMs);
2991
3095
  timer.unref();
2992
3096
  this.#evictionTimers.set(jobId, timer);
2993
3097
  }
2994
3098
 
2995
- #evictJob(jobId: string): void {
3099
+ #evictJob(job: AsyncJob): void {
3100
+ const jobId = job.id;
3101
+ if (this.#jobs.get(jobId) !== job) return;
2996
3102
  this.#expireMonitorTombstones();
2997
- this.#recordMonitorTombstone(jobId);
2998
- this.#runLifecycle(jobId, "evict");
2999
- this.#purgeTerminalSubagentStateForJob(jobId);
3000
- const job = this.#jobs.get(jobId);
3103
+ this.#recordMonitorTombstone(job);
3104
+ const execution = this.#executionsByJob.get(job);
3105
+ if (execution && !execution.physicallySettled) execution.pendingEvictionCleanup = true;
3106
+ else this.#finishEvictionCleanup(job);
3107
+ if (this.#jobs.get(jobId) !== job) return;
3001
3108
  const deadLetter = this.#deadLetteredDeliveries.get(jobId);
3002
3109
  const failure =
3003
- job?.deliveryFailure && (!deadLetter || deadLetter.generation === job.generation)
3110
+ job.deliveryFailure && (!deadLetter || deadLetter.generation === job.generation)
3004
3111
  ? job.deliveryFailure
3005
- : deadLetter && job?.generation === deadLetter.generation
3112
+ : deadLetter && job.generation === deadLetter.generation
3006
3113
  ? deadLetter
3007
3114
  : undefined;
3008
- if (job && failure) {
3115
+ if (failure) {
3009
3116
  this.#evictedDeadLetters.set(job.generation, {
3010
3117
  jobId: job.id,
3011
3118
  generation: job.generation,
@@ -3023,13 +3130,12 @@ export class AsyncJobManager {
3023
3130
  this.#evictedDeadLetters.delete(oldestGeneration);
3024
3131
  }
3025
3132
  }
3133
+ if (this.#jobs.get(jobId) !== job) return;
3026
3134
  this.#jobs.delete(jobId);
3027
- this.#settledJobIds.delete(jobId);
3028
- this.#lifecycles.delete(jobId);
3029
- this.#lifecyclePhases.delete(jobId);
3135
+ this.#settledJobs.delete(job);
3030
3136
  this.#deadLetteredDeliveries.delete(jobId);
3031
3137
  this.#deadLetteredDeliveryOwners.delete(jobId);
3032
- if (job) this.#publishedTerminalGenerations.delete(job.generation);
3138
+ this.#publishedTerminalGenerations.delete(job.generation);
3033
3139
  this.#outputState.delete(jobId);
3034
3140
  }
3035
3141
 
@@ -3337,16 +3443,22 @@ export class AsyncJobManager {
3337
3443
  return;
3338
3444
  }
3339
3445
 
3340
- this.#deliveryLoop = this.#runDeliveryLoop()
3446
+ const completion = Promise.withResolvers<void>();
3447
+ const promise = completion.promise;
3448
+ this.#deliveryLoop = promise;
3449
+ const operation = this.#runDeliveryLoop()
3341
3450
  .catch(error => {
3342
3451
  logger.error("Async job delivery loop crashed", { error: String(error) });
3343
3452
  })
3344
3453
  .finally(() => {
3345
- this.#deliveryLoop = undefined;
3346
- if (!this.#disposed && this.#hasDeliverable()) {
3347
- this.#ensureDeliveryLoop();
3454
+ if (this.#deliveryLoop === promise) {
3455
+ this.#deliveryLoop = undefined;
3456
+ if (!this.#disposed && this.#hasDeliverable()) {
3457
+ this.#ensureDeliveryLoop();
3458
+ }
3348
3459
  }
3349
3460
  });
3461
+ void operation.then(completion.resolve, completion.reject);
3350
3462
  }
3351
3463
 
3352
3464
  async #runDeliveryLoop(): Promise<void> {
@@ -3366,14 +3478,15 @@ export class AsyncJobManager {
3366
3478
  continue;
3367
3479
 
3368
3480
  this.#deliveries.splice(index, 1);
3369
- this.#notifyChange();
3370
3481
  await this.#deliverDelivery(delivery);
3371
3482
  }
3372
3483
  }
3373
3484
 
3374
3485
  #deliverDelivery(delivery: AsyncJobDelivery): Promise<void> {
3375
- const promise = (async () => {
3376
- this.#inFlightDeliveries.push(delivery);
3486
+ const completion = Promise.withResolvers<void>();
3487
+ delivery.promise = completion.promise;
3488
+ this.#inFlightDeliveries.push(delivery);
3489
+ const operation = (async () => {
3377
3490
  this.#notifyChange();
3378
3491
  try {
3379
3492
  const currentJob = this.#jobs.get(delivery.jobId);
@@ -3426,12 +3539,14 @@ export class AsyncJobManager {
3426
3539
  if (!this.#disposed && this.#hasDeliverable()) this.#ensureDeliveryLoop();
3427
3540
  }
3428
3541
  })();
3429
- delivery.promise = promise;
3430
- return promise;
3542
+ void operation.then(completion.resolve, completion.reject);
3543
+ return completion.promise;
3431
3544
  }
3432
3545
 
3433
3546
  async #waitForDeliveryPromise(promise: Promise<void> | undefined, deadline: number): Promise<boolean> {
3434
- if (!promise) return true;
3547
+ // Missing an enrolled in-flight future is uncertainty, never evidence of
3548
+ // successful settlement.
3549
+ if (!promise) return false;
3435
3550
  if (deadline === Number.POSITIVE_INFINITY) {
3436
3551
  await promise;
3437
3552
  return true;