@bastani/atomic 0.9.3 → 0.9.4-alpha.10

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 (312) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/dist/builtin/cursor/CHANGELOG.md +30 -0
  3. package/dist/builtin/cursor/package.json +4 -4
  4. package/dist/builtin/intercom/CHANGELOG.md +30 -0
  5. package/dist/builtin/intercom/package.json +2 -2
  6. package/dist/builtin/mcp/CHANGELOG.md +30 -0
  7. package/dist/builtin/mcp/package.json +4 -4
  8. package/dist/builtin/subagents/CHANGELOG.md +51 -0
  9. package/dist/builtin/subagents/agents/code-simplifier.md +2 -2
  10. package/dist/builtin/subagents/agents/codebase-analyzer.md +2 -2
  11. package/dist/builtin/subagents/agents/codebase-locator.md +2 -2
  12. package/dist/builtin/subagents/agents/codebase-online-researcher.md +2 -2
  13. package/dist/builtin/subagents/agents/codebase-pattern-finder.md +2 -2
  14. package/dist/builtin/subagents/agents/codebase-research-analyzer.md +2 -2
  15. package/dist/builtin/subagents/agents/codebase-research-locator.md +2 -2
  16. package/dist/builtin/subagents/agents/debugger.md +1 -1
  17. package/dist/builtin/subagents/agents/worker.md +2 -2
  18. package/dist/builtin/subagents/package.json +4 -4
  19. package/dist/builtin/subagents/src/runs/background/async-execution-chain.ts +11 -1
  20. package/dist/builtin/subagents/src/runs/background/async-execution-single.ts +11 -1
  21. package/dist/builtin/subagents/src/runs/background/async-execution-types.ts +2 -0
  22. package/dist/builtin/subagents/src/runs/background/subagent-runner-step.ts +27 -4
  23. package/dist/builtin/subagents/src/runs/background/subagent-runner-streaming.ts +25 -0
  24. package/dist/builtin/subagents/src/runs/foreground/chain-execution-dynamic-step.ts +1 -0
  25. package/dist/builtin/subagents/src/runs/foreground/chain-execution-parallel-runner.ts +1 -0
  26. package/dist/builtin/subagents/src/runs/foreground/chain-execution-parallel-step.ts +1 -0
  27. package/dist/builtin/subagents/src/runs/foreground/chain-execution-sequential-step.ts +1 -0
  28. package/dist/builtin/subagents/src/runs/foreground/chain-execution-types.ts +2 -0
  29. package/dist/builtin/subagents/src/runs/foreground/chain-execution.ts +3 -1
  30. package/dist/builtin/subagents/src/runs/foreground/execution-attempt.ts +22 -0
  31. package/dist/builtin/subagents/src/runs/foreground/execution-run-sync.ts +15 -5
  32. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-async.ts +5 -1
  33. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-parallel-task.ts +2 -0
  34. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-parallel.ts +4 -1
  35. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-single.ts +4 -1
  36. package/dist/builtin/subagents/src/runs/shared/attempt-watchdog.ts +126 -0
  37. package/dist/builtin/subagents/src/runs/shared/model-candidate-filter.ts +53 -0
  38. package/dist/builtin/subagents/src/runs/shared/model-fallback.ts +52 -53
  39. package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +2 -0
  40. package/dist/builtin/subagents/src/shared/model-info.ts +16 -0
  41. package/dist/builtin/subagents/src/shared/types-config.ts +2 -0
  42. package/dist/builtin/web-access/CHANGELOG.md +30 -0
  43. package/dist/builtin/web-access/package.json +2 -2
  44. package/dist/builtin/workflows/CHANGELOG.md +54 -0
  45. package/dist/builtin/workflows/README.md +4 -4
  46. package/dist/builtin/workflows/builtin/deep-research-codebase-utils.ts +24 -22
  47. package/dist/builtin/workflows/builtin/goal-runner.ts +12 -23
  48. package/dist/builtin/workflows/builtin/open-claude-design-runner.ts +4 -4
  49. package/dist/builtin/workflows/builtin/ralph-models.ts +53 -75
  50. package/dist/builtin/workflows/package.json +2 -2
  51. package/dist/builtin/workflows/src/engine/run-durable-finalize.ts +4 -2
  52. package/dist/builtin/workflows/src/engine/run-returned-status.ts +41 -0
  53. package/dist/builtin/workflows/src/engine/run.ts +6 -4
  54. package/dist/builtin/workflows/src/extension/config-file-loader.ts +1 -1
  55. package/dist/builtin/workflows/src/extension/config-loader.ts +1 -1
  56. package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +37 -12
  57. package/dist/builtin/workflows/src/runs/shared/model-fallback-failures.ts +59 -1
  58. package/dist/builtin/workflows/src/shared/persistence-restore-helpers.ts +1 -1
  59. package/dist/builtin/workflows/src/shared/store-public-types.ts +3 -2
  60. package/dist/builtin/workflows/src/shared/store-run-methods.ts +6 -1
  61. package/dist/bun/cli.d.ts.map +1 -1
  62. package/dist/bun/cli.js +3 -2
  63. package/dist/bun/cli.js.map +1 -1
  64. package/dist/cli/file-processor.d.ts.map +1 -1
  65. package/dist/cli/file-processor.js +13 -25
  66. package/dist/cli/file-processor.js.map +1 -1
  67. package/dist/cli.d.ts.map +1 -1
  68. package/dist/cli.js +15 -5
  69. package/dist/cli.js.map +1 -1
  70. package/dist/config.d.ts +10 -3
  71. package/dist/config.d.ts.map +1 -1
  72. package/dist/config.js +18 -1
  73. package/dist/config.js.map +1 -1
  74. package/dist/core/agent-session-auto-compaction.d.ts.map +1 -1
  75. package/dist/core/agent-session-auto-compaction.js +8 -1
  76. package/dist/core/agent-session-auto-compaction.js.map +1 -1
  77. package/dist/core/agent-session-events.d.ts.map +1 -1
  78. package/dist/core/agent-session-events.js +14 -8
  79. package/dist/core/agent-session-events.js.map +1 -1
  80. package/dist/core/agent-session-export.d.ts.map +1 -1
  81. package/dist/core/agent-session-export.js +4 -2
  82. package/dist/core/agent-session-export.js.map +1 -1
  83. package/dist/core/agent-session-extension-bindings.d.ts +5 -1
  84. package/dist/core/agent-session-extension-bindings.d.ts.map +1 -1
  85. package/dist/core/agent-session-extension-bindings.js +18 -7
  86. package/dist/core/agent-session-extension-bindings.js.map +1 -1
  87. package/dist/core/agent-session-methods.d.ts +9 -2
  88. package/dist/core/agent-session-methods.d.ts.map +1 -1
  89. package/dist/core/agent-session-methods.js.map +1 -1
  90. package/dist/core/agent-session-prompt.d.ts +2 -0
  91. package/dist/core/agent-session-prompt.d.ts.map +1 -1
  92. package/dist/core/agent-session-prompt.js +15 -2
  93. package/dist/core/agent-session-prompt.js.map +1 -1
  94. package/dist/core/agent-session-retry.d.ts +2 -0
  95. package/dist/core/agent-session-retry.d.ts.map +1 -1
  96. package/dist/core/agent-session-retry.js +66 -7
  97. package/dist/core/agent-session-retry.js.map +1 -1
  98. package/dist/core/agent-session-state.d.ts.map +1 -1
  99. package/dist/core/agent-session-state.js +2 -2
  100. package/dist/core/agent-session-state.js.map +1 -1
  101. package/dist/core/agent-session-tool-hooks.d.ts +7 -1
  102. package/dist/core/agent-session-tool-hooks.d.ts.map +1 -1
  103. package/dist/core/agent-session-tool-hooks.js +26 -4
  104. package/dist/core/agent-session-tool-hooks.js.map +1 -1
  105. package/dist/core/agent-session-tree.d.ts.map +1 -1
  106. package/dist/core/agent-session-tree.js +3 -1
  107. package/dist/core/agent-session-tree.js.map +1 -1
  108. package/dist/core/agent-session-types.d.ts +4 -0
  109. package/dist/core/agent-session-types.d.ts.map +1 -1
  110. package/dist/core/agent-session-types.js.map +1 -1
  111. package/dist/core/agent-session.d.ts +1 -0
  112. package/dist/core/agent-session.d.ts.map +1 -1
  113. package/dist/core/agent-session.js +1 -0
  114. package/dist/core/agent-session.js.map +1 -1
  115. package/dist/core/copilot-model-catalog.d.ts +38 -74
  116. package/dist/core/copilot-model-catalog.d.ts.map +1 -1
  117. package/dist/core/copilot-model-catalog.js +177 -68
  118. package/dist/core/copilot-model-catalog.js.map +1 -1
  119. package/dist/core/copilot-model-static-fallbacks.d.ts +43 -0
  120. package/dist/core/copilot-model-static-fallbacks.d.ts.map +1 -0
  121. package/dist/core/copilot-model-static-fallbacks.js +50 -0
  122. package/dist/core/copilot-model-static-fallbacks.js.map +1 -0
  123. package/dist/core/copilot-model-synthesis.d.ts +10 -0
  124. package/dist/core/copilot-model-synthesis.d.ts.map +1 -0
  125. package/dist/core/copilot-model-synthesis.js +89 -0
  126. package/dist/core/copilot-model-synthesis.js.map +1 -0
  127. package/dist/core/extensions/api-types.d.ts +2 -1
  128. package/dist/core/extensions/api-types.d.ts.map +1 -1
  129. package/dist/core/extensions/api-types.js.map +1 -1
  130. package/dist/core/extensions/index.d.ts +1 -1
  131. package/dist/core/extensions/index.d.ts.map +1 -1
  132. package/dist/core/extensions/index.js.map +1 -1
  133. package/dist/core/extensions/loader-core.d.ts.map +1 -1
  134. package/dist/core/extensions/loader-core.js +3 -3
  135. package/dist/core/extensions/loader-core.js.map +1 -1
  136. package/dist/core/extensions/loader-virtual-modules.d.ts +11 -0
  137. package/dist/core/extensions/loader-virtual-modules.d.ts.map +1 -1
  138. package/dist/core/extensions/loader-virtual-modules.js +79 -11
  139. package/dist/core/extensions/loader-virtual-modules.js.map +1 -1
  140. package/dist/core/extensions/session-events.d.ts +7 -1
  141. package/dist/core/extensions/session-events.d.ts.map +1 -1
  142. package/dist/core/extensions/session-events.js.map +1 -1
  143. package/dist/core/http-dispatcher.d.ts +5 -3
  144. package/dist/core/http-dispatcher.d.ts.map +1 -1
  145. package/dist/core/http-dispatcher.js +56 -10
  146. package/dist/core/http-dispatcher.js.map +1 -1
  147. package/dist/core/model-registry-builtins.d.ts.map +1 -1
  148. package/dist/core/model-registry-builtins.js +40 -6
  149. package/dist/core/model-registry-builtins.js.map +1 -1
  150. package/dist/core/model-registry.d.ts.map +1 -1
  151. package/dist/core/model-registry.js +4 -3
  152. package/dist/core/model-registry.js.map +1 -1
  153. package/dist/core/model-resolver-defaults.d.ts.map +1 -1
  154. package/dist/core/model-resolver-defaults.js +1 -1
  155. package/dist/core/model-resolver-defaults.js.map +1 -1
  156. package/dist/core/resource-loader-reload.d.ts.map +1 -1
  157. package/dist/core/resource-loader-reload.js +6 -3
  158. package/dist/core/resource-loader-reload.js.map +1 -1
  159. package/dist/core/resource-loader-types.d.ts +6 -0
  160. package/dist/core/resource-loader-types.d.ts.map +1 -1
  161. package/dist/core/resource-loader-types.js.map +1 -1
  162. package/dist/core/session-manager-core.d.ts +2 -12
  163. package/dist/core/session-manager-core.d.ts.map +1 -1
  164. package/dist/core/session-manager-core.js +8 -16
  165. package/dist/core/session-manager-core.js.map +1 -1
  166. package/dist/core/settings-manager-basic-accessors.d.ts.map +1 -1
  167. package/dist/core/settings-manager-basic-accessors.js +1 -1
  168. package/dist/core/settings-manager-basic-accessors.js.map +1 -1
  169. package/dist/core/settings-manager-resource-accessors.d.ts +1 -0
  170. package/dist/core/settings-manager-resource-accessors.d.ts.map +1 -1
  171. package/dist/core/settings-manager-resource-accessors.js +11 -0
  172. package/dist/core/settings-manager-resource-accessors.js.map +1 -1
  173. package/dist/core/settings-manager-ui-accessors.d.ts +2 -0
  174. package/dist/core/settings-manager-ui-accessors.d.ts.map +1 -1
  175. package/dist/core/settings-manager-ui-accessors.js +9 -0
  176. package/dist/core/settings-manager-ui-accessors.js.map +1 -1
  177. package/dist/core/settings-types.d.ts +2 -0
  178. package/dist/core/settings-types.d.ts.map +1 -1
  179. package/dist/core/settings-types.js.map +1 -1
  180. package/dist/core/timings.d.ts +7 -5
  181. package/dist/core/timings.d.ts.map +1 -1
  182. package/dist/core/timings.js +42 -24
  183. package/dist/core/timings.js.map +1 -1
  184. package/dist/core/tools/bash-pty-native.d.ts.map +1 -1
  185. package/dist/core/tools/bash-pty-native.js +2 -2
  186. package/dist/core/tools/bash-pty-native.js.map +1 -1
  187. package/dist/core/tools/read-selectors.d.ts.map +1 -1
  188. package/dist/core/tools/read-selectors.js +19 -1
  189. package/dist/core/tools/read-selectors.js.map +1 -1
  190. package/dist/core/tools/read.d.ts.map +1 -1
  191. package/dist/core/tools/read.js +12 -23
  192. package/dist/core/tools/read.js.map +1 -1
  193. package/dist/core/tools/search-native.d.ts.map +1 -1
  194. package/dist/core/tools/search-native.js +2 -2
  195. package/dist/core/tools/search-native.js.map +1 -1
  196. package/dist/index-extensions.d.ts +1 -1
  197. package/dist/index-extensions.d.ts.map +1 -1
  198. package/dist/index-extensions.js.map +1 -1
  199. package/dist/index.d.ts +1 -1
  200. package/dist/index.d.ts.map +1 -1
  201. package/dist/index.js.map +1 -1
  202. package/dist/main-session.d.ts.map +1 -1
  203. package/dist/main-session.js +14 -5
  204. package/dist/main-session.js.map +1 -1
  205. package/dist/main.d.ts.map +1 -1
  206. package/dist/main.js +56 -32
  207. package/dist/main.js.map +1 -1
  208. package/dist/modes/interactive/components/assistant-message.d.ts +3 -1
  209. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  210. package/dist/modes/interactive/components/assistant-message.js +22 -15
  211. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  212. package/dist/modes/interactive/components/chat-message-renderer.d.ts +1 -0
  213. package/dist/modes/interactive/components/chat-message-renderer.d.ts.map +1 -1
  214. package/dist/modes/interactive/components/chat-message-renderer.js +5 -5
  215. package/dist/modes/interactive/components/chat-message-renderer.js.map +1 -1
  216. package/dist/modes/interactive/components/chat-session-host-events.d.ts.map +1 -1
  217. package/dist/modes/interactive/components/chat-session-host-events.js +8 -0
  218. package/dist/modes/interactive/components/chat-session-host-events.js.map +1 -1
  219. package/dist/modes/interactive/components/chat-session-host-rendering.d.ts.map +1 -1
  220. package/dist/modes/interactive/components/chat-session-host-rendering.js +1 -0
  221. package/dist/modes/interactive/components/chat-session-host-rendering.js.map +1 -1
  222. package/dist/modes/interactive/components/user-message.d.ts +6 -2
  223. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  224. package/dist/modes/interactive/components/user-message.js +16 -5
  225. package/dist/modes/interactive/components/user-message.js.map +1 -1
  226. package/dist/modes/interactive/interactive-agent-events.d.ts.map +1 -1
  227. package/dist/modes/interactive/interactive-agent-events.js +6 -1
  228. package/dist/modes/interactive/interactive-agent-events.js.map +1 -1
  229. package/dist/modes/interactive/interactive-deferred-startup.d.ts +2 -0
  230. package/dist/modes/interactive/interactive-deferred-startup.d.ts.map +1 -0
  231. package/dist/modes/interactive/interactive-deferred-startup.js +59 -0
  232. package/dist/modes/interactive/interactive-deferred-startup.js.map +1 -0
  233. package/dist/modes/interactive/interactive-editor-actions.d.ts.map +1 -1
  234. package/dist/modes/interactive/interactive-editor-actions.js +1 -0
  235. package/dist/modes/interactive/interactive-editor-actions.js.map +1 -1
  236. package/dist/modes/interactive/interactive-extension-context.d.ts.map +1 -1
  237. package/dist/modes/interactive/interactive-extension-context.js +1 -0
  238. package/dist/modes/interactive/interactive-extension-context.js.map +1 -1
  239. package/dist/modes/interactive/interactive-mode-base.d.ts +3 -0
  240. package/dist/modes/interactive/interactive-mode-base.d.ts.map +1 -1
  241. package/dist/modes/interactive/interactive-mode-base.js +6 -0
  242. package/dist/modes/interactive/interactive-mode-base.js.map +1 -1
  243. package/dist/modes/interactive/interactive-mode-deps.d.ts +1 -1
  244. package/dist/modes/interactive/interactive-mode-deps.d.ts.map +1 -1
  245. package/dist/modes/interactive/interactive-mode-deps.js +1 -1
  246. package/dist/modes/interactive/interactive-mode-deps.js.map +1 -1
  247. package/dist/modes/interactive/interactive-mode-surface.d.ts +2 -0
  248. package/dist/modes/interactive/interactive-mode-surface.d.ts.map +1 -1
  249. package/dist/modes/interactive/interactive-mode-surface.js.map +1 -1
  250. package/dist/modes/interactive/interactive-mode-types.d.ts +2 -0
  251. package/dist/modes/interactive/interactive-mode-types.d.ts.map +1 -1
  252. package/dist/modes/interactive/interactive-mode-types.js.map +1 -1
  253. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  254. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  255. package/dist/modes/interactive/interactive-mode.js +1 -0
  256. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  257. package/dist/modes/interactive/interactive-model-routing.d.ts.map +1 -1
  258. package/dist/modes/interactive/interactive-model-routing.js +2 -4
  259. package/dist/modes/interactive/interactive-model-routing.js.map +1 -1
  260. package/dist/modes/interactive/interactive-render-chat.d.ts.map +1 -1
  261. package/dist/modes/interactive/interactive-render-chat.js +4 -3
  262. package/dist/modes/interactive/interactive-render-chat.js.map +1 -1
  263. package/dist/modes/interactive/interactive-session-runtime.d.ts.map +1 -1
  264. package/dist/modes/interactive/interactive-session-runtime.js +5 -2
  265. package/dist/modes/interactive/interactive-session-runtime.js.map +1 -1
  266. package/dist/modes/interactive/interactive-startup.d.ts.map +1 -1
  267. package/dist/modes/interactive/interactive-startup.js +11 -1
  268. package/dist/modes/interactive/interactive-startup.js.map +1 -1
  269. package/dist/modes/print-mode.d.ts.map +1 -1
  270. package/dist/modes/print-mode.js +15 -13
  271. package/dist/modes/print-mode.js.map +1 -1
  272. package/dist/modes/rpc/rpc-client.d.ts +28 -51
  273. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  274. package/dist/modes/rpc/rpc-client.js +27 -51
  275. package/dist/modes/rpc/rpc-client.js.map +1 -1
  276. package/dist/modes/rpc/rpc-command-handler.d.ts.map +1 -1
  277. package/dist/modes/rpc/rpc-command-handler.js +19 -0
  278. package/dist/modes/rpc/rpc-command-handler.js.map +1 -1
  279. package/dist/modes/rpc/rpc-types.d.ts +26 -0
  280. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  281. package/dist/modes/rpc/rpc-types.js.map +1 -1
  282. package/dist/rpc-entry.d.ts +3 -0
  283. package/dist/rpc-entry.d.ts.map +1 -0
  284. package/dist/rpc-entry.js +12 -0
  285. package/dist/rpc-entry.js.map +1 -0
  286. package/dist/utils/image-convert.d.ts +1 -0
  287. package/dist/utils/image-convert.d.ts.map +1 -1
  288. package/dist/utils/image-convert.js +21 -15
  289. package/dist/utils/image-convert.js.map +1 -1
  290. package/dist/utils/image-process.d.ts +18 -0
  291. package/dist/utils/image-process.d.ts.map +1 -0
  292. package/dist/utils/image-process.js +83 -0
  293. package/dist/utils/image-process.js.map +1 -0
  294. package/dist/utils/mime.d.ts.map +1 -1
  295. package/dist/utils/mime.js +41 -0
  296. package/dist/utils/mime.js.map +1 -1
  297. package/dist/utils/module-require.d.ts +11 -0
  298. package/dist/utils/module-require.d.ts.map +1 -0
  299. package/dist/utils/module-require.js +16 -0
  300. package/dist/utils/module-require.js.map +1 -0
  301. package/docs/compaction.md +2 -0
  302. package/docs/development.md +10 -0
  303. package/docs/extensions.md +11 -0
  304. package/docs/models.md +3 -3
  305. package/docs/providers.md +2 -1
  306. package/docs/rpc.md +60 -2
  307. package/docs/sdk.md +1 -1
  308. package/docs/settings.md +8 -5
  309. package/docs/subagents.md +6 -0
  310. package/docs/workflows.md +9 -5
  311. package/npm-shrinkwrap.json +4266 -0
  312. package/package.json +23 -13
@@ -26,6 +26,7 @@ export interface AsyncChainParams {
26
26
  agents: AgentConfig[];
27
27
  ctx: AsyncExecutionContext;
28
28
  availableModels?: AvailableModelInfo[];
29
+ knownModelProviders?: string[];
29
30
  cwd?: string;
30
31
  maxOutput?: MaxOutputConfig;
31
32
  artifactsDir?: string;
@@ -62,6 +63,7 @@ export interface AsyncSingleParams {
62
63
  outputMode?: "inline" | "file-only";
63
64
  modelOverride?: string;
64
65
  availableModels?: AvailableModelInfo[];
66
+ knownModelProviders?: string[];
65
67
  maxSubagentDepth: number;
66
68
  workflowStageSubagentGuard?: boolean;
67
69
  worktreeSetupHook?: string;
@@ -17,7 +17,7 @@ import {
17
17
  import { formatModelAttemptNote, isRetryableModelFailure } from "../shared/model-fallback.ts";
18
18
  import type { ArtifactPaths, ModelAttempt } from "../../shared/types.ts";
19
19
  import type { RunPiStreamingResult, SingleStepContext, SubagentStep } from "./subagent-runner-types.ts";
20
- import { fastModeForStepAttempt } from "./subagent-runner-utils.ts";
20
+ import { emptyUsage, fastModeForStepAttempt } from "./subagent-runner-utils.ts";
21
21
  import { runPiStreaming } from "./subagent-runner-streaming.ts";
22
22
 
23
23
  export { outputEntryFromAsyncResult };
@@ -61,14 +61,26 @@ export async function runSingleStep(
61
61
  }
62
62
  }
63
63
 
64
- const candidates = step.modelCandidates && step.modelCandidates.length > 0
64
+ // `!== undefined` is intentional: an explicitly empty array means every candidate
65
+ // was removed by pre-spawn filtering (see filterSpawnableModelCandidates) and must
66
+ // be respected — do not "simplify" this back to `.length > 0`, which would spawn a
67
+ // doomed default attempt. Pre-spawn filtering always records each removal as a
68
+ // skipped attempt in step.modelAttempts, so an empty array WITHOUT skipped attempts
69
+ // means no candidates were configured at all (no primary model, no fallbacks, no
70
+ // current model); mirror the foreground path (execution-run-sync.ts `modelsToTry`)
71
+ // and run one default-model attempt instead of silently exiting with no attempt.
72
+ // The filtered-to-empty case is surfaced as an error below.
73
+ const preSkippedAttempts = step.modelAttempts ?? [];
74
+ const candidates = step.modelCandidates !== undefined && (step.modelCandidates.length > 0 || preSkippedAttempts.length > 0)
65
75
  ? step.modelCandidates
66
76
  : step.model
67
77
  ? [step.model]
68
78
  : [undefined];
69
79
  const attemptedModels: string[] = [];
70
- const modelAttempts: ModelAttempt[] = [];
71
- const attemptNotes: string[] = [];
80
+ const modelAttempts: ModelAttempt[] = [...preSkippedAttempts];
81
+ const attemptNotes: string[] = modelAttempts
82
+ .filter((attempt) => !attempt.success && attempt.exitCode === null && attempt.error)
83
+ .map((attempt) => `[fallback] ${attempt.error}`);
72
84
  const pendingAttemptNotes: string[] = [];
73
85
  const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
74
86
  let finalResult: RunPiStreamingResult | undefined;
@@ -205,6 +217,17 @@ export async function runSingleStep(
205
217
  if (!tryNextModel) break;
206
218
  }
207
219
 
220
+ if (!finalResult && candidates.length === 0 && modelAttempts.length > 0) {
221
+ finalResult = {
222
+ stderr: "",
223
+ exitCode: 1,
224
+ messages: [],
225
+ usage: emptyUsage(),
226
+ error: "No spawnable subagent model candidates after pre-spawn filtering.",
227
+ finalOutput: "",
228
+ };
229
+ }
230
+
208
231
  const rawOutput = finalResult?.finalOutput ?? "";
209
232
  const outputForPersistence = rawOutput;
210
233
  const resolvedOutput = step.outputPath && finalResult?.exitCode === 0
@@ -12,6 +12,7 @@ import {
12
12
  shouldStartSubagentFinalDrain,
13
13
  } from "../shared/final-drain.ts";
14
14
  import { modelFailureMessage } from "../shared/model-fallback.ts";
15
+ import { createAttemptWatchdog } from "../shared/attempt-watchdog.ts";
15
16
  import type { ChildEvent, ChildEventContext, RunPiStreamingResult } from "./subagent-runner-types.ts";
16
17
  import { emptyUsage } from "./subagent-runner-utils.ts";
17
18
 
@@ -55,6 +56,7 @@ export function runPiStreaming(
55
56
  let assistantError: string | undefined;
56
57
  let assistantFailureSignal: unknown;
57
58
  let interrupted = false;
59
+ let activeToolExecutions = 0;
58
60
  const rawStdoutLines: string[] = [];
59
61
 
60
62
  const writeOutputLine = (line: string) => {
@@ -99,6 +101,11 @@ export function runPiStreaming(
99
101
  appendChildEvent(event);
100
102
  onChildEvent?.(event);
101
103
 
104
+ // Track in-flight tool executions so the idle watchdog does not mistake a
105
+ // slow, quiet tool call for a stalled attempt.
106
+ if (event.type === "tool_execution_start") activeToolExecutions += 1;
107
+ else if (event.type === "tool_execution_end") activeToolExecutions = Math.max(0, activeToolExecutions - 1);
108
+
102
109
  if (event.type === "tool_execution_start" && event.toolName) {
103
110
  const toolArgs = extractToolArgsPreview(event.args ?? {});
104
111
  writeOutputLine(toolArgs ? `${event.toolName}: ${toolArgs}` : event.toolName);
@@ -163,7 +170,22 @@ export function runPiStreaming(
163
170
  let finalHardKillTimer: NodeJS.Timeout | undefined;
164
171
  let settled = false;
165
172
  const clearStdioGuard = attachPostExitStdioGuard(child, { idleMs: 2000, hardMs: 8000 });
173
+ const attemptWatchdog = createAttemptWatchdog({
174
+ child,
175
+ isSettled: () => settled,
176
+ // An in-flight tool execution counts as watchdog activity so a slow, quiet
177
+ // tool call is not mistaken for a stalled attempt. Caveat: the counter only
178
+ // decrements on tool_execution_end, so if a tool ends abnormally without its
179
+ // end event the idle watchdog is deferred indefinitely and the wall-clock cap
180
+ // becomes the sole backstop — do not lower the wall cap assuming idle fires.
181
+ isToolActive: () => activeToolExecutions > 0,
182
+ onTimeout(message) {
183
+ forcedTerminationSignal = true;
184
+ error ??= message;
185
+ },
186
+ });
166
187
  child.stdout.on("data", (chunk: Buffer) => {
188
+ attemptWatchdog.activity();
167
189
  const text = chunk.toString();
168
190
  stdoutBuf += text;
169
191
  const lines = stdoutBuf.split("\n");
@@ -172,6 +194,7 @@ export function runPiStreaming(
172
194
  });
173
195
 
174
196
  child.stderr.on("data", (chunk: Buffer) => {
197
+ attemptWatchdog.activity();
175
198
  processStderrText(chunk.toString());
176
199
  });
177
200
  registerInterrupt?.(() => {
@@ -220,6 +243,7 @@ export function runPiStreaming(
220
243
  registerInterrupt?.(undefined);
221
244
  clearDrainTimers();
222
245
  clearStdioGuard();
246
+ attemptWatchdog.clear();
223
247
  if (stdoutBuf.trim()) processStdoutLine(stdoutBuf);
224
248
  if (stderrBuf.trim()) appendChildLine("subagent.child.stderr", stderrBuf);
225
249
  outputStream.end();
@@ -246,6 +270,7 @@ export function runPiStreaming(
246
270
  registerInterrupt?.(undefined);
247
271
  clearDrainTimers();
248
272
  clearStdioGuard();
273
+ attemptWatchdog.clear();
249
274
  outputStream.end();
250
275
  const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
251
276
  const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
@@ -96,6 +96,7 @@ export async function runDynamicParallelChainStep(input: {
96
96
  agents: context.agents,
97
97
  stepIndex,
98
98
  availableModels: context.availableModels,
99
+ knownModelProviders: context.knownModelProviders,
99
100
  chainDir: context.chainDir,
100
101
  prev: state.prev,
101
102
  originalTask: context.originalTask,
@@ -125,6 +125,7 @@ export async function runParallelChainTasks(input: ParallelChainRunInput): Promi
125
125
  nestedRoute: input.nestedRoute,
126
126
  modelOverride: effectiveModel,
127
127
  availableModels: input.availableModels,
128
+ knownModelProviders: input.knownModelProviders,
128
129
  currentModel: currentModelFullId(input.ctx.model),
129
130
  preferredModelProvider: input.ctx.model?.provider,
130
131
  skills: behavior.skills === false ? [] : behavior.skills,
@@ -78,6 +78,7 @@ export async function runStaticParallelChainStep(input: {
78
78
  agents: context.agents,
79
79
  stepIndex,
80
80
  availableModels: context.availableModels,
81
+ knownModelProviders: context.knownModelProviders,
81
82
  chainDir: context.chainDir,
82
83
  prev: state.prev,
83
84
  originalTask: context.originalTask,
@@ -126,6 +126,7 @@ export async function runSequentialChainStep(input: {
126
126
  nestedRoute: context.params.nestedRoute,
127
127
  modelOverride: effectiveModel,
128
128
  availableModels: context.availableModels,
129
+ knownModelProviders: context.knownModelProviders,
129
130
  currentModel: currentModelFullId(context.ctx.model),
130
131
  preferredModelProvider: context.ctx.model?.provider,
131
132
  skills: behavior.skills === false ? [] : behavior.skills,
@@ -109,6 +109,7 @@ export interface ParallelChainRunInput {
109
109
  agents: AgentConfig[];
110
110
  stepIndex: number;
111
111
  availableModels: ModelInfo[];
112
+ knownModelProviders: string[];
112
113
  chainDir: string;
113
114
  prev: string;
114
115
  originalTask: string;
@@ -179,6 +180,7 @@ export interface ChainRuntimeContext {
179
180
  chainSkills: string[];
180
181
  chainDir: string;
181
182
  availableModels: ModelInfo[];
183
+ knownModelProviders: string[];
182
184
  originalTask: string;
183
185
  chainAgents: string[];
184
186
  chainSteps: ChainStep[];
@@ -2,7 +2,7 @@
2
2
  * Public foreground chain execution API.
3
3
  */
4
4
 
5
- import { toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
5
+ import { collectKnownModelProviders, toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
6
6
  import {
7
7
  createChainDir,
8
8
  isDynamicParallelStep,
@@ -117,6 +117,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
117
117
  let templates: ResolvedTemplates = resolveChainTemplates(chainSteps);
118
118
  let tuiBehaviorOverrides: Awaited<ReturnType<typeof resolveChainClarification>>["tuiBehaviorOverrides"];
119
119
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
120
+ const knownModelProviders = collectKnownModelProviders(ctx.modelRegistry);
120
121
  const context: ChainRuntimeContext = {
121
122
  params,
122
123
  agents,
@@ -140,6 +141,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
140
141
  chainSkills,
141
142
  chainDir,
142
143
  availableModels,
144
+ knownModelProviders,
143
145
  originalTask,
144
146
  chainAgents,
145
147
  chainSteps,
@@ -17,6 +17,7 @@ import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "../shared/pi-a
17
17
  import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
18
18
  import { assistantStopReason, isAssistantFailureStopReason, shouldStartSubagentFinalDrain } from "../shared/final-drain.ts";
19
19
  import { modelFailureMessage } from "../shared/model-fallback.ts";
20
+ import { createAttemptWatchdog } from "../shared/attempt-watchdog.ts";
20
21
  import {
21
22
  createMutatingFailureState,
22
23
  didMutatingToolFail,
@@ -204,6 +205,7 @@ export async function runSingleAttempt(
204
205
  settled = true;
205
206
  clearFinalDrainTimers();
206
207
  clearStdioGuard();
208
+ attemptWatchdog.clear();
207
209
  if (activityTimer) {
208
210
  clearInterval(activityTimer);
209
211
  activityTimer = undefined;
@@ -358,13 +360,31 @@ export async function runSingleAttempt(
358
360
 
359
361
  let stderrBuf = "";
360
362
  const clearStdioGuard = attachPostExitStdioGuard(proc, { idleMs: 2000, hardMs: 8000 });
363
+ const attemptWatchdog = createAttemptWatchdog({
364
+ child: proc,
365
+ isSettled: () => settled || processClosed || detached,
366
+ // A slow, quiet tool call (long build/test run) must not be mistaken for a
367
+ // stalled attempt: an in-flight tool execution counts as watchdog activity.
368
+ // Caveat: `progress.currentTool` is cleared on tool_execution_end, so if a
369
+ // tool ends abnormally without emitting its end event the idle watchdog is
370
+ // deferred indefinitely and the wall-clock cap becomes the sole backstop for
371
+ // this attempt — do not lower the wall cap assuming idle always fires.
372
+ isToolActive: () => progress.currentTool !== undefined,
373
+ onTimeout(message) {
374
+ forcedTerminationSignal = true;
375
+ result.error ??= message;
376
+ progress.error = message;
377
+ },
378
+ });
361
379
  proc.stdout.on("data", (d) => {
380
+ attemptWatchdog.activity();
362
381
  buf += d.toString();
363
382
  const lines = buf.split("\n");
364
383
  buf = lines.pop() || "";
365
384
  lines.forEach(processLine);
366
385
  });
367
386
  proc.stderr.on("data", (d) => {
387
+ attemptWatchdog.activity();
368
388
  stderrBuf += d.toString();
369
389
  });
370
390
  proc.on("exit", () => {
@@ -374,6 +394,7 @@ export async function runSingleAttempt(
374
394
  proc.on("close", (code, signal) => {
375
395
  clearFinalDrainTimers();
376
396
  clearStdioGuard();
397
+ attemptWatchdog.clear();
377
398
  void jsonlWriter.close().catch(() => undefined);
378
399
  cleanupTempDir(tempDir);
379
400
  if (detached) {
@@ -394,6 +415,7 @@ export async function runSingleAttempt(
394
415
  proc.on("error", (error) => {
395
416
  clearFinalDrainTimers();
396
417
  clearStdioGuard();
418
+ attemptWatchdog.clear();
397
419
  void jsonlWriter.close().catch(() => undefined);
398
420
  cleanupTempDir(tempDir);
399
421
  if (!result.error) result.error = error instanceof Error ? error.message : String(error);
@@ -20,6 +20,7 @@ import {
20
20
  formatModelAttemptNote,
21
21
  isRetryableModelFailure,
22
22
  } from "../shared/model-fallback.ts";
23
+ import { filterSpawnableModelCandidates } from "../shared/model-candidate-filter.ts";
23
24
  import { artifactOutputByResult, emptyUsage, modelFailureSignalByResult, sumUsage } from "./execution-utils.ts";
24
25
  import { runSingleAttemptWithStructuredOutputRetries } from "./execution-structured-retries.ts";
25
26
  import { shouldSuppressIntermediateRetryableFailureUpdate } from "./execution-updates.ts";
@@ -65,7 +66,7 @@ export async function runSync(
65
66
  systemPrompt = systemPrompt ? `${systemPrompt}\n\n${skillInjection}` : skillInjection;
66
67
  }
67
68
 
68
- const candidates = buildModelCandidates(
69
+ const rawCandidates = buildModelCandidates(
69
70
  options.modelOverride ?? agent.model,
70
71
  agent.fallbackModels,
71
72
  options.availableModels,
@@ -73,13 +74,20 @@ export async function runSync(
73
74
  options.currentModel,
74
75
  agent.fallbackThinkingLevels,
75
76
  );
77
+ const filteredCandidates = filterSpawnableModelCandidates({
78
+ candidates: rawCandidates,
79
+ availableModels: options.availableModels,
80
+ knownModelProviders: options.knownModelProviders,
81
+ currentModel: options.currentModel,
82
+ });
83
+ const candidates = filteredCandidates.candidates;
76
84
  const fastModeCwd = options.cwd ?? runtimeCwd;
77
85
  const fastModeSettings = getSubagentCodexFastModeSettings(fastModeCwd);
78
86
  const fastModeScope = resolveSubagentCodexFastModeScope(options.workflowStageSubagentGuard);
79
87
  const attemptedModels: string[] = [];
80
- const modelAttempts: ModelAttempt[] = [];
88
+ const modelAttempts: ModelAttempt[] = [...filteredCandidates.skippedAttempts];
81
89
  const aggregateUsage = emptyUsage();
82
- const attemptNotes: string[] = [];
90
+ const attemptNotes: string[] = filteredCandidates.skippedAttempts.map((attempt) => `[fallback] ${attempt.error}`);
83
91
  const pendingAttemptNotes: string[] = [];
84
92
  let totalToolCount = 0;
85
93
  let totalDurationMs = 0;
@@ -96,7 +104,7 @@ export async function runSync(
96
104
  }
97
105
 
98
106
  let lastResult: SingleResult | undefined;
99
- const modelsToTry = candidates.length > 0 ? candidates : [undefined];
107
+ const modelsToTry = candidates.length > 0 ? candidates : (rawCandidates.length === 0 ? [undefined] : []);
100
108
  for (let i = 0; i < modelsToTry.length; i++) {
101
109
  const candidate = modelsToTry[i];
102
110
  if (candidate) attemptedModels.push(candidate);
@@ -156,7 +164,9 @@ export async function runSync(
156
164
  exitCode: 1,
157
165
  messages: [],
158
166
  usage: emptyUsage(),
159
- error: "Subagent did not produce a result.",
167
+ error: modelAttempts.length > 0
168
+ ? "No spawnable subagent model candidates after pre-spawn filtering."
169
+ : "Subagent did not produce a result.",
160
170
  } satisfies SingleResult;
161
171
 
162
172
  result.usage = aggregateUsage;
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { APP_NAME } from "@bastani/atomic";
3
3
  import { currentModelFullId, resolveModelCandidate } from "../shared/model-fallback.ts";
4
- import { toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
4
+ import { collectKnownModelProviders, toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
5
5
  import { normalizeSkillInput } from "../../agents/skills.ts";
6
6
  import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
7
7
  import {
@@ -76,6 +76,7 @@ export function runAsyncPath(data: ExecutionContextData, deps: ResolvedExecutorD
76
76
  currentModel: currentModelFullId(ctx.model),
77
77
  };
78
78
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
79
+ const knownModelProviders = collectKnownModelProviders(ctx.modelRegistry);
79
80
  const depthPolicy = resolveSubagentDepthPolicy(ctx, deps.config.maxSubagentDepth);
80
81
  const currentMaxSubagentDepth = depthPolicy.maxSubagentDepth;
81
82
  const workflowStageSubagentGuard = depthPolicy.workflowStageSubagentGuard;
@@ -110,6 +111,7 @@ export function runAsyncPath(data: ExecutionContextData, deps: ResolvedExecutorD
110
111
  agents,
111
112
  ctx: asyncCtx,
112
113
  availableModels,
114
+ knownModelProviders,
113
115
  cwd: effectiveCwd,
114
116
  maxOutput: params.maxOutput,
115
117
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
@@ -139,6 +141,7 @@ export function runAsyncPath(data: ExecutionContextData, deps: ResolvedExecutorD
139
141
  agents,
140
142
  ctx: asyncCtx,
141
143
  availableModels,
144
+ knownModelProviders,
142
145
  cwd: effectiveCwd,
143
146
  maxOutput: params.maxOutput,
144
147
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
@@ -181,6 +184,7 @@ export function runAsyncPath(data: ExecutionContextData, deps: ResolvedExecutorD
181
184
  agentConfig: a,
182
185
  ctx: asyncCtx,
183
186
  availableModels,
187
+ knownModelProviders,
184
188
  cwd: effectiveCwd,
185
189
  maxOutput: params.maxOutput,
186
190
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
@@ -38,6 +38,7 @@ interface ForegroundParallelRunInput {
38
38
  maxSubagentDepths: number[];
39
39
  workflowStageSubagentGuard?: boolean;
40
40
  availableModels: ModelInfo[];
41
+ knownModelProviders: string[];
41
42
  modelOverrides: (string | undefined)[];
42
43
  behaviors: ResolvedStepBehavior[];
43
44
  firstProgressIndex: number;
@@ -110,6 +111,7 @@ export async function runForegroundParallelTasks(input: ForegroundParallelRunInp
110
111
  nestedRoute: input.foregroundControl?.nestedRoute,
111
112
  modelOverride: input.modelOverrides[index],
112
113
  availableModels: input.availableModels,
114
+ knownModelProviders: input.knownModelProviders,
113
115
  preferredModelProvider: input.ctx.model?.provider,
114
116
  currentModel: currentModelFullId(input.ctx.model),
115
117
  skills: effectiveSkills === false ? [] : effectiveSkills,
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { APP_NAME } from "@bastani/atomic";
3
3
  import { ChainClarifyComponent, type ChainClarifyResult } from "./chain-clarify.ts";
4
4
  import { currentModelFullId, resolveModelCandidate } from "../shared/model-fallback.ts";
5
- import { toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
5
+ import { collectKnownModelProviders, toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
6
6
  import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
7
7
  import { aggregateParallelOutputs } from "../shared/parallel-utils.ts";
8
8
  import { recordRun } from "../shared/run-history.ts";
@@ -108,6 +108,7 @@ export async function runParallelPath(data: ExecutionContextData, deps: Resolved
108
108
 
109
109
  const currentProvider = ctx.model?.provider;
110
110
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
111
+ const knownModelProviders = collectKnownModelProviders(ctx.modelRegistry);
111
112
  let taskTexts = tasks.map((t) => t.task);
112
113
  const skillOverrides: (string[] | false | undefined)[] = tasks.map((t) =>
113
114
  normalizeSkillInput(t.skill),
@@ -205,6 +206,7 @@ export async function runParallelPath(data: ExecutionContextData, deps: Resolved
205
206
  agents,
206
207
  ctx: asyncCtx,
207
208
  availableModels,
209
+ knownModelProviders,
208
210
  cwd: effectiveCwd,
209
211
  maxOutput: params.maxOutput,
210
212
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
@@ -281,6 +283,7 @@ export async function runParallelPath(data: ExecutionContextData, deps: Resolved
281
283
  paramsCwd: effectiveCwd,
282
284
  workflowStageSubagentGuard,
283
285
  availableModels,
286
+ knownModelProviders,
284
287
  modelOverrides,
285
288
  behaviors,
286
289
  firstProgressIndex: parallelProgressPrecreated ? -1 : firstProgressIndex,
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { APP_NAME } from "@bastani/atomic";
3
3
  import { ChainClarifyComponent, type ChainClarifyResult } from "./chain-clarify.ts";
4
4
  import { currentModelFullId, resolveModelCandidate } from "../shared/model-fallback.ts";
5
- import { toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
5
+ import { collectKnownModelProviders, toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
6
6
  import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
7
7
  import { recordRun } from "../shared/run-history.ts";
8
8
  import { getSingleResultOutput, compactForegroundDetails } from "../../shared/utils.ts";
@@ -76,6 +76,7 @@ export async function runSinglePath(data: ExecutionContextData, deps: ResolvedEx
76
76
 
77
77
  const currentProvider = ctx.model?.provider;
78
78
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
79
+ const knownModelProviders = collectKnownModelProviders(ctx.modelRegistry);
79
80
  let task = params.task ?? "";
80
81
  let modelOverride: string | undefined = resolveModelCandidate(
81
82
  (params.model as string | undefined) ?? agentConfig.model,
@@ -145,6 +146,7 @@ export async function runSinglePath(data: ExecutionContextData, deps: ResolvedEx
145
146
  agentConfig,
146
147
  ctx: asyncCtx,
147
148
  availableModels,
149
+ knownModelProviders,
148
150
  cwd: effectiveCwd,
149
151
  maxOutput: params.maxOutput,
150
152
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
@@ -246,6 +248,7 @@ export async function runSinglePath(data: ExecutionContextData, deps: ResolvedEx
246
248
  index: 0,
247
249
  modelOverride,
248
250
  availableModels,
251
+ knownModelProviders,
249
252
  preferredModelProvider: currentProvider,
250
253
  currentModel: currentModelFullId(ctx.model),
251
254
  skills: effectiveSkills,
@@ -0,0 +1,126 @@
1
+ import type { ChildProcess } from "node:child_process";
2
+ import { trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
3
+
4
+ const DEFAULT_IDLE_MS = 5 * 60_000;
5
+ const DEFAULT_WALL_MS = 60 * 60_000;
6
+ const DEFAULT_KILL_GRACE_MS = 3_000;
7
+
8
+ export interface AttemptTimeoutConfig {
9
+ idleMs: number;
10
+ wallMs: number;
11
+ killGraceMs: number;
12
+ }
13
+
14
+ export interface AttemptWatchdog {
15
+ activity(): void;
16
+ clear(): void;
17
+ }
18
+
19
+ /** Parse a millisecond override from the environment. Non-numeric values are
20
+ * silently ignored (the default applies). Zero or negative values are clamped
21
+ * to `0`, which callers treat as "timeout disabled". */
22
+ function envMs(name: string): number | undefined {
23
+ const raw = process.env[name];
24
+ if (!raw) return undefined;
25
+ const value = Number(raw);
26
+ if (!Number.isFinite(value)) return undefined;
27
+ return value > 0 ? Math.floor(value) : 0;
28
+ }
29
+
30
+ function positiveEnvMs(name: string): number | undefined {
31
+ const value = envMs(name);
32
+ return value !== undefined && value > 0 ? value : undefined;
33
+ }
34
+
35
+ export function resolveAttemptTimeoutConfig(): AttemptTimeoutConfig {
36
+ return {
37
+ // `0` (or a negative value) disables the corresponding timeout entirely.
38
+ idleMs: envMs("ATOMIC_SUBAGENT_ATTEMPT_IDLE_TIMEOUT_MS") ?? DEFAULT_IDLE_MS,
39
+ wallMs: envMs("ATOMIC_SUBAGENT_ATTEMPT_TIMEOUT_MS") ?? DEFAULT_WALL_MS,
40
+ // The SIGTERM→SIGKILL grace period intentionally cannot be disabled: once a
41
+ // watchdog trips, escalation must always be bounded. `0`, negative, or
42
+ // non-numeric values fall back to the default.
43
+ killGraceMs: positiveEnvMs("ATOMIC_SUBAGENT_ATTEMPT_KILL_GRACE_MS") ?? DEFAULT_KILL_GRACE_MS,
44
+ };
45
+ }
46
+
47
+ export function idleTimeoutMessage(idleMs: number): string {
48
+ return `Subagent model attempt timed out after ${idleMs}ms without child activity.`;
49
+ }
50
+
51
+ export function wallTimeoutMessage(wallMs: number): string {
52
+ return `Subagent model attempt timed out after ${wallMs}ms.`;
53
+ }
54
+
55
+ export function createAttemptWatchdog(params: {
56
+ child: ChildProcess;
57
+ config?: Partial<AttemptTimeoutConfig>;
58
+ onTimeout: (message: string) => void;
59
+ isSettled: () => boolean;
60
+ /** Reports whether a tool call is currently executing in the child. A slow tool
61
+ * (long build, large test suite) can legitimately stay silent past the idle
62
+ * window, so an in-flight tool execution counts as activity and defers the idle
63
+ * trip. The wall-clock cap still bounds the whole attempt. */
64
+ isToolActive?: () => boolean;
65
+ }): AttemptWatchdog {
66
+ const config = { ...resolveAttemptTimeoutConfig(), ...(params.config ?? {}) };
67
+ let idleTimer: NodeJS.Timeout | undefined;
68
+ let wallTimer: NodeJS.Timeout | undefined;
69
+ let killTimer: NodeJS.Timeout | undefined;
70
+ let tripped = false;
71
+
72
+ const clearIdle = () => {
73
+ if (!idleTimer) return;
74
+ clearTimeout(idleTimer);
75
+ idleTimer = undefined;
76
+ };
77
+ const clearAll = () => {
78
+ clearIdle();
79
+ if (wallTimer) {
80
+ clearTimeout(wallTimer);
81
+ wallTimer = undefined;
82
+ }
83
+ if (killTimer) {
84
+ clearTimeout(killTimer);
85
+ killTimer = undefined;
86
+ }
87
+ };
88
+ const trip = (message: string) => {
89
+ if (tripped || params.isSettled()) return;
90
+ tripped = true;
91
+ clearIdle();
92
+ params.onTimeout(message);
93
+ trySignalChild(params.child, "SIGTERM");
94
+ killTimer = setTimeout(() => {
95
+ if (!params.isSettled()) trySignalChild(params.child, "SIGKILL");
96
+ }, config.killGraceMs);
97
+ killTimer.unref?.();
98
+ };
99
+ const scheduleIdle = () => {
100
+ clearIdle();
101
+ if (config.idleMs <= 0) return; // idle watchdog disabled
102
+ idleTimer = setTimeout(() => {
103
+ if (params.isToolActive?.()) {
104
+ // Do not kill a healthy attempt that is busy inside a slow tool call;
105
+ // re-arm the idle window and let the wall-clock cap bound the attempt.
106
+ scheduleIdle();
107
+ return;
108
+ }
109
+ trip(idleTimeoutMessage(config.idleMs));
110
+ }, config.idleMs);
111
+ idleTimer.unref?.();
112
+ };
113
+
114
+ scheduleIdle();
115
+ if (config.wallMs > 0) {
116
+ wallTimer = setTimeout(() => trip(wallTimeoutMessage(config.wallMs)), config.wallMs);
117
+ wallTimer.unref?.();
118
+ }
119
+
120
+ return {
121
+ activity() {
122
+ if (!tripped && !params.isSettled()) scheduleIdle();
123
+ },
124
+ clear: clearAll,
125
+ };
126
+ }
@@ -0,0 +1,53 @@
1
+ import { splitKnownThinkingSuffix } from "../../shared/model-info.ts";
2
+ import type { ModelAttempt } from "../../shared/types.ts";
3
+ import type { AvailableModelInfo } from "./model-fallback.ts";
4
+
5
+ export interface FilteredModelCandidates {
6
+ candidates: string[];
7
+ skippedAttempts: ModelAttempt[];
8
+ }
9
+
10
+ function providerFromModel(model: string | undefined): string | undefined {
11
+ if (!model) return undefined;
12
+ const { baseModel } = splitKnownThinkingSuffix(model);
13
+ const slash = baseModel.indexOf("/");
14
+ return slash > 0 ? baseModel.slice(0, slash) : undefined;
15
+ }
16
+
17
+ export function skippedModelAttempt(model: string, reason: string): ModelAttempt {
18
+ return {
19
+ model,
20
+ success: false,
21
+ exitCode: null,
22
+ error: reason,
23
+ };
24
+ }
25
+
26
+ export function filterSpawnableModelCandidates(params: {
27
+ candidates: string[];
28
+ availableModels?: AvailableModelInfo[];
29
+ knownModelProviders?: string[];
30
+ currentModel?: string;
31
+ }): FilteredModelCandidates {
32
+ const providersWithAvailableAuth = new Set((params.availableModels ?? []).map((model) => model.provider));
33
+ const knownProviders = new Set(params.knownModelProviders ?? []);
34
+ const currentModel = params.currentModel;
35
+ const filtered: string[] = [];
36
+ const skippedAttempts: ModelAttempt[] = [];
37
+ for (const candidate of params.candidates) {
38
+ if (currentModel && candidate === currentModel) {
39
+ filtered.push(candidate);
40
+ continue;
41
+ }
42
+ const provider = providerFromModel(candidate);
43
+ const shouldSkip = provider !== undefined
44
+ && knownProviders.has(provider)
45
+ && !providersWithAvailableAuth.has(provider);
46
+ if (!shouldSkip) {
47
+ filtered.push(candidate);
48
+ continue;
49
+ }
50
+ skippedAttempts.push(skippedModelAttempt(candidate, `Skipped ${candidate}: provider '${provider}' has no configured API key/auth in the current session.`));
51
+ }
52
+ return { candidates: filtered, skippedAttempts };
53
+ }