@gajae-code/coding-agent 0.14.2 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (345) hide show
  1. package/CHANGELOG.md +120 -1
  2. package/dist/types/async/job-manager.d.ts +4 -1
  3. package/dist/types/autoresearch/dashboard.d.ts +29 -4
  4. package/dist/types/autoresearch/data-context.d.ts +25 -0
  5. package/dist/types/autoresearch/git.d.ts +65 -7
  6. package/dist/types/autoresearch/harness.d.ts +58 -0
  7. package/dist/types/autoresearch/prompts.d.ts +51 -0
  8. package/dist/types/autoresearch/runs.d.ts +173 -0
  9. package/dist/types/autoresearch/types.d.ts +9 -139
  10. package/dist/types/capability/types.d.ts +20 -0
  11. package/dist/types/cli/accounts-cli.d.ts +1 -0
  12. package/dist/types/commands/{rlm.d.ts → autoresearch.d.ts} +2 -4
  13. package/dist/types/config/autorouting-contract.d.ts +146 -0
  14. package/dist/types/config/autorouting-generator.d.ts +40 -0
  15. package/dist/types/config/autorouting-tier-map.d.ts +16844 -0
  16. package/dist/types/config/autorouting.d.ts +35 -0
  17. package/dist/types/config/model-discovery-manager.d.ts +8 -1
  18. package/dist/types/config/model-registry.d.ts +30 -1
  19. package/dist/types/config/provider-selection-policy.d.ts +23 -0
  20. package/dist/types/config/settings-schema.d.ts +61 -3
  21. package/dist/types/config/settings.d.ts +3 -0
  22. package/dist/types/coordinator/contract.d.ts +1 -1
  23. package/dist/types/coordinator-mcp/durability.d.ts +29 -0
  24. package/dist/types/coordinator-mcp/event-webhook.d.ts +50 -0
  25. package/dist/types/coordinator-mcp/question-state.d.ts +1 -1
  26. package/dist/types/coordinator-mcp/server.d.ts +14 -0
  27. package/dist/types/crash/index-store.d.ts +3 -0
  28. package/dist/types/crash/record-loader.d.ts +2 -0
  29. package/dist/types/crash/upstream/relay.d.ts +5 -3
  30. package/dist/types/cursor.d.ts +12 -0
  31. package/dist/types/defaults/gjc-defaults.d.ts +35 -1
  32. package/dist/types/defaults/gjc-skills.generated.d.ts +1 -1
  33. package/dist/types/discovery/helpers.d.ts +1 -1
  34. package/dist/types/edit/index.d.ts +2 -1
  35. package/dist/types/eval/py/executor.d.ts +2 -0
  36. package/dist/types/extensibility/extensions/loader.d.ts +32 -0
  37. package/dist/types/extensibility/gjc-plugins/types.d.ts +1 -1
  38. package/dist/types/extensibility/skills.d.ts +2 -0
  39. package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +194 -0
  40. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  41. package/dist/types/gjc-runtime/psmux-detect.d.ts +1 -1
  42. package/dist/types/gjc-runtime/python-transcript.d.ts +19 -0
  43. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +2 -8
  44. package/dist/types/gjc-runtime/session-layout.d.ts +6 -3
  45. package/dist/types/gjc-runtime/state-renderer.d.ts +0 -22
  46. package/dist/types/gjc-runtime/state-schema.d.ts +7 -7
  47. package/dist/types/gjc-runtime/tmux-common.d.ts +2 -2
  48. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +38 -1
  49. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -24
  50. package/dist/types/gjc-runtime/ultragoal-validation-policy.d.ts +73 -0
  51. package/dist/types/gjc-runtime/workflow-recovery-projection.d.ts +100 -0
  52. package/dist/types/hooks/skill-keywords.d.ts +1 -1
  53. package/dist/types/main.d.ts +0 -10
  54. package/dist/types/modes/acp/acp-agent.d.ts +13 -6
  55. package/dist/types/modes/components/model-selector.d.ts +14 -0
  56. package/dist/types/modes/components/skill-hud/render.d.ts +6 -1
  57. package/dist/types/modes/components/smart-routing-panel.d.ts +82 -0
  58. package/dist/types/modes/controllers/selector-controller.d.ts +14 -0
  59. package/dist/types/modes/interactive-mode.d.ts +3 -0
  60. package/dist/types/modes/types.d.ts +9 -0
  61. package/dist/types/rlm/artifacts.d.ts +4 -4
  62. package/dist/types/runtime/optional-runtime-services.d.ts +2 -1
  63. package/dist/types/runtime/workspace-tree-service.d.ts +5 -1
  64. package/dist/types/runtime-mcp/config.d.ts +6 -0
  65. package/dist/types/runtime-mcp/manager.d.ts +10 -0
  66. package/dist/types/sdk/acp/adapter.d.ts +0 -4
  67. package/dist/types/sdk/broker/broker.d.ts +1 -1
  68. package/dist/types/sdk/broker/lifecycle.d.ts +57 -2
  69. package/dist/types/sdk/broker/session-index.d.ts +15 -0
  70. package/dist/types/sdk/bus/chat-daemon-control.d.ts +6 -0
  71. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -0
  72. package/dist/types/sdk/bus/index.d.ts +8 -0
  73. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +2 -0
  74. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -2
  75. package/dist/types/sdk/client/client.d.ts +39 -4
  76. package/dist/types/sdk/host/host.d.ts +1 -0
  77. package/dist/types/sdk/host/internal-autorouting-state.d.ts +3 -0
  78. package/dist/types/sdk/host/model-pin.d.ts +29 -0
  79. package/dist/types/sdk/host/session-runtime.d.ts +2 -1
  80. package/dist/types/sdk/lifecycle-session.d.ts +13 -0
  81. package/dist/types/sdk/router/session-router.d.ts +27 -1
  82. package/dist/types/sdk/session.d.ts +11 -1
  83. package/dist/types/session/agent-session.d.ts +36 -6
  84. package/dist/types/session/artifacts.d.ts +16 -1
  85. package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
  86. package/dist/types/session/session-manager.d.ts +84 -2
  87. package/dist/types/session/session-staging-paths.d.ts +3 -0
  88. package/dist/types/skill-state/canonical-skills.d.ts +1 -1
  89. package/dist/types/skill-state/workflow-hud.d.ts +12 -21
  90. package/dist/types/skill-state/workflow-mutation-guard.d.ts +1 -0
  91. package/dist/types/slash-commands/helpers/autorouting-status.d.ts +8 -0
  92. package/dist/types/system-prompt.d.ts +23 -0
  93. package/dist/types/task/executor.d.ts +35 -4
  94. package/dist/types/task/index.d.ts +17 -3
  95. package/dist/types/task/receipt.d.ts +3 -1
  96. package/dist/types/task/types.d.ts +144 -0
  97. package/dist/types/tools/atomic-file-write.d.ts +72 -0
  98. package/dist/types/tools/descriptors.d.ts +1 -0
  99. package/dist/types/tools/index.d.ts +36 -1
  100. package/dist/types/tools/move-session.d.ts +40 -0
  101. package/dist/types/tools/python.d.ts +12 -0
  102. package/dist/types/tools/resource-gc.d.ts +0 -3
  103. package/dist/types/tools/search.d.ts +8 -1
  104. package/dist/types/tools/skill.d.ts +9 -4
  105. package/dist/types/tools/subagent.d.ts +33 -3
  106. package/dist/types/utils/xml-escape.d.ts +2 -0
  107. package/dist/types/workflow/workflow-intent-diff.d.ts +2 -2
  108. package/dist/types/workflow/workflow-intent-report-consumer.d.ts +1 -1
  109. package/package.json +11 -12
  110. package/scripts/build-sdk-package-smoke.ts +7 -5
  111. package/scripts/check-autorouting-tier-map.ts +183 -0
  112. package/scripts/generate-sdk-operation-inventory.ts +12 -0
  113. package/scripts/generate-tool-catalog.ts +40 -5
  114. package/src/async/job-manager.ts +5 -2
  115. package/src/autoresearch/dashboard.ts +134 -314
  116. package/src/autoresearch/data-context.ts +35 -0
  117. package/src/autoresearch/git.ts +186 -26
  118. package/src/autoresearch/{helpers.ts → harness.ts} +65 -40
  119. package/src/autoresearch/{prompt-setup.md → prompts/prompt-setup.md} +7 -7
  120. package/src/autoresearch/prompts/prompt.md +103 -0
  121. package/src/autoresearch/prompts.ts +126 -0
  122. package/src/autoresearch/runs.ts +708 -0
  123. package/src/autoresearch/types.ts +9 -150
  124. package/src/capability/index.ts +4 -4
  125. package/src/capability/types.ts +20 -0
  126. package/src/cli/accounts-cli.ts +21 -5
  127. package/src/cli/config-cli.ts +25 -1
  128. package/src/cli/fast-help.ts +1 -2
  129. package/src/cli/list-models.ts +5 -8
  130. package/src/cli/setup-cli.ts +8 -0
  131. package/src/cli.ts +2 -21
  132. package/src/commands/autoresearch.ts +33 -0
  133. package/src/commands/coordinator.ts +15 -0
  134. package/src/commands/launch.ts +2 -4
  135. package/src/commands/quick-lane.ts +1 -1
  136. package/src/commands/sdk.ts +24 -0
  137. package/src/commands/state.ts +1 -1
  138. package/src/config/autorouting-contract.ts +474 -0
  139. package/src/config/autorouting-generator.ts +238 -0
  140. package/src/config/autorouting-tier-map.ts +6108 -0
  141. package/src/config/autorouting.ts +120 -0
  142. package/src/config/model-discovery-manager.ts +42 -11
  143. package/src/config/model-registry.ts +348 -68
  144. package/src/config/provider-selection-policy.ts +60 -9
  145. package/src/config/settings-schema.ts +195 -6
  146. package/src/config/settings.ts +62 -1
  147. package/src/config.ts +2 -3
  148. package/src/coordinator/contract.ts +0 -1
  149. package/src/coordinator-mcp/codex-handoff.ts +68 -42
  150. package/src/coordinator-mcp/durability.ts +200 -0
  151. package/src/coordinator-mcp/event-webhook.ts +278 -0
  152. package/src/coordinator-mcp/question-state.ts +109 -46
  153. package/src/coordinator-mcp/server.ts +932 -504
  154. package/src/crash/index-store.ts +126 -22
  155. package/src/crash/record-loader.ts +7 -0
  156. package/src/crash/upstream/relay.ts +82 -34
  157. package/src/cursor.ts +84 -1
  158. package/src/debug/index.ts +26 -0
  159. package/src/defaults/gjc/skills/autoresearch/SKILL.md +116 -0
  160. package/src/defaults/gjc/skills/autoresearch/auto-critic.md +46 -0
  161. package/src/defaults/gjc/skills/autoresearch/auto-iterate.md +42 -0
  162. package/src/defaults/gjc/skills/deep-interview/SKILL.md +17 -37
  163. package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -18
  164. package/src/defaults/gjc/skills/ultragoal/SKILL.md +6 -17
  165. package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +2 -2
  166. package/src/defaults/gjc-defaults.ts +89 -2
  167. package/src/defaults/gjc-skills.generated.ts +24 -17
  168. package/src/discovery/builtin.ts +18 -5
  169. package/src/discovery/helpers.ts +6 -6
  170. package/src/edit/index.ts +5 -2
  171. package/src/eval/py/executor.ts +8 -0
  172. package/src/extensibility/extensions/loader.ts +135 -13
  173. package/src/extensibility/extensions/wrapper.ts +1 -2
  174. package/src/extensibility/gjc-plugins/schema.ts +1 -1
  175. package/src/extensibility/runtime-skill-discovery.ts +2 -2
  176. package/src/extensibility/skill-management.ts +2 -3
  177. package/src/extensibility/skills.ts +16 -8
  178. package/src/gjc-runtime/autoresearch-runtime.ts +1509 -0
  179. package/src/gjc-runtime/gc-render.ts +0 -1
  180. package/src/gjc-runtime/gc-runtime.ts +1 -11
  181. package/src/gjc-runtime/launch-tmux.ts +3 -3
  182. package/src/gjc-runtime/psmux-detect.ts +2 -2
  183. package/src/gjc-runtime/python-transcript.ts +58 -0
  184. package/src/gjc-runtime/ralplan-runtime.ts +6 -18
  185. package/src/gjc-runtime/session-layout.ts +18 -7
  186. package/src/gjc-runtime/session-state-sidecar.ts +58 -9
  187. package/src/gjc-runtime/state-renderer.ts +0 -38
  188. package/src/gjc-runtime/state-runtime.ts +43 -22
  189. package/src/gjc-runtime/state-schema.ts +1 -1
  190. package/src/gjc-runtime/tmux-common.ts +7 -7
  191. package/src/gjc-runtime/ultragoal-change-set.ts +232 -11
  192. package/src/gjc-runtime/ultragoal-runtime.ts +309 -114
  193. package/src/gjc-runtime/ultragoal-validation-policy.ts +247 -0
  194. package/src/gjc-runtime/workflow-command-ref.ts +15 -11
  195. package/src/gjc-runtime/workflow-manifest.generated.json +413 -532
  196. package/src/gjc-runtime/workflow-manifest.ts +43 -83
  197. package/src/gjc-runtime/workflow-recovery-projection.ts +616 -0
  198. package/src/hooks/skill-keywords.ts +3 -3
  199. package/src/hooks/skill-state.ts +2 -2
  200. package/src/internal-urls/docs-index.generated.ts +26 -24
  201. package/src/lsp/index.ts +25 -5
  202. package/src/main.ts +2 -27
  203. package/src/modes/acp/acp-agent.ts +200 -75
  204. package/src/modes/components/model-selector.ts +216 -13
  205. package/src/modes/components/oauth-selector.ts +5 -5
  206. package/src/modes/components/skill-hud/render.ts +173 -35
  207. package/src/modes/components/smart-routing-panel.ts +531 -0
  208. package/src/modes/components/tool-status-header.ts +6 -3
  209. package/src/modes/controllers/command-controller.ts +17 -7
  210. package/src/modes/controllers/input-controller.ts +44 -9
  211. package/src/modes/controllers/runtime-mcp-command-controller.ts +4 -0
  212. package/src/modes/controllers/selector-controller.ts +298 -65
  213. package/src/modes/interactive-mode.ts +8 -1
  214. package/src/modes/shared/agent-wire/workflow-gate-types.ts +1 -1
  215. package/src/modes/types.ts +9 -1
  216. package/src/prompts/agents/planner.md +1 -1
  217. package/src/prompts/system/system-prompt.md +1 -1
  218. package/src/prompts/system/volatile-project-context.md +2 -1
  219. package/src/prompts/tools/move-session.md +15 -0
  220. package/src/prompts/tools/python.md +12 -0
  221. package/src/prompts/tools/skill-discovery.md +1 -1
  222. package/src/prompts/tools/skill.md +2 -2
  223. package/src/prompts/tools/task-summary.md +1 -0
  224. package/src/prompts/tools/task.md +6 -0
  225. package/src/rlm/artifacts.ts +12 -10
  226. package/src/runtime/optional-runtime-services.ts +3 -2
  227. package/src/runtime/workspace-tree-service.ts +11 -3
  228. package/src/runtime-mcp/config.ts +8 -1
  229. package/src/runtime-mcp/manager.ts +33 -0
  230. package/src/sdk/acp/adapter.ts +48 -1
  231. package/src/sdk/broker/broker.ts +30 -2
  232. package/src/sdk/broker/discovery.ts +217 -2
  233. package/src/sdk/broker/lifecycle.ts +385 -59
  234. package/src/sdk/broker/session-index.ts +243 -48
  235. package/src/sdk/broker/transport.ts +1 -0
  236. package/src/sdk/bus/chat-daemon-control.ts +8 -2
  237. package/src/sdk/bus/chat-daemon-runtime.ts +5 -0
  238. package/src/sdk/bus/index.ts +208 -11
  239. package/src/sdk/bus/kind-aware-reconciliation.ts +16 -0
  240. package/src/sdk/bus/reconciliation-store.ts +27 -5
  241. package/src/sdk/bus/telegram-daemon-contract.ts +12 -2
  242. package/src/sdk/client/client.ts +204 -22
  243. package/src/sdk/host/host.ts +16 -0
  244. package/src/sdk/host/internal-autorouting-state.ts +13 -0
  245. package/src/sdk/host/model-pin.ts +101 -0
  246. package/src/sdk/host/reverse-leases.ts +2 -1
  247. package/src/sdk/host/session-runtime.test.ts +224 -1
  248. package/src/sdk/host/session-runtime.ts +125 -37
  249. package/src/sdk/lifecycle/service.ts +3 -1
  250. package/src/sdk/lifecycle-session.ts +42 -1
  251. package/src/sdk/protocol/operation-inventory.generated.json +66 -0
  252. package/src/sdk/router/session-router.ts +375 -36
  253. package/src/sdk/session.ts +547 -47
  254. package/src/session/account-inventory.ts +1 -1
  255. package/src/session/agent-session.ts +765 -189
  256. package/src/session/artifacts.ts +234 -4
  257. package/src/session/internal/managed-session-storage.ts +91 -4
  258. package/src/session/provider-safety-stop-hint.ts +3 -2
  259. package/src/session/session-manager.ts +775 -16
  260. package/src/session/session-staging-paths.ts +8 -0
  261. package/src/setup/hermes/templates/operator-instructions.v1.md +0 -1
  262. package/src/setup/host-plugin-setup.ts +2 -2
  263. package/src/skill-state/active-state.ts +2 -2
  264. package/src/skill-state/canonical-skills.ts +1 -1
  265. package/src/skill-state/initial-phase.ts +1 -1
  266. package/src/skill-state/workflow-hud.ts +41 -41
  267. package/src/skill-state/workflow-mutation-guard.ts +26 -7
  268. package/src/slash-commands/builtin-registry.ts +77 -0
  269. package/src/slash-commands/helpers/autorouting-status.ts +55 -0
  270. package/src/system-prompt.ts +102 -5
  271. package/src/task/discovery.ts +2 -3
  272. package/src/task/executor.ts +617 -43
  273. package/src/task/index.ts +262 -15
  274. package/src/task/receipt.ts +30 -0
  275. package/src/task/render.ts +11 -1
  276. package/src/task/types.ts +316 -0
  277. package/src/tools/atomic-file-write.ts +594 -0
  278. package/src/tools/bisect.ts +3 -2
  279. package/src/tools/descriptors.test.ts +3 -0
  280. package/src/tools/descriptors.ts +24 -0
  281. package/src/tools/image-gen.ts +5 -1
  282. package/src/tools/index.ts +62 -5
  283. package/src/tools/move-session.ts +107 -0
  284. package/src/tools/python.ts +205 -0
  285. package/src/tools/read.ts +88 -15
  286. package/src/tools/renderers.ts +2 -0
  287. package/src/tools/resource-gc.ts +8 -177
  288. package/src/tools/search.ts +30 -6
  289. package/src/tools/skill-discovery.ts +1 -0
  290. package/src/tools/skill.ts +37 -7
  291. package/src/tools/subagent-render.ts +119 -13
  292. package/src/tools/subagent.ts +91 -74
  293. package/src/tools/tool-catalog.generated.ts +64 -2
  294. package/src/tools/write.ts +16 -12
  295. package/src/utils/xml-escape.ts +14 -0
  296. package/src/web/search/providers/codex.ts +2 -2
  297. package/src/workflow/workflow-intent-diff.ts +13 -12
  298. package/src/workflow/workflow-intent-report-consumer.ts +6 -6
  299. package/dist/types/autoresearch/helpers.d.ts +0 -24
  300. package/dist/types/autoresearch/index.d.ts +0 -2
  301. package/dist/types/autoresearch/state.d.ts +0 -17
  302. package/dist/types/autoresearch/storage.d.ts +0 -142
  303. package/dist/types/autoresearch/tools/init-experiment.d.ts +0 -31
  304. package/dist/types/autoresearch/tools/log-experiment.d.ts +0 -23
  305. package/dist/types/autoresearch/tools/run-experiment.d.ts +0 -8
  306. package/dist/types/autoresearch/tools/update-notes.d.ts +0 -12
  307. package/dist/types/commands/team.d.ts +0 -24
  308. package/dist/types/gjc-runtime/team-gc.d.ts +0 -7
  309. package/dist/types/gjc-runtime/team-launch.d.ts +0 -61
  310. package/dist/types/gjc-runtime/team-notify.d.ts +0 -45
  311. package/dist/types/gjc-runtime/team-runtime.d.ts +0 -336
  312. package/dist/types/gjc-runtime/team-store.d.ts +0 -168
  313. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +0 -43
  314. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +0 -92
  315. package/dist/types/gjc-runtime/team-workers.d.ts +0 -83
  316. package/dist/types/rlm/index.d.ts +0 -47
  317. package/dist/types/rlm/preset.d.ts +0 -23
  318. package/dist/types/rlm/python-tool.d.ts +0 -16
  319. package/src/autoresearch/command-resume.md +0 -14
  320. package/src/autoresearch/index.ts +0 -536
  321. package/src/autoresearch/prompt.md +0 -103
  322. package/src/autoresearch/resume-message.md +0 -10
  323. package/src/autoresearch/state.ts +0 -273
  324. package/src/autoresearch/storage.ts +0 -699
  325. package/src/autoresearch/tools/init-experiment.ts +0 -272
  326. package/src/autoresearch/tools/log-experiment.ts +0 -524
  327. package/src/autoresearch/tools/run-experiment.ts +0 -475
  328. package/src/autoresearch/tools/update-notes.ts +0 -109
  329. package/src/commands/rlm.ts +0 -19
  330. package/src/commands/team.ts +0 -238
  331. package/src/defaults/gjc/skills/deep-interview/auto-research-greenfield.md +0 -42
  332. package/src/defaults/gjc/skills/team/SKILL.md +0 -467
  333. package/src/gjc-runtime/team-gc.ts +0 -49
  334. package/src/gjc-runtime/team-launch.ts +0 -404
  335. package/src/gjc-runtime/team-notify.ts +0 -326
  336. package/src/gjc-runtime/team-runtime.ts +0 -5923
  337. package/src/gjc-runtime/team-store.ts +0 -878
  338. package/src/gjc-runtime/team-worker-heartbeat.ts +0 -149
  339. package/src/gjc-runtime/team-worker-memory-guard.ts +0 -363
  340. package/src/gjc-runtime/team-workers.ts +0 -1123
  341. package/src/prompts/system/rlm-report-command.md +0 -1
  342. package/src/prompts/system/rlm-research.md +0 -23
  343. package/src/rlm/index.ts +0 -365
  344. package/src/rlm/preset.ts +0 -76
  345. package/src/rlm/python-tool.ts +0 -68
@@ -23,10 +23,16 @@ import {
23
23
  modelSupportsServiceTier,
24
24
  type ServiceTier,
25
25
  } from "@gajae-code/ai/core";
26
+ import {
27
+ classifyFallbackTrigger,
28
+ type TransportFailureFacts,
29
+ transportFailureFacts,
30
+ } from "@gajae-code/ai/utils/fallback-transport";
26
31
  import { type JsonSchemaValidationIssue, validateJsonSchemaValue } from "@gajae-code/ai/utils/schema";
27
32
  import * as canonicalSdk from "@gajae-code/coding-agent/sdk";
28
33
  import { logger, prompt, untilAborted } from "@gajae-code/utils";
29
34
  import { AsyncJobManager } from "../async";
35
+ import { AUTOROUTING_SELECTOR_MAX_LENGTH, type AutoroutingReasonCode } from "../config/autorouting-contract";
30
36
  import { ModelRegistry } from "../config/model-registry";
31
37
  import { formatModelString, resolveModelOverrideWithAuthFallback } from "../config/model-resolver";
32
38
  import type { PromptTemplate } from "../config/prompt-templates";
@@ -45,7 +51,7 @@ import submitReminderTemplate from "../prompts/system/subagent-yield-reminder.md
45
51
  import { AgentRegistry } from "../registry/agent-registry";
46
52
  import { discoverAuthStorage } from "../sdk";
47
53
  import type { AgentSession, AgentSessionEvent, ForkContextSeed } from "../session/agent-session";
48
- import type { ArtifactManager } from "../session/artifacts";
54
+ import { ArtifactManager } from "../session/artifacts";
49
55
  import type { AuthStorage } from "../session/auth-storage";
50
56
  import { SKILL_PROMPT_MESSAGE_TYPE } from "../session/messages";
51
57
  import { SessionManager, type SessionMemoryMode } from "../session/session-manager";
@@ -67,8 +73,15 @@ import { persistTaskTokenLog, taskTokenLogFromUsage } from "./token-log";
67
73
  import {
68
74
  type AgentDefinition,
69
75
  type AgentProgress,
76
+ type AutoroutingAttempt,
77
+ type AutoroutingAttemptCode,
78
+ type AutoroutingPreflightFailure,
79
+ assertRoutingEvidenceInvariant,
80
+ createLocalErrorSummary,
70
81
  createSetupFailureSummary,
71
82
  hasCompleteUsageCostBreakdown,
83
+ isAssistantLocalErrorKind,
84
+ type LocalErrorSummary,
72
85
  MAX_OUTPUT_BYTES,
73
86
  MAX_OUTPUT_LINES,
74
87
  type ModelSubstitutionWarning,
@@ -78,8 +91,12 @@ import {
78
91
  TASK_SUBAGENT_EVENT_CHANNEL,
79
92
  TASK_SUBAGENT_LIFECYCLE_CHANNEL,
80
93
  TASK_SUBAGENT_PROGRESS_CHANNEL,
94
+ type TaskRoutingEvidence,
81
95
  type TaskToolDetails,
82
96
  } from "./types";
97
+
98
+ export type { AutoroutingPreflightFailure } from "./types";
99
+
83
100
  import { type ExecutorExecutionMode, resolveUltragoalRedTeamActivation } from "./ultragoal-redteam-activation";
84
101
 
85
102
  /** Agent event types to forward for progress tracking. */
@@ -211,6 +228,17 @@ export interface ExecutorOptions {
211
228
  executionMode?: ExecutorExecutionMode;
212
229
  context?: string;
213
230
  description?: string;
231
+ routing?: TaskRoutingEvidence;
232
+ /** Ordered, normalized autorouting candidates for the cross-phase preflight ledger. */
233
+ autoroutingCandidates?: string[];
234
+ autoroutingSkips?: Array<{ selector: string; code: AutoroutingReasonCode }>;
235
+ autoroutingPreflightErrors?: Map<string, unknown>;
236
+ autoroutingPreflight?: boolean;
237
+ autoroutingAttemptId?: string;
238
+ preflightProbe?: boolean;
239
+ preflightDurable?: boolean;
240
+ preflightFenceCallback?: () => void;
241
+
214
242
  index: number;
215
243
  id: string;
216
244
  modelOverride?: string | string[];
@@ -326,6 +354,23 @@ export class ManagedTaskPersistence {
326
354
  this.#artifacts.assertManagedBinding();
327
355
  return session;
328
356
  }
357
+ async openStagedSession(attemptId = this.#taskId): Promise<SessionManager> {
358
+ const store = this.#artifacts.getManagedStore();
359
+ if (!store) throw new Error("Managed task persistence authority is unavailable");
360
+ this.#artifacts.assertManagedBinding();
361
+ const sessionFile = path.join(this.#artifacts.dir, `${this.#taskId}.jsonl`);
362
+ const session = await SessionManager.stagedNestedManaged(
363
+ sessionFile,
364
+ SessionManager.nestedManagedDestination(store, this.#artifacts.dir),
365
+ store,
366
+ undefined,
367
+ attemptId,
368
+ );
369
+ const stagedArtifacts = this.#artifacts.createAttemptStaging(attemptId);
370
+ session.adoptArtifactManager(stagedArtifacts, this.#artifacts);
371
+ this.#artifacts.assertManagedBinding();
372
+ return session;
373
+ }
329
374
 
330
375
  async publishOutput(rawOutput: string, metadata: Uint8Array): Promise<void> {
331
376
  await withArtifactManagerFinalizationTurn(this.#artifacts, () =>
@@ -556,6 +601,7 @@ interface FinalizeSubprocessOutputArgs {
556
601
  rawOutput: string;
557
602
  exitCode: number;
558
603
  stderr: string;
604
+ terminalFailure?: boolean;
559
605
  doneAborted: boolean;
560
606
  signalAborted: boolean;
561
607
  yieldItems?: YieldItem[];
@@ -619,7 +665,7 @@ function buildPlaceholderYieldOutcome(
619
665
 
620
666
  export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): FinalizeSubprocessOutputResult {
621
667
  let { rawOutput, exitCode, stderr } = args;
622
- const { yieldItems, doneAborted, signalAborted, outputSchema } = args;
668
+ const { yieldItems, terminalFailure = false, doneAborted, signalAborted, outputSchema } = args;
623
669
  let abortedViaYield = false;
624
670
  const hasYield = Array.isArray(yieldItems) && yieldItems.length > 0;
625
671
 
@@ -673,8 +719,15 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
673
719
  const errorMessage = err instanceof Error ? err.message : String(err);
674
720
  rawOutput = `{"error":"Failed to serialize yield data: ${errorMessage}"}`;
675
721
  }
676
- exitCode = 0;
677
- stderr = "";
722
+ // A valid yield can preserve policy-safe public review output after a
723
+ // terminal provider failure, but it cannot convert that failed run
724
+ // into a successful subagent result. A normal yield starts with a
725
+ // non-zero provisional exit code, so use the explicit terminal fact
726
+ // rather than the provisional code to distinguish the two cases.
727
+ if (!terminalFailure) {
728
+ exitCode = 0;
729
+ stderr = "";
730
+ }
678
731
  }
679
732
  }
680
733
  }
@@ -807,10 +860,135 @@ export function createSubagentSettings(baseSettings: Settings, inheritedServiceT
807
860
  });
808
861
  }
809
862
 
863
+ /**
864
+ * Finalize routing evidence at the executor return boundary: the effective
865
+ * model is the terminal provider-reported model when present, otherwise the
866
+ * auth-resolved model; substitution causes are appended in order.
867
+ */
868
+ export function finalizeRoutingEvidence(
869
+ routing: TaskRoutingEvidence | undefined,
870
+ state: {
871
+ resolvedModelString: string | undefined;
872
+ lastAssistantModelString: string | undefined;
873
+ authFallbackUsed: boolean;
874
+ assistantModelMismatch: boolean;
875
+ },
876
+ ): TaskRoutingEvidence | undefined {
877
+ if (!routing) return undefined;
878
+ const terminalModel = state.lastAssistantModelString ?? state.resolvedModelString;
879
+ const effectiveModel = terminalModel === undefined ? undefined : boundedSelector(terminalModel);
880
+ const substitutions: TaskRoutingEvidence["substitutions"] = [];
881
+ if (state.authFallbackUsed) substitutions.push("auth_substituted");
882
+ if (state.assistantModelMismatch) substitutions.push("assistant_model_mismatch");
883
+ if (!effectiveModel) {
884
+ if (!routing.notExecuted && !routing.terminal && !routing.attempts) return undefined;
885
+ const terminalEvidence = { ...routing, substitutions };
886
+ assertRoutingEvidenceInvariant(terminalEvidence);
887
+ return terminalEvidence;
888
+ }
889
+ const evidence = {
890
+ ...routing,
891
+ effectiveModel,
892
+ ...(state.resolvedModelString && state.resolvedModelString !== terminalModel
893
+ ? { authResolvedModel: boundedSelector(state.resolvedModelString) }
894
+ : {}),
895
+ substitutions,
896
+ };
897
+ assertRoutingEvidenceInvariant(evidence);
898
+ return evidence;
899
+ }
900
+
810
901
  /**
811
902
  * Run a single agent in-process.
812
903
  */
813
- export async function runSubprocess(options: ExecutorOptions): Promise<SingleResult> {
904
+ class AutoroutingProbeAcceptedError extends Error {
905
+ readonly code = "autorouting_probe_accepted";
906
+ constructor() {
907
+ super("autorouting_probe_accepted");
908
+ }
909
+ }
910
+
911
+ function toError(error: unknown): Error {
912
+ return error instanceof Error ? error : new Error(String(error));
913
+ }
914
+
915
+ function formatExecutionError(error: unknown): string {
916
+ if (error instanceof AggregateError) {
917
+ const causes = error.errors.map(cause => (cause instanceof Error ? cause.message : String(cause))).join("; ");
918
+ return causes ? `${error.message}: ${causes}` : error.message;
919
+ }
920
+ return error instanceof Error ? error.stack || error.message : String(error);
921
+ }
922
+
923
+ function transportFactsFromError(error: unknown): TransportFailureFacts | undefined {
924
+ if (!error || typeof error !== "object") return undefined;
925
+ const value = error as { transportFailure?: unknown };
926
+ if (!value.transportFailure || typeof value.transportFailure !== "object") return undefined;
927
+ return value.transportFailure as TransportFailureFacts;
928
+ }
929
+
930
+ /**
931
+ * True only for the explicit "no credential found for this candidate" signal thrown at the
932
+ * auth_resolve preflight step, never for an unexpected exception the credential lookup itself
933
+ * raised (keychain access denied, corrupted store, I/O failure). auth_resolve is the one op
934
+ * that always advances to the next candidate regardless of any transient marker, so it must
935
+ * only ever be reached via this explicit, typed signal — not by inferring intent from which
936
+ * preflight phase happened to be executing when an unrelated error was thrown.
937
+ */
938
+ function isCredentialMissingSignal(error: unknown): boolean {
939
+ return Boolean(
940
+ error && typeof error === "object" && (error as { credentialMissing?: unknown }).credentialMissing === true,
941
+ );
942
+ }
943
+
944
+ export function classifyAutoroutingPreflightFailure(
945
+ error: unknown,
946
+ op: Extract<AutoroutingPreflightFailure, { kind: "local" }>["op"],
947
+ ): AutoroutingPreflightFailure {
948
+ const facts = transportFactsFromError(error) ?? transportFailureFacts(error);
949
+ if (facts) return { kind: "transport", class: classifyFallbackTrigger(facts).class };
950
+ const typedTransient =
951
+ error && typeof error === "object" && typeof (error as { transient?: unknown }).transient === "boolean"
952
+ ? (error as { transient: boolean }).transient
953
+ : undefined;
954
+ // An exception raised while op is auth_resolve is only the deliberate missing-credential
955
+ // signal when it carries the explicit marker; any other error surfacing during that window
956
+ // (an unexpected keychain/config failure) must fail closed like every other unclassified
957
+ // local error, not silently advance as if credentials were simply absent.
958
+ const resolvedOp = op === "auth_resolve" && !isCredentialMissingSignal(error) ? "preflight_validation" : op;
959
+ return {
960
+ kind: "local",
961
+ op: resolvedOp,
962
+ // Local setup failures are fail-closed: only an explicit transient marker
963
+ // can advance a candidate. Unknown session/tool errors are terminal.
964
+ transient: typedTransient === true,
965
+ };
966
+ }
967
+
968
+ /**
969
+ * Map a preflight failure to the attempt code that is recorded in routing evidence AND whether the
970
+ * ledger may advance to the next candidate. These are two different questions: a
971
+ * `config_invalid_terminal` / `unclassified_terminal` code is recorded *and* stops the ledger, so
972
+ * the caller must never infer "advance" from the mere presence of a code.
973
+ */
974
+ function autoroutingAttemptDisposition(failure: AutoroutingPreflightFailure): {
975
+ code: AutoroutingAttemptCode;
976
+ advance: boolean;
977
+ } {
978
+ if (failure.kind === "local" && failure.op === "auth_resolve")
979
+ return { code: "credential_unavailable", advance: true };
980
+ if (
981
+ failure.kind === "local" &&
982
+ (failure.op === "session_open" || failure.op === "tool_bootstrap") &&
983
+ failure.transient
984
+ )
985
+ return { code: "spawn_transient_retry", advance: true };
986
+ if (failure.kind === "local" && (!failure.transient || failure.op === "preflight_validation"))
987
+ return { code: "config_invalid_terminal", advance: false };
988
+ return { code: "unclassified_terminal", advance: false };
989
+ }
990
+
991
+ export async function runSubprocessOnce(options: ExecutorOptions): Promise<SingleResult> {
814
992
  const {
815
993
  cwd,
816
994
  agent,
@@ -948,6 +1126,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
948
1126
  let pauseRequested = false;
949
1127
  let paused = false;
950
1128
  let modelSubstitutionWarning: ModelSubstitutionWarning | undefined;
1129
+ let authFallbackUsed = false;
1130
+ let assistantModelMismatch = false;
951
1131
  let resolvedModelString: string | undefined;
952
1132
  let lastAssistantModelString: string | undefined;
953
1133
  let activeProviderModelString: string | undefined;
@@ -957,6 +1137,13 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
957
1137
  let retryStartOrdinal = 0;
958
1138
  const seenAssistantMessages = new WeakSet<AgentMessage>();
959
1139
  let llmRequestStarted = false;
1140
+ let preflightFenceCrossed = false;
1141
+ let preflightFailure: AutoroutingPreflightFailure | undefined;
1142
+ let preflightCommitFailure = false;
1143
+ let lifecycleStarted = false;
1144
+ let liveHandleRegistered = false;
1145
+ let probeAccepted = false;
1146
+ let preflightOperation: Extract<AutoroutingPreflightFailure, { kind: "local" }>["op"] = "preflight_validation";
960
1147
  const seenAssistantMessageIdentities = new Set<string>();
961
1148
 
962
1149
  // Accumulate usage incrementally from message_end events (no memory for streaming events)
@@ -1384,6 +1571,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1384
1571
  effective: assistantModel,
1385
1572
  reason: "assistant_model_mismatch",
1386
1573
  };
1574
+ assistantModelMismatch = true;
1575
+
1387
1576
  progress.modelSubstitutionWarning = modelSubstitutionWarning;
1388
1577
  activeSession?.sessionManager.appendModelChange(assistantModel, undefined, {
1389
1578
  previousModel: resolvedModelString,
@@ -1462,14 +1651,23 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1462
1651
  const runSubagent = async (): Promise<{
1463
1652
  exitCode: number;
1464
1653
  error?: string;
1654
+ localErrorSummary?: LocalErrorSummary;
1465
1655
  aborted?: boolean;
1466
1656
  abortReason?: string;
1467
1657
  setupFailure?: SetupFailureSummary;
1658
+ probeAccepted?: boolean;
1659
+ preflightFailure?: AutoroutingPreflightFailure;
1660
+ preflightFenceCrossed?: boolean;
1661
+ preflightCommitFailure?: boolean;
1468
1662
  durationMs: number;
1469
1663
  }> => {
1664
+ let openedSessionManager: SessionManager | null = null;
1665
+ let liveHandleManager: AsyncJobManager | undefined;
1666
+ const liveSubagentId = options.subagentId ?? id;
1470
1667
  const sessionAbortController = new AbortController();
1471
1668
  let exitCode = 0;
1472
1669
  let error: string | undefined;
1670
+ let localErrorSummary: LocalErrorSummary | undefined;
1473
1671
  let aborted = false;
1474
1672
  let abortReasonText: string | undefined;
1475
1673
  let setupFailure: SetupFailureSummary | undefined;
@@ -1524,11 +1722,12 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1524
1722
 
1525
1723
  const canonicalChildScope = getSubagentCanonicalScope(options.parentSessionId, options.subagentId ?? id);
1526
1724
 
1725
+ if (options.preflightProbe || options.preflightDurable) preflightOperation = "auth_resolve";
1527
1726
  const {
1528
1727
  model,
1529
1728
  thinkingLevel: resolvedThinkingLevel,
1530
1729
  explicitThinkingLevel,
1531
- authFallbackUsed,
1730
+ authFallbackUsed: resolvedAuthFallbackUsed,
1532
1731
  requestedModel,
1533
1732
  fallbackReason,
1534
1733
  activeIndex,
@@ -1537,7 +1736,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1537
1736
  } = await awaitAbortable(
1538
1737
  resolveModelOverrideWithAuthFallback(
1539
1738
  modelPatterns,
1540
- options.parentActiveModelPattern,
1739
+ options.preflightProbe || options.preflightDurable ? undefined : options.parentActiveModelPattern,
1541
1740
  modelRegistry,
1542
1741
  settings,
1543
1742
  options.parentCredentialSessionId ?? options.parentSessionId,
@@ -1548,6 +1747,25 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1548
1747
  canonicalChildScope,
1549
1748
  ),
1550
1749
  );
1750
+ if (
1751
+ (options.preflightProbe || options.preflightDurable) &&
1752
+ model &&
1753
+ typeof modelRegistry.getApiKey === "function"
1754
+ ) {
1755
+ preflightOperation = "auth_resolve";
1756
+ const exactKey = await awaitAbortable(
1757
+ modelRegistry.getApiKey(model, options.parentCredentialSessionId ?? options.parentSessionId),
1758
+ );
1759
+ if (!exactKey)
1760
+ throw Object.assign(new Error("autorouting credential unavailable"), {
1761
+ transient: false,
1762
+ // Explicit signal: credentials were looked up successfully and are simply absent for
1763
+ // this candidate. Distinguishes the deliberate "advance to next candidate" case from
1764
+ // an unexpected error the lookup itself threw (see classifyAutoroutingPreflightFailure).
1765
+ credentialMissing: true,
1766
+ });
1767
+ }
1768
+ authFallbackUsed = resolvedAuthFallbackUsed;
1551
1769
  if (model) {
1552
1770
  resolvedModelString = formatModelString(model);
1553
1771
  activeProviderModelString = resolvedModelString;
@@ -1588,23 +1806,55 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1588
1806
  : (thinkingLevel ?? resolvedThinkingLevel);
1589
1807
  effectiveThinkingLevelForWarning = effectiveThinkingLevel;
1590
1808
 
1591
- const sessionManager = options.managedPersistence
1592
- ? await awaitAbortable(
1593
- options.managedPersistence.openSession(worktree ?? cwd, subagentSettings.get("sessionMemory.mode")),
1594
- )
1595
- : sessionFile
1596
- ? await awaitAbortable(
1597
- SessionManager.open(
1598
- sessionFile,
1599
- SessionManager.explicitDestination(path.dirname(sessionFile)),
1600
- new FileSessionStorage(),
1601
- subagentSettings.get("session.directoryMigration") === "disabled" ? "disabled" : "copy-retain",
1602
- subagentSettings.get("sessionMemory.mode"),
1603
- ),
1604
- )
1605
- : SessionManager.inMemory(worktree ?? cwd);
1606
- if (options.parentArtifactManager) {
1809
+ preflightOperation = "session_open";
1810
+ const sessionManager = options.preflightProbe
1811
+ ? SessionManager.inMemory(worktree ?? cwd)
1812
+ : options.preflightDurable
1813
+ ? options.managedPersistence
1814
+ ? await awaitAbortable(
1815
+ options.managedPersistence.openStagedSession(options.autoroutingAttemptId ?? id),
1816
+ )
1817
+ : sessionFile
1818
+ ? await awaitAbortable(
1819
+ SessionManager.openStaged(sessionFile, undefined, options.autoroutingAttemptId ?? id),
1820
+ )
1821
+ : SessionManager.inMemory(worktree ?? cwd)
1822
+ : options.managedPersistence
1823
+ ? await awaitAbortable(
1824
+ options.managedPersistence.openSession(
1825
+ worktree ?? cwd,
1826
+ subagentSettings.get("sessionMemory.mode"),
1827
+ ),
1828
+ )
1829
+ : sessionFile
1830
+ ? await awaitAbortable(
1831
+ SessionManager.open(
1832
+ sessionFile,
1833
+ SessionManager.explicitDestination(path.dirname(sessionFile)),
1834
+ new FileSessionStorage(),
1835
+ subagentSettings.get("session.directoryMigration") === "disabled"
1836
+ ? "disabled"
1837
+ : "copy-retain",
1838
+ subagentSettings.get("sessionMemory.mode"),
1839
+ ),
1840
+ )
1841
+ : SessionManager.inMemory(worktree ?? cwd);
1842
+ openedSessionManager = sessionManager;
1843
+ if (options.parentArtifactManager && !options.preflightProbe && !options.preflightDurable) {
1607
1844
  sessionManager.adoptArtifactManager(options.parentArtifactManager);
1845
+ } else if (options.preflightDurable && !options.managedPersistence) {
1846
+ // Only the unmanaged durable preflight adopts its attempt staging here. With
1847
+ // managedPersistence the session came from openStagedSession(), which already
1848
+ // adopted exactly one task-scoped attempt root; adopting a second instance over
1849
+ // the same root would leave the first unreachable from commit/discard.
1850
+ const attemptId = options.autoroutingAttemptId ?? id;
1851
+ const parentArtifacts =
1852
+ options.parentArtifactManager ??
1853
+ (sessionFile
1854
+ ? new ArtifactManager(sessionFile.endsWith(".jsonl") ? sessionFile.slice(0, -6) : sessionFile)
1855
+ : undefined);
1856
+ if (parentArtifacts)
1857
+ sessionManager.adoptArtifactManager(parentArtifacts.createAttemptStaging(attemptId), parentArtifacts);
1608
1858
  }
1609
1859
 
1610
1860
  // Subagents do not inherit or discover MCP runtime tools in the GJC surface.
@@ -1685,6 +1935,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1685
1935
  logger.warn("Failed to render GJC plugin agent prompt additions", { error });
1686
1936
  }
1687
1937
 
1938
+ if (options.preflightProbe || options.preflightDurable) preflightOperation = "tool_bootstrap";
1688
1939
  const { session } = await awaitAbortable(
1689
1940
  canonicalSdk.createAgentSession({
1690
1941
  cwd: worktree ?? cwd,
@@ -1773,7 +2024,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1773
2024
  }
1774
2025
  const liveSubagentId = options.subagentId ?? id;
1775
2026
  const manager = options.asyncJobManager ?? AsyncJobManager.instance();
1776
- if (manager) {
2027
+ liveHandleManager = manager ?? undefined;
2028
+ const registerLiveHandle = (): void => {
2029
+ if (options.preflightProbe || liveHandleRegistered || !manager) return;
1777
2030
  manager.registerLiveHandle(liveSubagentId, {
1778
2031
  requestPause: () => {
1779
2032
  pauseRequested = true;
@@ -1790,10 +2043,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1790
2043
  await session.sendUserMessage(content, { deliverAs });
1791
2044
  },
1792
2045
  });
1793
- }
1794
-
1795
- // Emit lifecycle start event
1796
- if (options.eventBus) {
2046
+ liveHandleRegistered = true;
2047
+ };
2048
+ const emitLifecycleStart = (): void => {
2049
+ if (options.preflightProbe || lifecycleStarted || !options.eventBus) return;
1797
2050
  options.eventBus.emit(TASK_SUBAGENT_LIFECYCLE_CHANNEL, {
1798
2051
  id,
1799
2052
  agent: agent.name,
@@ -1803,6 +2056,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1803
2056
  sessionFile: sessionFile ?? undefined,
1804
2057
  index,
1805
2058
  });
2059
+ lifecycleStarted = true;
2060
+ };
2061
+ if (!options.preflightProbe && !options.preflightDurable) {
2062
+ registerLiveHandle();
2063
+ emitLifecycleStart();
1806
2064
  }
1807
2065
 
1808
2066
  const subagentToolNames = session.getActiveToolNames();
@@ -1812,13 +2070,15 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1812
2070
  await awaitAbortable(session.setActiveToolsByName(filteredSubagentTools));
1813
2071
  }
1814
2072
 
1815
- session.sessionManager.appendSessionInit({
1816
- systemPrompt: session.agent.state.systemPrompt.join("\n\n"),
1817
- task,
1818
- tools: session.getActiveToolNames(),
1819
- outputSchema,
1820
- forkContext: options.forkContextSeed?.metadata,
1821
- });
2073
+ if (!options.preflightProbe && !options.preflightDurable) {
2074
+ session.sessionManager.appendSessionInit({
2075
+ systemPrompt: session.agent.state.systemPrompt.join("\n\n"),
2076
+ task,
2077
+ tools: session.getActiveToolNames(),
2078
+ outputSchema,
2079
+ forkContext: options.forkContextSeed?.metadata,
2080
+ });
2081
+ }
1822
2082
 
1823
2083
  abortSignal.addEventListener(
1824
2084
  "abort",
@@ -1836,7 +2096,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1836
2096
 
1837
2097
  const extensionRunner = session.extensionRunner;
1838
2098
  const pendingExtensionMessages: Promise<void>[] = [];
1839
- if (extensionRunner) {
2099
+ // Disposable preflight probes execute an in-memory candidate session purely to observe a
2100
+ // live model provider; they are discarded at the acceptance fence, so user/session
2101
+ // extensions (session_start emits and tool wrappers) must never run against them.
2102
+ if (extensionRunner && !options.preflightProbe) {
1840
2103
  extensionRunner.initialize(
1841
2104
  {
1842
2105
  sendMessage: (message, options) => {
@@ -2021,9 +2284,50 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2021
2284
  );
2022
2285
  }
2023
2286
  }
2287
+ if (options.preflightProbe || options.preflightDurable) preflightOperation = "preflight_validation";
2024
2288
  const runMode = options.runMode ?? "initial";
2025
- const markLlmRequestStarted = () => {
2289
+ let postFencePublication: Promise<void> | undefined;
2290
+ const publishPostFence = async (): Promise<void> => {
2291
+ if (!options.preflightDurable) return;
2292
+ if (postFencePublication) return postFencePublication;
2293
+ postFencePublication = (async () => {
2294
+ if (!openedSessionManager) throw new Error("preflight session manager unavailable");
2295
+ try {
2296
+ // The staged transcript and artifacts become visible only here, at
2297
+ // the real provider acceptance fence.
2298
+ await openedSessionManager.commitStaged({ deferArtifactFinalize: true });
2299
+ session.sessionManager.appendSessionInit({
2300
+ systemPrompt: session.agent.state.systemPrompt.join("\n\n"),
2301
+ task,
2302
+ tools: session.getActiveToolNames(),
2303
+ outputSchema,
2304
+ forkContext: options.forkContextSeed?.metadata,
2305
+ });
2306
+ await openedSessionManager.refreshStagedCommitSnapshot();
2307
+ registerLiveHandle();
2308
+ emitLifecycleStart();
2309
+ openedSessionManager.finalizeStagedCommit();
2310
+ } catch (error) {
2311
+ preflightCommitFailure = true;
2312
+ try {
2313
+ await openedSessionManager.rollbackCommittedStaged();
2314
+ } catch (cleanupError) {
2315
+ throw new AggregateError(
2316
+ [toError(error), toError(cleanupError)],
2317
+ "Post-fence publication and cleanup both failed.",
2318
+ );
2319
+ }
2320
+ throw error;
2321
+ }
2322
+ })();
2323
+ return postFencePublication;
2324
+ };
2325
+ const markLlmRequestStarted = async () => {
2326
+ if (options.preflightProbe) throw new AutoroutingProbeAcceptedError();
2327
+ if (options.preflightDurable) await publishPostFence();
2026
2328
  llmRequestStarted = true;
2329
+ preflightFenceCrossed = true;
2330
+ options.preflightFenceCallback?.();
2027
2331
  };
2028
2332
  const promptOptions = {
2029
2333
  attribution: "agent" as const,
@@ -2098,6 +2402,29 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2098
2402
  } else if (lastAssistant.stopReason === "error") {
2099
2403
  exitCode = 1;
2100
2404
  error ??= lastAssistant.errorMessage || "Subagent failed";
2405
+ // A terminal local (non-provider) failure carries an actionable
2406
+ // kind: surface it in the parent receipt instead of the generic
2407
+ // "Task failed; error recorded." preview (#4618). Overflow
2408
+ // summaries are built ONLY from the structured, identity-checked
2409
+ // `bufferOverflow` shape - never the free-form `errorMessage`,
2410
+ // which a foreign self-labeled error can fill with arbitrary
2411
+ // text. A self-labeled overflow WITHOUT the shape degrades to a
2412
+ // neutral sentence instead of forwarding message text.
2413
+ if (isAssistantLocalErrorKind(lastAssistant.errorKind)) {
2414
+ localErrorSummary = createLocalErrorSummary(
2415
+ lastAssistant.errorKind,
2416
+ lastAssistant.errorMessage ?? error,
2417
+ lastAssistant.bufferOverflow,
2418
+ );
2419
+ }
2420
+ }
2421
+ if (options.preflightDurable && preflightFenceCrossed) {
2422
+ const facts =
2423
+ (lastAssistant as AssistantMessage & { transportFailure?: TransportFailureFacts }).transportFailure ??
2424
+ transportFailureFacts(lastAssistant);
2425
+ preflightFailure = facts
2426
+ ? { kind: "transport", class: classifyFallbackTrigger(facts).class }
2427
+ : { kind: "transport", class: "other" };
2101
2428
  }
2102
2429
  if (paused) {
2103
2430
  exitCode = 0;
@@ -2111,14 +2438,42 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2111
2438
  reason: "assistant_model_mismatch",
2112
2439
  };
2113
2440
  progress.modelSubstitutionWarning = modelSubstitutionWarning;
2441
+ assistantModelMismatch = true;
2114
2442
  }
2115
2443
  } catch (err) {
2116
2444
  exitCode = 1;
2117
- if (!abortSignal.aborted) {
2118
- error = err instanceof Error ? err.stack || err.message : String(err);
2119
- if (!llmRequestStarted) setupFailure = createSetupFailureSummary(err);
2445
+ if (err instanceof AutoroutingProbeAcceptedError) {
2446
+ probeAccepted = true;
2447
+ exitCode = 0;
2448
+ error = undefined;
2449
+ } else if (!abortSignal.aborted) {
2450
+ error = formatExecutionError(err);
2451
+ if (options.preflightDurable && preflightFenceCrossed) {
2452
+ const facts = transportFactsFromError(err) ?? transportFailureFacts(err);
2453
+ preflightFailure = facts
2454
+ ? { kind: "transport", class: classifyFallbackTrigger(facts).class }
2455
+ : { kind: "transport", class: "other" };
2456
+ } else if (options.preflightProbe || options.preflightDurable) {
2457
+ preflightFailure = classifyAutoroutingPreflightFailure(err, preflightOperation);
2458
+ }
2459
+ if (!llmRequestStarted)
2460
+ setupFailure = createSetupFailureSummary(err instanceof AggregateError ? error : err);
2120
2461
  }
2121
2462
  } finally {
2463
+ if (options.preflightDurable && !preflightFenceCrossed && openedSessionManager) {
2464
+ try {
2465
+ await openedSessionManager.discardStaged();
2466
+ } catch (cleanupError) {
2467
+ const primary = error ?? "Autorouting preflight candidate failed before acceptance.";
2468
+ error = `${primary}\nCleanup failure: ${formatExecutionError(cleanupError)}`;
2469
+ setupFailure ??= createSetupFailureSummary(error);
2470
+ // Fail closed: a failed pre-fence discard may have left candidate-owned staging
2471
+ // residue behind, so the zero-residue guarantee no longer holds for this attempt.
2472
+ // Downgrade any transient classification to terminal so the ledger stops here
2473
+ // instead of advancing to the next candidate over unknown residue.
2474
+ preflightFailure = { kind: "local", op: "preflight_validation", transient: false };
2475
+ }
2476
+ }
2122
2477
  if (abortSignal.aborted) {
2123
2478
  aborted = abortReason === "signal" || runtimeLimitExceeded || abortReason === undefined;
2124
2479
  if (aborted) {
@@ -2127,7 +2482,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2127
2482
  if (exitCode === 0) exitCode = 1;
2128
2483
  }
2129
2484
  sessionAbortController.abort();
2130
- (options.asyncJobManager ?? AsyncJobManager.instance())?.removeLiveHandle(options.subagentId ?? id);
2485
+ if (liveHandleRegistered) liveHandleManager?.removeLiveHandle(liveSubagentId);
2131
2486
  if (unsubscribe) {
2132
2487
  try {
2133
2488
  unsubscribe();
@@ -2150,9 +2505,14 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2150
2505
  return {
2151
2506
  exitCode,
2152
2507
  error,
2508
+ localErrorSummary,
2153
2509
  aborted,
2154
2510
  abortReason: aborted ? abortReasonText : undefined,
2155
2511
  setupFailure,
2512
+ probeAccepted,
2513
+ preflightFailure,
2514
+ preflightFenceCrossed,
2515
+ preflightCommitFailure,
2156
2516
  durationMs: Date.now() - startTime,
2157
2517
  };
2158
2518
  };
@@ -2183,6 +2543,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2183
2543
  rawOutput,
2184
2544
  exitCode,
2185
2545
  stderr,
2546
+ terminalFailure: done.error !== undefined || Boolean(done.aborted),
2186
2547
  doneAborted: Boolean(done.aborted),
2187
2548
  signalAborted: Boolean(signal?.aborted),
2188
2549
  yieldItems,
@@ -2273,10 +2634,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2273
2634
  : undefined;
2274
2635
  progress.setupFailure = done.setupFailure;
2275
2636
  progress.status = paused ? "paused" : wasAborted ? "aborted" : exitCode === 0 ? "completed" : "failed";
2276
- scheduleProgress(true);
2637
+ if (!options.preflightProbe) scheduleProgress(true);
2277
2638
 
2278
2639
  // Emit lifecycle end event after finalization so yield status is reflected
2279
- if (options.eventBus) {
2640
+ if (lifecycleStarted && !options.preflightProbe && options.eventBus) {
2280
2641
  options.eventBus.emit(TASK_SUBAGENT_LIFECYCLE_CHANNEL, {
2281
2642
  id,
2282
2643
  agent: agent.name,
@@ -2288,6 +2649,13 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2288
2649
  });
2289
2650
  }
2290
2651
 
2652
+ const routingEvidence = finalizeRoutingEvidence(options.routing, {
2653
+ resolvedModelString,
2654
+ lastAssistantModelString,
2655
+ authFallbackUsed,
2656
+ assistantModelMismatch,
2657
+ });
2658
+
2291
2659
  return {
2292
2660
  index,
2293
2661
  id,
@@ -2304,12 +2672,18 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2304
2672
  durationMs: Date.now() - startTime,
2305
2673
  tokens: progress.tokens,
2306
2674
  contextTokens: progress.contextTokens,
2675
+ routing: routingEvidence,
2307
2676
  contextWindow: progress.contextWindow,
2308
2677
  modelOverride,
2309
2678
  modelSubstitutionWarning,
2310
2679
  fastMode: progress.fastMode,
2311
2680
  error: exitCode !== 0 && stderr ? stderr : undefined,
2312
2681
  setupFailure: done.setupFailure,
2682
+ localErrorSummary: done.localErrorSummary,
2683
+ preflightFailure: done.preflightFailure,
2684
+ preflightFenceCrossed: done.preflightFenceCrossed,
2685
+ preflightCommitFailure: done.preflightCommitFailure,
2686
+ preflightProbeAccepted: done.probeAccepted,
2313
2687
  aborted: wasAborted,
2314
2688
  abortReason: finalAbortReason,
2315
2689
  paused,
@@ -2322,3 +2696,203 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2322
2696
  reviewFindingsRef,
2323
2697
  };
2324
2698
  }
2699
+
2700
+ export function boundedSelector(value: string): string {
2701
+ const bounded = value
2702
+ .normalize("NFKC")
2703
+ .replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/gu, "")
2704
+ .slice(0, AUTOROUTING_SELECTOR_MAX_LENGTH);
2705
+ return bounded || "<omitted-selector>";
2706
+ }
2707
+
2708
+ export function buildBoundedRoutingSkips(
2709
+ skips: ExecutorOptions["autoroutingSkips"],
2710
+ ): Pick<TaskRoutingEvidence, "skips" | "omittedSkipCount" | "omittedByCode"> {
2711
+ if (!skips || skips.length === 0) return {};
2712
+ const retained = skips.slice(0, 16).map(skip => ({ selector: boundedSelector(skip.selector), code: skip.code }));
2713
+ const omitted = skips.slice(16);
2714
+ const omittedByCode: NonNullable<TaskRoutingEvidence["omittedByCode"]> = {};
2715
+ for (const skip of omitted) omittedByCode[skip.code] = (omittedByCode[skip.code] ?? 0) + 1;
2716
+ return {
2717
+ skips: retained,
2718
+ ...(omitted.length > 0 ? { omittedSkipCount: omitted.length, omittedByCode } : {}),
2719
+ };
2720
+ }
2721
+
2722
+ function evidenceWithAttempts(
2723
+ routing: TaskRoutingEvidence | undefined,
2724
+ attempts: readonly AutoroutingAttempt[],
2725
+ terminal?: TaskRoutingEvidence["terminal"],
2726
+ ): TaskRoutingEvidence | undefined {
2727
+ if (!routing) return undefined;
2728
+ const evidence: TaskRoutingEvidence = {
2729
+ ...routing,
2730
+ requestedSelector: boundedSelector(routing.requestedSelector),
2731
+ attempts: attempts.map(attempt => ({ ...attempt, selector: boundedSelector(attempt.selector) })),
2732
+ ...(terminal ? { terminal, notExecuted: true, effectiveModel: undefined } : {}),
2733
+ };
2734
+ assertRoutingEvidenceInvariant(evidence);
2735
+ return evidence;
2736
+ }
2737
+
2738
+ function preflightTerminalResult(
2739
+ options: ExecutorOptions,
2740
+ attempts: readonly AutoroutingAttempt[],
2741
+ terminal: "preflight_exhausted" | "all_candidates_skipped",
2742
+ prior?: SingleResult,
2743
+ ): SingleResult {
2744
+ const result: SingleResult = prior
2745
+ ? { ...prior }
2746
+ : {
2747
+ index: options.index,
2748
+ id: options.id,
2749
+ agent: options.agent.name,
2750
+ agentSource: options.agent.source,
2751
+ task: options.task,
2752
+ assignment: options.assignment,
2753
+ description: options.description,
2754
+ exitCode: 1,
2755
+ output: "",
2756
+ stderr: "Autorouting preflight exhausted.",
2757
+ truncated: false,
2758
+ durationMs: 0,
2759
+ tokens: 0,
2760
+ };
2761
+ // Preserve a diagnostic from the last attempt so terminalization does not erase why the candidate
2762
+ // failed (including an appended pre-fence cleanup failure). It MUST go through
2763
+ // createSetupFailureSummary, the established egress sanitizer: it redacts authorization/cookie
2764
+ // headers, URL credentials, API-key labels, bare provider tokens and local/Windows absolute paths,
2765
+ // collapses all whitespace (so CR/LF/TAB cannot survive for newline injection), and caps length.
2766
+ // Never interpolate a raw `error` string here — it may carry a full stack with secrets or paths.
2767
+ const rawDiagnostic =
2768
+ typeof prior?.error === "string" && prior.error.trim().length > 0
2769
+ ? prior.error
2770
+ : (prior?.setupFailure?.summary ?? "");
2771
+ const sanitizedDiagnostic = rawDiagnostic.trim() ? createSetupFailureSummary(rawDiagnostic).summary : "";
2772
+ const priorDiagnostic =
2773
+ sanitizedDiagnostic && sanitizedDiagnostic !== "Subagent setup failed." ? sanitizedDiagnostic : "";
2774
+ result.exitCode = 1;
2775
+ result.output = "";
2776
+ result.stderr = priorDiagnostic
2777
+ ? `Autorouting preflight exhausted. Last candidate diagnostic: ${priorDiagnostic}`
2778
+ : "Autorouting preflight exhausted.";
2779
+ result.error = result.stderr;
2780
+ result.setupFailure = {
2781
+ summary: priorDiagnostic
2782
+ ? `Autorouting preflight did not accept a candidate. Last candidate diagnostic: ${priorDiagnostic}`
2783
+ : "Autorouting preflight did not accept a candidate.",
2784
+ };
2785
+ result.routing = evidenceWithAttempts(options.routing, attempts, terminal);
2786
+ return result;
2787
+ }
2788
+
2789
+ /** Run routed initial tasks through the bounded probe/durable candidate ledger. */
2790
+ export async function runSubprocess(options: ExecutorOptions): Promise<SingleResult> {
2791
+ if (
2792
+ !options.autoroutingPreflight ||
2793
+ (options.runMode ?? "initial") !== "initial" ||
2794
+ !options.autoroutingCandidates
2795
+ ) {
2796
+ return runSubprocessOnce(options);
2797
+ }
2798
+ const attempts: AutoroutingAttempt[] = [];
2799
+ const consumed = new Set<string>();
2800
+ const skips = buildBoundedRoutingSkips(options.autoroutingSkips);
2801
+ // Candidates were already validated and pinned against the live model snapshot by
2802
+ // normalizeTierSelector before reaching here. boundedSelector is an evidence/telemetry
2803
+ // sanitizer (NFKC normalization, control-char stripping, 256-char truncation) meant for
2804
+ // text that gets rendered or persisted; running it on the live selector could compose
2805
+ // characters differently or truncate a long-but-valid selector, sending execution to a
2806
+ // model that never passed preflight. Only bound the copies that reach evidence/telemetry
2807
+ // (attempts, skips, requestedSelector), never the selector actually used for modelOverride.
2808
+ const candidates = options.autoroutingCandidates.filter(selector => selector.length > 0);
2809
+ const durablePublicationAvailable =
2810
+ options.managedPersistence !== undefined ||
2811
+ (typeof options.sessionFile === "string" && options.sessionFile.length > 0);
2812
+ const routedOptions = options.routing ? { ...options.routing, ...skips } : options.routing;
2813
+ if (candidates.length === 0)
2814
+ return preflightTerminalResult({ ...options, routing: routedOptions }, attempts, "all_candidates_skipped");
2815
+ let prior: SingleResult | undefined;
2816
+ for (const selector of candidates) {
2817
+ if (consumed.has(selector) || consumed.size >= 3) continue;
2818
+ consumed.add(selector);
2819
+ if (options.autoroutingPreflightErrors?.has(selector)) {
2820
+ const preflightError = options.autoroutingPreflightErrors.get(selector);
2821
+ const failure = classifyAutoroutingPreflightFailure(preflightError, "auth_resolve");
2822
+ const { code } = autoroutingAttemptDisposition(failure);
2823
+ attempts.push({ selector, phase: "probe", code });
2824
+ return preflightTerminalResult({ ...options, routing: routedOptions }, attempts, "preflight_exhausted");
2825
+ }
2826
+ const probe = await runSubprocessOnce({
2827
+ ...options,
2828
+ autoroutingPreflight: false,
2829
+ preflightProbe: true,
2830
+ preflightDurable: false,
2831
+ modelOverride: [selector],
2832
+ parentActiveModelPattern: undefined,
2833
+ autoroutingCandidates: undefined,
2834
+ autoroutingSkips: undefined,
2835
+ sessionFile: null,
2836
+ artifactsDir: undefined,
2837
+ persistArtifacts: false,
2838
+ managedPersistence: undefined,
2839
+ parentArtifactManager: undefined,
2840
+ routing: undefined,
2841
+ });
2842
+ prior = probe;
2843
+ if (!probe.preflightProbeAccepted) {
2844
+ const failure = probe.preflightFailure ?? { kind: "local", op: "preflight_validation", transient: false };
2845
+ const { code, advance } = autoroutingAttemptDisposition(failure);
2846
+ attempts.push({ selector, phase: "probe", code });
2847
+ if (!advance)
2848
+ return preflightTerminalResult(
2849
+ { ...options, routing: routedOptions },
2850
+ attempts,
2851
+ "preflight_exhausted",
2852
+ probe,
2853
+ );
2854
+ continue;
2855
+ }
2856
+ attempts.push({ selector, phase: "probe", code: "probe_passed" });
2857
+ const durable = await runSubprocessOnce({
2858
+ ...options,
2859
+ autoroutingPreflight: false,
2860
+ preflightProbe: false,
2861
+ // A synchronous Task may have no child transcript or artifact authority. In
2862
+ // that case there is nothing durable to publish at the provider fence, so
2863
+ // execute the accepted candidate through the artifact-only path instead of
2864
+ // asking an in-memory SessionManager to commit a nonexistent staged session.
2865
+ preflightDurable: durablePublicationAvailable,
2866
+ autoroutingAttemptId: `${options.id}-${consumed.size}`,
2867
+ modelOverride: [selector],
2868
+ parentActiveModelPattern: undefined,
2869
+ autoroutingCandidates: undefined,
2870
+ autoroutingSkips: undefined,
2871
+ routing: routedOptions,
2872
+ });
2873
+ prior = durable;
2874
+ if (durable.preflightCommitFailure) {
2875
+ attempts.push({ selector, phase: "durable", code: "post_acceptance_failure" });
2876
+ return { ...durable, routing: evidenceWithAttempts(durable.routing ?? routedOptions, attempts) };
2877
+ }
2878
+ if (durable.preflightFenceCrossed) {
2879
+ if (durable.exitCode === 0) {
2880
+ attempts.push({ selector, phase: "durable", code: "accepted" });
2881
+ return { ...durable, routing: evidenceWithAttempts(durable.routing ?? routedOptions, attempts) };
2882
+ }
2883
+ attempts.push({ selector, phase: "durable", code: "post_acceptance_failure" });
2884
+ return { ...durable, routing: evidenceWithAttempts(durable.routing ?? routedOptions, attempts) };
2885
+ }
2886
+ const failure = durable.preflightFailure ?? { kind: "local", op: "preflight_validation", transient: false };
2887
+ const { code, advance } = autoroutingAttemptDisposition(failure);
2888
+ attempts.push({ selector, phase: "durable", code });
2889
+ if (!advance)
2890
+ return preflightTerminalResult(
2891
+ { ...options, routing: routedOptions },
2892
+ attempts,
2893
+ "preflight_exhausted",
2894
+ durable,
2895
+ );
2896
+ }
2897
+ return preflightTerminalResult({ ...options, routing: routedOptions }, attempts, "preflight_exhausted", prior);
2898
+ }