@bastani/atomic 0.9.4-alpha.5 → 0.9.4-alpha.7

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 (202) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/dist/builtin/cursor/CHANGELOG.md +6 -0
  3. package/dist/builtin/cursor/package.json +4 -4
  4. package/dist/builtin/intercom/CHANGELOG.md +6 -0
  5. package/dist/builtin/intercom/package.json +2 -2
  6. package/dist/builtin/mcp/CHANGELOG.md +6 -0
  7. package/dist/builtin/mcp/package.json +4 -4
  8. package/dist/builtin/subagents/CHANGELOG.md +20 -0
  9. package/dist/builtin/subagents/package.json +4 -4
  10. package/dist/builtin/subagents/src/runs/background/async-execution-chain.ts +11 -1
  11. package/dist/builtin/subagents/src/runs/background/async-execution-single.ts +11 -1
  12. package/dist/builtin/subagents/src/runs/background/async-execution-types.ts +2 -0
  13. package/dist/builtin/subagents/src/runs/background/subagent-runner-step.ts +27 -4
  14. package/dist/builtin/subagents/src/runs/background/subagent-runner-streaming.ts +25 -0
  15. package/dist/builtin/subagents/src/runs/foreground/chain-execution-dynamic-step.ts +1 -0
  16. package/dist/builtin/subagents/src/runs/foreground/chain-execution-parallel-runner.ts +1 -0
  17. package/dist/builtin/subagents/src/runs/foreground/chain-execution-parallel-step.ts +1 -0
  18. package/dist/builtin/subagents/src/runs/foreground/chain-execution-sequential-step.ts +1 -0
  19. package/dist/builtin/subagents/src/runs/foreground/chain-execution-types.ts +2 -0
  20. package/dist/builtin/subagents/src/runs/foreground/chain-execution.ts +3 -1
  21. package/dist/builtin/subagents/src/runs/foreground/execution-attempt.ts +22 -0
  22. package/dist/builtin/subagents/src/runs/foreground/execution-run-sync.ts +15 -5
  23. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-async.ts +5 -1
  24. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-parallel-task.ts +2 -0
  25. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-parallel.ts +4 -1
  26. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-single.ts +4 -1
  27. package/dist/builtin/subagents/src/runs/shared/attempt-watchdog.ts +126 -0
  28. package/dist/builtin/subagents/src/runs/shared/model-candidate-filter.ts +53 -0
  29. package/dist/builtin/subagents/src/runs/shared/model-fallback.ts +52 -53
  30. package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +2 -0
  31. package/dist/builtin/subagents/src/shared/model-info.ts +16 -0
  32. package/dist/builtin/subagents/src/shared/types-config.ts +2 -0
  33. package/dist/builtin/web-access/CHANGELOG.md +6 -0
  34. package/dist/builtin/web-access/package.json +2 -2
  35. package/dist/builtin/workflows/CHANGELOG.md +12 -0
  36. package/dist/builtin/workflows/package.json +2 -2
  37. package/dist/builtin/workflows/src/runs/shared/model-fallback-failures.ts +34 -4
  38. package/dist/cli/file-processor.d.ts.map +1 -1
  39. package/dist/cli/file-processor.js +13 -25
  40. package/dist/cli/file-processor.js.map +1 -1
  41. package/dist/core/agent-session-extension-bindings.d.ts +2 -0
  42. package/dist/core/agent-session-extension-bindings.d.ts.map +1 -1
  43. package/dist/core/agent-session-extension-bindings.js +11 -3
  44. package/dist/core/agent-session-extension-bindings.js.map +1 -1
  45. package/dist/core/agent-session-methods.d.ts +4 -1
  46. package/dist/core/agent-session-methods.d.ts.map +1 -1
  47. package/dist/core/agent-session-methods.js.map +1 -1
  48. package/dist/core/agent-session-prompt.d.ts.map +1 -1
  49. package/dist/core/agent-session-prompt.js +11 -4
  50. package/dist/core/agent-session-prompt.js.map +1 -1
  51. package/dist/core/agent-session-state.d.ts.map +1 -1
  52. package/dist/core/agent-session-state.js +2 -2
  53. package/dist/core/agent-session-state.js.map +1 -1
  54. package/dist/core/agent-session-tool-hooks.d.ts +7 -1
  55. package/dist/core/agent-session-tool-hooks.d.ts.map +1 -1
  56. package/dist/core/agent-session-tool-hooks.js +26 -4
  57. package/dist/core/agent-session-tool-hooks.js.map +1 -1
  58. package/dist/core/agent-session-tree.d.ts.map +1 -1
  59. package/dist/core/agent-session-tree.js +3 -1
  60. package/dist/core/agent-session-tree.js.map +1 -1
  61. package/dist/core/agent-session.d.ts +1 -0
  62. package/dist/core/agent-session.d.ts.map +1 -1
  63. package/dist/core/agent-session.js +1 -0
  64. package/dist/core/agent-session.js.map +1 -1
  65. package/dist/core/copilot-model-catalog.d.ts +25 -74
  66. package/dist/core/copilot-model-catalog.d.ts.map +1 -1
  67. package/dist/core/copilot-model-catalog.js +118 -68
  68. package/dist/core/copilot-model-catalog.js.map +1 -1
  69. package/dist/core/copilot-model-synthesis.d.ts +10 -0
  70. package/dist/core/copilot-model-synthesis.d.ts.map +1 -0
  71. package/dist/core/copilot-model-synthesis.js +89 -0
  72. package/dist/core/copilot-model-synthesis.js.map +1 -0
  73. package/dist/core/extensions/api-types.d.ts +2 -1
  74. package/dist/core/extensions/api-types.d.ts.map +1 -1
  75. package/dist/core/extensions/api-types.js.map +1 -1
  76. package/dist/core/extensions/index.d.ts +1 -1
  77. package/dist/core/extensions/index.d.ts.map +1 -1
  78. package/dist/core/extensions/index.js.map +1 -1
  79. package/dist/core/extensions/loader-core.d.ts.map +1 -1
  80. package/dist/core/extensions/loader-core.js +3 -3
  81. package/dist/core/extensions/loader-core.js.map +1 -1
  82. package/dist/core/extensions/session-events.d.ts +7 -1
  83. package/dist/core/extensions/session-events.d.ts.map +1 -1
  84. package/dist/core/extensions/session-events.js.map +1 -1
  85. package/dist/core/http-dispatcher.d.ts.map +1 -1
  86. package/dist/core/http-dispatcher.js +28 -1
  87. package/dist/core/http-dispatcher.js.map +1 -1
  88. package/dist/core/model-registry-builtins.d.ts.map +1 -1
  89. package/dist/core/model-registry-builtins.js +21 -3
  90. package/dist/core/model-registry-builtins.js.map +1 -1
  91. package/dist/core/model-resolver-defaults.d.ts.map +1 -1
  92. package/dist/core/model-resolver-defaults.js +1 -1
  93. package/dist/core/model-resolver-defaults.js.map +1 -1
  94. package/dist/core/resource-loader-reload.d.ts.map +1 -1
  95. package/dist/core/resource-loader-reload.js +2 -1
  96. package/dist/core/resource-loader-reload.js.map +1 -1
  97. package/dist/core/session-manager-core.d.ts +2 -12
  98. package/dist/core/session-manager-core.d.ts.map +1 -1
  99. package/dist/core/session-manager-core.js +8 -16
  100. package/dist/core/session-manager-core.js.map +1 -1
  101. package/dist/core/settings-manager-resource-accessors.d.ts +1 -0
  102. package/dist/core/settings-manager-resource-accessors.d.ts.map +1 -1
  103. package/dist/core/settings-manager-resource-accessors.js +11 -0
  104. package/dist/core/settings-manager-resource-accessors.js.map +1 -1
  105. package/dist/core/settings-manager-ui-accessors.d.ts +2 -0
  106. package/dist/core/settings-manager-ui-accessors.d.ts.map +1 -1
  107. package/dist/core/settings-manager-ui-accessors.js +9 -0
  108. package/dist/core/settings-manager-ui-accessors.js.map +1 -1
  109. package/dist/core/settings-types.d.ts +2 -0
  110. package/dist/core/settings-types.d.ts.map +1 -1
  111. package/dist/core/settings-types.js.map +1 -1
  112. package/dist/core/timings.d.ts +7 -5
  113. package/dist/core/timings.d.ts.map +1 -1
  114. package/dist/core/timings.js +42 -24
  115. package/dist/core/timings.js.map +1 -1
  116. package/dist/core/tools/read-selectors.d.ts.map +1 -1
  117. package/dist/core/tools/read-selectors.js +19 -1
  118. package/dist/core/tools/read-selectors.js.map +1 -1
  119. package/dist/core/tools/read.d.ts.map +1 -1
  120. package/dist/core/tools/read.js +12 -23
  121. package/dist/core/tools/read.js.map +1 -1
  122. package/dist/index-extensions.d.ts +1 -1
  123. package/dist/index-extensions.d.ts.map +1 -1
  124. package/dist/index-extensions.js.map +1 -1
  125. package/dist/index.d.ts +1 -1
  126. package/dist/index.d.ts.map +1 -1
  127. package/dist/index.js.map +1 -1
  128. package/dist/main-session.d.ts.map +1 -1
  129. package/dist/main-session.js +14 -5
  130. package/dist/main-session.js.map +1 -1
  131. package/dist/modes/interactive/components/assistant-message.d.ts +3 -1
  132. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  133. package/dist/modes/interactive/components/assistant-message.js +22 -15
  134. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  135. package/dist/modes/interactive/components/chat-message-renderer.d.ts +1 -0
  136. package/dist/modes/interactive/components/chat-message-renderer.d.ts.map +1 -1
  137. package/dist/modes/interactive/components/chat-message-renderer.js +5 -5
  138. package/dist/modes/interactive/components/chat-message-renderer.js.map +1 -1
  139. package/dist/modes/interactive/components/chat-session-host-rendering.d.ts.map +1 -1
  140. package/dist/modes/interactive/components/chat-session-host-rendering.js +1 -0
  141. package/dist/modes/interactive/components/chat-session-host-rendering.js.map +1 -1
  142. package/dist/modes/interactive/components/user-message.d.ts +6 -2
  143. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  144. package/dist/modes/interactive/components/user-message.js +16 -5
  145. package/dist/modes/interactive/components/user-message.js.map +1 -1
  146. package/dist/modes/interactive/interactive-agent-events.d.ts.map +1 -1
  147. package/dist/modes/interactive/interactive-agent-events.js +1 -1
  148. package/dist/modes/interactive/interactive-agent-events.js.map +1 -1
  149. package/dist/modes/interactive/interactive-editor-actions.d.ts.map +1 -1
  150. package/dist/modes/interactive/interactive-editor-actions.js +1 -0
  151. package/dist/modes/interactive/interactive-editor-actions.js.map +1 -1
  152. package/dist/modes/interactive/interactive-extension-context.d.ts.map +1 -1
  153. package/dist/modes/interactive/interactive-extension-context.js +1 -0
  154. package/dist/modes/interactive/interactive-extension-context.js.map +1 -1
  155. package/dist/modes/interactive/interactive-mode-base.d.ts +1 -0
  156. package/dist/modes/interactive/interactive-mode-base.d.ts.map +1 -1
  157. package/dist/modes/interactive/interactive-mode-base.js +2 -0
  158. package/dist/modes/interactive/interactive-mode-base.js.map +1 -1
  159. package/dist/modes/interactive/interactive-model-routing.d.ts.map +1 -1
  160. package/dist/modes/interactive/interactive-model-routing.js +1 -0
  161. package/dist/modes/interactive/interactive-model-routing.js.map +1 -1
  162. package/dist/modes/interactive/interactive-render-chat.d.ts.map +1 -1
  163. package/dist/modes/interactive/interactive-render-chat.js +4 -3
  164. package/dist/modes/interactive/interactive-render-chat.js.map +1 -1
  165. package/dist/modes/interactive/interactive-session-runtime.d.ts.map +1 -1
  166. package/dist/modes/interactive/interactive-session-runtime.js +1 -0
  167. package/dist/modes/interactive/interactive-session-runtime.js.map +1 -1
  168. package/dist/modes/rpc/rpc-client.d.ts +28 -51
  169. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  170. package/dist/modes/rpc/rpc-client.js +27 -51
  171. package/dist/modes/rpc/rpc-client.js.map +1 -1
  172. package/dist/modes/rpc/rpc-command-handler.d.ts.map +1 -1
  173. package/dist/modes/rpc/rpc-command-handler.js +19 -0
  174. package/dist/modes/rpc/rpc-command-handler.js.map +1 -1
  175. package/dist/modes/rpc/rpc-types.d.ts +26 -0
  176. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  177. package/dist/modes/rpc/rpc-types.js.map +1 -1
  178. package/dist/rpc-entry.d.ts +3 -0
  179. package/dist/rpc-entry.d.ts.map +1 -0
  180. package/dist/rpc-entry.js +12 -0
  181. package/dist/rpc-entry.js.map +1 -0
  182. package/dist/utils/image-convert.d.ts +1 -0
  183. package/dist/utils/image-convert.d.ts.map +1 -1
  184. package/dist/utils/image-convert.js +21 -15
  185. package/dist/utils/image-convert.js.map +1 -1
  186. package/dist/utils/image-process.d.ts +18 -0
  187. package/dist/utils/image-process.d.ts.map +1 -0
  188. package/dist/utils/image-process.js +83 -0
  189. package/dist/utils/image-process.js.map +1 -0
  190. package/dist/utils/mime.d.ts.map +1 -1
  191. package/dist/utils/mime.js +41 -0
  192. package/dist/utils/mime.js.map +1 -1
  193. package/docs/extensions.md +11 -0
  194. package/docs/models.md +3 -3
  195. package/docs/providers.md +1 -1
  196. package/docs/rpc.md +60 -2
  197. package/docs/sdk.md +1 -1
  198. package/docs/settings.md +4 -1
  199. package/docs/subagents.md +6 -0
  200. package/docs/workflows.md +2 -0
  201. package/npm-shrinkwrap.json +138 -140
  202. package/package.json +15 -10
package/CHANGELOG.md CHANGED
@@ -2,6 +2,61 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.4-alpha.7] - 2026-07-02
6
+
7
+ ### Added
8
+
9
+ - Added dynamic GitHub Copilot model population from the live CAPI `/models` catalog: picker-enabled, non-disabled plain chat ids are synthesized from catalog metadata (endpoints, capabilities, limits, and display names) while built-in `pi-ai` definitions still win, namespaced enterprise deployments such as `org/deployment/model` are skipped, and cached catalog metadata enables the same models on cold start.
10
+ - Added catalog-driven thinking-level gating for GitHub Copilot models so dynamically synthesized entries and bundled `pi-ai` Copilot models only offer the reasoning levels advertised by CAPI's `capabilities.supports.reasoning_effort` arrays, while models without an effort array keep their existing thinking behavior.
11
+ - Added a subagent watchdog escape hatch: setting `ATOMIC_SUBAGENT_ATTEMPT_IDLE_TIMEOUT_MS` or `ATOMIC_SUBAGENT_ATTEMPT_TIMEOUT_MS` to `0` (or a negative value) now disables the corresponding per-attempt timeout entirely; non-numeric values are ignored and the defaults apply. The `ATOMIC_SUBAGENT_ATTEMPT_KILL_GRACE_MS` SIGTERM→SIGKILL grace period intentionally cannot be disabled — `0`, negative, or non-numeric values fall back to its default so escalation always stays bounded ([#1581](https://github.com/bastani-inc/atomic/pull/1581)).
12
+
13
+ ### Fixed
14
+
15
+ - Reverted the `@types/node` devDependency from `26.0.1` back to `24.12.4` to match the deliberate root `overrides` pin that aligns with upstream Pi's resolved lockfile (see [#1489](https://github.com/bastani-inc/atomic/pull/1489)); the dependabot bump in [#1548](https://github.com/bastani-inc/atomic/pull/1548) was a semantic no-op because the override forced resolution to `24.12.4`. Dependabot now ignores `@types/node` so it stops proposing inert bumps.
16
+ - Fixed the `read` tool to parse colon-delimited `file:START:END` (and grep-style `file:LINE:COL`) path selectors as a line range instead of leaving the leading number glued to the path (`file:395`), which produced a bogus `ENOENT` and pushed models (e.g. Opus) to fall back to `sed` ([#1585](https://github.com/bastani-inc/atomic/issues/1585)).
17
+ - Fixed GitHub Copilot models to use the live `max_output_tokens` value from the Copilot model catalog, preventing `github-copilot/claude-opus-4.8` from being capped by Atomic's stale built-in output-token limit after compaction ([#1582](https://github.com/bastani-inc/atomic/issues/1582)).
18
+ - Fixed active GitHub Copilot sessions to adopt live catalog model metadata as soon as the catalog loads, so fallback models refresh their supported reasoning levels without requiring a restart.
19
+ - Fixed workflow and subagent model fallback chains so request/context incompatibility failures (HTTP 400/413/422 bad/unprocessable/payload-too-large request, unsupported tool/parameter, context-length/context-window overflow, `invalid_request`/`bad_request`/`too_large` errors) advance to the next candidate instead of stopping. This ensures that when none of the configured fallback candidates can serve the current request, Atomic falls back to the currently selected user model rather than failing outright. Refusals, content-filter/safety blocks, cancellations, and task failures still stop the chain and are never retried on another model ([#1580](https://github.com/bastani-inc/atomic/issues/1580)).
20
+ - Fixed foreground and background subagent model attempts that produced no child activity from hanging indefinitely. Atomic now bounds each candidate with an idle watchdog and wall-clock cap, records retryable timeout attempts so fallback continues, and skips known unauthenticated providers before spawning while preserving unknown/custom providers and the current-model last resort ([#1580](https://github.com/bastani-inc/atomic/issues/1580)).
21
+ - Fixed the subagent per-attempt idle watchdog so an in-flight tool execution counts as activity: a slow, quiet tool call (long build or test run with no interim output) is no longer killed as a stalled attempt; the wall-clock cap still bounds such attempts ([#1581](https://github.com/bastani-inc/atomic/pull/1581)).
22
+ - Aligned the subagents and workflows model-failure classifiers' direct-message precedence and added a cross-package conformance test suite so the two classifier copies cannot silently drift ([#1581](https://github.com/bastani-inc/atomic/pull/1581)).
23
+ - Fixed the background subagent runner to spawn one default-model attempt when no model candidates were ever configured (no primary model, no fallbacks, and no current model), mirroring the foreground path instead of silently exiting 1 with no error and no spawn; an explicitly empty candidate list produced by pre-spawn auth filtering is still respected and surfaced as an error ([#1581](https://github.com/bastani-inc/atomic/pull/1581)).
24
+
25
+ ## [0.9.4-alpha.6] - 2026-07-01
26
+
27
+ ### Added
28
+
29
+ - Added `get_entries` and `get_tree` RPC commands for reading session entries and tree snapshots over RPC, with corresponding `RpcClient.getEntries`/`getTree` helpers and response types (inherited from upstream Pi [#6078](https://github.com/earendil-works/pi/pull/6078)).
30
+ - Added a package `./rpc-entry` export and `src/rpc-entry.ts` entrypoint for launching Atomic directly in RPC mode (inherited from upstream Pi).
31
+ - Added `session_info_changed` as an extension event so extensions can observe session name changes, wired through `AgentSession.setSessionName` to the extension runner (inherited from upstream Pi [#6175](https://github.com/earendil-works/pi/pull/6175)).
32
+ - Added an `externalEditor` settings.json override for Ctrl+G external editor commands, with default fallbacks to Notepad on Windows and `nano` elsewhere (inherited from upstream Pi [#6122](https://github.com/earendil-works/pi/issues/6122)).
33
+ - Added an `outputPad` setting (`0 | 1`, default `1`) controlling horizontal padding for user messages, assistant messages, and thinking blocks (inherited from upstream Pi [#6168](https://github.com/earendil-works/pi/issues/6168)).
34
+ - Added BMP image detection and shared `processImage`/`convertImageBytesToPng` helpers so unsupported image formats are normalized (converted to PNG) before inlining (inherited from upstream Pi [#6047](https://github.com/earendil-works/pi/issues/6047)).
35
+ - Added `resetTimings`/`time` timing namespaces so extension load timings are recorded and reset separately from main startup timings (inherited from upstream Pi [#6030](https://github.com/earendil-works/pi/pull/6030), [#6063](https://github.com/earendil-works/pi/pull/6063)).
36
+
37
+ ### Changed
38
+
39
+ - Synced Atomic's `@earendil-works/pi-agent-core`, `pi-ai`, and `pi-tui` runtime dependencies from `^0.80.2` to `^0.80.3` and ported the relevant coding-agent changes while preserving Atomic branding, package names, the versionless-`main` release convention, bundled first-party extensions, and user-facing Atomic docs.
40
+ - Bumped `@earendil-works/pi-agent-core`, `pi-ai`, and `pi-tui` from `^0.80.2` to `^0.80.3` across `@bastani/atomic` and all bundled first-party extensions, and regenerated `bun.lock`, `package-lock.json`, and `npm-shrinkwrap.json`.
41
+ - Changed the default OpenAI model to `gpt-5.5` (inherited from upstream Pi). The Atomic-specific `github-copilot` default remains `gpt-5.4`.
42
+ - Deferred the shared `isRetryableAssistantError` retry classifier because Atomic's `_isRetryableError` carries Atomic-specific Copilot Gemini handling (`content_filter`/`finish_reason: error` for CAPI-mapped MALFORMED_FUNCTION_CALL); adopting it safely requires merging Atomic's extra patterns back in during a follow-up sync.
43
+ - Deferred the interactive status-indicator stabilization refactor (Loader → StatusIndicator classes) because it is a large TUI refactor that needs visual QA.
44
+
45
+ ### Fixed
46
+
47
+ - Fixed extension `setActiveTools` changes to apply before the next provider request in the same run, by installing a `prepareNextTurnWithContext` hook that refreshes system prompt and tools (inherited from upstream Pi [#6162](https://github.com/earendil-works/pi/issues/6162)).
48
+ - Fixed `before_agent_start` system prompt overrides to survive when extension tool changes occur mid-run (inherited from upstream Pi [#6162](https://github.com/earendil-works/pi/issues/6162)).
49
+ - Fixed invalid session file errors at CLI entry to print a clean red error message instead of a stack trace, via `openSessionOrExit` wrappers around `SessionManager.open` call sites (inherited from upstream Pi [#6002](https://github.com/earendil-works/pi/issues/6002)).
50
+ - Fixed resumed/transcript chat rendering to respect the `outputPad` setting for user and assistant messages rendered through `renderChatMessageEntry` and the chat-session-host render path (inherited from upstream Pi [#6168](https://github.com/earendil-works/pi/issues/6168)).
51
+ - Fixed the dedicated `rpc-entry` to use Atomic's APP_NAME-based env marker (`ATOMIC_CODING_AGENT`) instead of the upstream `PI_CODING_AGENT`, and to force `--mode rpc` as the last CLI argument so a caller-supplied `--mode` cannot override it.
52
+ - Fixed pre-prompt compaction to stop after compaction instead of continuing immediately (already present in Atomic; documented for parity with upstream Pi [#6074](https://github.com/earendil-works/pi/pull/6074)).
53
+ - Fixed `--session` and `SessionManager` to reject non-empty invalid session files without overwriting them (inherited from upstream Pi [#6002](https://github.com/earendil-works/pi/issues/6002)).
54
+ - Fixed user-message transcript rendering to keep visible backslashes in Markdown escape sequences such as `\"` (inherited from upstream Pi [#6105](https://github.com/earendil-works/pi/issues/6105)).
55
+ - Fixed assistant messages stopped by output length to show a visible incomplete-response error (inherited from upstream Pi [#4290](https://github.com/earendil-works/pi/issues/4290)).
56
+ - Fixed `--no-session --session-id` so ephemeral CLI runs can use deterministic session IDs for provider cache affinity (inherited from upstream Pi [#6070](https://github.com/earendil-works/pi/issues/6070)).
57
+ - Fixed disk BMP image files to be detected, converted to PNG, and attached through `read` and CLI `@file` inputs (inherited from upstream Pi [#6047](https://github.com/earendil-works/pi/issues/6047)).
58
+ - Fixed a crash when undici emits an internal client error while terminating a mid-stream HTTP response, by attaching error-suppressing listeners to the global dispatcher and per-origin pools (inherited from upstream Pi [#6133](https://github.com/earendil-works/pi/issues/6133)).
59
+
5
60
  ## [0.9.4-alpha.5] - 2026-07-01
6
61
 
7
62
  ### Fixed
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.4-alpha.6] - 2026-07-01
6
+
7
+ ### Changed
8
+
9
+ - Aligned the Cursor provider dependency with upstream pi-ai `^0.80.3`; no Cursor provider source changes were needed for this metadata sync.
10
+
5
11
  ## [0.9.4-alpha.5] - 2026-07-01
6
12
 
7
13
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/cursor",
3
- "version": "0.9.4-alpha.5",
3
+ "version": "0.9.4-alpha.7",
4
4
  "private": true,
5
5
  "description": "Experimental first-party Atomic extension for Cursor OAuth, model discovery, and streaming provider registration.",
6
6
  "contributors": [
@@ -40,8 +40,8 @@
40
40
  }
41
41
  },
42
42
  "dependencies": {
43
- "@bastani/atomic-natives": "0.9.4-alpha.5",
44
- "@bufbuild/protobuf": "^2.0.0",
45
- "@earendil-works/pi-ai": "^0.80.2"
43
+ "@bastani/atomic-natives": "0.9.4-alpha.7",
44
+ "@bufbuild/protobuf": "^2.12.1",
45
+ "@earendil-works/pi-ai": "^0.80.3"
46
46
  }
47
47
  }
@@ -4,6 +4,12 @@ All notable changes to the `pi-intercom` extension will be documented in this fi
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.4-alpha.6] - 2026-07-01
8
+
9
+ ### Changed
10
+
11
+ - Aligned the intercom extension peer dependency with upstream pi TUI `^0.80.3`; no intercom extension source changes were needed for this metadata sync.
12
+
7
13
  ## [0.9.4-alpha.5] - 2026-07-01
8
14
 
9
15
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/intercom",
3
- "version": "0.9.4-alpha.5",
3
+ "version": "0.9.4-alpha.7",
4
4
  "private": true,
5
5
  "description": "Atomic extension providing a private coordination channel between parent and child agent sessions. Fork of: https://github.com/nicobailon/pi-intercom",
6
6
  "contributors": [
@@ -39,7 +39,7 @@
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@bastani/atomic": "*",
42
- "@earendil-works/pi-tui": "^0.80.2"
42
+ "@earendil-works/pi-tui": "^0.80.3"
43
43
  },
44
44
  "peerDependenciesMeta": {
45
45
  "@bastani/atomic": {
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.9.4-alpha.6] - 2026-07-01
11
+
12
+ ### Changed
13
+
14
+ - Aligned the MCP extension peer dependencies with upstream pi AI/TUI `^0.80.3`; no MCP extension source changes were needed for this metadata sync.
15
+
10
16
  ## [0.9.4-alpha.5] - 2026-07-01
11
17
 
12
18
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/mcp",
3
- "version": "0.9.4-alpha.5",
3
+ "version": "0.9.4-alpha.7",
4
4
  "private": true,
5
5
  "description": "Atomic extension that adapts MCP (Model Context Protocol) servers into the coding agent. Fork of: https://github.com/nicobailon/pi-mcp-adapter",
6
6
  "contributors": [
@@ -32,8 +32,8 @@
32
32
  },
33
33
  "peerDependencies": {
34
34
  "@bastani/atomic": "*",
35
- "@earendil-works/pi-ai": "^0.80.2",
36
- "@earendil-works/pi-tui": "^0.80.2",
35
+ "@earendil-works/pi-ai": "^0.80.3",
36
+ "@earendil-works/pi-tui": "^0.80.3",
37
37
  "zod": "^3.25.0 || ^4.0.0"
38
38
  },
39
39
  "peerDependenciesMeta": {
@@ -48,7 +48,7 @@
48
48
  }
49
49
  },
50
50
  "dependencies": {
51
- "@modelcontextprotocol/ext-apps": "^1.7.2",
51
+ "@modelcontextprotocol/ext-apps": "^1.7.4",
52
52
  "@modelcontextprotocol/sdk": "^1.25.1",
53
53
  "open": "^11.0.0",
54
54
  "typebox": "^1.1.24",
@@ -2,6 +2,26 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.4-alpha.7] - 2026-07-02
6
+
7
+ ### Fixed
8
+
9
+ - Fixed subagent model fallback so request/context incompatibility failures (HTTP 400/413/422 bad/unprocessable/payload-too-large request, unsupported tool/parameter, context-length/context-window overflow, `invalid_request`/`bad_request`/`too_large` errors) advance the chain to the next candidate instead of stopping. When none of the configured candidates can serve the request, the subagent now falls back to the current user-selected model. Refusals, content-filter/safety blocks, cancellations, and task failures still stop the chain ([#1580](https://github.com/bastani-inc/atomic/issues/1580)).
10
+ - Fixed foreground and background subagent model attempts that produced no child activity from hanging indefinitely. Each candidate attempt now has a conservative idle watchdog and absolute wall-clock cap, records a retryable timeout failure, and advances to the next fallback candidate; known providers without configured auth are skipped before spawning while unknown/custom providers and the current-model last resort are still attempted ([#1580](https://github.com/bastani-inc/atomic/issues/1580)).
11
+ - Fixed the per-attempt idle watchdog so an in-flight tool execution counts as activity: a slow, quiet tool call (long build or test run with no interim output) is no longer killed as a stalled attempt; the wall-clock cap still bounds such attempts ([#1581](https://github.com/bastani-inc/atomic/pull/1581)).
12
+ - Aligned the subagents model-failure classifier's direct-message precedence with the workflows classifier (direct-message classification now runs after nested cause/diagnostic traversal and before outer status/code classification), and added a cross-package conformance test suite so the two classifier copies cannot silently drift ([#1581](https://github.com/bastani-inc/atomic/pull/1581)).
13
+ - Fixed the background subagent runner to spawn one default-model attempt when no model candidates were ever configured (no primary model, no fallbacks, and no current model), mirroring the foreground path instead of silently exiting 1 with no error and no spawn. An explicitly empty candidate list produced by pre-spawn auth filtering is still respected and surfaced as an error ([#1581](https://github.com/bastani-inc/atomic/pull/1581)).
14
+
15
+ ### Added
16
+
17
+ - Added a watchdog escape hatch: setting `ATOMIC_SUBAGENT_ATTEMPT_IDLE_TIMEOUT_MS` or `ATOMIC_SUBAGENT_ATTEMPT_TIMEOUT_MS` to `0` (or a negative value) now disables the corresponding per-attempt timeout entirely; non-numeric values are ignored and the defaults apply. The `ATOMIC_SUBAGENT_ATTEMPT_KILL_GRACE_MS` SIGTERM→SIGKILL grace period intentionally cannot be disabled — `0`, negative, or non-numeric values fall back to its default so escalation always stays bounded ([#1581](https://github.com/bastani-inc/atomic/pull/1581)).
18
+
19
+ ## [0.9.4-alpha.6] - 2026-07-01
20
+
21
+ ### Changed
22
+
23
+ - Aligned the subagents extension peer dependencies with upstream pi `^0.80.3` runtime packages; no subagents extension source changes were needed for this metadata sync.
24
+
5
25
  ## [0.9.4-alpha.5] - 2026-07-01
6
26
 
7
27
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/subagents",
3
- "version": "0.9.4-alpha.5",
3
+ "version": "0.9.4-alpha.7",
4
4
  "private": true,
5
5
  "description": "Atomic extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification. Fork of: https://github.com/nicobailon/pi-subagents",
6
6
  "contributors": [
@@ -38,9 +38,9 @@
38
38
  },
39
39
  "peerDependencies": {
40
40
  "@bastani/atomic": "*",
41
- "@earendil-works/pi-agent-core": "^0.80.2",
42
- "@earendil-works/pi-ai": "^0.80.2",
43
- "@earendil-works/pi-tui": "^0.80.2"
41
+ "@earendil-works/pi-agent-core": "^0.80.3",
42
+ "@earendil-works/pi-ai": "^0.80.3",
43
+ "@earendil-works/pi-tui": "^0.80.3"
44
44
  },
45
45
  "peerDependenciesMeta": {
46
46
  "@bastani/atomic": {
@@ -12,6 +12,7 @@ import type { RunnerStep } from "../shared/parallel-utils.ts";
12
12
  import { injectSingleOutputInstruction, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
13
13
  import { ChainOutputValidationError, validateChainOutputBindings } from "../shared/chain-outputs.ts";
14
14
  import { buildModelCandidates, resolveModelCandidate } from "../shared/model-fallback.ts";
15
+ import { filterSpawnableModelCandidates } from "../shared/model-candidate-filter.ts";
15
16
  import { NESTED_RUNS_DIR, nestedResultsPath, resolveInheritedNestedRouteFromEnv, resolveNestedParentAddressFromEnv, writeNestedEvent } from "../shared/nested-events.ts";
16
17
  import { createStructuredOutputRuntime } from "../shared/structured-output.ts";
17
18
  import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
@@ -57,6 +58,7 @@ export function executeAsyncChain(
57
58
  const resultMode = params.resultMode ?? "chain";
58
59
  const chainSkills = params.chainSkills ?? [];
59
60
  const availableModels = params.availableModels;
61
+ const knownModelProviders = params.knownModelProviders;
60
62
  const fastModeScope = resolveSubagentCodexFastModeScope(workflowStageSubagentGuard);
61
63
  const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
62
64
  const firstStep = chain[0];
@@ -149,9 +151,16 @@ export function executeAsyncChain(
149
151
 
150
152
  const primaryModel = resolveModelCandidate(behavior.model ?? a.model, availableModels, ctx.currentModelProvider);
151
153
  const model = applyThinkingSuffix(primaryModel, a.thinking);
152
- const modelCandidates = buildModelCandidates(behavior.model ?? a.model, a.fallbackModels, availableModels, ctx.currentModelProvider, ctx.currentModel, a.fallbackThinkingLevels)
154
+ const rawModelCandidates = buildModelCandidates(behavior.model ?? a.model, a.fallbackModels, availableModels, ctx.currentModelProvider, ctx.currentModel, a.fallbackThinkingLevels)
153
155
  .map((candidate) => applyThinkingSuffix(candidate, a.thinking))
154
156
  .filter((candidate): candidate is string => typeof candidate === "string");
157
+ const filteredCandidates = filterSpawnableModelCandidates({
158
+ candidates: rawModelCandidates,
159
+ availableModels,
160
+ knownModelProviders,
161
+ currentModel: applyThinkingSuffix(ctx.currentModel, a.thinking),
162
+ });
163
+ const modelCandidates = filteredCandidates.candidates;
155
164
  const fastModeSettings = getSubagentCodexFastModeSettings(stepCwd);
156
165
  return {
157
166
  agent: s.agent,
@@ -165,6 +174,7 @@ export function executeAsyncChain(
165
174
  thinking: resolveEffectiveThinking(model, a.thinking),
166
175
  ...resolveSubagentModelFastModeMetadata({ model, modelCandidates, cwd: stepCwd, settings: fastModeSettings, scope: fastModeScope }),
167
176
  modelCandidates,
177
+ modelAttempts: filteredCandidates.skippedAttempts,
168
178
  codexFastModeSettings: fastModeSettings,
169
179
  codexFastModeScope: fastModeScope,
170
180
  tools: a.tools,
@@ -9,6 +9,7 @@ import { resolveChildCwd } from "../../shared/utils.ts";
9
9
  import { applyThinkingSuffix, SUBAGENT_INTERCOM_SESSION_NAME_ENV } from "../shared/pi-args.ts";
10
10
  import { injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
11
11
  import { buildModelCandidates, resolveModelCandidate } from "../shared/model-fallback.ts";
12
+ import { filterSpawnableModelCandidates } from "../shared/model-candidate-filter.ts";
12
13
  import { NESTED_RUNS_DIR, nestedResultsPath, resolveInheritedNestedRouteFromEnv, resolveNestedParentAddressFromEnv, writeNestedEvent } from "../shared/nested-events.ts";
13
14
  import {
14
15
  UNAVAILABLE_SUBAGENT_SKILL_ERROR,
@@ -50,6 +51,7 @@ export function executeAsyncSingle(
50
51
  const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
51
52
  const skillNames = params.skills ?? agentConfig.skills ?? [];
52
53
  const availableModels = params.availableModels;
54
+ const knownModelProviders = params.knownModelProviders;
53
55
  const { resolved: resolvedSkills, missing: missingSkills } = resolveSkillsWithFallback(skillNames, runnerCwd, ctx.cwd);
54
56
  if (missingSkills.includes("subagent")) return formatAsyncStartError("single", UNAVAILABLE_SUBAGENT_SKILL_ERROR);
55
57
  let systemPrompt = agentConfig.systemPrompt?.trim() ?? "";
@@ -84,9 +86,16 @@ export function executeAsyncSingle(
84
86
  resolveModelCandidate(params.modelOverride ?? agentConfig.model, availableModels, ctx.currentModelProvider),
85
87
  agentConfig.thinking,
86
88
  );
87
- const modelCandidates = buildModelCandidates(params.modelOverride ?? agentConfig.model, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider, ctx.currentModel)
89
+ const rawModelCandidates = buildModelCandidates(params.modelOverride ?? agentConfig.model, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider, ctx.currentModel)
88
90
  .map((candidate) => applyThinkingSuffix(candidate, agentConfig.thinking))
89
91
  .filter((candidate): candidate is string => typeof candidate === "string");
92
+ const filteredCandidates = filterSpawnableModelCandidates({
93
+ candidates: rawModelCandidates,
94
+ availableModels,
95
+ knownModelProviders,
96
+ currentModel: applyThinkingSuffix(ctx.currentModel, agentConfig.thinking),
97
+ });
98
+ const modelCandidates = filteredCandidates.candidates;
90
99
  const fastModeSettings = getSubagentCodexFastModeSettings(runnerCwd);
91
100
  const fastModeScope = resolveSubagentCodexFastModeScope(workflowStageSubagentGuard);
92
101
  let spawnResult: AsyncSpawnResult = {};
@@ -103,6 +112,7 @@ export function executeAsyncSingle(
103
112
  thinking: resolveEffectiveThinking(model, agentConfig.thinking),
104
113
  ...resolveSubagentModelFastModeMetadata({ model, modelCandidates, cwd: runnerCwd, settings: fastModeSettings, scope: fastModeScope }),
105
114
  modelCandidates,
115
+ modelAttempts: filteredCandidates.skippedAttempts,
106
116
  codexFastModeSettings: fastModeSettings,
107
117
  codexFastModeScope: fastModeScope,
108
118
  tools: agentConfig.tools,
@@ -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);