@selesai/code 0.13.16 → 0.13.18

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 (467) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +2 -14
  3. package/dist/bun/cli.d.ts +3 -1
  4. package/dist/bun/cli.js +3 -9
  5. package/dist/bun/{register-bedrock.js → runtime-setup.js} +5 -0
  6. package/dist/bun/sandbox-env-setup.d.ts +1 -0
  7. package/dist/bun/sandbox-env-setup.js +3 -0
  8. package/dist/cli/auth-command.d.ts +23 -0
  9. package/dist/cli/auth-command.js +102 -0
  10. package/dist/cli/config-selector.js +2 -1
  11. package/dist/cli/setup.d.ts +1 -0
  12. package/dist/cli/setup.js +11 -0
  13. package/dist/cli.js +2 -14
  14. package/dist/config.d.ts +3 -2
  15. package/dist/config.js +9 -4
  16. package/dist/core/agent-session-runtime.js +15 -7
  17. package/dist/core/agent-session.d.ts +2 -6
  18. package/dist/core/agent-session.js +15 -4
  19. package/dist/core/compaction/branch-summarization.d.ts +1 -1
  20. package/dist/core/compaction/branch-summarization.js +2 -1
  21. package/dist/core/extensions/loader.js +3 -0
  22. package/dist/core/extensions/types.d.ts +16 -2
  23. package/dist/core/http-dispatcher.js +2 -0
  24. package/dist/core/keybindings.d.ts +5 -0
  25. package/dist/core/keybindings.js +4 -0
  26. package/dist/core/messages.d.ts +1 -1
  27. package/dist/core/model-config.d.ts +15 -0
  28. package/dist/core/model-config.js +3 -0
  29. package/dist/core/model-runtime.d.ts +1 -0
  30. package/dist/core/model-runtime.js +5 -2
  31. package/dist/core/provider-composer.js +8 -2
  32. package/dist/core/session-export.d.ts +3 -0
  33. package/dist/core/session-export.js +31 -0
  34. package/dist/core/session-manager.d.ts +3 -2
  35. package/dist/core/session-manager.js +40 -13
  36. package/dist/core/skills.d.ts +1 -1
  37. package/dist/core/skills.js +4 -2
  38. package/dist/core/system-prompt.js +10 -10
  39. package/dist/core/tools/bash.js +3 -117
  40. package/dist/core/tools/edit.d.ts +1 -12
  41. package/dist/core/tools/edit.js +5 -163
  42. package/dist/core/tools/find.js +4 -55
  43. package/dist/core/tools/grep.js +4 -60
  44. package/dist/core/tools/ls.js +4 -49
  45. package/dist/core/tools/read.d.ts +4 -0
  46. package/dist/core/tools/read.js +10 -122
  47. package/dist/core/tools/renderers/bash.d.ts +11 -0
  48. package/dist/core/tools/renderers/bash.js +129 -0
  49. package/dist/core/tools/renderers/edit.d.ts +22 -0
  50. package/dist/core/tools/renderers/edit.js +166 -0
  51. package/dist/core/tools/renderers/find.d.ts +9 -0
  52. package/dist/core/tools/renderers/find.js +63 -0
  53. package/dist/core/tools/renderers/grep.d.ts +9 -0
  54. package/dist/core/tools/renderers/grep.js +68 -0
  55. package/dist/core/tools/renderers/index.d.ts +27 -0
  56. package/dist/core/tools/renderers/index.js +46 -0
  57. package/dist/core/tools/renderers/ls.d.ts +9 -0
  58. package/dist/core/tools/renderers/ls.js +57 -0
  59. package/dist/core/tools/renderers/read.d.ts +10 -0
  60. package/dist/core/tools/renderers/read.js +128 -0
  61. package/dist/core/tools/renderers/write.d.ts +9 -0
  62. package/dist/core/tools/renderers/write.js +152 -0
  63. package/dist/core/tools/write.js +5 -146
  64. package/dist/extensions/pi-hermes-memory/CHANGELOG.md +18 -0
  65. package/dist/extensions/pi-hermes-memory/README.md +16 -12
  66. package/dist/extensions/pi-hermes-memory/docs/ROADMAP.md +1 -1
  67. package/dist/extensions/pi-hermes-memory/package-lock.json +587 -62
  68. package/dist/extensions/pi-hermes-memory/package.json +4 -4
  69. package/dist/extensions/pi-hermes-memory/src/config.ts +32 -0
  70. package/dist/extensions/pi-hermes-memory/src/constants.ts +12 -2
  71. package/dist/extensions/pi-hermes-memory/src/handlers/background-review.ts +109 -51
  72. package/dist/extensions/pi-hermes-memory/src/handlers/index-sessions.ts +9 -3
  73. package/dist/extensions/pi-hermes-memory/src/handlers/review-memory-ops.ts +257 -98
  74. package/dist/extensions/pi-hermes-memory/src/handlers/session-backfill.ts +9 -2
  75. package/dist/extensions/pi-hermes-memory/src/handlers/sync-markdown-memories.ts +5 -0
  76. package/dist/extensions/pi-hermes-memory/src/index.ts +22 -2
  77. package/dist/extensions/pi-hermes-memory/src/paths.ts +1 -1
  78. package/dist/extensions/pi-hermes-memory/src/store/db.ts +6 -0
  79. package/dist/extensions/pi-hermes-memory/src/store/fts-query.ts +58 -1
  80. package/dist/extensions/pi-hermes-memory/src/store/memory-store.ts +60 -8
  81. package/dist/extensions/pi-hermes-memory/src/store/session-indexer.ts +169 -7
  82. package/dist/extensions/pi-hermes-memory/src/store/session-search.ts +7 -21
  83. package/dist/extensions/pi-hermes-memory/src/store/sqlite-memory-store.ts +134 -15
  84. package/dist/extensions/pi-hermes-memory/src/tools/memory-search-tool.ts +4 -2
  85. package/dist/extensions/pi-hermes-memory/src/tools/memory-tool.ts +24 -9
  86. package/dist/extensions/pi-hermes-memory/src/types.ts +11 -0
  87. package/dist/extensions/pi-hermes-memory/tests/config.test.ts +42 -0
  88. package/dist/extensions/pi-hermes-memory/tests/handlers/background-review.test.ts +159 -3
  89. package/dist/extensions/pi-hermes-memory/tests/handlers/review-memory-ops.test.ts +452 -0
  90. package/dist/extensions/pi-hermes-memory/tests/handlers/session-backfill.test.ts +39 -0
  91. package/dist/extensions/pi-hermes-memory/tests/integration/corruption-recovery-integration.test.ts +91 -0
  92. package/dist/extensions/pi-hermes-memory/tests/store/db.test.ts +29 -0
  93. package/dist/extensions/pi-hermes-memory/tests/store/fts-query.test.ts +116 -0
  94. package/dist/extensions/pi-hermes-memory/tests/store/memory-store.test.ts +163 -0
  95. package/dist/extensions/pi-hermes-memory/tests/store/session-indexer.test.ts +233 -0
  96. package/dist/extensions/pi-hermes-memory/tests/store/session-search.test.ts +12 -0
  97. package/dist/extensions/pi-hermes-memory/tests/store/sqlite-memory-store.test.ts +175 -0
  98. package/dist/extensions/pi-hermes-memory/tests/store/sqlite-native.test.ts +2 -2
  99. package/dist/extensions/pi-hermes-memory/tests/tools/memory-search-tool.test.ts +22 -0
  100. package/dist/extensions/pi-hermes-memory/tests/tools/memory-tool.test.ts +37 -1
  101. package/dist/extensions/pi-intercom/CHANGELOG.md +13 -0
  102. package/dist/extensions/pi-intercom/README.md +16 -2
  103. package/dist/extensions/pi-intercom/broker/spawn.test.ts +19 -2
  104. package/dist/extensions/pi-intercom/broker/spawn.ts +4 -4
  105. package/dist/extensions/pi-intercom/index.ts +1 -1
  106. package/dist/extensions/pi-intercom/intercom.integration.test.ts +121 -2
  107. package/dist/extensions/pi-intercom/package.json +1 -1
  108. package/dist/extensions/pi-intercom/project-agent.test.ts +1 -1
  109. package/dist/extensions/pi-intercom/project-agent.ts +1 -6
  110. package/dist/extensions/pi-intercom/skills/pi-intercom/SKILL.md +7 -6
  111. package/dist/extensions/pi-rewind-hook/index.test.ts +7 -0
  112. package/dist/extensions/pi-subagents/.github/workflows/test.yml +36 -4
  113. package/dist/extensions/pi-subagents/CHANGELOG.md +120 -0
  114. package/dist/extensions/pi-subagents/README.md +3 -3
  115. package/dist/extensions/pi-subagents/agents/researcher.md +23 -13
  116. package/dist/extensions/pi-subagents/agents/reviewer.md +1 -1
  117. package/dist/extensions/pi-subagents/agents/scout.md +1 -1
  118. package/dist/extensions/pi-subagents/docs/agents.md +32 -16
  119. package/dist/extensions/pi-subagents/docs/configuration.md +41 -15
  120. package/dist/extensions/pi-subagents/docs/extension-api.md +109 -6
  121. package/dist/extensions/pi-subagents/docs/missions.md +2 -0
  122. package/dist/extensions/pi-subagents/docs/models.md +58 -1
  123. package/dist/extensions/pi-subagents/docs/observability.md +50 -12
  124. package/dist/extensions/pi-subagents/docs/tool-reference.md +17 -10
  125. package/dist/extensions/pi-subagents/docs/watchdog.md +1 -1
  126. package/dist/extensions/pi-subagents/docs/workflows.md +26 -9
  127. package/dist/extensions/pi-subagents/package-lock.json +747 -177
  128. package/dist/extensions/pi-subagents/package.json +6 -4
  129. package/dist/extensions/pi-subagents/runner-server-preload.mjs +13 -0
  130. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +2 -1
  131. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  132. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +21 -4
  133. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
  134. package/dist/extensions/pi-subagents/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
  135. package/dist/extensions/pi-subagents/src/agents/advertised-agent-prompt.ts +63 -0
  136. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +61 -16
  137. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
  138. package/dist/extensions/pi-subagents/src/agents/agents.ts +8 -0
  139. package/dist/extensions/pi-subagents/src/api/capability-ceiling.ts +0 -1
  140. package/dist/extensions/pi-subagents/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
  141. package/dist/extensions/pi-subagents/src/api/preflight.ts +7 -4
  142. package/dist/extensions/pi-subagents/src/api/shared-types.ts +2 -2
  143. package/dist/extensions/pi-subagents/src/api/workflow-resources.ts +6 -0
  144. package/dist/extensions/pi-subagents/src/extension/config.ts +4 -2
  145. package/dist/extensions/pi-subagents/src/extension/doctor.ts +2 -10
  146. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +9 -11
  147. package/dist/extensions/pi-subagents/src/extension/index.ts +99 -10
  148. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +13 -0
  149. package/dist/extensions/pi-subagents/src/extension/rpc.ts +7 -23
  150. package/dist/extensions/pi-subagents/src/extension/schemas.ts +11 -8
  151. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +25 -7
  152. package/dist/extensions/pi-subagents/src/integrations/pi-web-session-liveness.ts +73 -0
  153. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +228 -136
  154. package/dist/extensions/pi-subagents/src/intercom/supervisor-ui.ts +244 -0
  155. package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +37 -16
  156. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +18 -18
  157. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +51 -25
  158. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +46 -3
  159. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -3
  160. package/dist/extensions/pi-subagents/src/runs/background/async-retention.ts +9 -0
  161. package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +10 -12
  162. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +18 -15
  163. package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +40 -29
  164. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +8 -0
  165. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +79 -247
  166. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +88 -12
  167. package/dist/extensions/pi-subagents/src/runs/background/owned-process-tree.ts +6 -6
  168. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +24 -24
  169. package/dist/extensions/pi-subagents/src/runs/background/retained-nested-route-tracker.ts +96 -0
  170. package/dist/extensions/pi-subagents/src/runs/background/run-child-session.ts +642 -0
  171. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +75 -6
  172. package/dist/extensions/pi-subagents/src/runs/background/runner-aliases.ts +163 -0
  173. package/dist/extensions/pi-subagents/src/runs/background/runner-child-launch.ts +86 -0
  174. package/dist/extensions/pi-subagents/src/runs/background/runner-child-sessions.ts +31 -0
  175. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +18 -4
  176. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +3 -1
  177. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +597 -1092
  178. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +3 -0
  179. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +4 -0
  180. package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +20 -17
  181. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +296 -394
  182. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +4 -0
  183. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +3 -1
  184. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +9 -5
  185. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +630 -283
  186. package/dist/extensions/pi-subagents/src/runs/foreground/workflow-detach-reconcile.ts +8 -5
  187. package/dist/extensions/pi-subagents/src/runs/foreground/workflow-foreground-steering.ts +78 -98
  188. package/dist/extensions/pi-subagents/src/runs/shared/abort-recovery.ts +3 -3
  189. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +16 -3
  190. package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +1 -1
  191. package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +47 -47
  192. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +1 -2
  193. package/dist/extensions/pi-subagents/src/runs/shared/child-hooks.ts +174 -0
  194. package/dist/extensions/pi-subagents/src/runs/shared/child-identity.ts +14 -3
  195. package/dist/extensions/pi-subagents/src/runs/shared/child-launch.ts +319 -0
  196. package/dist/extensions/pi-subagents/src/runs/shared/child-lifecycle.ts +25 -0
  197. package/dist/extensions/pi-subagents/src/runs/shared/child-runtime-config.ts +126 -0
  198. package/dist/extensions/pi-subagents/src/runs/shared/child-session.ts +373 -0
  199. package/dist/extensions/pi-subagents/src/runs/shared/child-tool-plan.ts +530 -0
  200. package/dist/extensions/pi-subagents/src/runs/shared/completion-evidence.ts +2 -2
  201. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +2 -1
  202. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-preflight.ts +16 -0
  203. package/dist/extensions/pi-subagents/src/runs/shared/host-step-status.ts +11 -11
  204. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +30 -20
  205. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
  206. package/dist/extensions/pi-subagents/src/runs/shared/model-exclusions.ts +84 -15
  207. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +88 -12
  208. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +37 -53
  209. package/dist/extensions/pi-subagents/src/runs/shared/nested-path.ts +1 -1
  210. package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +18 -7
  211. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +57 -12
  212. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +3 -4
  213. package/dist/extensions/pi-subagents/src/runs/shared/permissions.ts +0 -11
  214. package/dist/extensions/pi-subagents/src/runs/shared/process-signal.ts +4 -1
  215. package/dist/extensions/pi-subagents/src/runs/shared/readonly-drain-observation.ts +42 -0
  216. package/dist/extensions/pi-subagents/src/runs/shared/readonly-model-continuation.ts +69 -0
  217. package/dist/extensions/pi-subagents/src/runs/shared/readonly-session-evidence.ts +307 -0
  218. package/dist/extensions/pi-subagents/src/runs/shared/run-fanout-budget.ts +8 -21
  219. package/dist/extensions/pi-subagents/src/runs/shared/runtime-acknowledged-extensions.ts +3 -30
  220. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +17 -4
  221. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +2 -0
  222. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +99 -386
  223. package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +18 -62
  224. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +0 -14
  225. package/dist/extensions/pi-subagents/src/runs/shared/worktree-cleanup-plan.ts +31 -9
  226. package/dist/extensions/pi-subagents/src/runs/shared/worktree-setup-command.ts +190 -0
  227. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +506 -226
  228. package/dist/extensions/pi-subagents/src/shared/child-session-name.ts +1 -1
  229. package/dist/extensions/pi-subagents/src/shared/jsonl-writer.ts +11 -0
  230. package/dist/extensions/pi-subagents/src/shared/model-response-aliases.ts +13 -0
  231. package/dist/extensions/pi-subagents/src/shared/thinking-ceiling.ts +0 -6
  232. package/dist/extensions/pi-subagents/src/shared/types.ts +145 -89
  233. package/dist/extensions/pi-subagents/src/shared/utils.ts +13 -6
  234. package/dist/extensions/pi-subagents/src/shared/watch-strategy.ts +2 -0
  235. package/dist/extensions/pi-subagents/src/shared/workflow-child-permit.ts +18 -13
  236. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +0 -6
  237. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +1 -1
  238. package/dist/extensions/pi-subagents/src/tui/fleet.ts +11 -6
  239. package/dist/extensions/pi-subagents/src/tui/render.ts +277 -46
  240. package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +0 -1
  241. package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +12 -12
  242. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +3 -3
  243. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +116 -15
  244. package/dist/extensions/pi-subagents/src/workflows/workflow-checklist.ts +15 -18
  245. package/dist/extensions/pi-subagents/src/workflows/workflow-child-summary.ts +57 -8
  246. package/dist/extensions/pi-subagents/src/workflows/workflow-preflight.ts +19 -19
  247. package/dist/extensions/pi-subagents/src/workflows/workflow-receipt.ts +3 -3
  248. package/dist/extensions/pi-subagents/src/workflows/workflow-resources.ts +97 -22
  249. package/dist/extensions/pi-subagents/src/workflows/workflow-settlement.ts +3 -0
  250. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +336 -21
  251. package/dist/extensions/pi-subagents/test/integration/async-execution.part-1.test.ts +1181 -0
  252. package/dist/extensions/pi-subagents/test/integration/async-execution.part-2.test.ts +2240 -0
  253. package/dist/extensions/pi-subagents/test/integration/async-execution.part-3.test.ts +2268 -0
  254. package/dist/extensions/pi-subagents/test/integration/async-execution.part-4.test.ts +1512 -0
  255. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +231 -16
  256. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +4 -18
  257. package/dist/extensions/pi-subagents/test/integration/foreign-workflow-steering.test.ts +95 -0
  258. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +9 -15
  259. package/dist/extensions/pi-subagents/test/integration/in-process-child.test.ts +374 -0
  260. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +20 -10
  261. package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +18 -6
  262. package/dist/extensions/pi-subagents/test/integration/recovered-assistant-error.test.ts +151 -0
  263. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +42 -1
  264. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +150 -25
  265. package/dist/extensions/pi-subagents/test/integration/result-publication.test.ts +354 -0
  266. package/dist/extensions/pi-subagents/test/integration/shared-cwd-terminal-evidence.test.ts +117 -0
  267. package/dist/extensions/pi-subagents/test/integration/single-execution.part-1.test.ts +3678 -0
  268. package/dist/extensions/pi-subagents/test/integration/{single-execution.test.ts → single-execution.part-2.test.ts} +500 -4207
  269. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1 -2
  270. package/dist/extensions/pi-subagents/test/integration/workflow-result-publication.test.ts +127 -0
  271. package/dist/extensions/pi-subagents/test/integration/workflow-steer-inbox.test.ts +365 -0
  272. package/dist/extensions/pi-subagents/test/smoke/pi085-child.ts +31 -0
  273. package/dist/extensions/pi-subagents/test/smoke/pi085-clean-install.mjs +74 -0
  274. package/dist/extensions/pi-subagents/test/smoke/pi085-extension.ts +16 -0
  275. package/dist/extensions/pi-subagents/test/support/async-execution-fixture.ts +699 -0
  276. package/dist/extensions/pi-subagents/test/support/fake-child-session.ts +418 -0
  277. package/dist/extensions/pi-subagents/test/support/foreign-workflow-steer-host.mjs +13 -0
  278. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +30 -76
  279. package/dist/extensions/pi-subagents/test/support/notify-diagnostics-fixture.ts +84 -0
  280. package/dist/extensions/pi-subagents/test/support/result-publication-capacity-preload.mjs +70 -0
  281. package/dist/extensions/pi-subagents/test/support/runner-child-session-factory.ts +17 -0
  282. package/dist/extensions/pi-subagents/test/support/single-execution-fixture.ts +390 -0
  283. package/dist/extensions/pi-subagents/test/unit/acceptance-compaction.test.ts +120 -0
  284. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +24 -3
  285. package/dist/extensions/pi-subagents/test/unit/advertised-agent-prompt.test.ts +87 -0
  286. package/dist/extensions/pi-subagents/test/unit/advertised-agent-refresh.test.ts +163 -0
  287. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +70 -31
  288. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +78 -3
  289. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +2 -8
  290. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +1 -1
  291. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +73 -66
  292. package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +51 -1
  293. package/dist/extensions/pi-subagents/test/unit/async-retention.test.ts +49 -1
  294. package/dist/extensions/pi-subagents/test/unit/async-spawn-preload.test.ts +103 -0
  295. package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +3 -3
  296. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-agent-allowlist.test.ts +9 -8
  297. package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +10 -7
  298. package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +48 -6
  299. package/dist/extensions/pi-subagents/test/unit/child-runtime-config.test.ts +90 -0
  300. package/dist/extensions/pi-subagents/test/unit/{pi-args-permission-system.test.ts → child-tool-plan-permission-system.test.ts} +48 -86
  301. package/dist/extensions/pi-subagents/test/unit/child-tool-plan.test.ts +33 -0
  302. package/dist/extensions/pi-subagents/test/unit/compaction-resume.test.ts +18 -18
  303. package/dist/extensions/pi-subagents/test/unit/completion-evidence.test.ts +9 -9
  304. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +8 -7
  305. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +225 -65
  306. package/dist/extensions/pi-subagents/test/unit/extension-bindings.test.ts +17 -22
  307. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +11 -1
  308. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +47 -3
  309. package/dist/extensions/pi-subagents/test/unit/foreign-workflow-steering.test.ts +74 -0
  310. package/dist/extensions/pi-subagents/test/unit/fork-cache-key.test.ts +23 -29
  311. package/dist/extensions/pi-subagents/test/unit/host-peer-runtime-imports.test.ts +96 -4
  312. package/dist/extensions/pi-subagents/test/unit/host-step-status.test.ts +4 -4
  313. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +116 -55
  314. package/dist/extensions/pi-subagents/test/unit/index-segment.test.ts +58 -1
  315. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +104 -3
  316. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +10 -1
  317. package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +31 -1
  318. package/dist/extensions/pi-subagents/test/unit/model-exclusions.test.ts +102 -51
  319. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +196 -6
  320. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +181 -38
  321. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +62 -70
  322. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +58 -53
  323. package/dist/extensions/pi-subagents/test/unit/notify-diagnostics.test.ts +42 -0
  324. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +84 -0
  325. package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +13 -5
  326. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +21 -5
  327. package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +5 -4
  328. package/dist/extensions/pi-subagents/test/unit/permissions.test.ts +2 -6
  329. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +28 -2
  330. package/dist/extensions/pi-subagents/test/unit/pi-web-session-liveness.test.ts +209 -0
  331. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +38 -1
  332. package/dist/extensions/pi-subagents/test/unit/prompt-audit-optional-peers.test.ts +43 -0
  333. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +24 -2
  334. package/dist/extensions/pi-subagents/test/unit/readonly-drain-observation.test.ts +174 -0
  335. package/dist/extensions/pi-subagents/test/unit/readonly-model-continuation.test.ts +94 -0
  336. package/dist/extensions/pi-subagents/test/unit/readonly-session-evidence.test.ts +1329 -0
  337. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +29 -80
  338. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +11 -8
  339. package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +2 -2
  340. package/dist/extensions/pi-subagents/test/unit/retained-nested-route-tracker.test.ts +175 -0
  341. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +38 -12
  342. package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +2 -5
  343. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +89 -1
  344. package/dist/extensions/pi-subagents/test/unit/runtime-acknowledged-extensions.test.ts +2 -16
  345. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +27 -3
  346. package/dist/extensions/pi-subagents/test/unit/scheduled-supervisor-demand.test.ts +215 -0
  347. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +15 -11
  348. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +237 -1
  349. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +2 -5
  350. package/dist/extensions/pi-subagents/test/unit/streamed-progress-bounds.test.ts +2 -2
  351. package/dist/extensions/pi-subagents/test/unit/subagent-guide.test.ts +14 -1
  352. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +130 -739
  353. package/dist/extensions/pi-subagents/test/unit/supervisor-ask-registration.test.ts +682 -0
  354. package/dist/extensions/pi-subagents/test/unit/supervisor-ui.test.ts +137 -0
  355. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +1 -11
  356. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +23 -3
  357. package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +8 -1
  358. package/dist/extensions/pi-subagents/test/unit/watchdog-tool-actions.test.ts +19 -1
  359. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +72 -4
  360. package/dist/extensions/pi-subagents/test/unit/workflow-checklist.test.ts +17 -3
  361. package/dist/extensions/pi-subagents/test/unit/workflow-child-summary.test.ts +34 -0
  362. package/dist/extensions/pi-subagents/test/unit/workflow-detach-reconcile.test.ts +12 -2
  363. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +46 -6
  364. package/dist/extensions/pi-subagents/test/unit/workflow-receipt.test.ts +3 -3
  365. package/dist/extensions/pi-subagents/test/unit/workflow-resources.test.ts +133 -1
  366. package/dist/extensions/pi-subagents/test/unit/workflow-resume-hint.test.ts +53 -2
  367. package/dist/extensions/pi-subagents/test/unit/worktree-cleanup-plan.test.ts +222 -14
  368. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +526 -85
  369. package/dist/extensions/pi-zentui/README.md +17 -0
  370. package/dist/extensions/pi-zentui/assets/screenshots/footer-status-hyperlink.png +0 -0
  371. package/dist/extensions/pi-zentui/docs/configuration.md +71 -2
  372. package/dist/extensions/pi-zentui/extensions/zentui/config.ts +11 -0
  373. package/dist/extensions/pi-zentui/extensions/zentui/editor-transfer.ts +14 -0
  374. package/dist/extensions/pi-zentui/extensions/zentui/extension-status.ts +29 -13
  375. package/dist/extensions/pi-zentui/extensions/zentui/footer-layout.ts +5 -4
  376. package/dist/extensions/pi-zentui/extensions/zentui/footer-text.ts +11 -0
  377. package/dist/extensions/pi-zentui/extensions/zentui/footer.ts +11 -10
  378. package/dist/extensions/pi-zentui/extensions/zentui/index.ts +77 -3
  379. package/dist/extensions/pi-zentui/extensions/zentui/presets.ts +77 -0
  380. package/dist/extensions/pi-zentui/extensions/zentui/settings-command.ts +103 -2
  381. package/dist/extensions/pi-zentui/extensions/zentui/working-line-extension-segments.ts +115 -0
  382. package/dist/extensions/pi-zentui/extensions/zentui/working-line.ts +87 -5
  383. package/dist/extensions/pi-zentui/package-lock.json +277 -517
  384. package/dist/extensions/pi-zentui/package.json +7 -4
  385. package/dist/extensions/pi-zentui/test/config-presets.test.ts +238 -0
  386. package/dist/extensions/pi-zentui/test/editor-transfer.test.ts +24 -1
  387. package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +570 -2
  388. package/dist/extensions/pi-zentui/test/extension-status.test.ts +90 -0
  389. package/dist/extensions/pi-zentui/test/footer-hyperlink.test.ts +243 -0
  390. package/dist/extensions/pi-zentui/test/package-contents.mjs +1 -0
  391. package/dist/extensions/pi-zentui/test/presets.test.ts +121 -0
  392. package/dist/extensions/pi-zentui/test/settings-command.test.ts +205 -1
  393. package/dist/extensions/pi-zentui/test/working-line-extension-segments.test.ts +139 -0
  394. package/dist/extensions/pi-zentui/test/working-line-lifecycle.integration.test.ts +237 -3
  395. package/dist/extensions/pi-zentui/test/working-line.test.ts +94 -2
  396. package/dist/index.d.ts +1 -1
  397. package/dist/main.js +4 -1
  398. package/dist/modes/interactive/chat-viewport.d.ts +19 -0
  399. package/dist/modes/interactive/chat-viewport.js +27 -0
  400. package/dist/modes/interactive/components/assistant-message.d.ts +6 -2
  401. package/dist/modes/interactive/components/assistant-message.js +30 -13
  402. package/dist/modes/interactive/components/custom-editor.d.ts +10 -1
  403. package/dist/modes/interactive/components/custom-editor.js +39 -1
  404. package/dist/modes/interactive/components/first-time-setup.js +1 -1
  405. package/dist/modes/interactive/components/index.d.ts +1 -1
  406. package/dist/modes/interactive/components/markdown-transform.d.ts +2 -0
  407. package/dist/modes/interactive/components/markdown-transform.js +18 -0
  408. package/dist/modes/interactive/components/model-selector.js +19 -17
  409. package/dist/modes/interactive/components/scoped-models-selector.js +21 -23
  410. package/dist/modes/interactive/components/settings-selector.d.ts +7 -1
  411. package/dist/modes/interactive/components/settings-selector.js +128 -20
  412. package/dist/modes/interactive/components/settings-submenu.d.ts +71 -0
  413. package/dist/modes/interactive/components/settings-submenu.js +164 -0
  414. package/dist/modes/interactive/components/status-indicator.d.ts +3 -1
  415. package/dist/modes/interactive/components/status-indicator.js +10 -3
  416. package/dist/modes/interactive/components/thinking-selector.d.ts +15 -3
  417. package/dist/modes/interactive/components/thinking-selector.js +75 -18
  418. package/dist/modes/interactive/components/tool-execution.d.ts +21 -4
  419. package/dist/modes/interactive/components/tool-execution.js +35 -33
  420. package/dist/modes/interactive/components/trust-selector.js +2 -2
  421. package/dist/modes/interactive/interactive-mode.d.ts +13 -15
  422. package/dist/modes/interactive/interactive-mode.js +144 -80
  423. package/dist/modes/interactive/session-share.d.ts +15 -0
  424. package/dist/modes/interactive/session-share.js +183 -0
  425. package/dist/modes/interactive/theme/dark.json +2 -1
  426. package/dist/modes/interactive/theme/light.json +2 -1
  427. package/dist/modes/interactive/theme/theme-controller.d.ts +1 -0
  428. package/dist/modes/interactive/theme/theme-controller.js +5 -0
  429. package/dist/modes/interactive/theme/theme-json.d.ts +83 -0
  430. package/dist/modes/interactive/theme/theme-json.js +129 -0
  431. package/dist/modes/interactive/theme/theme-schema.json +9 -1
  432. package/dist/modes/interactive/theme/theme.d.ts +14 -5
  433. package/dist/modes/interactive/theme/theme.js +18 -119
  434. package/dist/modes/interactive/tui-renderer.d.ts +20 -0
  435. package/dist/modes/interactive/tui-renderer.js +65 -0
  436. package/dist/utils/exif-orientation.js +2 -3
  437. package/dist/utils/management-http.d.ts +24 -0
  438. package/dist/utils/management-http.js +53 -0
  439. package/dist/utils/text.d.ts +7 -0
  440. package/dist/utils/text.js +8 -0
  441. package/dist/utils/tools-manager.d.ts +1 -0
  442. package/dist/utils/tools-manager.js +43 -21
  443. package/docs/docs.json +0 -4
  444. package/package.json +4 -4
  445. package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +0 -415
  446. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +0 -1059
  447. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +0 -116
  448. package/dist/extensions/pi-subagents/src/shared/post-exit-stdio-guard.ts +0 -85
  449. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +0 -224
  450. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +0 -6986
  451. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +0 -447
  452. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +0 -185
  453. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +0 -311
  454. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-pi-args.test.ts +0 -84
  455. package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +0 -202
  456. package/dist/extensions/pi-subagents/test/unit/close-grace-timer.test.ts +0 -101
  457. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +0 -2347
  458. package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +0 -104
  459. package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +0 -49
  460. package/dist/skills/imagegen-frontend-mobile/SKILL.md +0 -1465
  461. package/dist/skills/imagegen-frontend-web/SKILL.md +0 -987
  462. package/dist/skills/implanger/SKILL.md +0 -70
  463. package/dist/skills/output/SKILL.md +0 -49
  464. package/dist/skills/planger/SKILL.md +0 -167
  465. package/dist/skills/workflow/SKILL.md +0 -95
  466. package/docs/workflows.md +0 -18
  467. /package/dist/bun/{register-bedrock.d.ts → runtime-setup.d.ts} +0 -0
@@ -1,6 +1,6 @@
1
1
  import type {
2
2
  AsyncStatus,
3
- HostStepNodeV1,
3
+ HostStepNode,
4
4
  HostStepState,
5
5
  HostStepVerdict,
6
6
  WorkflowGraphNode,
@@ -57,7 +57,7 @@ function assertTimestamp(value: unknown, field: string, source: string, required
57
57
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error(`Invalid host step '${source}': ${field} must be a non-negative safe integer.`);
58
58
  }
59
59
 
60
- function assertFreshness(value: unknown, source: string): asserts value is HostStepNodeV1["freshness"] {
60
+ function assertFreshness(value: unknown, source: string): asserts value is HostStepNode["freshness"] {
61
61
  if (!isRecord(value)) throw new Error(`Invalid host step '${source}': freshness must be an object.`);
62
62
  const unknownFields = Object.keys(value).filter((field) => !FRESHNESS_FIELDS.has(field));
63
63
  if (unknownFields.length > 0) throw new Error(`Invalid host step '${source}': freshness has unsupported fields: ${unknownFields.join(", ")}.`);
@@ -70,7 +70,7 @@ function assertFreshness(value: unknown, source: string): asserts value is HostS
70
70
  * Validate the persisted host-step contract. This deliberately rejects unknown
71
71
  * fields and unbounded values so status and receipt loaders fail closed.
72
72
  */
73
- export function assertHostStepNode(value: unknown, source = "status"): asserts value is HostStepNodeV1 {
73
+ export function assertHostStepNode(value: unknown, source = "status"): asserts value is HostStepNode {
74
74
  if (!isRecord(value)) throw new Error(`Invalid host step '${source}': expected an object.`);
75
75
  const unknownFields = Object.keys(value).filter((field) => !HOST_STEP_FIELDS.has(field));
76
76
  if (unknownFields.length > 0) throw new Error(`Invalid host step '${source}': unsupported fields: ${unknownFields.join(", ")}.`);
@@ -105,7 +105,7 @@ export function assertHostStepNode(value: unknown, source = "status"): asserts v
105
105
  assertTimestamp(value.deadlineAt, "deadlineAt", source);
106
106
  }
107
107
 
108
- export function parseHostStepNode(value: unknown, source = "status"): HostStepNodeV1 {
108
+ export function parseHostStepNode(value: unknown, source = "status"): HostStepNode {
109
109
  assertHostStepNode(value, source);
110
110
  return {
111
111
  ...value,
@@ -113,7 +113,7 @@ export function parseHostStepNode(value: unknown, source = "status"): HostStepNo
113
113
  };
114
114
  }
115
115
 
116
- export function assertUniqueHostStepIds(hostSteps: readonly HostStepNodeV1[], source = "status"): void {
116
+ export function assertUniqueHostStepIds(hostSteps: readonly HostStepNode[], source = "status"): void {
117
117
  const ids = new Set<string>();
118
118
  for (const hostStep of hostSteps) {
119
119
  if (ids.has(hostStep.id)) throw new Error(`Invalid host step '${source}': duplicate host step id '${hostStep.id}'.`);
@@ -122,11 +122,11 @@ export function assertUniqueHostStepIds(hostSteps: readonly HostStepNodeV1[], so
122
122
  }
123
123
 
124
124
  /** Return only valid host nodes so an untrusted in-memory projection fails closed. */
125
- export function validHostStepNodes(graph: WorkflowGraphSnapshot | undefined): HostStepNodeV1[] {
125
+ export function validHostStepNodes(graph: WorkflowGraphSnapshot | undefined): HostStepNode[] {
126
126
  const nodes = graph?.nodes ?? [];
127
127
  const nodeIdCounts = new Map<string, number>();
128
128
  for (const node of nodes) nodeIdCounts.set(node.id, (nodeIdCounts.get(node.id) ?? 0) + 1);
129
- const hostSteps: HostStepNodeV1[] = [];
129
+ const hostSteps: HostStepNode[] = [];
130
130
  for (const [index, node] of nodes.entries()) {
131
131
  if (node.kind !== "host-step") continue;
132
132
  try {
@@ -147,7 +147,7 @@ export function assertWorkflowGraphHostSteps(graph: WorkflowGraphSnapshot | unde
147
147
  if (!Array.isArray(graph.nodes)) throw new Error(`Invalid host step '${source}.workflowGraph': nodes must be an array.`);
148
148
  const hostStepCount = graph.nodes.filter((node) => node.kind === "host-step").length;
149
149
  if (hostStepCount > HOST_STEP_MAX_COUNT) throw new Error(`Invalid host step '${source}': workflowGraph contains more than ${HOST_STEP_MAX_COUNT} host steps.`);
150
- const hostSteps: HostStepNodeV1[] = [];
150
+ const hostSteps: HostStepNode[] = [];
151
151
  for (const [index, node] of graph.nodes.entries()) {
152
152
  if (node.kind !== "host-step") continue;
153
153
  const hostStep = parseHostStepNode(node.hostStep, `${source}.workflowGraph.nodes[${index}].hostStep`);
@@ -161,7 +161,7 @@ export function assertWorkflowGraphHostSteps(graph: WorkflowGraphSnapshot | unde
161
161
  }
162
162
  }
163
163
 
164
- export function hostStepWorkflowNode(hostStep: HostStepNodeV1): WorkflowGraphNode {
164
+ export function hostStepWorkflowNode(hostStep: HostStepNode): WorkflowGraphNode {
165
165
  assertHostStepNode(hostStep, hostStep.id);
166
166
  return {
167
167
  id: hostStep.id,
@@ -172,7 +172,7 @@ export function hostStepWorkflowNode(hostStep: HostStepNodeV1): WorkflowGraphNod
172
172
  };
173
173
  }
174
174
 
175
- function workflowNodeStatus(hostStep: HostStepNodeV1): WorkflowNodeStatus {
175
+ function workflowNodeStatus(hostStep: HostStepNode): WorkflowNodeStatus {
176
176
  if (hostStep.state === "pending") return "pending";
177
177
  if (hostStep.state === "running") return "running";
178
178
  if (hostStep.state === "cancelled") return "stopped";
@@ -187,7 +187,7 @@ function workflowNodeStatus(hostStep: HostStepNodeV1): WorkflowNodeStatus {
187
187
  */
188
188
  export function upsertHostStep(input: {
189
189
  status: AsyncStatus;
190
- hostStep: HostStepNodeV1;
190
+ hostStep: HostStepNode;
191
191
  persist: (status: AsyncStatus) => void;
192
192
  }): AsyncStatus {
193
193
  const hostStep = parseHostStepNode(input.hostStep, input.hostStep.id);
@@ -1,7 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
- import { Agent, type AgentTool, type StreamFn } from "@earendil-works/pi-agent-core";
3
- import { convertToLlm, type ExtensionContext } from "@selesai/code";
4
- import { streamSimple } from "@earendil-works/pi-ai/compat";
2
+ import type { Agent, AgentTool, StreamFn } from "@earendil-works/pi-agent-core";
3
+ import type { ExtensionContext } from "@selesai/code";
5
4
  import type { ProviderHeaders } from "@earendil-works/pi-ai";
6
5
  import { Type, type Static } from "typebox";
7
6
  import { agentStreamOptions } from "../../shared/agent-stream-options.ts";
@@ -52,7 +51,11 @@ export function mapArbiterDecision(
52
51
 
53
52
  interface ArbiterRuntime {
54
53
  model: NonNullable<RegistryModel>;
55
- baseStreamFn: StreamFn;
54
+ /** Explicit override; it always wins over a registered provider stream. */
55
+ explicitStreamFn?: StreamFn;
56
+ /** Registered provider stream, usable only when its api matches the model. */
57
+ registeredStreamFn?: StreamFn;
58
+ registeredApi?: string;
56
59
  timeoutMs: number;
57
60
  }
58
61
 
@@ -73,9 +76,10 @@ export interface TaskMutationArbiterOptions {
73
76
  const DEFAULT_ARBITER_TIMEOUT_MS = 10_000;
74
77
 
75
78
  type RegistryModel = ReturnType<NonNullable<ExtensionContext["modelRegistry"]["find"]>>;
79
+ type ArbiterModelContext = Pick<ExtensionContext, "model" | "modelRegistry">;
76
80
 
77
81
  function resolveArbiterModel(
78
- ctx: ExtensionContext,
82
+ ctx: ArbiterModelContext,
79
83
  options?: TaskMutationArbiterOptions,
80
84
  ): NonNullable<RegistryModel> | null {
81
85
  const registry = ctx.modelRegistry as {
@@ -95,7 +99,7 @@ function resolveArbiterModel(
95
99
  }
96
100
 
97
101
  function resolveArbiterRuntime(
98
- ctx: ExtensionContext,
102
+ ctx: ArbiterModelContext,
99
103
  options?: TaskMutationArbiterOptions,
100
104
  ): ArbiterRuntime | null {
101
105
  const model = resolveArbiterModel(ctx, options);
@@ -103,23 +107,20 @@ function resolveArbiterRuntime(
103
107
  const registry = ctx.modelRegistry as {
104
108
  getRegisteredProviderConfig?: (provider: string) => { api?: string; streamSimple?: StreamFn } | undefined;
105
109
  };
106
- const modelApi = (model as { api?: string }).api;
107
110
  const registered = registry.getRegisteredProviderConfig?.(model.provider);
108
- const baseStreamFn = options?.streamFn
109
- ?? (registered?.streamSimple && registered.api === modelApi
110
- ? registered.streamSimple
111
- : streamSimple);
112
111
  return {
113
112
  model,
114
- baseStreamFn,
113
+ explicitStreamFn: options?.streamFn,
114
+ registeredStreamFn: registered?.streamSimple,
115
+ registeredApi: registered?.api,
115
116
  timeoutMs: options?.timeoutMs ?? DEFAULT_ARBITER_TIMEOUT_MS,
116
117
  };
117
118
  }
118
119
 
119
120
  async function resolveArbiterAuth(
120
- ctx: ExtensionContext,
121
+ ctx: ArbiterModelContext,
121
122
  model: RegistryModel,
122
- ): Promise<ArbiterAuth> {
123
+ ): Promise<ArbiterAuth | undefined> {
123
124
  const registry = ctx.modelRegistry as {
124
125
  getApiKeyAndHeaders?: (m: RegistryModel) => Promise<{
125
126
  ok: boolean;
@@ -135,14 +136,14 @@ async function resolveArbiterAuth(
135
136
  if (!registry.getApiKeyAndHeaders) return {};
136
137
  try {
137
138
  const auth = await registry.getApiKeyAndHeaders(model);
138
- if (auth.ok === false) return {};
139
+ if (auth.ok === false) return undefined;
139
140
  return {
140
141
  ...(auth.apiKey ? { apiKey: auth.apiKey } : {}),
141
142
  ...(auth.headers ? { headers: auth.headers } : {}),
142
143
  ...(auth.env ? { env: auth.env } : {}),
143
144
  };
144
145
  } catch {
145
- return {};
146
+ return undefined;
146
147
  }
147
148
  }
148
149
 
@@ -164,6 +165,15 @@ async function runArbitration(
164
165
  auth: ArbiterAuth,
165
166
  task: string,
166
167
  ): Promise<TaskMutationVerdict> {
168
+ // Keep optional Pi peers out of the detached runner's static import graph.
169
+ const [{ Agent }, { convertToLlm }, { streamSimple }] = await Promise.all([
170
+ import("@earendil-works/pi-agent-core"),
171
+ import("@selesai/code"),
172
+ import("@earendil-works/pi-ai/compat"),
173
+ ]);
174
+ const streamFn: StreamFn = runtime.explicitStreamFn
175
+ ?? (runtime.registeredApi !== undefined && runtime.registeredApi === runtime.model.api ? runtime.registeredStreamFn : undefined)
176
+ ?? streamSimple;
167
177
  let decision: DecisionParams | undefined;
168
178
  const tool: AgentTool<typeof DecisionParams, { recorded: boolean }> = {
169
179
  name: "task_mutation_decision",
@@ -190,7 +200,7 @@ async function runArbitration(
190
200
  tools: [tool],
191
201
  },
192
202
  convertToLlm,
193
- ...agentStreamOptions(authWrappedStreamFn(runtime.baseStreamFn, auth)),
203
+ ...agentStreamOptions(authWrappedStreamFn(streamFn, auth)),
194
204
  getApiKey: (providerName) =>
195
205
  providerName === runtime.model.provider ? auth.apiKey : undefined,
196
206
  beforeToolCall: async ({ toolCall }) =>
@@ -221,9 +231,9 @@ async function runArbitration(
221
231
  }
222
232
  }
223
233
 
224
- /** Create a memoized arbiter bound to the parent session's model, or undefined when disabled/unavailable. */
234
+ /** Create a memoized arbiter bound to the supplied model services, or undefined when disabled/unavailable. */
225
235
  export function createTaskMutationArbiter(
226
- ctx: ExtensionContext,
236
+ ctx: ArbiterModelContext,
227
237
  options?: TaskMutationArbiterOptions,
228
238
  ): TaskMutationArbiter | undefined {
229
239
  if (process.env.SELESAI_SUBAGENTS_LLM_INTENT_ARBITER === "0") return undefined;
@@ -235,7 +245,7 @@ export function createTaskMutationArbiter(
235
245
  const cached = cache.get(key);
236
246
  if (cached) return cached;
237
247
  const auth = await resolveArbiterAuth(ctx, runtime.model);
238
- const verdict = await runArbitration(runtime, auth, task);
248
+ const verdict = auth ? await runArbitration(runtime, auth, task) : "unavailable";
239
249
  if (cache.size > 200) cache.clear();
240
250
  cache.set(key, verdict);
241
251
  return verdict;
@@ -115,8 +115,11 @@ export function resolveMcpDirectToolResolution(
115
115
  ? mergeConfigs(config, { mcpServers: runtimeServers })
116
116
  : config;
117
117
  validateSelectedServerDefinitions(resolvedConfig, selectors);
118
+ const runtime = Object.keys(runtimeServers).length > 0
119
+ ? { mcpConfig: resolvedConfig, runtimeServerNames: Object.keys(runtimeServers) }
120
+ : {};
118
121
  const cache = loadMetadataCache();
119
- if (!cache) return { selections: [], unresolvedSelectors: selectors };
122
+ if (!cache) return { selections: [], unresolvedSelectors: selectors, ...runtime };
120
123
  const validMetadata: Record<string, ServerCacheEntry> = {};
121
124
  for (const [serverName, definition] of Object.entries(resolvedConfig.mcpServers)) {
122
125
  const serverCache = cache.servers[serverName];
@@ -131,9 +134,7 @@ export function resolveMcpDirectToolResolution(
131
134
  return {
132
135
  selections: grant.selections,
133
136
  unresolvedSelectors: grant.unresolvedSelectors,
134
- ...(Object.keys(runtimeServers).length > 0
135
- ? { mcpConfig: resolvedConfig, runtimeServerNames: Object.keys(runtimeServers) }
136
- : {}),
137
+ ...runtime,
137
138
  };
138
139
  }
139
140
 
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { splitKnownThinkingSuffix } from "../../shared/model-info.ts";
4
4
  import { TEMP_ROOT_DIR } from "../../shared/types.ts";
5
+ import { getAgentDir } from "../../shared/utils.ts";
5
6
 
6
7
  export const EXCLUSIONS_PATH_ENV = "PI_MODEL_EXCLUSIONS_PATH";
7
8
 
@@ -30,6 +31,43 @@ let loadedTTLCeilingMs: number | undefined;
30
31
  let persistTimer: ReturnType<typeof setTimeout> | null = null;
31
32
  let persistSeq = 0;
32
33
 
34
+ const AUTH_FAILURE_PATTERNS = [
35
+ /auth(?:entication)?/i,
36
+ /unauthori[sz]ed/i,
37
+ /forbidden/i,
38
+ /api key/i,
39
+ /token expired/i,
40
+ /invalid key/i,
41
+ ];
42
+
43
+ function isAuthModelExclusion(entry: ModelExclusion): boolean {
44
+ const reason = entry.reason;
45
+ return typeof reason === "string" && AUTH_FAILURE_PATTERNS.some((pattern) => pattern.test(reason));
46
+ }
47
+
48
+ function getAuthStoreMtimeMs(): number | undefined {
49
+ const authStorePath = path.join(getAgentDir(), "auth.json");
50
+ try {
51
+ const stats = fs.statSync(authStorePath);
52
+ return stats.isFile() && Number.isFinite(stats.mtimeMs) ? stats.mtimeMs : undefined;
53
+ } catch (error) {
54
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
55
+ console.error(`[model-exclusions] Failed to stat Pi auth store at ${authStorePath}; preserving auth-related exclusions:`, error);
56
+ }
57
+ return undefined;
58
+ }
59
+ }
60
+
61
+ function invalidateAuthExclusions(): void {
62
+ if (!exclusions.some(isAuthModelExclusion)) return;
63
+ const authStoreMtimeMs = getAuthStoreMtimeMs();
64
+ if (authStoreMtimeMs === undefined) return;
65
+ const retained = exclusions.filter((entry) => !isAuthModelExclusion(entry) || authStoreMtimeMs <= entry.recordedAt);
66
+ if (retained.length === exclusions.length) return;
67
+ exclusions = retained;
68
+ schedulePersist();
69
+ }
70
+
33
71
  /**
34
72
  * Override the default TTL applied to newly recorded model exclusions.
35
73
  *
@@ -95,23 +133,23 @@ function ensureLoaded(): void {
95
133
  const data = JSON.parse(raw);
96
134
  if (data.version === 1) {
97
135
  if (!Array.isArray(data.exclusions)) throw new Error("Model exclusion store version 1 must contain an exclusions array.");
98
- for (let index = 0; index < data.exclusions.length; index++) {
99
- const entry = data.exclusions[index];
100
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw new Error(`Model exclusion store entry ${index} must be an object.`);
101
- if (entry.reason !== undefined && typeof entry.reason !== "string") throw new Error(`Model exclusion store entry ${index} has an invalid reason.`);
102
- for (const field of ["recordedAt", "expiresAt"] as const) {
103
- const timestamp = entry[field];
104
- if (typeof timestamp !== "number" || !Number.isFinite(timestamp) || timestamp <= 0 || timestamp > MAX_DATE_TIMESTAMP_MS) {
105
- throw new Error(`Model exclusion store entry ${index} has an invalid ${field}.`);
106
- }
107
- }
108
- }
109
136
  const now = Date.now();
110
- exclusions = (data.exclusions ?? []).filter((e: ModelExclusion) => e.expiresAt > now);
137
+ let droppedInvalid = false;
138
+ exclusions = data.exclusions.flatMap((entry: unknown, index: number) => {
139
+ const result = readPersistedExclusion(entry, index);
140
+ if (!result.ok) {
141
+ droppedInvalid = true;
142
+ console.warn(`[model-exclusions] Ignoring invalid exclusion: ${result.message}`);
143
+ return [];
144
+ }
145
+ return [result.exclusion];
146
+ }).filter((e: ModelExclusion) => e.expiresAt > now);
111
147
  const shortened = loadedTTLCeilingMs !== undefined && shortenExclusionsToTTL(exclusions, loadedTTLCeilingMs, now);
112
148
  exclusions = deduplicate(exclusions);
113
- if (shortened) schedulePersist();
149
+ if (droppedInvalid) flushPersist();
150
+ else if (shortened) schedulePersist();
114
151
  }
152
+ invalidateAuthExclusions();
115
153
  } catch (error) {
116
154
  if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
117
155
  console.error(`[model-exclusions] Failed to load exclusions from ${getExclusionsFilePath()}:`, error);
@@ -119,6 +157,32 @@ function ensureLoaded(): void {
119
157
  }
120
158
  }
121
159
 
160
+ type PersistedExclusionRead =
161
+ | { ok: true; exclusion: ModelExclusion }
162
+ | { ok: false; message: string };
163
+
164
+ function invalidPersistedExclusion(index: number, message: string): PersistedExclusionRead {
165
+ return { ok: false, message: `Model exclusion store entry ${index} ${message}.` };
166
+ }
167
+
168
+ function readPersistedExclusion(entry: unknown, index: number): PersistedExclusionRead {
169
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return invalidPersistedExclusion(index, "must be an object");
170
+ const candidate = entry as { modelId?: unknown; provider?: unknown; reason?: unknown; recordedAt?: unknown; expiresAt?: unknown };
171
+ const { modelId, provider, reason } = candidate;
172
+ if (modelId !== undefined && (typeof modelId !== "string" || modelId.length === 0)) return invalidPersistedExclusion(index, "has an invalid modelId");
173
+ if (provider !== undefined && (typeof provider !== "string" || provider.length === 0)) return invalidPersistedExclusion(index, "has an invalid provider");
174
+ if (reason !== undefined && typeof reason !== "string") return invalidPersistedExclusion(index, "has an invalid reason");
175
+ const { recordedAt, expiresAt } = candidate;
176
+ if (typeof recordedAt !== "number" || !Number.isFinite(recordedAt) || recordedAt <= 0 || recordedAt > MAX_DATE_TIMESTAMP_MS) return invalidPersistedExclusion(index, "has an invalid recordedAt");
177
+ if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt) || expiresAt <= 0 || expiresAt > MAX_DATE_TIMESTAMP_MS) return invalidPersistedExclusion(index, "has an invalid expiresAt");
178
+ const metadata = { ...(reason === undefined ? {} : { reason }), recordedAt, expiresAt };
179
+ if (modelId === undefined) {
180
+ if (provider === undefined) return invalidPersistedExclusion(index, "must include modelId or provider");
181
+ return { ok: true, exclusion: { provider, ...metadata } };
182
+ }
183
+ return { ok: true, exclusion: { modelId, ...(provider === undefined ? {} : { provider }), ...metadata } };
184
+ }
185
+
122
186
  function dedupKey(entry: ModelExclusion): string {
123
187
  return `${entry.provider ?? ""}|${entry.modelId ?? ""}`;
124
188
  }
@@ -165,6 +229,7 @@ export function recordModelFailure(options: RecordModelFailureOptions): void {
165
229
  */
166
230
  export function clearExpiredExclusions(): void {
167
231
  ensureLoaded();
232
+ invalidateAuthExclusions();
168
233
  prune(exclusions, Date.now());
169
234
  schedulePersist();
170
235
  }
@@ -190,7 +255,7 @@ export function clearExclusions(): void {
190
255
  */
191
256
  function entryMatches(entry: ModelExclusion, candidateModelId: string, candidateProvider: string | undefined, now: number): boolean {
192
257
  if (entry.expiresAt <= now) return false;
193
- if (entry.modelId) {
258
+ if (entry.modelId !== undefined) {
194
259
  if (entry.modelId !== candidateModelId) return false;
195
260
  return !entry.provider || !candidateProvider || entry.provider === candidateProvider;
196
261
  }
@@ -202,6 +267,7 @@ function entryMatches(entry: ModelExclusion, candidateModelId: string, candidate
202
267
  */
203
268
  export function isExcluded(modelId: string, provider: string): boolean {
204
269
  ensureLoaded();
270
+ invalidateAuthExclusions();
205
271
  return exclusions.some((entry) => entryMatches(entry, modelId, provider, Date.now()));
206
272
  }
207
273
 
@@ -213,6 +279,7 @@ export function isExcluded(modelId: string, provider: string): boolean {
213
279
  */
214
280
  export function findModelExclusion(fullId: string, now = Date.now()): Readonly<ModelExclusion> | undefined {
215
281
  ensureLoaded();
282
+ invalidateAuthExclusions();
216
283
  const { provider, modelId } = parseModelKey(fullId);
217
284
  return exclusions.find((entry) => entryMatches(entry, modelId, provider, now));
218
285
  }
@@ -250,15 +317,17 @@ export function parseModelKey(fullId: string): { provider?: string; modelId: str
250
317
  export function filterFallbackCandidates(candidates: string[], opts?: {
251
318
  now?: number;
252
319
  onExcluded?: (candidate: string, exclusion: Readonly<ModelExclusion>) => void;
320
+ ignoreExclusion?: (candidate: string, exclusion: Readonly<ModelExclusion>) => boolean;
253
321
  }): string[] {
254
322
  ensureLoaded();
323
+ invalidateAuthExclusions();
255
324
  const timestamp = opts?.now ?? Date.now();
256
325
  const seen = new Set<string>();
257
326
  const filtered: string[] = [];
258
327
  for (const raw of candidates) {
259
328
  if (!raw || seen.has(raw)) continue;
260
329
  const { provider: candidateProvider, modelId: candidateModelId } = parseModelKey(raw);
261
- const exclusion = exclusions.find((entry) => entryMatches(entry, candidateModelId, candidateProvider, timestamp));
330
+ const exclusion = exclusions.find((entry) => entryMatches(entry, candidateModelId, candidateProvider, timestamp) && opts?.ignoreExclusion?.(raw, entry) !== true);
262
331
  if (exclusion) {
263
332
  opts?.onExcluded?.(raw, exclusion);
264
333
  continue;
@@ -1,4 +1,4 @@
1
- import { splitKnownThinkingSuffix, type ModelInfo as AvailableModelInfo } from "../../shared/model-info.ts";
1
+ import { splitKnownThinkingSuffix as splitThinkingSuffix, type ModelInfo as AvailableModelInfo } from "../../shared/model-info.ts";
2
2
  import type { Usage } from "../../shared/types.ts";
3
3
  import { filterFallbackCandidates, findModelExclusion, parseModelKey, recordModelFailure } from "./model-exclusions.ts";
4
4
  import { checkModelScope, type ModelScopeCheckRule, type ModelScopeViolation, type ModelSource } from "./model-scope.ts";
@@ -14,13 +14,19 @@ interface ModelAttemptSummary {
14
14
  usage?: Usage;
15
15
  }
16
16
 
17
- export function splitThinkingSuffix(model: string): { baseModel: string; thinkingSuffix: string } {
18
- return splitKnownThinkingSuffix(model);
19
- }
17
+ export { splitThinkingSuffix };
20
18
 
21
- export function formatSubagentModelVerificationError(expectedModel: string, observedModel: string, availableModels: AvailableModelInfo[] | undefined): string | undefined {
19
+ /** Aliases apply only to the resolved launch candidate (without its thinking suffix) and the exact raw response ID. */
20
+ export function formatSubagentModelVerificationError(
21
+ expectedModel: string,
22
+ observedModel: string,
23
+ availableModels: AvailableModelInfo[] | undefined,
24
+ modelResponseAliases?: Record<string, string[]>,
25
+ ): string | undefined {
22
26
  if (!availableModels || availableModels.length === 0) return undefined;
23
27
  const expectedBase = splitThinkingSuffix(expectedModel).baseModel;
28
+ if (modelResponseAliases && Object.hasOwn(modelResponseAliases, expectedBase)
29
+ && modelResponseAliases[expectedBase]?.includes(observedModel)) return undefined;
24
30
  const observedBase = splitThinkingSuffix(observedModel).baseModel;
25
31
  if (expectedBase === observedBase) return undefined;
26
32
  const expectedEntry = availableModels.find((entry) => entry.fullId === expectedBase);
@@ -30,7 +36,7 @@ export function formatSubagentModelVerificationError(expectedModel: string, obse
30
36
  const expectedFullIdLeaf = expectedEntry.fullId.slice(expectedEntry.fullId.lastIndexOf("/") + 1);
31
37
  if (expectedIdLeaf === observedBase || expectedFullIdLeaf === observedBase) return undefined;
32
38
  }
33
- return `model_verification_failed: child reported a different model than the launch candidate. Expected '${expectedModel}' but observed '${observedModel}'.`;
39
+ return `model_verification_failed: native Pi child reported a different model than the launch candidate. Expected '${expectedModel}' but observed '${observedModel}'. If you have independently verified this response ID identifies the requested model, declare the exact mapping in modelResponseAliases in ~/.selesai/agent/extensions/subagent/config.json (see docs/configuration.md#modelresponsealiases). Use the resolved provider/model ID without its thinking suffix as the key. This leaves the outgoing request unchanged. Configuration changes affect new independent native runs; resumed native runs retain their launch-time declaration. External CLI adapters do not use this setting.`;
34
40
  }
35
41
 
36
42
  /** Sentinel model value requesting that a subagent inherit the parent session's model. */
@@ -284,6 +290,49 @@ function enforceModelScopes(
284
290
  for (const violation of violations) (onWarn ?? defaultScopeWarn)(violation);
285
291
  }
286
292
 
293
+ const MODEL_EXCLUSION_DIAGNOSTIC_MAX_LENGTH = 240;
294
+ const MODEL_EXCLUSION_DIAGNOSTIC_MAX_ENTRIES = 20;
295
+
296
+ function sanitizeModelExclusionDiagnostic(value: string | undefined, fallback: string): string {
297
+ const normalized = typeof value === "string"
298
+ ? value.replace(/[\u0000-\u001f\u007f\u2028\u2029]+/g, " ").trim()
299
+ : "";
300
+ return redactSecretValues(normalized || fallback).slice(0, MODEL_EXCLUSION_DIAGNOSTIC_MAX_LENGTH);
301
+ }
302
+
303
+ function formatModelExclusionExpiry(expiresAt: number): string {
304
+ if (!Number.isFinite(expiresAt)) return "unknown";
305
+ const date = new Date(expiresAt);
306
+ return Number.isNaN(date.getTime()) ? "unknown" : date.toISOString();
307
+ }
308
+
309
+ function formatExcludedCandidateEvidence(candidate: string, exclusion: NonNullable<ReturnType<typeof findModelExclusion>>): string {
310
+ const { provider, modelId } = parseModelKey(candidate);
311
+ const displayCandidate = sanitizeModelExclusionDiagnostic(candidate, "unknown");
312
+ const displayModel = sanitizeModelExclusionDiagnostic(modelId, "unknown");
313
+ const displayProvider = sanitizeModelExclusionDiagnostic(provider ?? exclusion.provider, "unspecified");
314
+ const reason = sanitizeModelExclusionDiagnostic(exclusion.reason, "runtime-failure");
315
+ return `${displayCandidate} — model: ${displayModel}; provider: ${displayProvider}; reason: ${reason}; expires: ${formatModelExclusionExpiry(exclusion.expiresAt)}`;
316
+ }
317
+
318
+ const MODEL_UNAVAILABLE_EXCLUSION_PATTERNS = [
319
+ /model.*not found/i,
320
+ /unknown model/i,
321
+ /model.*unavailable/i,
322
+ /model.*disabled/i,
323
+ ];
324
+
325
+ function isCurrentRegistryModel(candidate: string, availableModels: AvailableModelInfo[] | undefined): boolean {
326
+ if (!availableModels || availableModels.length === 0) return false;
327
+ const { baseModel } = splitThinkingSuffix(candidate);
328
+ return availableModels.some((entry) => entry.fullId === baseModel);
329
+ }
330
+
331
+ function ignoreStaleModelUnavailableExclusion(candidate: string, exclusion: NonNullable<ReturnType<typeof findModelExclusion>>, availableModels: AvailableModelInfo[] | undefined): boolean {
332
+ const reason = exclusion.reason ?? "";
333
+ return MODEL_UNAVAILABLE_EXCLUSION_PATTERNS.some((pattern) => pattern.test(reason)) && isCurrentRegistryModel(candidate, availableModels);
334
+ }
335
+
287
336
  function throwForExplicitModelExclusion(model: string): void {
288
337
  const exclusion = findModelExclusion(model);
289
338
  if (!exclusion) return;
@@ -419,9 +468,15 @@ export function buildModelCandidates(
419
468
  if (!primaryModel) throwForUnresolvedEnforcedInheritScope(options?.scope, true);
420
469
  const origin = options?.origin ?? (options?.primaryModelFromParent ? "inherited" : "configured");
421
470
  const scopes = configuredScopes(options?.scope);
471
+ type ExcludedCandidate = { candidate: string; exclusion: NonNullable<ReturnType<typeof findModelExclusion>> };
472
+ const excludedCandidates: ExcludedCandidate[] = [];
473
+ let excludedCandidateCount = 0;
422
474
  const warnCachedExclusion = (candidate: string, exclusion: NonNullable<ReturnType<typeof findModelExclusion>>) => {
423
- const reason = redactSecretValues((exclusion.reason ?? "runtime-failure").replace(/[\u0000-\u001f\u007f]+/g, " ")).slice(0, 240);
424
- console.warn(`[pi-subagents] Skipping model '${candidate}' due to a cached exclusion (reason: ${reason}; expires: ${new Date(exclusion.expiresAt).toISOString()}).`);
475
+ excludedCandidateCount++;
476
+ if (excludedCandidates.length < MODEL_EXCLUSION_DIAGNOSTIC_MAX_ENTRIES) excludedCandidates.push({ candidate, exclusion });
477
+ const displayCandidate = sanitizeModelExclusionDiagnostic(candidate, "unknown");
478
+ const reason = sanitizeModelExclusionDiagnostic(exclusion.reason, "runtime-failure");
479
+ console.warn(`[pi-subagents] Skipping model '${displayCandidate}' due to a cached exclusion (reason: ${reason}; expires: ${formatModelExclusionExpiry(exclusion.expiresAt)}).`);
425
480
  };
426
481
  if (origin === "explicit" && primaryModel) {
427
482
  const normalized = resolveRequiredSubagentModelCandidate(primaryModel.trim(), availableModels, preferredProvider);
@@ -433,6 +488,7 @@ export function buildModelCandidates(
433
488
  const candidates: string[] = [];
434
489
  const rawCandidates = [primaryModel, ...(fallbackModels ?? [])];
435
490
  let skippedPrimary: string | undefined;
491
+ let skippedFallback: string | undefined;
436
492
  for (let index = 0; index < rawCandidates.length; index++) {
437
493
  const raw = rawCandidates[index];
438
494
  if (!raw) continue;
@@ -442,7 +498,10 @@ export function buildModelCandidates(
442
498
  : resolveSubagentModelCandidate(model, availableModels, preferredProvider);
443
499
  if (!normalized) {
444
500
  if (index === 0) skippedPrimary = model;
445
- else console.warn(`[pi-subagents] Skipping fallback model '${model}' because it is unavailable in this environment.`);
501
+ else {
502
+ skippedFallback ??= model;
503
+ console.warn(`[pi-subagents] Skipping fallback model '${model}' because it is unavailable in this environment.`);
504
+ }
446
505
  continue;
447
506
  }
448
507
  if (seen.has(normalized)) continue;
@@ -452,10 +511,21 @@ export function buildModelCandidates(
452
511
  seen.add(normalized);
453
512
  candidates.push(normalized);
454
513
  }
455
- const resolved = filterFallbackCandidates(candidates, { onExcluded: warnCachedExclusion });
514
+ const resolved = filterFallbackCandidates(candidates, {
515
+ onExcluded: warnCachedExclusion,
516
+ ignoreExclusion: (candidate, exclusion) => ignoreStaleModelUnavailableExclusion(candidate, exclusion, availableModels),
517
+ });
456
518
  if (resolved.length === 0) {
457
519
  if (skippedPrimary) resolveRequiredSubagentModelCandidate(skippedPrimary, availableModels, preferredProvider);
458
- if (candidates.length > 0) throw new Error(ZERO_USABLE_MODEL_CANDIDATES_ERROR);
520
+ if (candidates.length === 0 && skippedFallback) resolveRequiredSubagentModelCandidate(skippedFallback, availableModels, preferredProvider);
521
+ if (candidates.length > 0) {
522
+ const shownExclusions = excludedCandidates;
523
+ const omittedExclusions = excludedCandidateCount - shownExclusions.length;
524
+ const evidence = shownExclusions.length > 0
525
+ ? ` (excluded: ${shownExclusions.map(({ candidate, exclusion }) => formatExcludedCandidateEvidence(candidate, exclusion)).join("; ")}${omittedExclusions > 0 ? `; ... and ${omittedExclusions} more` : ""})`
526
+ : "";
527
+ throw new Error(`${ZERO_USABLE_MODEL_CANDIDATES_ERROR}${evidence}`);
528
+ }
459
529
  return resolved;
460
530
  }
461
531
  if (skippedPrimary) {
@@ -465,6 +535,7 @@ export function buildModelCandidates(
465
535
  }
466
536
 
467
537
  const RETRYABLE_MODEL_FAILURE_PATTERNS = [
538
+ /^REQUEST_LIMIT_EXCEEDED$/,
468
539
  /rate\s*limit/i,
469
540
  /usage\s*limit/i,
470
541
  /too many requests/i,
@@ -536,8 +607,13 @@ export function isRetryableModelFailureAttempt(input: { error: string | undefine
536
607
  return Boolean(error && input.messages?.some((message) => messageError(message)?.trim() === error));
537
608
  }
538
609
 
610
+ // Request-shape failures can match broad fallback signals such as "upstream",
611
+ // but do not establish that the model is unhealthy for subsequent requests.
612
+ const REQUEST_SHAPE_FAILURE_PATTERN = /\b(?:bad[ _]request|invalid[ _]argument|invalid_request_error)\b/i;
613
+
539
614
  export function recordRetryableModelFailure(model: string | undefined, error: string | undefined): void {
540
- if (!model || !isRetryableModelFailure(error)) return;
615
+ if (!model || !error || !isRetryableModelFailure(error) || isContextOverflow(error)) return;
616
+ if (REQUEST_SHAPE_FAILURE_PATTERN.test(error)) return;
541
617
  const { provider, modelId } = parseModelKey(model);
542
618
  recordModelFailure({ modelId, reason: error, ...(provider ? { provider } : {}) });
543
619
  }