muonroi-cli 1.8.4 → 1.8.5

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 (458) hide show
  1. package/LICENSE +17 -5
  2. package/dist/packages/agent-harness-core/src/driver.d.ts +27 -1
  3. package/dist/packages/agent-harness-core/src/driver.js +46 -0
  4. package/dist/packages/agent-harness-core/src/event-tee.d.ts +48 -0
  5. package/dist/packages/agent-harness-core/src/event-tee.js +77 -0
  6. package/dist/packages/agent-harness-core/src/mcp-server.d.ts +11 -0
  7. package/dist/packages/agent-harness-core/src/mcp-server.js +87 -15
  8. package/dist/packages/agent-harness-core/src/protocol.d.ts +66 -2
  9. package/dist/packages/agent-harness-core/src/protocol.js +15 -0
  10. package/dist/packages/agent-harness-core/src/visual-quality.d.ts +58 -0
  11. package/dist/packages/agent-harness-core/src/visual-quality.js +141 -0
  12. package/dist/packages/agent-harness-opentui/src/agent-mode.d.ts +6 -0
  13. package/dist/packages/agent-harness-opentui/src/agent-mode.js +14 -1
  14. package/dist/packages/agent-harness-opentui/src/input-bridge.d.ts +2 -10
  15. package/dist/packages/agent-harness-opentui/src/input-bridge.js +103 -16
  16. package/dist/packages/agent-harness-opentui/src/install.d.ts +8 -0
  17. package/dist/packages/agent-harness-opentui/src/install.js +10 -0
  18. package/dist/packages/agent-harness-opentui/src/semantic.js +12 -10
  19. package/dist/packages/agent-harness-opentui/src/visual-capture.d.ts +56 -0
  20. package/dist/packages/agent-harness-opentui/src/visual-capture.js +103 -0
  21. package/dist/src/agent-harness/mock-model.d.ts +28 -0
  22. package/dist/src/agent-harness/mock-model.js +63 -1
  23. package/dist/src/agent-harness/test-spawn.js +31 -0
  24. package/dist/src/cli/config/screen-providers.js +1 -1
  25. package/dist/src/cli/cost-forensics.d.ts +10 -0
  26. package/dist/src/cli/cost-forensics.js +18 -3
  27. package/dist/src/cli/keys-bundle.d.ts +1 -1
  28. package/dist/src/cli/keys-bundle.js +1 -1
  29. package/dist/src/cli/keys.d.ts +2 -2
  30. package/dist/src/cli/keys.js +19 -81
  31. package/dist/src/council/clarifier.d.ts +28 -2
  32. package/dist/src/council/clarifier.js +81 -15
  33. package/dist/src/council/context.js +49 -15
  34. package/dist/src/council/debate-checkpoint.d.ts +129 -0
  35. package/dist/src/council/debate-checkpoint.js +176 -0
  36. package/dist/src/council/debate-planner.js +51 -3
  37. package/dist/src/council/debate-summary.d.ts +25 -0
  38. package/dist/src/council/debate-summary.js +85 -0
  39. package/dist/src/council/debate.d.ts +169 -2
  40. package/dist/src/council/debate.js +1210 -134
  41. package/dist/src/council/index.d.ts +85 -1
  42. package/dist/src/council/index.js +634 -196
  43. package/dist/src/council/leader.d.ts +26 -0
  44. package/dist/src/council/leader.js +150 -9
  45. package/dist/src/council/llm.d.ts +32 -0
  46. package/dist/src/council/llm.js +231 -38
  47. package/dist/src/council/panel-select.d.ts +30 -0
  48. package/dist/src/council/panel-select.js +72 -0
  49. package/dist/src/council/planner.js +23 -0
  50. package/dist/src/council/preflight.d.ts +7 -0
  51. package/dist/src/council/preflight.js +14 -2
  52. package/dist/src/council/prompts.d.ts +30 -3
  53. package/dist/src/council/prompts.js +234 -64
  54. package/dist/src/council/stance-recall.d.ts +42 -0
  55. package/dist/src/council/stance-recall.js +57 -0
  56. package/dist/src/council/strip-think.d.ts +17 -0
  57. package/dist/src/council/strip-think.js +33 -0
  58. package/dist/src/council/types.d.ts +128 -0
  59. package/dist/src/ee/artifact-cache.d.ts +16 -0
  60. package/dist/src/ee/artifact-cache.js +32 -0
  61. package/dist/src/ee/auth.d.ts +1 -0
  62. package/dist/src/ee/auth.js +15 -2
  63. package/dist/src/ee/bridge.d.ts +10 -0
  64. package/dist/src/ee/bridge.js +58 -0
  65. package/dist/src/ee/client.js +81 -18
  66. package/dist/src/ee/export-transcripts.d.ts +1 -0
  67. package/dist/src/ee/export-transcripts.js +8 -10
  68. package/dist/src/ee/extract-session.js +29 -0
  69. package/dist/src/ee/extract-style.d.ts +58 -0
  70. package/dist/src/ee/extract-style.js +270 -0
  71. package/dist/src/ee/recall-ledger.d.ts +9 -0
  72. package/dist/src/ee/recall-ledger.js +3 -0
  73. package/dist/src/ee/scope.d.ts +1 -0
  74. package/dist/src/ee/scope.js +26 -1
  75. package/dist/src/ee/search.d.ts +7 -0
  76. package/dist/src/ee/search.js +24 -0
  77. package/dist/src/ee/transcript-emit.js +2 -0
  78. package/dist/src/ee/types.d.ts +22 -0
  79. package/dist/src/ee/who-am-i-brain.d.ts +35 -0
  80. package/dist/src/ee/who-am-i-brain.js +220 -0
  81. package/dist/src/ee/who-am-i.d.ts +10 -3
  82. package/dist/src/ee/who-am-i.js +12 -0
  83. package/dist/src/ee/workflow-event.d.ts +48 -0
  84. package/dist/src/ee/workflow-event.js +81 -0
  85. package/dist/src/flow/compaction/compress.d.ts +3 -3
  86. package/dist/src/flow/compaction/compress.js +45 -8
  87. package/dist/src/flow/compaction/extract.d.ts +4 -7
  88. package/dist/src/flow/compaction/extract.js +50 -10
  89. package/dist/src/flow/compaction/index.d.ts +13 -1
  90. package/dist/src/flow/compaction/index.js +70 -3
  91. package/dist/src/flow/compaction/input-guard.d.ts +24 -0
  92. package/dist/src/flow/compaction/input-guard.js +43 -0
  93. package/dist/src/flow/fold-planning.d.ts +36 -0
  94. package/dist/src/flow/fold-planning.js +83 -0
  95. package/dist/src/flow/hierarchy.d.ts +146 -0
  96. package/dist/src/flow/hierarchy.js +427 -0
  97. package/dist/src/flow/index.d.ts +1 -0
  98. package/dist/src/flow/index.js +2 -0
  99. package/dist/src/flow/run-artifacts.d.ts +102 -0
  100. package/dist/src/flow/run-artifacts.js +208 -0
  101. package/dist/src/generated/version.d.ts +1 -1
  102. package/dist/src/generated/version.js +1 -1
  103. package/dist/src/gsd/assessment-schema.d.ts +44 -0
  104. package/dist/src/gsd/assessment-schema.js +134 -0
  105. package/dist/src/gsd/capability-registry.d.ts +45 -0
  106. package/dist/src/gsd/capability-registry.js +337 -0
  107. package/dist/src/gsd/complexity-assessor.d.ts +39 -0
  108. package/dist/src/gsd/complexity-assessor.js +152 -0
  109. package/dist/src/gsd/config-bridge.d.ts +7 -0
  110. package/dist/src/gsd/config-bridge.js +114 -0
  111. package/dist/src/gsd/config-loader.d.ts +27 -0
  112. package/dist/src/gsd/config-loader.js +50 -0
  113. package/dist/src/gsd/council-context.d.ts +44 -0
  114. package/dist/src/gsd/council-context.js +114 -0
  115. package/dist/src/gsd/ee-closure.d.ts +28 -0
  116. package/dist/src/gsd/ee-closure.js +49 -0
  117. package/dist/src/gsd/flags.d.ts +55 -0
  118. package/dist/src/gsd/flags.js +83 -0
  119. package/dist/src/gsd/gsd-dispatch.d.ts +58 -0
  120. package/dist/src/gsd/gsd-dispatch.js +131 -0
  121. package/dist/src/gsd/gsd-runtime.d.ts +22 -0
  122. package/dist/src/gsd/gsd-runtime.js +37 -0
  123. package/dist/src/gsd/host-adapter.d.ts +11 -0
  124. package/dist/src/gsd/host-adapter.js +29 -0
  125. package/dist/src/gsd/index.d.ts +24 -1
  126. package/dist/src/gsd/index.js +27 -0
  127. package/dist/src/gsd/loop-host-contract.d.ts +21 -0
  128. package/dist/src/gsd/loop-host-contract.js +39 -0
  129. package/dist/src/gsd/loop-host.d.ts +69 -0
  130. package/dist/src/gsd/loop-host.js +245 -0
  131. package/dist/src/gsd/loop-resolver.d.ts +36 -0
  132. package/dist/src/gsd/loop-resolver.js +79 -0
  133. package/dist/src/gsd/model-tier.d.ts +13 -0
  134. package/dist/src/gsd/model-tier.js +45 -0
  135. package/dist/src/gsd/mutation-gate.d.ts +16 -0
  136. package/dist/src/gsd/mutation-gate.js +41 -0
  137. package/dist/src/gsd/native-roadmap.d.ts +89 -0
  138. package/dist/src/gsd/native-roadmap.js +343 -0
  139. package/dist/src/gsd/native-state.d.ts +47 -0
  140. package/dist/src/gsd/native-state.js +220 -0
  141. package/dist/src/gsd/paths.d.ts +23 -0
  142. package/dist/src/gsd/paths.js +66 -0
  143. package/dist/src/gsd/phase-dag.d.ts +12 -0
  144. package/dist/src/gsd/phase-dag.js +94 -0
  145. package/dist/src/gsd/phase-sync.d.ts +42 -0
  146. package/dist/src/gsd/phase-sync.js +321 -0
  147. package/dist/src/gsd/pil-gate-context.d.ts +13 -0
  148. package/dist/src/gsd/pil-gate-context.js +64 -0
  149. package/dist/src/gsd/pil-gate-critic.d.ts +19 -0
  150. package/dist/src/gsd/pil-gate-critic.js +74 -0
  151. package/dist/src/gsd/plan-council-prompts.d.ts +25 -0
  152. package/dist/src/gsd/plan-council-prompts.js +79 -0
  153. package/dist/src/gsd/plan-council.d.ts +44 -0
  154. package/dist/src/gsd/plan-council.js +251 -0
  155. package/dist/src/gsd/plan-gate-vocabulary.d.ts +40 -0
  156. package/dist/src/gsd/plan-gate-vocabulary.js +64 -0
  157. package/dist/src/gsd/product-workspace.d.ts +13 -0
  158. package/dist/src/gsd/product-workspace.js +124 -0
  159. package/dist/src/gsd/ship-bridge.d.ts +25 -0
  160. package/dist/src/gsd/ship-bridge.js +65 -0
  161. package/dist/src/gsd/state-document.d.ts +40 -0
  162. package/dist/src/gsd/state-document.js +163 -0
  163. package/dist/src/gsd/verdict-schema.d.ts +39 -0
  164. package/dist/src/gsd/verdict-schema.js +144 -0
  165. package/dist/src/gsd/verify-context.d.ts +22 -0
  166. package/dist/src/gsd/verify-context.js +27 -0
  167. package/dist/src/gsd/verify-council-prompts.d.ts +19 -0
  168. package/dist/src/gsd/verify-council-prompts.js +85 -0
  169. package/dist/src/gsd/verify-council.d.ts +25 -0
  170. package/dist/src/gsd/verify-council.js +119 -0
  171. package/dist/src/gsd/verify-gate-vocabulary.d.ts +25 -0
  172. package/dist/src/gsd/verify-gate-vocabulary.js +46 -0
  173. package/dist/src/gsd/workflow-engine.d.ts +60 -0
  174. package/dist/src/gsd/workflow-engine.js +207 -0
  175. package/dist/src/gsd/workflow-tools.d.ts +13 -0
  176. package/dist/src/gsd/workflow-tools.js +277 -0
  177. package/dist/src/hooks/index.js +1 -1
  178. package/dist/src/index.js +44 -11
  179. package/dist/src/maintain/pr-builder.js +23 -13
  180. package/dist/src/mcp/auto-setup.js +57 -32
  181. package/dist/src/mcp/client-pool.js +1 -1
  182. package/dist/src/mcp/ee-tools.js +1 -0
  183. package/dist/src/mcp/research-onboarding.js +8 -7
  184. package/dist/src/mcp/runtime.js +34 -2
  185. package/dist/src/models/catalog-client.d.ts +87 -0
  186. package/dist/src/models/catalog-client.js +105 -38
  187. package/dist/src/models/catalog.json +528 -265
  188. package/dist/src/models/registry.d.ts +22 -7
  189. package/dist/src/models/registry.js +73 -10
  190. package/dist/src/ops/doctor.js +1 -1
  191. package/dist/src/orchestrator/auto-commit.js +1 -1
  192. package/dist/src/orchestrator/batch-turn-runner.js +2 -2
  193. package/dist/src/orchestrator/cache-prefix.d.ts +67 -0
  194. package/dist/src/orchestrator/cache-prefix.js +83 -0
  195. package/dist/src/orchestrator/compact-request.d.ts +32 -0
  196. package/dist/src/orchestrator/compact-request.js +41 -0
  197. package/dist/src/orchestrator/compaction.d.ts +10 -0
  198. package/dist/src/orchestrator/compaction.js +27 -7
  199. package/dist/src/orchestrator/council-manager.d.ts +12 -3
  200. package/dist/src/orchestrator/council-manager.js +65 -24
  201. package/dist/src/orchestrator/cross-turn-dedup.d.ts +6 -0
  202. package/dist/src/orchestrator/cross-turn-dedup.js +22 -1
  203. package/dist/src/orchestrator/error-utils.d.ts +29 -0
  204. package/dist/src/orchestrator/error-utils.js +132 -24
  205. package/dist/src/orchestrator/grounding-check.js +39 -1
  206. package/dist/src/orchestrator/message-processor.js +242 -33
  207. package/dist/src/orchestrator/orchestrator.d.ts +39 -3
  208. package/dist/src/orchestrator/orchestrator.js +651 -102
  209. package/dist/src/orchestrator/preprocessor.js +1 -1
  210. package/dist/src/orchestrator/proactive-compact-detector.d.ts +26 -0
  211. package/dist/src/orchestrator/proactive-compact-detector.js +36 -0
  212. package/dist/src/orchestrator/prompts.js +17 -17
  213. package/dist/src/orchestrator/reactive-delegation.d.ts +39 -0
  214. package/dist/src/orchestrator/reactive-delegation.js +59 -0
  215. package/dist/src/orchestrator/retry-classifier.d.ts +2 -1
  216. package/dist/src/orchestrator/retry-classifier.js +46 -2
  217. package/dist/src/orchestrator/safety-intercept.d.ts +45 -0
  218. package/dist/src/orchestrator/safety-intercept.js +55 -0
  219. package/dist/src/orchestrator/scope-reminder.js +1 -1
  220. package/dist/src/orchestrator/session-experience.d.ts +2 -1
  221. package/dist/src/orchestrator/session-experience.js +2 -1
  222. package/dist/src/orchestrator/should-run-gate.d.ts +5 -0
  223. package/dist/src/orchestrator/should-run-gate.js +18 -0
  224. package/dist/src/orchestrator/stall-watchdog.d.ts +24 -3
  225. package/dist/src/orchestrator/stall-watchdog.js +47 -13
  226. package/dist/src/orchestrator/stream-runner.js +62 -29
  227. package/dist/src/orchestrator/sub-agent-cap.d.ts +13 -1
  228. package/dist/src/orchestrator/sub-agent-cap.js +16 -1
  229. package/dist/src/orchestrator/sub-agent-model-tier.d.ts +13 -1
  230. package/dist/src/orchestrator/sub-agent-model-tier.js +16 -2
  231. package/dist/src/orchestrator/subagent-compactor.d.ts +53 -1
  232. package/dist/src/orchestrator/subagent-compactor.js +126 -15
  233. package/dist/src/orchestrator/tool-engine.d.ts +22 -0
  234. package/dist/src/orchestrator/tool-engine.js +620 -56
  235. package/dist/src/orchestrator/tool-limit-auto-recover.d.ts +22 -0
  236. package/dist/src/orchestrator/tool-limit-auto-recover.js +30 -0
  237. package/dist/src/orchestrator/turn-runner-deps.d.ts +19 -0
  238. package/dist/src/orchestrator/turn-watchdog.d.ts +37 -0
  239. package/dist/src/orchestrator/turn-watchdog.js +55 -0
  240. package/dist/src/pil/agent-operating-contract.d.ts +1 -1
  241. package/dist/src/pil/agent-operating-contract.js +1 -1
  242. package/dist/src/pil/cheap-model-playbook.d.ts +1 -1
  243. package/dist/src/pil/cheap-model-playbook.js +5 -1
  244. package/dist/src/pil/cheap-model-workbooks.d.ts +1 -1
  245. package/dist/src/pil/cheap-model-workbooks.js +1 -1
  246. package/dist/src/pil/discovery-types.d.ts +1 -0
  247. package/dist/src/pil/discovery.js +16 -11
  248. package/dist/src/pil/layer1-intent.d.ts +18 -6
  249. package/dist/src/pil/layer1-intent.js +66 -757
  250. package/dist/src/pil/layer15-context-scan.js +15 -1
  251. package/dist/src/pil/layer3-ee-injection.js +23 -8
  252. package/dist/src/pil/layer4-gsd.js +69 -16
  253. package/dist/src/pil/layer5-context.js +7 -3
  254. package/dist/src/pil/layer6-output.d.ts +23 -0
  255. package/dist/src/pil/layer6-output.js +5 -1
  256. package/dist/src/pil/llm-classify.d.ts +33 -2
  257. package/dist/src/pil/llm-classify.js +123 -131
  258. package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
  259. package/dist/src/pil/native-capabilities-workbook.js +1 -0
  260. package/dist/src/pil/pipeline.js +34 -2
  261. package/dist/src/pil/response-tools.js +5 -3
  262. package/dist/src/pil/schema.d.ts +1 -0
  263. package/dist/src/pil/schema.js +2 -0
  264. package/dist/src/pil/types.d.ts +18 -0
  265. package/dist/src/playbook/directives.d.ts +4 -0
  266. package/dist/src/playbook/directives.js +17 -5
  267. package/dist/src/product-loop/backlog-builder.d.ts +14 -1
  268. package/dist/src/product-loop/backlog-builder.js +30 -6
  269. package/dist/src/product-loop/discovery-context-format.js +3 -1
  270. package/dist/src/product-loop/discovery-ecosystem.js +4 -1
  271. package/dist/src/product-loop/discovery-interview.js +32 -3
  272. package/dist/src/product-loop/discovery-schema.js +5 -1
  273. package/dist/src/product-loop/ideal-trace.d.ts +7 -0
  274. package/dist/src/product-loop/ideal-trace.js +64 -0
  275. package/dist/src/product-loop/index.d.ts +13 -1
  276. package/dist/src/product-loop/index.js +333 -52
  277. package/dist/src/product-loop/loop-driver.d.ts +7 -0
  278. package/dist/src/product-loop/loop-driver.js +310 -99
  279. package/dist/src/product-loop/phase-plan.d.ts +5 -0
  280. package/dist/src/product-loop/phase-plan.js +39 -2
  281. package/dist/src/product-loop/phase-runner.js +9 -1
  282. package/dist/src/product-loop/sprint-runner.d.ts +111 -0
  283. package/dist/src/product-loop/sprint-runner.js +559 -16
  284. package/dist/src/product-loop/types.d.ts +36 -5
  285. package/dist/src/providers/adapter.d.ts +1 -1
  286. package/dist/src/providers/adapter.js +3 -4
  287. package/dist/src/providers/auth/browser-flow.d.ts +1 -1
  288. package/dist/src/providers/auth/browser-flow.js +1 -1
  289. package/dist/src/providers/auth/openai-oauth.js +1 -1
  290. package/dist/src/providers/auth/registry.js +0 -34
  291. package/dist/src/providers/auth/token-store.js +4 -1
  292. package/dist/src/providers/auth/types.d.ts +1 -1
  293. package/dist/src/providers/auth/types.js +1 -1
  294. package/dist/src/providers/capabilities.d.ts +24 -5
  295. package/dist/src/providers/capabilities.js +42 -24
  296. package/dist/src/providers/endpoints.d.ts +2 -2
  297. package/dist/src/providers/endpoints.js +11 -10
  298. package/dist/src/providers/keychain.d.ts +1 -1
  299. package/dist/src/providers/keychain.js +7 -9
  300. package/dist/src/providers/mcp-vision-bridge.js +56 -146
  301. package/dist/src/providers/openai-compatible.js +8 -1
  302. package/dist/src/providers/pricing.d.ts +2 -2
  303. package/dist/src/providers/pricing.js +3 -13
  304. package/dist/src/providers/runtime.d.ts +27 -2
  305. package/dist/src/providers/runtime.js +78 -15
  306. package/dist/src/providers/strategies/base.strategy.d.ts +16 -0
  307. package/dist/src/providers/strategies/base.strategy.js +24 -1
  308. package/dist/src/providers/strategies/{siliconflow.strategy.d.ts → opencode-go.strategy.d.ts} +3 -3
  309. package/dist/src/providers/strategies/opencode-go.strategy.js +83 -0
  310. package/dist/src/providers/strategies/registry.js +4 -4
  311. package/dist/src/providers/strategies/thinking-mode.d.ts +109 -0
  312. package/dist/src/providers/strategies/thinking-mode.js +280 -1
  313. package/dist/src/providers/strategies/zai.strategy.d.ts +14 -0
  314. package/dist/src/providers/strategies/zai.strategy.js +44 -0
  315. package/dist/src/providers/types.d.ts +5 -6
  316. package/dist/src/providers/types.js +2 -2
  317. package/dist/src/providers/vision-backend.d.ts +47 -0
  318. package/dist/src/providers/vision-backend.js +258 -0
  319. package/dist/src/providers/vision-proxy.d.ts +22 -9
  320. package/dist/src/providers/vision-proxy.js +63 -132
  321. package/dist/src/providers/wire-debug.js +95 -0
  322. package/dist/src/router/decide.d.ts +13 -0
  323. package/dist/src/router/decide.js +138 -36
  324. package/dist/src/router/peak-hour.d.ts +38 -0
  325. package/dist/src/router/peak-hour.js +107 -0
  326. package/dist/src/router/step-router.js +3 -2
  327. package/dist/src/router/warm.js +4 -5
  328. package/dist/src/scaffold/continuation-prompt.d.ts +11 -0
  329. package/dist/src/scaffold/continuation-prompt.js +26 -0
  330. package/dist/src/scaffold/point-to-existing.d.ts +21 -0
  331. package/dist/src/scaffold/point-to-existing.js +25 -0
  332. package/dist/src/self-qa/agentic-loop.js +3 -3
  333. package/dist/src/{ui/state → state}/active-run.d.ts +19 -0
  334. package/dist/src/{ui/state → state}/active-run.js +21 -0
  335. package/dist/src/{ui/status-bar/store.d.ts → state/status-bar-store.d.ts} +4 -1
  336. package/dist/src/{ui/status-bar/store.js → state/status-bar-store.js} +12 -9
  337. package/dist/src/state/turn-trace.d.ts +43 -0
  338. package/dist/src/state/turn-trace.js +32 -0
  339. package/dist/src/storage/db.js +2 -1
  340. package/dist/src/storage/index.d.ts +1 -1
  341. package/dist/src/storage/index.js +1 -1
  342. package/dist/src/storage/interaction-log.d.ts +1 -1
  343. package/dist/src/storage/migrations.js +71 -1
  344. package/dist/src/storage/sessions.d.ts +28 -10
  345. package/dist/src/storage/sessions.js +78 -21
  346. package/dist/src/storage/transcript-view.js +1 -1
  347. package/dist/src/storage/transcript.d.ts +51 -0
  348. package/dist/src/storage/transcript.js +284 -13
  349. package/dist/src/tools/file.d.ts +15 -0
  350. package/dist/src/tools/file.js +32 -0
  351. package/dist/src/tools/native-tools.js +5 -0
  352. package/dist/src/tools/registry.d.ts +3 -0
  353. package/dist/src/tools/registry.js +460 -22
  354. package/dist/src/tools/research.d.ts +29 -0
  355. package/dist/src/tools/research.js +233 -0
  356. package/dist/src/types/index.d.ts +118 -3
  357. package/dist/src/ui/app.js +0 -0
  358. package/dist/src/ui/cards/product-status-card.js +1 -1
  359. package/dist/src/ui/components/bubble-body-guard.d.ts +27 -0
  360. package/dist/src/ui/components/bubble-body-guard.js +50 -0
  361. package/dist/src/ui/components/context-rail.d.ts +26 -0
  362. package/dist/src/ui/components/context-rail.js +33 -0
  363. package/dist/src/ui/components/council-conclusion-card.d.ts +73 -0
  364. package/dist/src/ui/components/council-conclusion-card.js +420 -0
  365. package/dist/src/ui/components/council-debate-pill.d.ts +36 -0
  366. package/dist/src/ui/components/council-debate-pill.js +34 -0
  367. package/dist/src/ui/components/council-info-card.js +2 -2
  368. package/dist/src/ui/components/council-leader-bubble.d.ts +10 -3
  369. package/dist/src/ui/components/council-leader-bubble.js +21 -11
  370. package/dist/src/ui/components/council-message-bubble.d.ts +15 -2
  371. package/dist/src/ui/components/council-message-bubble.js +16 -15
  372. package/dist/src/ui/components/council-phase-timeline.d.ts +9 -1
  373. package/dist/src/ui/components/council-phase-timeline.js +49 -15
  374. package/dist/src/ui/components/council-placeholder-bubble.d.ts +16 -9
  375. package/dist/src/ui/components/council-placeholder-bubble.js +32 -29
  376. package/dist/src/ui/components/council-question-card.js +12 -12
  377. package/dist/src/ui/components/council-rail-rounds.d.ts +26 -0
  378. package/dist/src/ui/components/council-rail-rounds.js +57 -0
  379. package/dist/src/ui/components/council-round-group.d.ts +38 -0
  380. package/dist/src/ui/components/council-round-group.js +88 -0
  381. package/dist/src/ui/components/council-status-list.d.ts +3 -1
  382. package/dist/src/ui/components/council-status-list.js +36 -24
  383. package/dist/src/ui/components/council-synthesis-banner.d.ts +7 -2
  384. package/dist/src/ui/components/council-synthesis-banner.js +20 -5
  385. package/dist/src/ui/components/halt-recovery-card.js +9 -5
  386. package/dist/src/ui/components/jump-to-latest-pill.d.ts +11 -0
  387. package/dist/src/ui/components/jump-to-latest-pill.js +14 -0
  388. package/dist/src/ui/components/prompt-box.js +18 -16
  389. package/dist/src/ui/components/session-tree-card.d.ts +14 -0
  390. package/dist/src/ui/components/session-tree-card.js +46 -0
  391. package/dist/src/ui/components/slash-inline-menu.d.ts +12 -0
  392. package/dist/src/ui/components/slash-inline-menu.js +26 -5
  393. package/dist/src/ui/components/task-list-panel.d.ts +14 -1
  394. package/dist/src/ui/components/task-list-panel.js +22 -2
  395. package/dist/src/ui/containers/modals-layer.d.ts +2 -1
  396. package/dist/src/ui/hooks/use-session-picker.d.ts +3 -3
  397. package/dist/src/ui/mcp-modal.js +2 -4
  398. package/dist/src/ui/modals/api-key-modal.js +1 -1
  399. package/dist/src/ui/modals/connect-modal.js +4 -3
  400. package/dist/src/ui/modals/session-picker-modal.d.ts +2 -2
  401. package/dist/src/ui/modals/session-picker-modal.js +3 -5
  402. package/dist/src/ui/picker-providers.d.ts +1 -1
  403. package/dist/src/ui/picker-providers.js +1 -1
  404. package/dist/src/ui/primitives/index.d.ts +1 -0
  405. package/dist/src/ui/primitives/index.js +2 -0
  406. package/dist/src/ui/primitives/semantic-primitives.d.ts +76 -0
  407. package/dist/src/ui/primitives/semantic-primitives.js +81 -0
  408. package/dist/src/ui/slash/compact.js +5 -7
  409. package/dist/src/ui/slash/cost.js +1 -1
  410. package/dist/src/ui/slash/council.js +19 -1
  411. package/dist/src/ui/slash/debug.d.ts +3 -31
  412. package/dist/src/ui/slash/debug.js +9 -20
  413. package/dist/src/ui/slash/ideal.d.ts +6 -2
  414. package/dist/src/ui/slash/ideal.js +97 -7
  415. package/dist/src/ui/slash/menu-items.d.ts +7 -0
  416. package/dist/src/ui/slash/menu-items.js +12 -18
  417. package/dist/src/ui/slash/registry.d.ts +2 -0
  418. package/dist/src/ui/slash/registry.js +4 -0
  419. package/dist/src/ui/status-bar/cache-hit.d.ts +6 -0
  420. package/dist/src/ui/status-bar/cache-hit.js +9 -0
  421. package/dist/src/ui/status-bar/index.d.ts +1 -1
  422. package/dist/src/ui/status-bar/index.js +7 -3
  423. package/dist/src/ui/status-bar/usd-meter.d.ts +5 -4
  424. package/dist/src/ui/status-bar/usd-meter.js +6 -4
  425. package/dist/src/ui/theme.d.ts +1 -0
  426. package/dist/src/ui/theme.js +2 -0
  427. package/dist/src/ui/types.d.ts +7 -0
  428. package/dist/src/ui/use-app-logic.js +0 -0
  429. package/dist/src/ui/utils/format.d.ts +14 -0
  430. package/dist/src/ui/utils/format.js +23 -3
  431. package/dist/src/usage/downgrade.js +2 -2
  432. package/dist/src/usage/product-ledger.js +2 -2
  433. package/dist/src/utils/install-manager.js +2 -1
  434. package/dist/src/utils/logger.js +2 -2
  435. package/dist/src/utils/permission-mode.js +5 -3
  436. package/dist/src/utils/redactor.js +1 -1
  437. package/dist/src/utils/settings.d.ts +153 -5
  438. package/dist/src/utils/settings.js +233 -29
  439. package/dist/src/utils/visible-retry.d.ts +11 -0
  440. package/dist/src/utils/visible-retry.js +10 -1
  441. package/dist/src/verify/entrypoint.d.ts +1 -1
  442. package/dist/src/verify/entrypoint.js +1 -1
  443. package/dist/src/verify/recipes.d.ts +13 -0
  444. package/dist/src/verify/recipes.js +15 -0
  445. package/package.json +135 -132
  446. package/dist/src/providers/auth/gcloud.d.ts +0 -28
  447. package/dist/src/providers/auth/gcloud.js +0 -102
  448. package/dist/src/providers/auth/gemini-oauth.d.ts +0 -82
  449. package/dist/src/providers/auth/gemini-oauth.js +0 -472
  450. package/dist/src/providers/gemini.d.ts +0 -11
  451. package/dist/src/providers/gemini.js +0 -45
  452. package/dist/src/providers/siliconflow-sse-repair.d.ts +0 -58
  453. package/dist/src/providers/siliconflow-sse-repair.js +0 -177
  454. package/dist/src/providers/strategies/google.strategy.d.ts +0 -22
  455. package/dist/src/providers/strategies/google.strategy.js +0 -174
  456. package/dist/src/providers/strategies/siliconflow.strategy.js +0 -29
  457. package/dist/src/ui/containers/chat-feed.d.ts +0 -40
  458. package/dist/src/ui/containers/chat-feed.js +0 -66
@@ -5,6 +5,7 @@ import { getTenantId } from "../ee/tenant.js";
5
5
  import { emitTranscriptToDisk } from "../ee/transcript-emit.js";
6
6
  import { createRun, getActiveRunId, setActiveRunId } from "../flow/run-manager.js";
7
7
  import { ensureFlowDir } from "../flow/scaffold.js";
8
+ import { isContextRailEnabled } from "../gsd/flags.js";
8
9
  import { executeEventHooks } from "../hooks/index.js";
9
10
  import { shutdownWorkspaceLspManager } from "../lsp/runtime.js";
10
11
  import { ensureDefaultMcpServers } from "../mcp/auto-setup.js";
@@ -12,27 +13,27 @@ import { getModelInfo, normalizeModelId } from "../models/registry.js";
12
13
  import { getProviderCapabilities } from "../providers/capabilities.js";
13
14
  import { apiBaseFor } from "../providers/endpoints.js";
14
15
  import { loadKeyForProvider } from "../providers/keychain.js";
15
- import { createProviderFactory, createProviderFactoryAsync, detectProviderForModel, requireRuntimeProvider, resolveModelRuntime as resolveRuntime, } from "../providers/runtime.js";
16
+ import { createProviderFactory, createProviderFactoryAsync, detectProviderForModel, requireRuntimeProvider, resolveModelRuntime as resolveRuntime, resolveTemperatureParam, } from "../providers/runtime.js";
16
17
  import { ALL_PROVIDER_IDS } from "../providers/types.js";
17
- import { appendCompaction, appendMessages, appendSystemMessage, buildChatEntries, getLastTodoWriteArgs, getNextMessageSequence, getSessionTotalTokens, loadTranscript, loadTranscriptState, logInteraction, markMessageCompleted, recordUsageEvent, SessionStore, } from "../storage/index.js";
18
+ import { statusBarStore } from "../state/status-bar-store.js";
19
+ import { appendCompaction, appendMessages, appendSystemMessage, buildChatEntries, getLastTodoWriteArgs, getNextMessageSequence, getSessionChainInfos, getSessionTotalTokens, loadSessionChainTranscriptState, logInteraction, markMessageCompleted, persistApprovedPlan, recordUsageEvent, SessionStore, } from "../storage/index.js";
18
20
  import { BashTool } from "../tools/bash.js";
19
21
  import { createBuiltinTools } from "../tools/registry.js";
20
22
  import { ScheduleManager } from "../tools/schedule.js";
21
23
  import { snapshotFromTodoWriteArgs } from "../tools/todo-write-snapshot.js";
22
- import { statusBarStore } from "../ui/status-bar/store.js";
23
24
  import { appendCostLog } from "../usage/cost-log.js";
24
25
  import { appendDecisionLog } from "../usage/decision-log.js";
25
26
  import { projectCostUSD, sanitizeInputTokens } from "../usage/estimator.js";
26
27
  import { logger } from "../utils/logger.js";
27
- import { getAutoCompactThresholdPct, getCouncilRounds, getCurrentModel, getCurrentShellSettings, getModeSpecificModel, getRoleModel, getRoleModels, isAutoCompactAfterTurnEnabled, isCouncilMultiProviderPreferred, isProviderDisabled, loadUserSettings, } from "../utils/settings.js";
28
+ import { getAutoCompactMinNewTokens, getAutoCompactThresholdPct, getCouncilRounds, getCurrentModel, getCurrentShellSettings, getModeSpecificModel, getRoleModel, getRoleModels, getSubAgentCompactKeepLast, getSubAgentCompactThresholdChars, isAutoCompactAfterTurnEnabled, isCouncilMultiProviderPreferred, isProviderDisabled, loadUserSettings, } from "../utils/settings.js";
28
29
  import { runSideQuestion } from "../utils/side-question.js";
29
- import { buildVerifyDetectPrompt, normalizeVerifyRecipe } from "../verify/entrypoint.js";
30
+ import { buildVerifyDetectPrompt, inferVerifyProjectProfile, normalizeVerifyRecipe, shouldTrustDeterministicRecipe, } from "../verify/entrypoint.js";
30
31
  import { runVerifyOrchestration } from "../verify/orchestrator.js";
31
32
  import { COUNCIL_COLOR_BG, COUNCIL_COLOR_RESET, COUNCIL_ROLE_COLORS, } from "./agent-options.js";
32
33
  import { AUTO_COMMIT_ATTRIBUTION, isAutoCommitEnabled, maybeAutoCommitTurn, snapshotDirtyPaths, } from "./auto-commit.js";
33
34
  import { BatchTurnRunner } from "./batch-turn-runner.js";
34
35
  import { accumulateUsage, buildAssistantBatchMessage, buildBatchChatCompletionRequest, buildBatchName, buildToolBatchMessage, extractJsonObject, getBatchUsage, hasUsage, parseToolArgumentsOrRaw, toLocalToolCall, } from "./batch-utils.js";
35
- import { createCompactionSummaryMessage, DEFAULT_KEEP_RECENT_TOKENS, DEFAULT_RESERVE_TOKENS, estimateConversationTokens, extractUserContent, generateCompactionSummary, POST_TURN_MIN_TOKENS, prepareCompaction, proposeCompaction, shouldCompactContext, } from "./compaction.js";
36
+ import { createCompactionSummaryMessage, DEFAULT_KEEP_RECENT_TOKENS, DEFAULT_RESERVE_TOKENS, estimateConversationTokens, extractUserContent, generateCompactionSummary, isCompactionSummaryMessage, isCompactionThrash, POST_TURN_MIN_TOKENS, prepareCompaction, proposeCompaction, shouldCompactContext, } from "./compaction.js";
36
37
  import { CouncilManager } from "./council-manager.js";
37
38
  import { CrossTurnDedup, isCrossTurnDedupEnabled } from "./cross-turn-dedup.js";
38
39
  import { DelegationManager } from "./delegations.js";
@@ -40,11 +41,14 @@ import { loadFlowResumeDigest } from "./flow-resume.js";
40
41
  import { MessageProcessor } from "./message-processor.js";
41
42
  import { lastPersistedSeq } from "./message-seq.js";
42
43
  import { buildSystemPrompt, HARD_MAX_TOOL_ROUNDS, MAX_TOOL_ROUNDS } from "./prompts.js";
44
+ import { getReactiveDelegationThresholdChars, shouldReactivelyEscalate } from "./reactive-delegation.js";
43
45
  import { getReadPathBudgetCap, ReadPathBudget } from "./read-path-budget.js";
44
46
  import { withStreamRetry } from "./retry-stream.js";
45
47
  import { StreamRunner } from "./stream-runner.js";
46
48
  import { resolveModelForTask } from "./sub-agent-model-tier.js";
49
+ import { compactSubAgentMessages } from "./subagent-compactor.js";
47
50
  import { setProviderHint } from "./token-counter.js";
51
+ import { getToolLimitAutoRecoverCap } from "./tool-limit-auto-recover.js";
48
52
  import { firstLine, formatSubagentActivity, toToolResult } from "./tool-utils.js";
49
53
  // ---------------------------------------------------------------------------
50
54
  // Provider implementations
@@ -69,15 +73,65 @@ function isAnyProviderApiBase(url) {
69
73
  }
70
74
  return false;
71
75
  }
76
+ const TITLE_SYSTEM_PROMPT = `You are a session-naming assistant. Given the first message a user sent to an AI coding assistant, produce a short session title.
77
+
78
+ Rules:
79
+ - 5-7 words that capture the essence of the request (the goal, not the phrasing).
80
+ - Same language as the user's message (English request → English title, Vietnamese → Vietnamese, etc.).
81
+ - Title Case for languages that use it; natural casing otherwise.
82
+ - No surrounding quotes, no trailing punctuation, no emoji, no markdown.
83
+ - Output ONLY the title text — nothing else.`;
84
+ /** Deterministic fallback title: truncated first user message, or "" when the
85
+ * message is empty or a JSON-ish payload (so the caller leaves title = NULL
86
+ * and the picker renders a clean "(untitled)" instead of a literal "{}"). */
87
+ export function fallbackTitle(userMessage) {
88
+ const trimmed = userMessage.trim();
89
+ if (!trimmed)
90
+ return "";
91
+ // JSON object/array payloads (programmatic first messages) make useless titles.
92
+ if (/^[{[]/.test(trimmed) && /[}\]]$/.test(trimmed))
93
+ return "";
94
+ return trimmed.slice(0, 60).trim();
95
+ }
96
+ /** Strip quotes/backticks, collapse whitespace, drop trailing punctuation, cap at ~60 chars. */
97
+ function sanitizeTitle(raw) {
98
+ let t = raw.trim().split("\n")[0]?.trim() ?? "";
99
+ t = t.replace(/^["'`“”‘’]+|["'`“”‘’]+$/g, "");
100
+ t = t
101
+ .replace(/\s+/g, " ")
102
+ .replace(/[.,:;!?…]+$/g, "")
103
+ .trim();
104
+ if (t.length > 60)
105
+ t = t.slice(0, 60).trim();
106
+ return t;
107
+ }
72
108
  /**
73
- * Generate a session title using the Anthropic provider.
74
- * Kept as a lightweight stub for Phase 0 title generation ships in Phase 1.
109
+ * Generate a session title with a single lightweight LLM call on the current
110
+ * session model. Falls back to a truncated first-message title on any failure.
75
111
  */
76
- function genTitle(_provider, userMessage) {
77
- // Phase 0 stub: return a truncated version of the first user message as title.
78
- // Phase 1 will replace this with a real LLM-based title generation call.
79
- const title = userMessage.slice(0, 60).trim() || "New session";
80
- return Promise.resolve({ title, modelId: getCurrentModel() });
112
+ async function genTitle(provider, userMessage, modelId) {
113
+ try {
114
+ const { generateText } = await import("ai");
115
+ const runtime = resolveModelRuntime(provider, modelId);
116
+ const snippet = userMessage.length > 1500 ? `${userMessage.slice(0, 1500)}…` : userMessage;
117
+ const { text, usage } = await generateText({
118
+ model: runtime.model,
119
+ system: TITLE_SYSTEM_PROMPT,
120
+ prompt: `User's first message:\n\n${snippet}\n\nTitle:`,
121
+ ...resolveTemperatureParam(runtime, 0.3),
122
+ ...(runtime.modelInfo?.supportsMaxOutputTokens === false ? {} : { maxOutputTokens: 64 }),
123
+ ...(runtime.providerOptions ? { providerOptions: runtime.providerOptions } : {}),
124
+ });
125
+ const title = sanitizeTitle(text ?? "");
126
+ if (title) {
127
+ return { title, modelId, usage };
128
+ }
129
+ console.error(`[orchestrator/genTitle] title generation returned empty text (model=${modelId}); using fallback`);
130
+ }
131
+ catch (err) {
132
+ console.error(`[orchestrator/genTitle] title generation failed (model=${modelId}): ${err?.message}; using fallback`);
133
+ }
134
+ return { title: fallbackTitle(userMessage), modelId };
81
135
  }
82
136
  /**
83
137
  * Resolve a model ID to a runnable AI SDK LanguageModel.
@@ -110,6 +164,7 @@ function createTools(_bash, _provider, _mode, _opts) {
110
164
  listDelegations: _opts?.listDelegations,
111
165
  killDelegation: _opts?.killDelegation,
112
166
  modelId: _opts?.modelId,
167
+ sessionId: _opts?.sessionId,
113
168
  });
114
169
  }
115
170
  // ---------------------------------------------------------------------------
@@ -138,6 +193,8 @@ export class Agent {
138
193
  steerDrain = null;
139
194
  maxToolRounds;
140
195
  hardMaxToolRounds;
196
+ /** Original hard ceiling at construction — bounds auto-compaction extension. */
197
+ _initialHardMaxToolRounds = 0;
141
198
  mode = "agent";
142
199
  modelId;
143
200
  maxTokens;
@@ -196,6 +253,9 @@ export class Agent {
196
253
  _turnUserGoalExcerpt = "";
197
254
  /** Compaction statistics tracking count and total tokens saved. */
198
255
  _compactionStats = { count: 0, totalSaved: 0 };
256
+ /** Token count immediately after the last successful compaction (or null if none yet this
257
+ * session). Used by the cross-turn anti-thrash guard in postTurnCompact. */
258
+ _lastCompactionTokensAfter = null;
199
259
  /**
200
260
  * Pinned message sequences. A pinned user message is preserved verbatim across
201
261
  * compaction — it is re-injected as a system note immediately after the
@@ -208,6 +268,15 @@ export class Agent {
208
268
  // Phase C3: cross-turn tool-output dedup. One instance per session; bumped
209
269
  // on each user turn. Lazily initialized so disabled-via-env path stays cheap.
210
270
  _crossTurnDedup = isCrossTurnDedupEnabled() ? new CrossTurnDedup() : null;
271
+ // Reactive sub-session escalation: the PREVIOUS turn's cumulative tool-output
272
+ // chars (from the top-level cap), reported by the tool engine at turn end and
273
+ // read at the next turn's route decision. See reactive-delegation.ts.
274
+ _lastTurnToolChars = 0;
275
+ // Instrument (cold-first-turn measurement): ordinal of the top-level turn whose
276
+ // tool load was last reported. Lets telemetry distinguish a turn-1 blow-up
277
+ // (which reactive escalation CANNOT isolate) from a later one (which it can).
278
+ // Gates the decision to build an in-turn checkpoint. See CONTEXT-CONTROL-LAYERS.md.
279
+ _turnLoadOrdinal = 0;
211
280
  // Phase C4 — input-keyed read-path budget. Complements C3 (output hash) by
212
281
  // catching re-reads of files the agent edited between rounds. Disabled
213
282
  // when MUONROI_MAX_READS_PER_PATH=0.
@@ -246,6 +315,10 @@ export class Agent {
246
315
  this.maxToolRounds = maxToolRounds || settings.maxToolRounds || MAX_TOOL_ROUNDS;
247
316
  const baseHardMax = settings.hardMaxToolRounds || HARD_MAX_TOOL_ROUNDS;
248
317
  this.hardMaxToolRounds = Math.max(Math.floor(this.maxToolRounds * 1.5), baseHardMax);
318
+ // Baseline captured so auto-compaction ceiling extension (see
319
+ // extendHardCeilingForAutoCompaction) is bounded relative to the ORIGINAL
320
+ // hard cap and cannot grow without limit across a runaway turn.
321
+ this._initialHardMaxToolRounds = this.hardMaxToolRounds;
249
322
  const envMax = Number(process.env.MUONROI_MAX_TOKENS);
250
323
  this.maxTokens = Number.isFinite(envMax) && envMax > 0 ? envMax : 16_384;
251
324
  this.batchApi = options.batchApi ?? false;
@@ -280,7 +353,7 @@ export class Agent {
280
353
  this.workspace = this.sessionStore.getWorkspace();
281
354
  this.session = this.sessionStore.openSession(options.session, this.modelId, this.mode, this.bash.getCwd());
282
355
  this.mode = this.session.mode;
283
- const transcript = loadTranscriptState(this.session.id);
356
+ const transcript = loadSessionChainTranscriptState(this.session.id);
284
357
  this.messages = transcript.messages;
285
358
  this.messageSeqs = transcript.seqs;
286
359
  this.sessionStore.setModel(this.session.id, this.modelId);
@@ -333,26 +406,36 @@ export class Agent {
333
406
  setModel(model) {
334
407
  this.modelId = normalizeModelId(model);
335
408
  const newProviderId = detectProviderForModel(this.modelId);
336
- if (newProviderId !== this.providerId && this.apiKey) {
409
+ if (newProviderId !== this.providerId) {
337
410
  this.providerId = newProviderId;
338
411
  setProviderHint(this.providerId);
339
412
  // Drop this.baseURL when it points at a DIFFERENT provider's default
340
413
  // apiBase — otherwise the rebuilt factory binds the new provider's
341
414
  // strategy to the OLD provider's URL, sending requests to the wrong
342
415
  // host. Evidence: session 2492d6579b1d — user switched defaultProvider
343
- // siliconflow→ (via UI), this.baseURL was still api.deepseek.com from
344
- // startup, SiliconflowStrategy.createFactory was created with that
345
- // baseURL, requests landed at api.deepseek.com which rejected the SF-
346
- // style model id ("deepseek-ai/DeepSeek-V4-Flash") with "supported API
347
- // model names are deepseek-v4-pro or deepseek-v4-flash".
416
+ // (via UI), this.baseURL was still the old provider's apiBase from
417
+ // startup, the rebuilt strategy factory was created with that stale
418
+ // baseURL, and requests landed at the wrong host which rejected the
419
+ // model id.
348
420
  // A user-supplied custom baseURL is preserved only when it does NOT
349
421
  // match any known provider's apiBase (i.e. it's a real override, not
350
422
  // a stale default).
351
423
  const staleBaseURL = isAnyProviderApiBase(this.baseURL) && this.baseURL !== apiBaseFor(this.providerId);
352
- const effectiveBaseURL = staleBaseURL ? undefined : (this.baseURL ?? undefined);
353
424
  if (staleBaseURL)
354
425
  this.baseURL = null;
355
- this.provider = createProvider(this.providerId, this.apiKey, effectiveBaseURL);
426
+ // Provider changed — DEFER factory construction. The current this.apiKey
427
+ // belongs to the PREVIOUS provider (or is the "oauth" OAuth sentinel) and
428
+ // is invalid for the new one. Rebuilding here with it sent provider A's
429
+ // key to provider B → 401 "Authentication Fails, your api key … is
430
+ // invalid" on the next chat, for EVERY TUI provider switch (evidence:
431
+ // routing log routed to "deepseek-v4-flash via deepseek" with a
432
+ // non-deepseek key; and the "oauth" leak in session ff932f8568e8). Clear
433
+ // the stale key, null the provider, and re-arm _initOAuthProvider, which
434
+ // resolves the NEW provider's own key (or OAuth tokens) before the next
435
+ // turn (message-processor calls initOAuthProvider at turn start).
436
+ this.apiKey = null;
437
+ this.provider = null;
438
+ this._oauthInitDone = false;
356
439
  }
357
440
  if (this.sessionStore && this.session) {
358
441
  this.sessionStore.setModel(this.session.id, this.modelId);
@@ -392,6 +475,17 @@ export class Agent {
392
475
  }
393
476
  setPlanContext(ctx) {
394
477
  this.planContext = ctx;
478
+ // Persist so that bare continuation phrases ("tiếp tục", "continue") after
479
+ // interrupt or cross-process resume can re-hydrate the APPROVED PLAN section
480
+ // instead of forgetting context and re-asking for details.
481
+ if (this.session && ctx) {
482
+ try {
483
+ persistApprovedPlan(this.session.id, ctx);
484
+ }
485
+ catch {
486
+ /* best-effort */
487
+ }
488
+ }
395
489
  }
396
490
  setSendTelegramFile(fn) {
397
491
  this.sendTelegramFile = fn;
@@ -403,7 +497,7 @@ export class Agent {
403
497
  this.apiKey = apiKey;
404
498
  // Drop baseURL when it points at a DIFFERENT provider's default apiBase
405
499
  // (e.g. caller passed the legacy anthropic URL while providerId is
406
- // siliconflow — without this we'd send siliconflow requests to
500
+ // deepseek — without this we'd send deepseek requests to
407
501
  // api.anthropic.com or similar). User-supplied custom URLs that don't
408
502
  // match any known provider's apiBase are preserved as real overrides.
409
503
  const stale = isAnyProviderApiBase(baseURL) && baseURL !== apiBaseFor(this.providerId);
@@ -419,6 +513,14 @@ export class Agent {
419
513
  getProviderId() {
420
514
  return this.providerId;
421
515
  }
516
+ /**
517
+ * Expose the provider factory for external callers (e.g., /compact slash command,
518
+ * sub-agent spawning) that need to make LLM calls outside the normal processMessage flow.
519
+ * Throws if no provider is configured.
520
+ */
521
+ getProvider() {
522
+ return this.requireProvider();
523
+ }
422
524
  getCwd() {
423
525
  return this.bash.getCwd();
424
526
  }
@@ -432,6 +534,9 @@ export class Agent {
432
534
  setMessages(messages) {
433
535
  this.messages = messages;
434
536
  this.messageSeqs = messages.map(() => null);
537
+ // Manual compaction (/compact) or /clear replaces the conversation; reset
538
+ // the per-turn auto-compact latch so future tool loops can compact again.
539
+ this._compactedThisTurn = false;
435
540
  }
436
541
  async listSchedules() {
437
542
  return this.schedules.list();
@@ -460,7 +565,7 @@ export class Agent {
460
565
  if (!provider) {
461
566
  return "New session";
462
567
  }
463
- const generated = await genTitle(provider, userMessage);
568
+ const generated = await genTitle(provider, userMessage, this.modelId);
464
569
  this.recordUsage(generated.usage, "title", generated.modelId);
465
570
  if (this.sessionStore && this.session && !this.session.title && generated.title) {
466
571
  this.sessionStore.setTitle(this.session.id, generated.title);
@@ -530,6 +635,22 @@ export class Agent {
530
635
  setToolLoopCapHandler(fn) {
531
636
  this._toolLoopCapHandler = fn;
532
637
  }
638
+ /**
639
+ * Extend the absolute hard-cap ceiling when the turn is being sustained by
640
+ * AUTO-COMPACTION (a productive long task, not a runaway). Rationale: each
641
+ * auto-compacted round resets context to O(N) input, so it is cheap — the
642
+ * hard cap's cost-runaway purpose is already served by the compaction. We
643
+ * grant `maxToolRounds/2` more headroom per compaction, bounded to at most
644
+ * one extra auto-recover budget's worth above the ORIGINAL ceiling so a
645
+ * genuinely wedged turn (which trips the pattern guard, not this path) can
646
+ * never grow the cap without limit. Called from the tool-engine auto-recover
647
+ * branch after a successful compaction.
648
+ */
649
+ extendHardCeilingForAutoCompaction() {
650
+ const bump = Math.max(25, Math.floor(this.maxToolRounds / 2));
651
+ const absoluteMax = this._initialHardMaxToolRounds + bump * getToolLimitAutoRecoverCap();
652
+ this.hardMaxToolRounds = Math.min(this.hardMaxToolRounds + bump, absoluteMax);
653
+ }
533
654
  // Safety-override handler — set by the UI (app.tsx) at startup. Invoked
534
655
  // from the message-processor tool-result pipeline when a safety block
535
656
  // (catastrophic, dangerous, git-safety, empty-bash) is detected. The UI
@@ -585,6 +706,7 @@ export class Agent {
585
706
  compaction_summary: undefined,
586
707
  });
587
708
  this._compactionStats = { count: 0, totalSaved: 0 };
709
+ this._lastCompactionTokensAfter = null;
588
710
  this._pinnedSeqs.clear();
589
711
  if (!this.sessionStore) {
590
712
  this.messages = [];
@@ -663,9 +785,35 @@ export class Agent {
663
785
  return [];
664
786
  return buildChatEntries(this.session.id);
665
787
  }
788
+ /**
789
+ * The session tree this TUI currently hosts: the root conversation plus every
790
+ * rotation / sub-agent descendant, with per-session metadata. Content from
791
+ * these sessions is already merged into the transcript on resume; this exposes
792
+ * WHICH sessions produced it so the rail can show the multi-session structure.
793
+ */
794
+ getSessionTree() {
795
+ if (!this.session)
796
+ return [];
797
+ try {
798
+ return getSessionChainInfos(this.session.id);
799
+ }
800
+ catch (err) {
801
+ logger.error("orchestrator", "getSessionTree failed", { message: err?.message });
802
+ return [];
803
+ }
804
+ }
666
805
  getLastTodoSnapshot() {
667
806
  if (!this.session)
668
807
  return null;
808
+ try {
809
+ const { getTaskListSnapshotFromGsd } = require("../gsd/phase-sync.js");
810
+ const gsdSnap = getTaskListSnapshotFromGsd(this.bash?.getCwd() ?? process.cwd());
811
+ if (gsdSnap)
812
+ return gsdSnap;
813
+ }
814
+ catch (err) {
815
+ // fail-open to legacy todo_write args
816
+ }
669
817
  const argsJson = getLastTodoWriteArgs(this.session.id);
670
818
  if (!argsJson)
671
819
  return null;
@@ -677,7 +825,7 @@ export class Agent {
677
825
  return {
678
826
  workspace: this.workspace,
679
827
  session: this.session,
680
- messages: loadTranscript(this.session.id),
828
+ messages: loadSessionChainTranscriptState(this.session.id).messages,
681
829
  entries: buildChatEntries(this.session.id),
682
830
  totalTokens: getSessionTotalTokens(this.session.id),
683
831
  };
@@ -796,7 +944,7 @@ export class Agent {
796
944
  // where orchestrator/task/title traffic is actually spending.
797
945
  // Best-effort: failures inside appendCostLog are swallowed (see cost-log.ts).
798
946
  const breakdown = source === "message" ? (this._lastPromptBreakdown ?? undefined) : undefined;
799
- // Sanitize actualInputTokens for providers (e.g. SiliconFlow) that return
947
+ // Sanitize actualInputTokens for providers that return
800
948
  // implausibly low prompt_tokens (e.g. 10) regardless of prompt size.
801
949
  const estIn = breakdown ? Math.ceil(((breakdown.systemChars ?? 0) + (breakdown.messagesChars ?? 0)) / 4) : 0;
802
950
  const actualInput = sanitizeInputTokens(totalInput, estIn);
@@ -919,8 +1067,27 @@ export class Agent {
919
1067
  const totalUsage = {};
920
1068
  let assistantText = "";
921
1069
  let lastActivity = initialDetail;
1070
+ // Phase B3 parity: the streamText sub-agent path compacts older tool
1071
+ // results in prepareStep, but this batch path historically concatenated
1072
+ // `[...childMessages, ...turnMessages]` unbounded every round — on a long
1073
+ // batch loop the re-sent history balloons exactly like the stream path did
1074
+ // before B3. Apply the same compactor here (round >= 1, mirroring the
1075
+ // stream path's `stepNumber >= 1` gate). High-value results stay verbatim.
1076
+ const batchCompactThreshold = getSubAgentCompactThresholdChars();
1077
+ const batchCompactKeepLast = getSubAgentCompactKeepLast();
1078
+ const batchChildCtxWindow = childRuntime.modelInfo?.contextWindow ?? 0;
1079
+ const batchIsReasoningModel = childRuntime.modelInfo?.reasoning === true;
922
1080
  for (let round = 0; round < maxSteps; round++) {
923
1081
  const batchRequestId = `task-${Date.now()}-${round + 1}`;
1082
+ const roundMessages = round < 1
1083
+ ? [...childMessages, ...turnMessages]
1084
+ : compactSubAgentMessages([...childMessages, ...turnMessages], {
1085
+ thresholdChars: batchCompactThreshold,
1086
+ keepLastTurns: batchCompactKeepLast,
1087
+ contextWindowTokens: batchChildCtxWindow,
1088
+ contextFillRatio: batchIsReasoningModel ? 0.3 : undefined,
1089
+ stripOldReasoning: batchIsReasoningModel,
1090
+ });
924
1091
  await addBatchRequests({
925
1092
  ...this.getBatchClientOptions(signal),
926
1093
  batchId: batch.batch_id,
@@ -931,8 +1098,8 @@ export class Agent {
931
1098
  chat_get_completion: buildBatchChatCompletionRequest({
932
1099
  modelId: childRuntime.modelId,
933
1100
  system: childSystem,
934
- messages: [...childMessages, ...turnMessages],
935
- temperature: request.agent === "explore" ? 0.2 : 0.5,
1101
+ messages: roundMessages,
1102
+ temperature: childRuntime.modelInfo?.fixedTemperature ?? (request.agent === "explore" ? 0.2 : 0.5),
936
1103
  maxOutputTokens: !childCaps.acceptsParam("maxOutputTokens", childRuntime.modelInfo)
937
1104
  ? undefined
938
1105
  : Math.min(this.maxTokens, 8_192),
@@ -1244,7 +1411,10 @@ export class Agent {
1244
1411
  return this._resolveModelForTask("compact");
1245
1412
  }
1246
1413
  _resolveModelForTask(task) {
1247
- return resolveModelForTask(task, this.providerId, this.modelId);
1414
+ const parentTier = getModelInfo(this.modelId)?.tier;
1415
+ return resolveModelForTask(task, this.providerId, this.modelId, undefined, {
1416
+ parentTier,
1417
+ });
1248
1418
  }
1249
1419
  async compactForContext(provider, system, contextWindow, signal, settings = this.getCompactionSettings(contextWindow), force = false) {
1250
1420
  if (!this.session)
@@ -1387,6 +1557,7 @@ export class Agent {
1387
1557
  // grossSaved tells the user how many context tokens were reclaimed — a real benefit that
1388
1558
  // reduces subsequent turn costs.
1389
1559
  const tokensAfter = estimateConversationTokens(system, this.messages);
1560
+ this._lastCompactionTokensAfter = tokensAfter;
1390
1561
  const saved = Math.max(0, preparation.tokensBefore - tokensAfter);
1391
1562
  const pct = preparation.tokensBefore > 0 ? ((saved / preparation.tokensBefore) * 100).toFixed(1) : "0.0";
1392
1563
  this._compactionStats.count++;
@@ -1452,6 +1623,13 @@ export class Agent {
1452
1623
  minMeaningfulTokens,
1453
1624
  });
1454
1625
  }
1626
+ const minNew = getAutoCompactMinNewTokens();
1627
+ if (isCompactionThrash(tokens, this._lastCompactionTokensAfter, minNew)) {
1628
+ return log(false, `anti-thrash (only ${tokens - (this._lastCompactionTokensAfter ?? 0)} new < ${minNew})`, {
1629
+ tokens,
1630
+ lastAfter: this._lastCompactionTokensAfter,
1631
+ });
1632
+ }
1455
1633
  log(true, `over-threshold (${tokens} >= ${minMeaningfulTokens})`, { tokens, thresholdPct, minMeaningfulTokens });
1456
1634
  await this.compactForContext(provider, system, contextWindow, signal, this.getCompactionSettings(contextWindow), true).catch((err) => logger.warn("orchestrator", "compaction failed", { error: err }));
1457
1635
  }
@@ -1464,8 +1642,8 @@ export class Agent {
1464
1642
  // (respondToCouncilQuestion/Preflight + the internal _create*Responder
1465
1643
  // hooks used by orchestrator.agent.test.ts).
1466
1644
  // ========================================================================
1467
- respondToCouncilQuestion(questionId, answer) {
1468
- this.councilManager.respondToQuestion(questionId, answer);
1645
+ respondToCouncilQuestion(questionId, answer, questionText) {
1646
+ this.councilManager.respondToQuestion(questionId, answer, questionText);
1469
1647
  }
1470
1648
  respondToCouncilPreflight(preflightId, approved) {
1471
1649
  this.councilManager.respondToPreflight(preflightId, approved);
@@ -1474,7 +1652,7 @@ export class Agent {
1474
1652
  // Council v2 — Clarify → Confirm → Debate → Plan → Execute
1475
1653
  // ========================================================================
1476
1654
  async *runCouncilV2(topic, options) {
1477
- const { runCouncil } = await import("../council/index.js");
1655
+ const { runCouncil, postDebateContinuation } = await import("../council/index.js");
1478
1656
  const { createCouncilLLM } = await import("../council/llm.js");
1479
1657
  const councilStats = { calls: 0, startMs: Date.now(), phases: [] };
1480
1658
  const llm = createCouncilLLM(this.bash, this.mode, this.session?.id, councilStats);
@@ -1514,6 +1692,9 @@ export class Agent {
1514
1692
  catch (err) {
1515
1693
  logger.error("router", "runDir resolution failed (decisions.lock will be skipped)", { error: err });
1516
1694
  }
1695
+ // Captures the post-debate action so we can auto-continue on
1696
+ // "continue_session" (user chose to keep working with the debate result).
1697
+ let chosenAction;
1517
1698
  try {
1518
1699
  const gen = runCouncil(topic, this.modelId, this.messages, this.session?.id, llm, this.councilManager.createQuestionResponder(), this.councilManager.createPreflightResponder(), processMessageFn, {
1519
1700
  skipClarification: options?.skipClarification,
@@ -1521,12 +1702,39 @@ export class Agent {
1521
1702
  cwd: this.bash.getCwd(),
1522
1703
  councilStats, // NEW — share orchestrator's stats object with runCouncil (Phase 14 CQ-01)
1523
1704
  signal,
1705
+ // When the Context Rail is active it carries leader/panel/cost as
1706
+ // ambient sidebar rows, so suppress the duplicate inline summary.
1707
+ suppressInlineMeta: isContextRailEnabled(),
1524
1708
  runDir, // B1 — persist decisions.lock.md for the /council slash path
1709
+ // #2 — isolated research bridge. Same StreamRunner sub-agent the /ideal
1710
+ // sprint-runner uses (runTaskRequest): a budget-capped, near-empty
1711
+ // explore child runs the debate's research phase so its multi-step tool
1712
+ // clutter never accretes into the council thread/context. Threads the
1713
+ // council abort signal so Esc cancels the research child too.
1714
+ runIsolatedTask: (request) => this.runTaskRequest(request, undefined, signal),
1715
+ onPostDebateAction: (action) => {
1716
+ chosenAction = action;
1717
+ // Relay to the auto-council caller (tool-engine) so nested runs honor
1718
+ // the user's choice instead of always continuing.
1719
+ this.councilManager.setLastPostDebateAction(action);
1720
+ },
1525
1721
  });
1526
1722
  let result;
1723
+ // Hold back runCouncil's terminal `done`. The /council slash consumer
1724
+ // (use-app-logic.tsx) does `for await (chunk of gen) { if (chunk.type ===
1725
+ // "done") break; }` — so forwarding runCouncil's end-of-debate `done`
1726
+ // strands THIS generator before the continue_session auto-continue below
1727
+ // ever runs (the generator is suspended at the yield and never resumes).
1728
+ // Swallow it here and re-emit our own `done` after the optional
1729
+ // continuation so the consumer terminates at the right boundary.
1730
+ let innerDoneSeen = false;
1527
1731
  do {
1528
1732
  result = await gen.next();
1529
1733
  if (!result.done && result.value) {
1734
+ if (result.value.type === "done") {
1735
+ innerDoneSeen = true;
1736
+ continue;
1737
+ }
1530
1738
  yield result.value;
1531
1739
  }
1532
1740
  } while (!result.done);
@@ -1535,6 +1743,124 @@ export class Agent {
1535
1743
  if (options?.userModelMessage && synthesis) {
1536
1744
  this.appendCompletedTurn(options.userModelMessage, [{ role: "assistant", content: synthesis }]);
1537
1745
  }
1746
+ // Keep working in THIS session when the chosen action calls for it
1747
+ // (continue_session → carry the conclusion; generate_plan/implement →
1748
+ // execute action items). postDebateContinuation is the single source of
1749
+ // truth shared with the auto-council caller (tool-engine). Re-entering
1750
+ // processMessage also writes real message rows, which is what makes the
1751
+ // session resumable (the /council slash path otherwise leaves no messages
1752
+ // and is filtered from the resume picker). Guarded by ownsController so it
1753
+ // fires ONLY on the top-level slash path, never when nested inside
1754
+ // processMessage (auto-council) or drained by the runDebate tool — those
1755
+ // callers manage their own continuation.
1756
+ const continuationPrompt = ownsController && synthesis ? postDebateContinuation(chosenAction, synthesis) : null;
1757
+ const isBuildContinuation = chosenAction === "implement" || chosenAction === "generate_plan";
1758
+ if (continuationPrompt && isBuildContinuation && process.env.MUONROI_COUNCIL_ISOLATE_IMPL !== "0") {
1759
+ // #1 — build the council decision in an ISOLATED sub-agent instead of
1760
+ // re-entering the full processMessage turn. The flat turn inherited the
1761
+ // entire multi-round debate history and could overflow the context window
1762
+ // (the exact wedge 97bc9d12 fixed for the /ideal sprint-runner but that
1763
+ // /council never got). runTaskRequest starts near-empty and returns a
1764
+ // compact ToolResult. ANTI-MÙ: the child is NOT blind — it is seeded with
1765
+ // (a) the approved synthesis-as-spec (already in continuationPrompt) and
1766
+ // (b) councilManager.buildContext(), the same compaction-summary + Recent
1767
+ // Conversation + [Council Decision]/[Council Memory] "Key Decisions"
1768
+ // checkpoint the anti-mù layer surfaces — so it has the decision + its
1769
+ // rationale without carrying the raw transcript. Opt out with
1770
+ // MUONROI_COUNCIL_ISOLATE_IMPL=0 (falls back to the processMessage path).
1771
+ yield { type: "content", content: "\n[Implementing the council decision in an isolated context…]\n" };
1772
+ this.councilManager.setContinuation(true);
1773
+ try {
1774
+ let councilCheckpoint = "";
1775
+ try {
1776
+ councilCheckpoint = this.councilManager.buildContext();
1777
+ }
1778
+ catch {
1779
+ /* anti-mù bundle is best-effort — the synthesis alone still grounds the build */
1780
+ }
1781
+ const seededPrompt = councilCheckpoint
1782
+ ? `${continuationPrompt}\n\n## Council context (decision + recent session — do NOT re-debate, just build)\n${councilCheckpoint}`
1783
+ : continuationPrompt;
1784
+ let result;
1785
+ try {
1786
+ result = await this.runTaskRequest({ agent: "general", description: "Council implementation", prompt: seededPrompt, modelId: this.modelId }, undefined, signal);
1787
+ }
1788
+ catch (err) {
1789
+ // A throw (vs a returned {success:false}) must not escape and take the
1790
+ // whole /council run down — surface it in-band like the processMessage
1791
+ // path's TurnStallError handling and end the turn cleanly.
1792
+ yield {
1793
+ type: "error",
1794
+ content: `Council implementation failed: ${err instanceof Error ? err.message : String(err)}`,
1795
+ };
1796
+ yield { type: "done" };
1797
+ return;
1798
+ }
1799
+ if (result.success && result.output?.trim()) {
1800
+ yield { type: "content", content: `\n${result.output.trim()}\n` };
1801
+ }
1802
+ else if (result.error) {
1803
+ yield { type: "error", content: `Council implementation failed: ${result.error}` };
1804
+ }
1805
+ // Persist a COMPACT record so the /council slash session stays resumable
1806
+ // (the processMessage path wrote rows for this) WITHOUT re-inheriting the
1807
+ // debate bloat the isolated child deliberately avoided.
1808
+ try {
1809
+ this.appendCompletedTurn({ role: "user", content: "[Council implementation of the debate decision]" }, [{ role: "assistant", content: (result.output ?? result.error ?? "(no output)").trim() }]);
1810
+ }
1811
+ catch {
1812
+ /* non-critical persistence */
1813
+ }
1814
+ yield { type: "done" };
1815
+ }
1816
+ finally {
1817
+ this.councilManager.setContinuation(false);
1818
+ }
1819
+ }
1820
+ else if (continuationPrompt) {
1821
+ yield { type: "content", content: "\n[Continuing with the debate conclusion…]\n" };
1822
+ this.councilManager.setContinuation(true);
1823
+ try {
1824
+ // processMessage emits its own terminal `done`, which becomes the
1825
+ // consumer's break boundary — so the UI stops AFTER the continuation
1826
+ // turn, not before it.
1827
+ //
1828
+ // Guard the continuation with an idle+total watchdog: the per-chunk
1829
+ // stall watchdog only covers streamText byte flow, NOT a turn that
1830
+ // wedges inside a tool call (a `task` sub-agent or long `bash`).
1831
+ // Session 578b2eae7099 hung exactly there — the UI froze at "Council
1832
+ // working… elapsed 0s" with no rescue. On fire we abort the turn and
1833
+ // surface a toast instead of hanging forever.
1834
+ const { withTurnWatchdog, TurnStallError } = await import("./turn-watchdog.js");
1835
+ const idleMs = Number(process.env.MUONROI_COUNCIL_CONTINUATION_IDLE_MS ?? 120_000);
1836
+ const totalMs = Number(process.env.MUONROI_COUNCIL_CONTINUATION_TOTAL_MS ?? 600_000);
1837
+ try {
1838
+ yield* withTurnWatchdog(this.processMessage(continuationPrompt, options?.observer), {
1839
+ idleMs,
1840
+ totalMs,
1841
+ label: "council continuation turn",
1842
+ });
1843
+ }
1844
+ catch (err) {
1845
+ if (err instanceof TurnStallError) {
1846
+ this.abortController?.abort(new DOMException(err.message, "TimeoutError"));
1847
+ yield { type: "error", content: `Council continuation stalled — ${err.message}` };
1848
+ yield { type: "done" };
1849
+ }
1850
+ else {
1851
+ throw err;
1852
+ }
1853
+ }
1854
+ }
1855
+ finally {
1856
+ this.councilManager.setContinuation(false);
1857
+ }
1858
+ }
1859
+ else if (innerDoneSeen) {
1860
+ // Non-continuation paths (save_exit, nested auto-council, sprint drain)
1861
+ // still need the terminal `done` the consumer expects.
1862
+ yield { type: "done" };
1863
+ }
1538
1864
  }
1539
1865
  finally {
1540
1866
  if (ownsController && this.abortController?.signal === signal) {
@@ -1556,25 +1882,67 @@ export class Agent {
1556
1882
  };
1557
1883
  const llm = createCouncilLLM(this.bash, this.mode, this.session?.id, productStats);
1558
1884
  const processMessageFn = (m) => this.processMessage(m, options?.observer);
1885
+ // Isolated bounded task-runner bridge for the sprint implement stage: a fresh
1886
+ // child context (getSubAgentBudgetChars cap, independent compaction) that does
1887
+ // NOT inherit this turn's council-debate history — the root fix for the live
1888
+ // ctx-overflow wedge. Returns a compact ToolResult (absorbed, no parent bloat).
1889
+ const runIsolatedTask = (request) => this.runTaskRequest(request, undefined, this.abortController?.signal);
1559
1890
  const flowDir = nodePath.join(this.bash.getCwd(), ".muonroi-flow");
1560
- // P2.7: compute complexity from the idea using PIL Layer 1 heuristics (cheap,
1561
- // no LLM calls). Only meaningful for "start"; other subcommands ignore it.
1891
+ // P2.7 (LLM-first no-regex routing): the work-depth tier that decides
1892
+ // /ideal's route is judged by the MODEL (the same depthTier the PIL Layer-1
1893
+ // classifier emits for chat turns), NOT by a keyword scorer. The old regex
1894
+ // `scoreComplexity` mis-tiered plainly-phrased heavy work — a "merge / vendor
1895
+ // / rename" refactor scored LOW because it lacked the FORCE_HIGH keywords —
1896
+ // which hot-pathed genuinely heavy tasks with zero interview/council. Only
1897
+ // meaningful for "start"; other subcommands ignore it.
1562
1898
  let complexity;
1899
+ let needsClarification;
1563
1900
  let sufficiencyMissing;
1901
+ let routeForceCouncil = payload.flags.forceCouncil;
1564
1902
  if (payload.subcommand === "start" && payload.idea) {
1565
- const { scoreComplexity, scoreSufficiency } = await import("../pil/layer1-intent.js");
1566
- const result = scoreComplexity({
1567
- rawText: payload.idea,
1568
- taskType: null,
1569
- t0HitCount: 0,
1570
- hasMaxSprintsOne: payload.flags.maxSprints === 1,
1571
- });
1572
- complexity = result.complexity;
1573
- // Sufficiency gate — vague briefs ("todo app") force Council so the
1574
- // discovery AskCard can fill in persona/MVP/architecture/verify before
1575
- // any code is scaffolded.
1576
- const suff = scoreSufficiency({ rawText: payload.idea });
1577
- sufficiencyMissing = suff.sufficient ? undefined : suff.missing;
1903
+ let depth = "standard";
1904
+ try {
1905
+ const { createLlmClassifier } = await import("../pil/llm-classify.js");
1906
+ const classify = createLlmClassifier(this.requireProvider(), this.modelId);
1907
+ const res = await classify(payload.idea);
1908
+ if (res?.depthTier) {
1909
+ depth = res.depthTier;
1910
+ }
1911
+ else {
1912
+ // Same convention as PIL Layer 1: a null/garbled depth defaults to the
1913
+ // safe middle. Logged so a persistently-null classifier is diagnosable.
1914
+ console.error(`[ideal/route] model depth classify returned no depthTier — defaulting to "standard". idea=${JSON.stringify(payload.idea.slice(0, 80))}`);
1915
+ }
1916
+ // LLM-first clarity signal — feeds the downstream interview as a hint.
1917
+ if (res?.needsClarification === true)
1918
+ needsClarification = true;
1919
+ }
1920
+ catch (err) {
1921
+ // Fail-open: never block /ideal on a classify hiccup (No-Silent-Catch).
1922
+ console.error(`[ideal/route] depth classify failed, defaulting to "standard": ${err?.message}`);
1923
+ }
1924
+ // The cheap 8-word classify above is only a HINT for the downstream GSD
1925
+ // depth pipeline — it is NOT a routing gate. It was too noisy to gate the
1926
+ // council decision: the SAME architectural prompt scored `heavy` on one run
1927
+ // and `standard` the next, so routing flip-flopped between full Council and
1928
+ // a straight-to-Edit maint-edit that skipped the debate. /ideal is the
1929
+ // DELIBERATE path — it must ALWAYS run the full Council/loop-driver
1930
+ // pipeline, in this order:
1931
+ // 1. scan the source (discovery/scoping) to understand what the user's
1932
+ // request actually touches,
1933
+ // 2. interview the user (AskCard — adaptive: only asks what the source
1934
+ // can't answer, so an existing repo isn't dragged through 6 questions),
1935
+ // 3. leader-tier complexity assessment + debate INSIDE that flow, AFTER
1936
+ // scope is understood — never a "no debate" verdict from a bare-prompt
1937
+ // guess.
1938
+ // So we force Council for every /ideal start; the fast classify only
1939
+ // seeds depth/clarity hints for that flow. Explicit `--maintain` still opts
1940
+ // out of Council (handled at the top of runProductLoop dispatch).
1941
+ complexity = depth === "heavy" ? "high" : depth === "quick" ? "low" : "medium";
1942
+ sufficiencyMissing = undefined;
1943
+ if (payload.flags.mode !== "maintain") {
1944
+ routeForceCouncil = true;
1945
+ }
1578
1946
  }
1579
1947
  const gen = runProductLoop({
1580
1948
  subcommand: payload.subcommand,
@@ -1589,16 +1957,18 @@ export class Agent {
1589
1957
  doneThreshold: payload.flags.doneThreshold,
1590
1958
  budgetTokens: payload.flags.budgetTokens,
1591
1959
  stack: payload.flags.stack,
1592
- forceCouncil: payload.flags.forceCouncil,
1960
+ forceCouncil: routeForceCouncil,
1593
1961
  },
1594
1962
  respondToQuestion: this.councilManager.createQuestionResponder(),
1595
1963
  respondToPreflight: this.councilManager.createPreflightResponder(),
1596
1964
  cwd: this.bash.getCwd(),
1597
1965
  processMessageFn,
1966
+ runIsolatedTask,
1598
1967
  // Mode C — wire verify-recipe detector so runProductLoop auto-detect can probe cwd.
1599
1968
  detectVerifyRecipe: () => this.detectVerifyRecipe(),
1600
1969
  skipPriorContext: payload.flags.noPriorContext === true,
1601
1970
  complexity,
1971
+ needsClarification,
1602
1972
  sufficiencyMissing,
1603
1973
  // Mode C explicit override + gh pr create opt-in (see .planning/MAINTAIN-MODE.md).
1604
1974
  mode: payload.flags.mode,
@@ -2064,9 +2434,11 @@ export class Agent {
2064
2434
  setLastProviderOptionsShape: (shape) => {
2065
2435
  self._lastProviderOptionsShape = shape;
2066
2436
  },
2437
+ getCompactionStats: () => self.getCompactionStats(),
2067
2438
  getBatchClientOptions: (signal) => self.getBatchClientOptions(signal),
2068
2439
  getCompactionSettings: (cw) => self.getCompactionSettings(cw),
2069
2440
  compactForContext: (provider, system, cw, signal, settings, overflow) => self.compactForContext(provider, system, cw, signal, settings, overflow),
2441
+ extendHardCeilingForAutoCompaction: () => self.extendHardCeilingForAutoCompaction(),
2070
2442
  postTurnCompact: (provider, system, cw, signal) => self.postTurnCompact(provider, system, cw, signal),
2071
2443
  createTools: (bash, provider, mode, opts) => createTools(bash, provider, mode, opts),
2072
2444
  runTask: (request, signal) => self.runTask(request, signal),
@@ -2175,7 +2547,16 @@ export class Agent {
2175
2547
  else {
2176
2548
  try {
2177
2549
  const { classifySubSessionAction } = await import("../pil/llm-classify.js");
2178
- const routeResult = await classifySubSessionAction(this.requireProvider(), this.modelId, userMessage);
2550
+ // Feed the recent-conversation digest so the router can actually honour
2551
+ // its "decide based on the conversation history" contract — otherwise a
2552
+ // continuation ("ok làm phần đó đi") is judged in isolation and can be
2553
+ // mis-routed as a fresh/unrelated task. Passing contextInfo also turns
2554
+ // on the session-size metadata block the ROTATE_SESSION rule relies on.
2555
+ const routeResult = await classifySubSessionAction(this.requireProvider(), this.modelId, userMessage, {
2556
+ currentChars,
2557
+ threshold,
2558
+ recentTurns: this._buildRecentTurnsSummary(),
2559
+ });
2179
2560
  if (routeResult) {
2180
2561
  routeAction = routeResult.action;
2181
2562
  logger.info("orchestrator", "Routing action selected for user message", {
@@ -2189,6 +2570,18 @@ export class Agent {
2189
2570
  logger.error("orchestrator", "Routing classification failed, falling back to DIRECT_ANSWER", { error: err });
2190
2571
  }
2191
2572
  }
2573
+ // Reactive escalation — override a DIRECT_ANSWER route (the router's blind
2574
+ // spot on read-heavy analysis, and its silent-degrade to DIRECT on classify
2575
+ // failure) when the PREVIOUS turn's observed tool load proves the session is
2576
+ // doing heavy multi-tool work. Deterministic, based on real execution, not a
2577
+ // prompt guess. Only rescues DIRECT_ANSWER — never hijacks a deliberate
2578
+ // ROTATE_SESSION. See reactive-delegation.ts.
2579
+ if (routeAction === "DIRECT_ANSWER" && shouldReactivelyEscalate(this._lastTurnToolChars)) {
2580
+ logger.info("orchestrator", "Reactive escalation to sub-session (prior turn tool-heavy)", {
2581
+ prevTurnToolChars: this._lastTurnToolChars,
2582
+ });
2583
+ routeAction = "SPAWN_SUB_SESSION";
2584
+ }
2192
2585
  const shouldRotate = currentChars > threshold || routeAction === "ROTATE_SESSION";
2193
2586
  if (shouldRotate && this.session && this.sessionStore) {
2194
2587
  logger.info("orchestrator", "Triggering silent session rotation", {
@@ -2206,7 +2599,7 @@ export class Agent {
2206
2599
  const cr = await deliberateCompact(flowDir, this.messages, "", 4096, this.requireProvider(), this.modelId);
2207
2600
  const newSession = this.sessionStore.createSession(this.modelId, this.mode, this.bash.getCwd());
2208
2601
  const db = getDatabase();
2209
- db.prepare("UPDATE sessions SET parent_session_id = ? WHERE id = ?").run(parentSessionId, newSession.id);
2602
+ this.sessionStore.linkChild(newSession.id, parentSessionId, "rotation");
2210
2603
  const summaryMessage = createCompactionSummaryMessage(cr.summary);
2211
2604
  const nextSeq = getNextMessageSequence(newSession.id);
2212
2605
  appendCompaction(newSession.id, nextSeq, cr.summary, cr.tokensBeforeCompress);
@@ -2229,10 +2622,10 @@ export class Agent {
2229
2622
  let parentSessionId = null;
2230
2623
  let subSessionId = null;
2231
2624
  if (routeAction === "SPAWN_SUB_SESSION" && this.session && this.sessionStore) {
2232
- yield { type: "content", content: "\n⋯ Đang khởi tạo sub-session ngầm để xử lý tác vụ...\n" };
2625
+ yield { type: "toast", toastLevel: "info", content: "Đang khởi tạo sub-session ngầm để xử lý tác vụ..." };
2233
2626
  parentSessionId = this.session.id;
2234
2627
  try {
2235
- const { loadLatestCompaction, getNextMessageSequence, appendCompaction, loadTranscriptState } = await import("../storage/transcript.js");
2628
+ const { loadLatestCompaction, getNextMessageSequence, appendCompaction } = await import("../storage/transcript.js");
2236
2629
  const { getDatabase } = await import("../storage/db.js");
2237
2630
  const db = getDatabase();
2238
2631
  // Check if there is already an active child session for this parent session
@@ -2259,9 +2652,13 @@ export class Agent {
2259
2652
  }
2260
2653
  }
2261
2654
  if (shouldResume && subSessionId) {
2262
- yield { type: "content", content: "\n⋯ Phát hiện sub-session trước đó bị gián đoạn, đang khôi phục...\n" };
2655
+ yield {
2656
+ type: "toast",
2657
+ toastLevel: "info",
2658
+ content: "Phát hiện sub-session trước đó bị gián đoạn, đang khôi phục...",
2659
+ };
2263
2660
  this.session = this.sessionStore.getRequiredSession(subSessionId);
2264
- const childState = loadTranscriptState(subSessionId);
2661
+ const childState = loadSessionChainTranscriptState(subSessionId);
2265
2662
  this.messages = childState.messages;
2266
2663
  this.messageSeqs = childState.seqs;
2267
2664
  this.sessionStore.touchSession(subSessionId, this.bash.getCwd());
@@ -2274,21 +2671,26 @@ export class Agent {
2274
2671
  else {
2275
2672
  const latest = loadLatestCompaction(parentSessionId);
2276
2673
  const newSession = this.sessionStore.createSession(this.modelId, this.mode, this.bash.getCwd());
2277
- db.prepare("UPDATE sessions SET parent_session_id = ? WHERE id = ?").run(parentSessionId, newSession.id);
2674
+ this.sessionStore.linkChild(newSession.id, parentSessionId, "subagent");
2278
2675
  subSessionId = newSession.id;
2279
- let seedMessages = [];
2280
- let seedSeqs = [];
2676
+ // Seed the child with the parent's CURRENT working set — after a
2677
+ // compaction that is already [summary, ...kept raw tail] (see the
2678
+ // compactForContext assignment). Previously the compaction branch
2679
+ // reseeded ONLY [summary], discarding the kept raw tail — so a forked
2680
+ // council/debate saw a bare generic summary instead of the actual
2681
+ // recent discussion the user pointed at ("debate THESE parts"), and
2682
+ // drifted off-topic. Keep the tail; just guarantee the summary is
2683
+ // present and persist the compaction row for the child's continuity.
2684
+ const seedMessages = [...this.messages];
2685
+ const seedSeqs = [...this.messageSeqs];
2281
2686
  if (latest?.summary) {
2282
- const summaryMsg = createCompactionSummaryMessage(latest.summary);
2283
- seedMessages = [summaryMsg];
2284
- seedSeqs = [null];
2687
+ if (!isCompactionSummaryMessage(seedMessages[0])) {
2688
+ seedMessages.unshift(createCompactionSummaryMessage(latest.summary));
2689
+ seedSeqs.unshift(null);
2690
+ }
2285
2691
  const nextSeq = getNextMessageSequence(subSessionId);
2286
2692
  appendCompaction(subSessionId, nextSeq, latest.summary, latest.tokensBefore);
2287
2693
  }
2288
- else {
2289
- seedMessages = [...this.messages];
2290
- seedSeqs = [...this.messageSeqs];
2291
- }
2292
2694
  // Add sub-session overlay system message
2293
2695
  const overlayMessage = {
2294
2696
  role: "system",
@@ -2322,6 +2724,22 @@ export class Agent {
2322
2724
  const autoCommitOn = isAutoCommitEnabled();
2323
2725
  const cwd = this.bash.getCwd();
2324
2726
  const dirtyBefore = autoCommitOn ? await snapshotDirtyPaths(cwd) : new Set();
2727
+ // Top-level turn watchdog. The per-chunk stall watchdog only covers
2728
+ // streamText byte flow; it does NOT cover a turn generator that keeps the
2729
+ // socket open but never RETURNS after the model is done. Observed live:
2730
+ // xai/grok-composer-2.5-fast fires onFinish/llm-done (finishReason=stop) but
2731
+ // its stream generator never terminates, so processor.run() hangs, the UI's
2732
+ // `for await` never ends, finalizeActiveTurn never runs, and the TUI stays
2733
+ // frozen in the "processing" state (partial/raw markdown) until the next
2734
+ // message forces a new run. Mirrors the council-continuation guard (idle +
2735
+ // hard total). On fire we abort the turn and emit a terminal `done` so the
2736
+ // consumer finalizes cleanly instead of hanging forever. idleMs resets on
2737
+ // every yielded chunk, so long legitimate tool calls are safe; totalMs is a
2738
+ // hard ceiling (0 = disabled by default, since a big multi-tool turn is not a
2739
+ // hang). Env-overridable.
2740
+ const { withTurnWatchdog, TurnStallError } = await import("./turn-watchdog.js");
2741
+ const turnIdleMs = Number(process.env.MUONROI_TURN_IDLE_MS ?? 120_000);
2742
+ const turnTotalMs = Number(process.env.MUONROI_TURN_TOTAL_MS ?? 0);
2325
2743
  try {
2326
2744
  let attempts = 0;
2327
2745
  const maxAttempts = 3;
@@ -2330,7 +2748,29 @@ export class Agent {
2330
2748
  while (attempts < maxAttempts) {
2331
2749
  try {
2332
2750
  attempts++;
2333
- yield* processor.run(userMessage, observer, images);
2751
+ try {
2752
+ yield* withTurnWatchdog(processor.run(userMessage, observer, images), {
2753
+ idleMs: turnIdleMs,
2754
+ totalMs: turnTotalMs,
2755
+ label: "assistant turn",
2756
+ });
2757
+ }
2758
+ catch (stallErr) {
2759
+ // A hung turn is NOT a transient error — retrying it (below) would
2760
+ // just hang again. Abort, surface a toast, and terminate the turn.
2761
+ if (stallErr instanceof TurnStallError) {
2762
+ logger.warn("orchestrator", "Top-level turn watchdog fired — finalizing turn", {
2763
+ kind: stallErr.kind,
2764
+ message: stallErr.message,
2765
+ });
2766
+ this.abortController?.abort(new DOMException(stallErr.message, "TimeoutError"));
2767
+ yield { type: "toast", toastLevel: "warn", content: `Turn ended by watchdog: ${stallErr.message}` };
2768
+ yield { type: "done" };
2769
+ }
2770
+ else {
2771
+ throw stallErr;
2772
+ }
2773
+ }
2334
2774
  break;
2335
2775
  }
2336
2776
  catch (err) {
@@ -2516,6 +2956,42 @@ export class Agent {
2516
2956
  self._compactedThisTurn = v;
2517
2957
  },
2518
2958
  getCompactedThisTurn: () => self._compactedThisTurn,
2959
+ getCompactionStats: () => self.getCompactionStats(),
2960
+ reportTurnToolLoad: (chars) => {
2961
+ self._lastTurnToolChars = Number.isFinite(chars) && chars > 0 ? chars : 0;
2962
+ self._turnLoadOrdinal += 1;
2963
+ // Cold-first-turn instrument: record every turn whose load crosses the
2964
+ // reactive threshold, tagged with its ordinal. A later query counts what
2965
+ // fraction of threshold-crossing turns are ordinal 1 (the un-isolatable
2966
+ // cold turn) — the evidence that gates building an in-turn checkpoint.
2967
+ try {
2968
+ if (self.session && self._lastTurnToolChars >= getReactiveDelegationThresholdChars()) {
2969
+ // isolated = the heavy turn ran in a forked child (router SPAWN,
2970
+ // reactive escalation, or rotation), so the parent never held it.
2971
+ // The cold-first-turn HOLE is specifically coldFirstTurn && !isolated
2972
+ // — a turn-1 blow-up that landed in the parent because neither the
2973
+ // router nor reactive escalation caught it. Recording `isolated`
2974
+ // stops the metric from over-counting router-isolated heavy turns.
2975
+ const kind = self.sessionStore?.getSessionKind(self.session.id) ?? "conversation";
2976
+ const isolated = kind !== "conversation";
2977
+ logInteraction(self.session.id, "turn_tool_load", {
2978
+ data: {
2979
+ chars: self._lastTurnToolChars,
2980
+ ordinal: self._turnLoadOrdinal,
2981
+ coldFirstTurn: self._turnLoadOrdinal === 1,
2982
+ isolated,
2983
+ kind,
2984
+ threshold: getReactiveDelegationThresholdChars(),
2985
+ },
2986
+ });
2987
+ }
2988
+ }
2989
+ catch (err) {
2990
+ logger.debug("orchestrator", "turn_tool_load telemetry failed (best-effort)", {
2991
+ error: err?.message,
2992
+ });
2993
+ }
2994
+ },
2519
2995
  setTurnUserGoalExcerpt: (v) => {
2520
2996
  self._turnUserGoalExcerpt = v;
2521
2997
  },
@@ -2562,7 +3038,7 @@ export class Agent {
2562
3038
  model: runtime.model,
2563
3039
  system: systemPrompt,
2564
3040
  prompt: `Child Sub-session is stuck. Question:\n${question}`,
2565
- temperature: 0.2,
3041
+ ...resolveTemperatureParam(runtime, 0.2),
2566
3042
  ...(runtime.providerOptions ? { providerOptions: runtime.providerOptions } : {}),
2567
3043
  });
2568
3044
  return result.text;
@@ -2575,6 +3051,7 @@ export class Agent {
2575
3051
  countFilesTouched: () => self._countFilesTouched(),
2576
3052
  getCompactionSettings: (cw) => self.getCompactionSettings(cw),
2577
3053
  compactForContext: (provider, system, cw, signal, settings, overflow) => self.compactForContext(provider, system, cw, signal, settings, overflow),
3054
+ extendHardCeilingForAutoCompaction: () => self.extendHardCeilingForAutoCompaction(),
2578
3055
  postTurnCompact: (provider, system, cw, signal) => self.postTurnCompact(provider, system, cw, signal),
2579
3056
  runTask: (request, signal) => self.runTask(request, signal),
2580
3057
  runDelegation: (request, signal) => self.runDelegation(request, signal),
@@ -2633,20 +3110,31 @@ export class Agent {
2633
3110
  const recent = this.messages.slice(-6);
2634
3111
  const parts = [];
2635
3112
  for (const msg of recent) {
2636
- if (msg.role !== "user" && msg.role !== "assistant")
3113
+ if (msg.role !== "user" && msg.role !== "assistant" && msg.role !== "tool")
2637
3114
  continue;
2638
- const text = typeof msg.content === "string"
2639
- ? msg.content
2640
- : Array.isArray(msg.content)
2641
- ? msg.content
2642
- .filter((p) => p.type === "text")
2643
- .map((p) => p.text ?? "")
2644
- .join("")
2645
- : "";
3115
+ let text = "";
3116
+ if (msg.role === "tool") {
3117
+ text = Array.isArray(msg.content)
3118
+ ? msg.content.map((p) => (typeof p.result === "string" ? p.result : JSON.stringify(p.result))).join(" ")
3119
+ : typeof msg.content === "string"
3120
+ ? msg.content
3121
+ : JSON.stringify(msg.content);
3122
+ }
3123
+ else {
3124
+ text =
3125
+ typeof msg.content === "string"
3126
+ ? msg.content
3127
+ : Array.isArray(msg.content)
3128
+ ? msg.content
3129
+ .filter((p) => p.type === "text")
3130
+ .map((p) => p.text ?? "")
3131
+ .join("")
3132
+ : "";
3133
+ }
2646
3134
  if (!text)
2647
3135
  continue;
2648
- const snippet = text.length > 80 ? `${text.slice(0, 77)}...` : text;
2649
- parts.push(`[${msg.role}]: ${snippet}`);
3136
+ const snippet = text.length > 300 ? `${text.slice(0, 297)}...` : text;
3137
+ parts.push(`[${msg.role}]: ${snippet.replace(/\n/g, " ")}`);
2650
3138
  }
2651
3139
  return parts.length > 0 ? parts.join(" | ") : null;
2652
3140
  }
@@ -2716,48 +3204,109 @@ export class Agent {
2716
3204
  if (this._oauthInitDone)
2717
3205
  return;
2718
3206
  this._oauthInitDone = true;
2719
- // Only upgrade when there is no explicit API key — OAuth is an alternative
2720
- // auth path, not an override when the user deliberately passed a key.
2721
- // The boot wizard in src/index.ts uses the literal "oauth" as a sentinel
2722
- // to signal "no API key but OAuth tokens exist", so treat that as "no
2723
- // key" here.
2724
- if (this.apiKey && this.apiKey !== "oauth")
3207
+ // Re-resolve auth for the CURRENT provider. Runs when either:
3208
+ // (a) no explicit API key / the "oauth" sentinel is held — OAuth may
3209
+ // apply (the boot wizard in src/index.ts uses the literal "oauth" to
3210
+ // mean "no key but OAuth tokens exist"); OR
3211
+ // (b) setModel deferred provider construction on a provider switch
3212
+ // (this.provider === null) the PREVIOUS provider's key was cleared
3213
+ // and must be re-resolved from the NEW provider's own credentials.
3214
+ const providerDeferred = this.provider === null;
3215
+ const keyIsSentinelOrEmpty = !this.apiKey || this.apiKey === "oauth";
3216
+ if (!providerDeferred && !keyIsSentinelOrEmpty)
2725
3217
  return;
2726
3218
  try {
2727
3219
  const { listOAuthProviderIds } = await import("../providers/auth/registry.js");
2728
3220
  const ids = await listOAuthProviderIds();
2729
- if (!ids.includes(this.providerId))
3221
+ // OAuth path: the provider supports OAuth AND we don't hold a real API
3222
+ // key, so inject subscription tokens as Bearer headers.
3223
+ if (ids.includes(this.providerId) && keyIsSentinelOrEmpty) {
3224
+ const effectiveBaseURL = this.baseURL &&
3225
+ this.baseURL !== (await import("../providers/endpoints.js").then((m) => m.apiBaseFor("anthropic")))
3226
+ ? this.baseURL
3227
+ : undefined;
3228
+ const result = await createProviderFactoryAsync(this.providerId, {
3229
+ baseURL: effectiveBaseURL ?? undefined,
3230
+ });
3231
+ this.apiKey = "oauth";
3232
+ this.provider = result.factory;
2730
3233
  return;
2731
- const effectiveBaseURL = this.baseURL &&
2732
- this.baseURL !== (await import("../providers/endpoints.js").then((m) => m.apiBaseFor("anthropic")))
2733
- ? this.baseURL
2734
- : undefined;
2735
- const result = await createProviderFactoryAsync(this.providerId, {
2736
- baseURL: effectiveBaseURL ?? undefined,
2737
- });
2738
- this.provider = result.factory;
3234
+ }
3235
+ // API-key path: resolve THIS provider's own stored key. Reached when the
3236
+ // provider isn't OAuth-capable, or when a provider switch deferred
3237
+ // construction (we must swap in the NEW provider's key — never reuse the
3238
+ // previous provider's, which 401'd on every TUI provider switch). If no
3239
+ // key exists for a deferred switch, leave the provider null so
3240
+ // requireProvider() surfaces a clear "API key required".
3241
+ try {
3242
+ const key = await loadKeyForProvider(this.providerId);
3243
+ if (key) {
3244
+ this.apiKey = key;
3245
+ const staleBaseURL = isAnyProviderApiBase(this.baseURL) && this.baseURL !== apiBaseFor(this.providerId);
3246
+ if (staleBaseURL)
3247
+ this.baseURL = null;
3248
+ this.provider = createProvider(this.providerId, key, this.baseURL ?? undefined);
3249
+ }
3250
+ else if (providerDeferred) {
3251
+ this.provider = null;
3252
+ }
3253
+ }
3254
+ catch (err) {
3255
+ // ProviderKeyMissingError (or keychain failure) — no usable key. For a
3256
+ // deferred provider switch, null the provider so the next turn fails
3257
+ // loudly ("API key required") instead of 401-ing on a wrong/sentinel
3258
+ // key. For a non-deferred empty-key case, leave the existing provider
3259
+ // untouched (fail-open, as before).
3260
+ if (providerDeferred) {
3261
+ console.error(`[orchestrator] no API key for provider '${this.providerId}' after model switch: ${err instanceof Error ? err.message : String(err)}`);
3262
+ this.provider = null;
3263
+ }
3264
+ }
2739
3265
  }
2740
3266
  catch {
2741
- // Fail-open provider remains null; requireProvider() will surface the error
3267
+ // Registry unavailable — fail-open; the existing provider (if any) is
3268
+ // left untouched and requireProvider() will surface any error.
2742
3269
  }
2743
3270
  }
2744
3271
  async detectVerifyRecipe(settings, abortSignal) {
3272
+ const cwd = this.bash.getCwd();
3273
+ const effectiveSettings = settings ?? this.bash.getSandboxSettings();
3274
+ // Primary: LLM verify-detect turn — a codebase-aware, richer recipe.
3275
+ let llmRecipe = null;
2745
3276
  try {
2746
3277
  const result = await this.runTaskRequest({
2747
3278
  agent: "verify-detect",
2748
3279
  description: "Detect verification recipe",
2749
- prompt: buildVerifyDetectPrompt(this.bash.getCwd(), settings ?? this.bash.getSandboxSettings()),
3280
+ prompt: buildVerifyDetectPrompt(cwd, effectiveSettings),
2750
3281
  }, undefined, abortSignal);
2751
- if (!result.success || !result.output)
2752
- return null;
2753
- const maybeJson = extractJsonObject(result.output);
2754
- if (!maybeJson)
2755
- return null;
2756
- return normalizeVerifyRecipe(JSON.parse(maybeJson));
3282
+ if (result.success && result.output) {
3283
+ const maybeJson = extractJsonObject(result.output);
3284
+ if (maybeJson)
3285
+ llmRecipe = normalizeVerifyRecipe(JSON.parse(maybeJson));
3286
+ }
2757
3287
  }
2758
- catch {
2759
- return null;
3288
+ catch (err) {
3289
+ console.error(`[orchestrator] verify-detect turn failed: ${err instanceof Error ? err.message : String(err)}`);
3290
+ }
3291
+ if (llmRecipe)
3292
+ return llmRecipe;
3293
+ // Deterministic fallback: an existing repo with a recognizable manifest
3294
+ // (package.json test script, *.sln, pyproject, …) yields a reliable recipe
3295
+ // WITHOUT an LLM. This stops CB-3 from false-halting an in-place /ideal
3296
+ // migration with the "Recovery options" card purely because the flaky
3297
+ // verify-detect model returned no parseable JSON (root cause of the
3298
+ // existing-repo implementation-reachability halt). Only trusted when the
3299
+ // profiler recognizes the ecosystem AND found real test commands.
3300
+ try {
3301
+ const profile = inferVerifyProjectProfile(cwd, effectiveSettings);
3302
+ if (shouldTrustDeterministicRecipe(profile.recipe)) {
3303
+ return profile.recipe;
3304
+ }
3305
+ }
3306
+ catch (err) {
3307
+ console.error(`[orchestrator] deterministic verify-recipe fallback failed: ${err instanceof Error ? err.message : String(err)}`);
2760
3308
  }
3309
+ return null;
2761
3310
  }
2762
3311
  async runVerify(onProgress, abortSignal) {
2763
3312
  this.abortController = new AbortController();