@cjhyy/code-shell-core 0.5.0-rc.1 → 0.6.0-rc.1

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 (528) hide show
  1. package/README.md +13 -10
  2. package/dist/agent/agent-definition-registry.d.ts +21 -3
  3. package/dist/agent/agent-definition-registry.js +49 -18
  4. package/dist/agent/agent-definition.d.ts +32 -0
  5. package/dist/agent/agent-definition.js +49 -4
  6. package/dist/arena/arena.js +11 -12
  7. package/dist/arena/context/context-tools.js +47 -6
  8. package/dist/arena/context/within-root.d.ts +7 -0
  9. package/dist/arena/context/within-root.js +15 -0
  10. package/dist/arena/iterate/phases/argue.js +8 -2
  11. package/dist/arena/iterate/phases/revise.js +1 -1
  12. package/dist/arena/iterate/phases/tournament.js +3 -3
  13. package/dist/arena/ledger.js +9 -1
  14. package/dist/arena/phases/adjudication.js +1 -4
  15. package/dist/arena/phases/build-consensus.js +1 -4
  16. package/dist/arena/phases/cross-review.js +2 -8
  17. package/dist/arena/phases/debate-rounds.js +1 -4
  18. package/dist/arena/phases/participant-research.js +1 -4
  19. package/dist/arena/phases/planning-detail-expansion.js +8 -10
  20. package/dist/arena/planner.js +0 -1
  21. package/dist/arena/providers/docs.d.ts +1 -0
  22. package/dist/arena/providers/docs.js +5 -2
  23. package/dist/arena/providers/repo.d.ts +1 -0
  24. package/dist/arena/providers/repo.js +5 -2
  25. package/dist/arena/strategies/utils.d.ts +1 -4
  26. package/dist/arena/strategies/utils.js +7 -51
  27. package/dist/arena/transitions.d.ts +7 -2
  28. package/dist/arena/transitions.js +9 -4
  29. package/dist/arena/types.d.ts +8 -2
  30. package/dist/automation/cron-expr.d.ts +41 -0
  31. package/dist/automation/cron-expr.js +169 -0
  32. package/dist/automation/index.d.ts +41 -0
  33. package/dist/automation/index.js +39 -0
  34. package/dist/automation/runner.d.ts +84 -0
  35. package/dist/automation/runner.js +92 -0
  36. package/dist/automation/scheduler.d.ts +194 -0
  37. package/dist/automation/scheduler.js +623 -0
  38. package/dist/automation/store.d.ts +39 -0
  39. package/dist/automation/store.js +122 -0
  40. package/dist/automation/write-policy.d.ts +34 -0
  41. package/dist/automation/write-policy.js +113 -0
  42. package/dist/automation/write-run.d.ts +51 -0
  43. package/dist/automation/write-run.js +38 -0
  44. package/dist/capability-control/disabled-lists.d.ts +25 -0
  45. package/dist/capability-control/disabled-lists.js +57 -0
  46. package/dist/capability-control/index.d.ts +5 -0
  47. package/dist/capability-control/index.js +3 -0
  48. package/dist/capability-control/overlay.d.ts +64 -0
  49. package/dist/capability-control/overlay.js +116 -0
  50. package/dist/capability-control/project.d.ts +65 -0
  51. package/dist/capability-control/project.js +147 -0
  52. package/dist/capability-control/service.d.ts +70 -0
  53. package/dist/capability-control/service.js +147 -0
  54. package/dist/capability-control/types.d.ts +62 -0
  55. package/dist/capability-control/types.js +17 -0
  56. package/dist/cc-orchestrator/agent-adapter.d.ts +50 -0
  57. package/dist/cc-orchestrator/agent-adapter.js +133 -0
  58. package/dist/cc-orchestrator/cc-capability.d.ts +19 -0
  59. package/dist/cc-orchestrator/cc-capability.js +53 -0
  60. package/dist/cc-orchestrator/codex-session-discovery.d.ts +24 -0
  61. package/dist/cc-orchestrator/codex-session-discovery.js +191 -0
  62. package/dist/cc-orchestrator/codex-session-history.d.ts +25 -0
  63. package/dist/cc-orchestrator/codex-session-history.js +187 -0
  64. package/dist/cc-orchestrator/external-agent-changes.d.ts +19 -0
  65. package/dist/cc-orchestrator/external-agent-changes.js +214 -0
  66. package/dist/cc-orchestrator/external-agent-driver.d.ts +18 -0
  67. package/dist/cc-orchestrator/external-agent-driver.js +69 -0
  68. package/dist/cc-orchestrator/index.d.ts +8 -0
  69. package/dist/cc-orchestrator/index.js +8 -0
  70. package/dist/cc-orchestrator/relevance-judge.d.ts +15 -0
  71. package/dist/cc-orchestrator/relevance-judge.js +29 -0
  72. package/dist/cc-orchestrator/session-discovery.d.ts +46 -0
  73. package/dist/cc-orchestrator/session-discovery.js +125 -0
  74. package/dist/cc-orchestrator/session-history.d.ts +19 -0
  75. package/dist/cc-orchestrator/session-history.js +67 -0
  76. package/dist/cli/agent-server-stdio.d.ts +26 -1
  77. package/dist/cli/agent-server-stdio.js +221 -28
  78. package/dist/cli/agent-server-tcp.d.ts +20 -0
  79. package/dist/cli/agent-server-tcp.js +134 -0
  80. package/dist/cli/graceful-shutdown.d.ts +27 -0
  81. package/dist/cli/graceful-shutdown.js +30 -0
  82. package/dist/context/compaction.d.ts +56 -0
  83. package/dist/context/compaction.js +209 -15
  84. package/dist/context/manager.d.ts +0 -20
  85. package/dist/context/manager.js +25 -41
  86. package/dist/cost-tracker.js +5 -61
  87. package/dist/credentials/cipher.d.ts +49 -0
  88. package/dist/credentials/cipher.js +45 -0
  89. package/dist/credentials/cookie-jar.d.ts +24 -0
  90. package/dist/credentials/cookie-jar.js +40 -0
  91. package/dist/credentials/index.d.ts +7 -0
  92. package/dist/credentials/index.js +5 -0
  93. package/dist/credentials/inject-credential-tool.d.ts +20 -0
  94. package/dist/credentials/inject-credential-tool.js +130 -0
  95. package/dist/credentials/store.d.ts +72 -0
  96. package/dist/credentials/store.js +184 -0
  97. package/dist/credentials/types.d.ts +56 -0
  98. package/dist/credentials/use-credential-tool.d.ts +29 -0
  99. package/dist/credentials/use-credential-tool.js +205 -0
  100. package/dist/credentials/use-gate.d.ts +56 -0
  101. package/dist/credentials/use-gate.js +52 -0
  102. package/dist/cron/cron-runtime.d.ts +2 -0
  103. package/dist/cron/cron-runtime.js +2 -0
  104. package/dist/cron/cron-store.d.ts +2 -0
  105. package/dist/cron/cron-store.js +2 -0
  106. package/dist/cron/scheduler.d.ts +5 -32
  107. package/dist/cron/scheduler.js +5 -113
  108. package/dist/data/model-metadata.d.ts +77 -0
  109. package/dist/data/model-metadata.js +56 -0
  110. package/dist/data/model-metadata.json +216 -0
  111. package/dist/data/openrouter-models.d.ts +18 -7
  112. package/dist/data/openrouter-models.js +35 -8
  113. package/dist/data/openrouter-sync.js +1 -1
  114. package/dist/engine/aux-key.d.ts +10 -0
  115. package/dist/engine/aux-key.js +11 -0
  116. package/dist/engine/dynamic-tool-defs.d.ts +19 -0
  117. package/dist/engine/dynamic-tool-defs.js +36 -0
  118. package/dist/engine/engine.d.ts +431 -108
  119. package/dist/engine/engine.js +1630 -279
  120. package/dist/engine/friendly-error.d.ts +18 -0
  121. package/dist/engine/friendly-error.js +63 -0
  122. package/dist/engine/goal.d.ts +187 -0
  123. package/dist/engine/goal.js +196 -0
  124. package/dist/engine/image-compression.d.ts +81 -0
  125. package/dist/engine/image-compression.js +186 -0
  126. package/dist/engine/image-policy.d.ts +165 -0
  127. package/dist/engine/image-policy.js +189 -0
  128. package/dist/engine/model-connections-pool.d.ts +17 -0
  129. package/dist/engine/model-connections-pool.js +66 -0
  130. package/dist/engine/model-facade.d.ts +10 -0
  131. package/dist/engine/model-facade.js +25 -0
  132. package/dist/engine/patch-orphaned-tools.js +3 -0
  133. package/dist/engine/reactive-threshold.d.ts +13 -0
  134. package/dist/engine/reactive-threshold.js +17 -0
  135. package/dist/engine/resolve-llm-config.d.ts +16 -0
  136. package/dist/engine/resolve-llm-config.js +44 -0
  137. package/dist/engine/runtime.js +9 -1
  138. package/dist/engine/sandbox-cache-key.d.ts +10 -0
  139. package/dist/engine/sandbox-cache-key.js +9 -0
  140. package/dist/engine/sandbox-config.d.ts +31 -0
  141. package/dist/engine/sandbox-config.js +38 -0
  142. package/dist/engine/session-title.d.ts +13 -0
  143. package/dist/engine/session-title.js +34 -0
  144. package/dist/engine/session-usage.d.ts +19 -0
  145. package/dist/engine/session-usage.js +25 -0
  146. package/dist/engine/steer-queue.d.ts +32 -0
  147. package/dist/engine/steer-queue.js +26 -0
  148. package/dist/engine/streaming-tool-queue.d.ts +12 -0
  149. package/dist/engine/streaming-tool-queue.js +50 -10
  150. package/dist/engine/turn-loop.d.ts +94 -9
  151. package/dist/engine/turn-loop.js +775 -342
  152. package/dist/engine/types.d.ts +175 -0
  153. package/dist/engine/types.js +13 -0
  154. package/dist/external-agents/config.d.ts +2 -0
  155. package/dist/external-agents/config.js +15 -0
  156. package/dist/external-agents/types.d.ts +31 -0
  157. package/dist/external-agents/types.js +1 -0
  158. package/dist/git/parse-log.d.ts +13 -0
  159. package/dist/git/parse-log.js +21 -0
  160. package/dist/git/utils.d.ts +8 -6
  161. package/dist/git/utils.js +62 -26
  162. package/dist/git/worktree.d.ts +48 -0
  163. package/dist/git/worktree.js +111 -20
  164. package/dist/hooks/events.d.ts +23 -1
  165. package/dist/hooks/goal-stop-hook.d.ts +58 -0
  166. package/dist/hooks/goal-stop-hook.js +279 -0
  167. package/dist/hooks/hook-output.d.ts +33 -0
  168. package/dist/hooks/hook-output.js +79 -0
  169. package/dist/hooks/registry.d.ts +15 -0
  170. package/dist/hooks/registry.js +57 -1
  171. package/dist/hooks/shell-runner.d.ts +2 -0
  172. package/dist/hooks/shell-runner.js +48 -18
  173. package/dist/index.d.ts +79 -11
  174. package/dist/index.js +89 -6
  175. package/dist/llm/capabilities/param-specs.d.ts +14 -0
  176. package/dist/llm/capabilities/param-specs.js +62 -0
  177. package/dist/llm/capabilities/reasoning-control.d.ts +24 -0
  178. package/dist/llm/capabilities/reasoning-control.js +38 -0
  179. package/dist/llm/capabilities/rules.js +37 -3
  180. package/dist/llm/capabilities/types.d.ts +32 -2
  181. package/dist/llm/clamp-max-tokens.d.ts +11 -0
  182. package/dist/llm/clamp-max-tokens.js +17 -0
  183. package/dist/llm/client-base.d.ts +69 -5
  184. package/dist/llm/client-base.js +185 -25
  185. package/dist/llm/client-factory.d.ts +3 -3
  186. package/dist/llm/client-factory.js +2 -2
  187. package/dist/llm/model-cache.js +4 -2
  188. package/dist/llm/model-pool.d.ts +29 -11
  189. package/dist/llm/model-pool.js +36 -20
  190. package/dist/llm/provider-auth.d.ts +41 -0
  191. package/dist/llm/provider-auth.js +76 -0
  192. package/dist/llm/provider-catalog.d.ts +6 -2
  193. package/dist/llm/providers/anthropic.d.ts +32 -2
  194. package/dist/llm/providers/anthropic.js +207 -18
  195. package/dist/llm/providers/openai.d.ts +47 -2
  196. package/dist/llm/providers/openai.js +478 -71
  197. package/dist/llm/reasoning-setting.d.ts +51 -0
  198. package/dist/llm/reasoning-setting.js +40 -0
  199. package/dist/llm/stop-reason.d.ts +13 -0
  200. package/dist/llm/stop-reason.js +21 -0
  201. package/dist/llm/stream-watchdog.js +5 -1
  202. package/dist/llm/strip-vision.d.ts +32 -0
  203. package/dist/llm/strip-vision.js +94 -0
  204. package/dist/llm/token-counter.js +9 -2
  205. package/dist/llm/types.d.ts +9 -6
  206. package/dist/logging/logger.js +18 -4
  207. package/dist/logging/sanitize-messages.d.ts +10 -0
  208. package/dist/logging/sanitize-messages.js +112 -0
  209. package/dist/lsp/client.js +28 -8
  210. package/dist/lsp/manager.d.ts +1 -1
  211. package/dist/lsp/manager.js +42 -11
  212. package/dist/lsp/root-path.d.ts +9 -0
  213. package/dist/lsp/root-path.js +12 -0
  214. package/dist/model-catalog/builtin.d.ts +12 -0
  215. package/dist/model-catalog/builtin.js +407 -0
  216. package/dist/model-catalog/gen-connections.d.ts +20 -0
  217. package/dist/model-catalog/gen-connections.js +28 -0
  218. package/dist/model-catalog/index.d.ts +41 -0
  219. package/dist/model-catalog/index.js +90 -0
  220. package/dist/model-catalog/params.d.ts +20 -0
  221. package/dist/model-catalog/params.js +45 -0
  222. package/dist/model-catalog/resolve.d.ts +48 -0
  223. package/dist/model-catalog/resolve.js +33 -0
  224. package/dist/model-catalog/save-entry.d.ts +32 -0
  225. package/dist/model-catalog/save-entry.js +104 -0
  226. package/dist/model-catalog/types.d.ts +561 -0
  227. package/dist/model-catalog/types.js +93 -0
  228. package/dist/model-catalog/upsert.d.ts +9 -0
  229. package/dist/model-catalog/upsert.js +8 -0
  230. package/dist/onboarding.d.ts +12 -82
  231. package/dist/onboarding.js +75 -333
  232. package/dist/plugins/gitOps.d.ts +53 -0
  233. package/dist/plugins/gitOps.js +130 -5
  234. package/dist/plugins/installer/checkUpdate.d.ts +16 -0
  235. package/dist/plugins/installer/checkUpdate.js +32 -0
  236. package/dist/plugins/installer/codex/convertAgents.d.ts +7 -0
  237. package/dist/plugins/installer/codex/convertAgents.js +55 -0
  238. package/dist/plugins/installer/codex/convertCommands.d.ts +19 -0
  239. package/dist/plugins/installer/codex/convertCommands.js +46 -0
  240. package/dist/plugins/installer/codex/convertMcp.d.ts +11 -0
  241. package/dist/plugins/installer/codex/convertMcp.js +85 -0
  242. package/dist/plugins/installer/codex/convertSkills.d.ts +12 -0
  243. package/dist/plugins/installer/codex/convertSkills.js +33 -0
  244. package/dist/plugins/installer/detectFormat.d.ts +2 -0
  245. package/dist/plugins/installer/detectFormat.js +6 -0
  246. package/dist/plugins/installer/install.d.ts +7 -0
  247. package/dist/plugins/installer/install.js +123 -0
  248. package/dist/plugins/installer/installFromArchive.d.ts +43 -0
  249. package/dist/plugins/installer/installFromArchive.js +134 -0
  250. package/dist/plugins/installer/installFromSource.d.ts +13 -0
  251. package/dist/plugins/installer/installFromSource.js +51 -0
  252. package/dist/plugins/installer/list.d.ts +9 -0
  253. package/dist/plugins/installer/list.js +30 -0
  254. package/dist/plugins/installer/loadPluginAgents.d.ts +3 -0
  255. package/dist/plugins/installer/loadPluginAgents.js +27 -0
  256. package/dist/plugins/installer/loadPluginMcp.d.ts +14 -0
  257. package/dist/plugins/installer/loadPluginMcp.js +107 -0
  258. package/dist/plugins/installer/parseSource.d.ts +20 -0
  259. package/dist/plugins/installer/parseSource.js +74 -0
  260. package/dist/plugins/installer/paths.d.ts +5 -0
  261. package/dist/plugins/installer/paths.js +25 -0
  262. package/dist/plugins/installer/pruneDisabled.d.ts +24 -0
  263. package/dist/plugins/installer/pruneDisabled.js +73 -0
  264. package/dist/plugins/installer/types.d.ts +52 -0
  265. package/dist/plugins/installer/types.js +28 -0
  266. package/dist/plugins/installer/uninstall.d.ts +2 -0
  267. package/dist/plugins/installer/uninstall.js +18 -0
  268. package/dist/plugins/installer/unzip.d.ts +14 -0
  269. package/dist/plugins/installer/unzip.js +82 -0
  270. package/dist/plugins/installer/update.d.ts +28 -0
  271. package/dist/plugins/installer/update.js +97 -0
  272. package/dist/plugins/loadPluginHooks.d.ts +54 -1
  273. package/dist/plugins/loadPluginHooks.js +83 -1
  274. package/dist/plugins/marketplaceManager.d.ts +15 -1
  275. package/dist/plugins/marketplaceManager.js +58 -4
  276. package/dist/plugins/pluginCommandHook.js +34 -14
  277. package/dist/plugins/pluginContent.d.ts +30 -0
  278. package/dist/plugins/pluginContent.js +83 -0
  279. package/dist/plugins/pluginInstaller.d.ts +34 -0
  280. package/dist/plugins/pluginInstaller.js +168 -35
  281. package/dist/plugins/schemas.js +41 -6
  282. package/dist/plugins/types.d.ts +10 -0
  283. package/dist/preset/index.d.ts +7 -1
  284. package/dist/preset/index.js +112 -6
  285. package/dist/product/types.d.ts +1 -1
  286. package/dist/prompt/composer.d.ts +31 -0
  287. package/dist/prompt/composer.js +95 -25
  288. package/dist/prompt/instruction-scanner.js +5 -3
  289. package/dist/prompt/section-loader.js +1 -0
  290. package/dist/prompt/sections/base.md +1 -0
  291. package/dist/prompt/sections/browser.md +10 -0
  292. package/dist/prompt/sections/coding.md +4 -0
  293. package/dist/prompt/sections/orchestration.md +8 -0
  294. package/dist/protocol/chat-session-manager.d.ts +15 -2
  295. package/dist/protocol/chat-session-manager.js +46 -0
  296. package/dist/protocol/chat-session.d.ts +82 -0
  297. package/dist/protocol/chat-session.js +121 -1
  298. package/dist/protocol/client.d.ts +27 -0
  299. package/dist/protocol/client.js +38 -0
  300. package/dist/protocol/redact.d.ts +50 -0
  301. package/dist/protocol/redact.js +71 -0
  302. package/dist/protocol/server.d.ts +153 -2
  303. package/dist/protocol/server.js +807 -39
  304. package/dist/protocol/tcp-transport.d.ts +45 -0
  305. package/dist/protocol/tcp-transport.js +74 -0
  306. package/dist/protocol/transport.js +3 -2
  307. package/dist/protocol/types.d.ts +69 -0
  308. package/dist/protocol/types.js +22 -0
  309. package/dist/quota/credentials.d.ts +3 -0
  310. package/dist/quota/credentials.js +80 -0
  311. package/dist/quota/index.d.ts +36 -0
  312. package/dist/quota/index.js +155 -0
  313. package/dist/quota/types.d.ts +48 -0
  314. package/dist/quota/types.js +13 -0
  315. package/dist/remote/bridge.d.ts +11 -2
  316. package/dist/remote/bridge.js +61 -33
  317. package/dist/review/review-prompt.d.ts +28 -0
  318. package/dist/review/review-prompt.js +81 -0
  319. package/dist/run/ArtifactTracker.js +5 -4
  320. package/dist/run/EngineRunner.d.ts +27 -1
  321. package/dist/run/EngineRunner.js +46 -8
  322. package/dist/run/FileRunStore.js +40 -11
  323. package/dist/run/RunApprovalBackend.js +25 -5
  324. package/dist/run/RunLock.d.ts +28 -2
  325. package/dist/run/RunLock.js +49 -7
  326. package/dist/run/RunManager.d.ts +12 -0
  327. package/dist/run/RunManager.js +86 -3
  328. package/dist/run/RunQueue.d.ts +1 -0
  329. package/dist/run/RunQueue.js +8 -2
  330. package/dist/run/factory.d.ts +8 -1
  331. package/dist/run/factory.js +1 -0
  332. package/dist/run/index.d.ts +2 -2
  333. package/dist/run/index.js +1 -1
  334. package/dist/run/redirect-target.d.ts +7 -0
  335. package/dist/run/redirect-target.js +13 -0
  336. package/dist/run/types.js +1 -1
  337. package/dist/runtime/background-shell.d.ts +139 -0
  338. package/dist/runtime/background-shell.js +495 -0
  339. package/dist/runtime/output-clean.d.ts +24 -0
  340. package/dist/runtime/output-clean.js +41 -0
  341. package/dist/runtime/ring-file.d.ts +64 -0
  342. package/dist/runtime/ring-file.js +174 -0
  343. package/dist/runtime/safe-spawn.js +15 -34
  344. package/dist/runtime/spawn-common.d.ts +148 -0
  345. package/dist/runtime/spawn-common.js +303 -0
  346. package/dist/runtime/truncate-output.d.ts +22 -0
  347. package/dist/runtime/truncate-output.js +49 -0
  348. package/dist/runtime/utf8-cut.d.ts +11 -0
  349. package/dist/runtime/utf8-cut.js +23 -0
  350. package/dist/services/auto-dream.d.ts +4 -0
  351. package/dist/services/auto-dream.js +26 -26
  352. package/dist/services/browser-open.d.ts +13 -0
  353. package/dist/services/browser-open.js +16 -0
  354. package/dist/services/diagnostics.d.ts +1 -2
  355. package/dist/services/diagnostics.js +12 -7
  356. package/dist/services/dream-consolidation.d.ts +57 -0
  357. package/dist/services/dream-consolidation.js +151 -0
  358. package/dist/services/extract-memories.d.ts +14 -1
  359. package/dist/services/extract-memories.js +45 -6
  360. package/dist/services/memory-orchestrator.d.ts +21 -0
  361. package/dist/services/memory-orchestrator.js +125 -32
  362. package/dist/services/notifier.d.ts +8 -0
  363. package/dist/services/notifier.js +39 -25
  364. package/dist/services/oauth.js +5 -7
  365. package/dist/services/session-memory-sort.d.ts +8 -0
  366. package/dist/services/session-memory-sort.js +9 -0
  367. package/dist/services/session-memory.js +30 -19
  368. package/dist/session/file-history.d.ts +124 -1
  369. package/dist/session/file-history.js +232 -9
  370. package/dist/session/memory.d.ts +116 -2
  371. package/dist/session/memory.js +250 -28
  372. package/dist/session/session-manager.d.ts +63 -1
  373. package/dist/session/session-manager.js +186 -5
  374. package/dist/session/simple-diff.d.ts +23 -0
  375. package/dist/session/simple-diff.js +84 -0
  376. package/dist/session/transcript.d.ts +25 -1
  377. package/dist/session/transcript.js +35 -2
  378. package/dist/session/undo-target.d.ts +67 -0
  379. package/dist/session/undo-target.js +144 -0
  380. package/dist/settings/disk-defaults.d.ts +42 -0
  381. package/dist/settings/disk-defaults.js +33 -0
  382. package/dist/settings/feature-flags.d.ts +64 -0
  383. package/dist/settings/feature-flags.js +61 -0
  384. package/dist/settings/manager.d.ts +101 -1
  385. package/dist/settings/manager.js +345 -15
  386. package/dist/settings/migrate-config.d.ts +45 -0
  387. package/dist/settings/migrate-config.js +125 -0
  388. package/dist/settings/personalization.d.ts +19 -0
  389. package/dist/settings/personalization.js +7 -0
  390. package/dist/settings/schema-export.d.ts +25 -0
  391. package/dist/settings/schema-export.js +38 -0
  392. package/dist/settings/schema.d.ts +1827 -396
  393. package/dist/settings/schema.js +361 -81
  394. package/dist/skills/scanner.d.ts +9 -0
  395. package/dist/skills/scanner.js +30 -2
  396. package/dist/stt/resolve-transcribe.d.ts +31 -0
  397. package/dist/stt/resolve-transcribe.js +108 -0
  398. package/dist/stt/transcribe.d.ts +51 -0
  399. package/dist/stt/transcribe.js +65 -0
  400. package/dist/tool-system/browser-bridge.d.ts +226 -0
  401. package/dist/tool-system/browser-bridge.js +163 -0
  402. package/dist/tool-system/builtin/add-marketplace.d.ts +12 -0
  403. package/dist/tool-system/builtin/add-marketplace.js +76 -0
  404. package/dist/tool-system/builtin/agent-heartbeat.d.ts +49 -0
  405. package/dist/tool-system/builtin/agent-heartbeat.js +89 -0
  406. package/dist/tool-system/builtin/agent-notifications.d.ts +12 -3
  407. package/dist/tool-system/builtin/agent-notifications.js +9 -3
  408. package/dist/tool-system/builtin/agent-output-file.d.ts +38 -0
  409. package/dist/tool-system/builtin/agent-output-file.js +72 -0
  410. package/dist/tool-system/builtin/agent-registry.d.ts +23 -0
  411. package/dist/tool-system/builtin/agent-registry.js +16 -4
  412. package/dist/tool-system/builtin/agent.d.ts +50 -11
  413. package/dist/tool-system/builtin/agent.js +576 -73
  414. package/dist/tool-system/builtin/apply-patch/applier.js +66 -8
  415. package/dist/tool-system/builtin/apply-patch/backup-targets.d.ts +10 -0
  416. package/dist/tool-system/builtin/apply-patch/backup-targets.js +30 -0
  417. package/dist/tool-system/builtin/apply-patch/index.js +7 -3
  418. package/dist/tool-system/builtin/arena.js +6 -7
  419. package/dist/tool-system/builtin/background-jobs.d.ts +76 -0
  420. package/dist/tool-system/builtin/background-jobs.js +124 -0
  421. package/dist/tool-system/builtin/background-shell-tools.d.ts +20 -0
  422. package/dist/tool-system/builtin/background-shell-tools.js +108 -0
  423. package/dist/tool-system/builtin/background-work.d.ts +67 -0
  424. package/dist/tool-system/builtin/background-work.js +86 -0
  425. package/dist/tool-system/builtin/bash-output-style.d.ts +32 -0
  426. package/dist/tool-system/builtin/bash-output-style.js +40 -0
  427. package/dist/tool-system/builtin/bash.d.ts +5 -2
  428. package/dist/tool-system/builtin/bash.js +98 -62
  429. package/dist/tool-system/builtin/browser-tools.d.ts +33 -0
  430. package/dist/tool-system/builtin/browser-tools.js +312 -0
  431. package/dist/tool-system/builtin/cancel-goal.d.ts +31 -0
  432. package/dist/tool-system/builtin/cancel-goal.js +64 -0
  433. package/dist/tool-system/builtin/check-quota.d.ts +15 -0
  434. package/dist/tool-system/builtin/check-quota.js +34 -0
  435. package/dist/tool-system/builtin/complete-goal.d.ts +25 -0
  436. package/dist/tool-system/builtin/complete-goal.js +45 -0
  437. package/dist/tool-system/builtin/config.js +7 -0
  438. package/dist/tool-system/builtin/cron.d.ts +7 -0
  439. package/dist/tool-system/builtin/cron.js +105 -9
  440. package/dist/tool-system/builtin/drive-claude-code.d.ts +30 -0
  441. package/dist/tool-system/builtin/drive-claude-code.js +157 -0
  442. package/dist/tool-system/builtin/edit-model-catalog.d.ts +3 -0
  443. package/dist/tool-system/builtin/edit-model-catalog.js +94 -0
  444. package/dist/tool-system/builtin/edit.d.ts +2 -1
  445. package/dist/tool-system/builtin/edit.js +21 -12
  446. package/dist/tool-system/builtin/eol.d.ts +29 -0
  447. package/dist/tool-system/builtin/eol.js +37 -0
  448. package/dist/tool-system/builtin/file-cache.d.ts +6 -0
  449. package/dist/tool-system/builtin/file-cache.js +8 -0
  450. package/dist/tool-system/builtin/generate-image.d.ts +60 -0
  451. package/dist/tool-system/builtin/generate-image.js +374 -0
  452. package/dist/tool-system/builtin/generate-video.d.ts +55 -0
  453. package/dist/tool-system/builtin/generate-video.js +361 -0
  454. package/dist/tool-system/builtin/grep.js +6 -2
  455. package/dist/tool-system/builtin/image-providers.d.ts +86 -0
  456. package/dist/tool-system/builtin/image-providers.js +190 -0
  457. package/dist/tool-system/builtin/image-uploader.d.ts +33 -0
  458. package/dist/tool-system/builtin/image-uploader.js +74 -0
  459. package/dist/tool-system/builtin/index.d.ts +24 -1
  460. package/dist/tool-system/builtin/index.js +310 -25
  461. package/dist/tool-system/builtin/mcp-tools.js +23 -3
  462. package/dist/tool-system/builtin/memory.js +45 -7
  463. package/dist/tool-system/builtin/notebook-edit.d.ts +2 -1
  464. package/dist/tool-system/builtin/notebook-edit.js +1 -1
  465. package/dist/tool-system/builtin/powershell.js +4 -1
  466. package/dist/tool-system/builtin/read.d.ts +2 -1
  467. package/dist/tool-system/builtin/read.js +11 -3
  468. package/dist/tool-system/builtin/repl.js +4 -1
  469. package/dist/tool-system/builtin/skill.js +9 -0
  470. package/dist/tool-system/builtin/sleep.js +12 -5
  471. package/dist/tool-system/builtin/task.js +4 -1
  472. package/dist/tool-system/builtin/tool-search.js +25 -7
  473. package/dist/tool-system/builtin/update-automation-memory.d.ts +24 -0
  474. package/dist/tool-system/builtin/update-automation-memory.js +60 -0
  475. package/dist/tool-system/builtin/video-providers.d.ts +154 -0
  476. package/dist/tool-system/builtin/video-providers.js +235 -0
  477. package/dist/tool-system/builtin/view-image.d.ts +18 -0
  478. package/dist/tool-system/builtin/view-image.js +96 -0
  479. package/dist/tool-system/builtin/web-fetch.js +12 -2
  480. package/dist/tool-system/builtin/web-search.d.ts +4 -1
  481. package/dist/tool-system/builtin/web-search.js +57 -10
  482. package/dist/tool-system/builtin/worktree.d.ts +2 -1
  483. package/dist/tool-system/builtin/worktree.js +31 -4
  484. package/dist/tool-system/builtin/write.d.ts +2 -1
  485. package/dist/tool-system/builtin/write.js +1 -1
  486. package/dist/tool-system/context.d.ts +166 -6
  487. package/dist/tool-system/executor.d.ts +3 -2
  488. package/dist/tool-system/executor.js +197 -119
  489. package/dist/tool-system/investigation-guard.d.ts +4 -1
  490. package/dist/tool-system/investigation-guard.js +20 -4
  491. package/dist/tool-system/mcp-manager.d.ts +78 -16
  492. package/dist/tool-system/mcp-manager.js +357 -28
  493. package/dist/tool-system/path-policy.d.ts +99 -0
  494. package/dist/tool-system/path-policy.js +578 -0
  495. package/dist/tool-system/permission.d.ts +36 -1
  496. package/dist/tool-system/permission.js +322 -117
  497. package/dist/tool-system/plan-mode-allowlist.d.ts +32 -0
  498. package/dist/tool-system/plan-mode-allowlist.js +65 -0
  499. package/dist/tool-system/registry.d.ts +1 -0
  500. package/dist/tool-system/registry.js +26 -1
  501. package/dist/tool-system/sandbox/index.d.ts +8 -0
  502. package/dist/tool-system/sandbox/index.js +7 -2
  503. package/dist/tool-system/validate-tool-metadata.d.ts +36 -0
  504. package/dist/tool-system/validate-tool-metadata.js +63 -0
  505. package/dist/tool-system/validation.d.ts +10 -0
  506. package/dist/tool-system/validation.js +16 -3
  507. package/dist/types.d.ts +301 -15
  508. package/dist/updater.js +20 -9
  509. package/dist/utils/envUtils.d.ts +0 -9
  510. package/dist/utils/envUtils.js +3 -28
  511. package/dist/utils/exec.d.ts +40 -0
  512. package/dist/utils/exec.js +144 -0
  513. package/dist/utils/format.js +4 -2
  514. package/dist/utils/json.d.ts +12 -0
  515. package/dist/utils/json.js +92 -0
  516. package/dist/utils/lockfile.js +9 -2
  517. package/dist/utils/theme.d.ts +7 -1
  518. package/dist/utils/theme.js +5 -3
  519. package/dist/utils/toolDisplay.js +0 -1
  520. package/package.json +14 -7
  521. package/dist/agent/coordinator.d.ts +0 -49
  522. package/dist/agent/coordinator.js +0 -77
  523. package/dist/settings/manager.test.js +0 -73
  524. package/dist/tool-system/builtin/remote-trigger.d.ts +0 -6
  525. package/dist/tool-system/builtin/remote-trigger.js +0 -54
  526. package/dist/tool-system/builtin/send-message.d.ts +0 -6
  527. package/dist/tool-system/builtin/send-message.js +0 -47
  528. /package/dist/{settings/manager.test.d.ts → credentials/types.js} +0 -0
@@ -14,14 +14,43 @@
14
14
  * falls back to borrowing any live session's engine for those reads.
15
15
  */
16
16
  import { Methods, ErrorCodes, createResponse, createErrorResponse, createNotification, isRequest, } from "./types.js";
17
+ import { diskDefaultsFrom } from "../engine/engine.js";
18
+ import { isProtectedSettingKey } from "../settings/manager.js";
17
19
  import { setInteractiveApprovalFn } from "../tool-system/permission.js";
18
20
  import { getArenaStatus } from "../tool-system/builtin/arena.js";
19
- import { agentNotificationBus } from "../tool-system/builtin/agent-notifications.js";
21
+ import { agentNotificationBus, notificationQueue, buildNotificationMessage, } from "../tool-system/builtin/agent-notifications.js";
22
+ import { backgroundShellManager } from "../runtime/background-shell.js";
23
+ import { backgroundJobRegistry } from "../tool-system/builtin/background-jobs.js";
24
+ import { listBackgroundWorkForUI } from "../tool-system/builtin/background-work.js";
25
+ import { logger } from "../logging/logger.js";
20
26
  import { nanoid } from "nanoid";
27
+ import { redactLlmConfig, maskSecretValue } from "./redact.js";
28
+ import { redactSecrets } from "../logging/sanitize-messages.js";
21
29
  export class AgentServer {
22
30
  chatManager;
23
31
  legacyEngine;
32
+ globalQueryEngine = null;
24
33
  transport;
34
+ /** Fresh-disk settings reader for config hot-reload; null when not wired. */
35
+ settingsReader;
36
+ /** Disk-only active-goal reader for agent/goalGet on a non-live session. */
37
+ readActiveGoalFromDisk;
38
+ /**
39
+ * Monotonic config-reload version, bumped per reloadSettings request so each
40
+ * Engine.refreshRuntimeConfig can drop out-of-order (stale) deliveries (Q5).
41
+ */
42
+ configVersion = 0;
43
+ /**
44
+ * JSON of the last disk-default patch broadcast to ALL live sessions (#6).
45
+ * When a new reloadSettings request produces a byte-identical patch we SKIP
46
+ * the entire forEachSession broadcast — every session's refreshRuntimeConfig
47
+ * would otherwise unconditionally reloadHooks() (invalidate + full disk
48
+ * re-read + hook teardown/re-register) per call, and the personalization UI
49
+ * auto-saves on a 600ms debounce, so identical re-saves caused K× hook churn
50
+ * per keystroke-pause. A genuine change still differs in JSON → propagates.
51
+ * null until the first broadcast.
52
+ */
53
+ lastBroadcastPatch = null;
25
54
  // ── Legacy single-engine state (used when chatManager is null) ──
26
55
  running = false;
27
56
  abortController = null;
@@ -41,6 +70,8 @@ export class AgentServer {
41
70
  constructor(options) {
42
71
  this.chatManager = options.chatManager ?? null;
43
72
  this.legacyEngine = options.engine ?? null;
73
+ this.settingsReader = options.settingsReader ?? null;
74
+ this.readActiveGoalFromDisk = options.readActiveGoalFromDisk ?? null;
44
75
  if (!this.chatManager && !this.legacyEngine) {
45
76
  throw new Error("AgentServer: either chatManager or engine must be supplied");
46
77
  }
@@ -53,29 +84,138 @@ export class AgentServer {
53
84
  });
54
85
  }
55
86
  });
56
- // Wire approval flow for the legacy single-engine path.
57
- // In the chatManager path approval flows are per-session and registered
58
- // when the session's engine raises an approval request via onStream.
87
+ setInteractiveApprovalFn((request) => {
88
+ return this.requestApprovalFromClient(request);
89
+ });
59
90
  if (this.legacyEngine) {
60
- setInteractiveApprovalFn((request) => {
61
- return this.requestApprovalFromClient(request);
62
- });
63
- this.legacyEngine.setAskUser((question, opts) => {
64
- return this.requestAskUserFromClient(question, opts);
65
- });
91
+ // Only wire an interactive askUser when a human is present. For
92
+ // unattended (headless) runs we leave askUser undefined so
93
+ // AskUserQuestion hits its headless-error branch and returns immediately
94
+ // instead of suspending until the tool-exec timeout (~300s).
95
+ if (!this.legacyEngine.isHeadless()) {
96
+ this.legacyEngine.setAskUser((question, opts) => {
97
+ return this.requestAskUserFromClient(question, opts);
98
+ });
99
+ }
66
100
  }
67
101
  // B2.2 — forward background sub-agent completion events through the
68
102
  // protocol so Desktop / SDK / remote AgentClients see them too. The
69
103
  // bus is fed from `NotificationQueue.enqueue`, so this single
70
- // subscription covers every enqueue site (today: only agent.ts). The
71
- // bus now guarantees a real sessionId (no more legacy-bucket coercion
72
- // to ""), so we forward whatever sid it hands us.
104
+ // subscription covers every enqueue site background sub-agents
105
+ // (agent.ts), background shells (background-shell.ts), and video/job
106
+ // polls (background-jobs.ts). The bus now guarantees a real sessionId
107
+ // (no more legacy-bucket coercion to ""), so we forward whatever sid it
108
+ // hands us.
73
109
  this.bgAgentBusUnsubscribe = agentNotificationBus.subscribe((sessionId, event) => {
74
110
  this.notify(Methods.StreamEvent, { sessionId, event });
111
+ // Background work that finishes while the session is IDLE (a
112
+ // run_in_background Bash like a download, a background sub-agent, or a
113
+ // video poll — the engine no longer parks on any of them) would otherwise
114
+ // leave its completion sitting in the queue until the user manually sends.
115
+ // Wake the session with one run carrying the notification so the model
116
+ // reads "download complete" and continues on its own (the persisted goal
117
+ // is judged that turn). If the work finishes while a run is still in
118
+ // flight, the idle guard below skips it and the run-boundary re-check
119
+ // (trigger B) drains it at end-of-turn instead. A never-exiting dev server
120
+ // emits no completion, so it never wakes anything (no task/service
121
+ // classification needed).
122
+ this.maybeWakeIdleSession(sessionId);
75
123
  });
76
124
  // Notify client we're ready
77
125
  this.notify(Methods.Status, { status: "ready" });
78
126
  }
127
+ /**
128
+ * Wake an IDLE chatManager session that has pending background-completion
129
+ * notifications, by enqueueing one turn whose task is the drained
130
+ * notification(s). The model then sees the completion and continues; the
131
+ * session's persisted goal (if any) is judged on that turn.
132
+ *
133
+ * Guards:
134
+ * - chatManager path only (the legacy single-engine / headless path drives
135
+ * its own loop and has no idle-session-resume concept).
136
+ * - Session must exist AND be idle. If it's busy, we do nothing: the in-flight
137
+ * run's end-of-turn `drainAll` already collects every pending notification,
138
+ * so a second run would be redundant — and `enqueueTurn` while busy would
139
+ * queue a spurious extra turn.
140
+ * - We `drainAll` exactly here and feed the items into the woken turn. This
141
+ * also merges a burst of near-simultaneous completions into one wakeup:
142
+ * the first drains all currently-pending items; subsequent bus events for
143
+ * the same session find it busy (or find an empty queue) and no-op.
144
+ *
145
+ * INVARIANT (the burst-merge correctness depends on it): the merge only
146
+ * holds because (a) the bus fans out to subscribers SYNCHRONOUSLY, and (b)
147
+ * `enqueueTurn` sets the session's `active` (→ isBusy()===true) SYNCHRONOUSLY
148
+ * before its first await. So when bus events #2..N arrive — still on the same
149
+ * synchronous fan-out as #1 — they already see the session as busy and no-op.
150
+ * If either path is ever made async (e.g. an await is added inside
151
+ * enqueueTurn before `active` is set, or the bus starts deferring delivery),
152
+ * this degrades into N concurrent wakeups for N completions. Keep both
153
+ * synchronous, or replace this comment's assumption with an explicit
154
+ * "wakeup in flight" guard flag.
155
+ */
156
+ maybeWakeIdleSession(sessionId) {
157
+ if (!this.chatManager)
158
+ return;
159
+ const session = this.chatManager.get(sessionId);
160
+ if (!session || session.isBusy())
161
+ return;
162
+ // Headless / automation runs are one-shot: the caller takes result.text and
163
+ // is gone, so there's no consumer for a woken continuation turn. Headless
164
+ // already drained its background sub-agents inside engine.run before
165
+ // returning; any remaining queued notification (video/shell) must NOT spin
166
+ // an orphan turn. Only the interactive path auto-continues.
167
+ if (session.engine.isHeadless())
168
+ return;
169
+ // Don't resurrect a session the user just Stopped: cancel() leaves it idle
170
+ // (active=null) so isBusy() reads false, but auto-running a fresh turn here
171
+ // would defeat the Stop. The flag clears the moment the user sends again.
172
+ if (session.wasCancelledSinceLastTurn())
173
+ return;
174
+ const pending = notificationQueue.drainAll(sessionId);
175
+ if (pending.length === 0)
176
+ return;
177
+ const task = `<system-reminder>\n${buildNotificationMessage(pending)}\n</system-reminder>`;
178
+ void session
179
+ .enqueueTurn(task, {
180
+ // Synthetic notification, not the user's own input: persisted with an
181
+ // `injected` flag so a disk rebuild doesn't render it as a phantom user
182
+ // bubble (the live UI shows only the woken assistant's reply).
183
+ injected: true,
184
+ onStream: (event) => this.notify(Methods.StreamEvent, { sessionId, event }),
185
+ })
186
+ .catch((err) => {
187
+ // A wakeup turn failing must not crash the bus fan-out. The drained
188
+ // notifications are already in the transcript via the run's messages;
189
+ // log and move on.
190
+ logger.warn("bg_wakeup.turn_failed", {
191
+ sessionId,
192
+ error: err.message,
193
+ });
194
+ // Belt-and-braces, mirroring the send() path's run().then(clear-busy):
195
+ // the renderer set the composer "working" spinner on this run's
196
+ // session_started, and clears it on turn_complete/error. A failure
197
+ // BEFORE the turn-loop runs (e.g. a setup error) emits neither, which
198
+ // would leave the spinner stuck. Emit a terminal `error` (NOT a
199
+ // turn_complete) so the renderer clears busy AND a woken automation
200
+ // session's runStatus flips to "failed" rather than being mislabeled
201
+ // "completed". If the turn-loop already emitted its own `error`, this is
202
+ // a harmless duplicate (busy already cleared, status already failed).
203
+ this.notify(Methods.StreamEvent, {
204
+ sessionId,
205
+ event: { type: "error", error: err?.message ?? "background wakeup failed" },
206
+ });
207
+ })
208
+ .finally(() => {
209
+ // Run-boundary re-check (trigger B): the woken summarize turn may have
210
+ // spawned NEW background work (e.g. goal "generate 2 videos" → after #1
211
+ // completes, this turn submits #2). When #2 finishes its notification
212
+ // arrives while we're idle again — but if it arrived DURING this turn
213
+ // (busy), trigger A skipped it. Re-checking at the run boundary drains
214
+ // anything that landed while busy, chaining wakeups until the queue is
215
+ // truly empty. Replaces the old engine for(;;) outer loop.
216
+ this.maybeWakeIdleSession(sessionId);
217
+ });
218
+ }
79
219
  // ─── Request Dispatch ───────────────────────────────────────────
80
220
  async handleRequest(req) {
81
221
  switch (req.method) {
@@ -97,9 +237,30 @@ export class AgentServer {
97
237
  case Methods.Inject:
98
238
  this.handleInject(req);
99
239
  break;
240
+ case Methods.Steer:
241
+ this.handleSteer(req);
242
+ break;
243
+ case Methods.Unsteer:
244
+ this.handleUnsteer(req);
245
+ break;
100
246
  case Methods.CloseSession:
101
247
  this.handleCloseSession(req);
102
248
  break;
249
+ case Methods.GoalExtend:
250
+ this.handleGoalExtend(req);
251
+ break;
252
+ case Methods.GoalClear:
253
+ this.handleGoalClear(req);
254
+ break;
255
+ case Methods.GoalGet:
256
+ this.handleGoalGet(req);
257
+ break;
258
+ case Methods.BackgroundShells:
259
+ this.handleBackgroundShells(req);
260
+ break;
261
+ case Methods.BackgroundWork:
262
+ this.handleBackgroundWork(req);
263
+ break;
103
264
  default:
104
265
  this.transport.send(createErrorResponse(req.id, ErrorCodes.MethodNotFound, `Unknown method: ${req.method}`));
105
266
  }
@@ -129,6 +290,7 @@ export class AgentServer {
129
290
  session = cm.getOrCreate(params.sessionId, {
130
291
  permissionMode: params.permissionMode,
131
292
  cwd: params.cwd,
293
+ projectTrusted: params.projectTrusted,
132
294
  });
133
295
  }
134
296
  catch (err) {
@@ -136,13 +298,46 @@ export class AgentServer {
136
298
  this.transport.send(createErrorResponse(req.id, code, err.message));
137
299
  return;
138
300
  }
301
+ // `requireExisting`: reject if the target session isn't on disk rather than
302
+ // running the prompt against a freshly-created blank session. A cron
303
+ // "continue this conversation" job whose session the user deleted must fail
304
+ // loudly here (SessionNotFound) so the scheduler can auto-disable it,
305
+ // instead of silently executing with no transcript/goal/context.
306
+ if (params.requireExisting === true && !session.engine.sessionExistsOnDisk(params.sessionId)) {
307
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionNotFound, `session ${params.sessionId} does not exist`));
308
+ return;
309
+ }
139
310
  if (typeof params.planMode === "boolean") {
140
311
  session.engine.setPlanMode(params.planMode);
141
312
  }
142
313
  const sid = params.sessionId;
314
+ // Wire AskUserQuestion for this interactive session. The chatManager path
315
+ // builds a fresh per-session Engine via engineFactory, which (unlike the
316
+ // legacy single-engine path) never had askUser wired — so AskUserQuestion
317
+ // always fell into its "not available in headless mode" branch in normal
318
+ // chat (and in a resumed automation session). Route it to the client with
319
+ // this session's id so the renderer attributes the question to the right
320
+ // tab, resolving via the session's own pendingApprovals. Skip genuinely
321
+ // headless engines (no human to answer).
322
+ if (!session.engine.isHeadless()) {
323
+ session.engine.setAskUser((question, opts) => this.requestAskUserForSession(session, sid, question, opts));
324
+ // Browser automation bridge: each method routes a browser action to the
325
+ // client (Electron main drives the webview via CDP) over the SAME
326
+ // request/response channel as askUser (pendingApprovals + requestId),
327
+ // reusing its proven resolve/timeout/cleanup. The main-side handler
328
+ // replies with the action's JSON result.
329
+ session.engine.setBrowserBridge(this.makeBrowserBridge(session, sid));
330
+ // Cookie→browser injection (InjectCredential tool): same cross-process
331
+ // channel; main restores the cookie jar into the built-in browser.
332
+ session.engine.setInjectCredential((credentialId) => this.requestCredentialInjectForSession(session, sid, credentialId));
333
+ }
143
334
  try {
144
335
  const result = await session.enqueueTurn(params.task, {
145
336
  cwd: params.cwd,
337
+ goal: typeof params.goal === "string" ||
338
+ (params.goal != null && typeof params.goal === "object")
339
+ ? params.goal
340
+ : undefined,
146
341
  onStream: (event) => this.notify(Methods.StreamEvent, { sessionId: sid, event }),
147
342
  });
148
343
  const runResult = {
@@ -153,9 +348,18 @@ export class AgentServer {
153
348
  usage: result.usage,
154
349
  };
155
350
  this.transport.send(createResponse(req.id, runResult));
351
+ // Run-boundary re-check (trigger B): the session is idle now. If a
352
+ // background task (shell/video) completed DURING this run, its bus event
353
+ // fired while we were busy and trigger A skipped it — drain it now and
354
+ // wake a continuation turn. Interactive path only; headless already
355
+ // drained its sub-agents inside engine.run before returning.
356
+ this.maybeWakeIdleSession(sid);
156
357
  }
157
358
  catch (err) {
158
359
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
360
+ // Even on a failed run the session goes idle — re-check so a background
361
+ // completion that landed during the failed run isn't orphaned.
362
+ this.maybeWakeIdleSession(sid);
159
363
  }
160
364
  }
161
365
  async handleRunLegacy(req) {
@@ -191,12 +395,20 @@ export class AgentServer {
191
395
  };
192
396
  // TodoWrite emits task_update directly through ToolContext.streamCallback;
193
397
  // no module singleton wiring needed anymore.
398
+ // Snapshot the controller for the catch block — `this.abortController`
399
+ // is nulled in the `finally`, so by the time we'd want to check
400
+ // `signal.aborted` below it's already gone.
401
+ const runController = this.abortController;
194
402
  try {
195
403
  const result = await this.legacyEngine.run(params.task, {
196
404
  cwd: params.cwd,
197
405
  sessionId: params.sessionId,
198
- signal: this.abortController.signal,
406
+ signal: runController.signal,
199
407
  onStream: streamToClient,
408
+ goal: typeof params.goal === "string" ||
409
+ (params.goal != null && typeof params.goal === "object")
410
+ ? params.goal
411
+ : undefined,
200
412
  });
201
413
  const runResult = {
202
414
  text: result.text,
@@ -208,7 +420,28 @@ export class AgentServer {
208
420
  this.transport.send(createResponse(req.id, runResult));
209
421
  }
210
422
  catch (err) {
211
- this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
423
+ // ESC / Stop path. If the user cancelled, the engine's
424
+ // in-flight LLM call or Bash spawn rejects with an AbortError
425
+ // that bubbles up here. Don't surface it as InternalError —
426
+ // the user already knows they pressed stop; clients should
427
+ // just clear busy.
428
+ const errName = err.name;
429
+ const aborted = runController.signal.aborted ||
430
+ errName === "AbortError" ||
431
+ errName === "APIUserAbortError";
432
+ if (aborted) {
433
+ const cancelledResult = {
434
+ text: "",
435
+ reason: "aborted_streaming",
436
+ sessionId: params.sessionId ?? "",
437
+ turnCount: 0,
438
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
439
+ };
440
+ this.transport.send(createResponse(req.id, cancelledResult));
441
+ }
442
+ else {
443
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
444
+ }
212
445
  }
213
446
  finally {
214
447
  this.running = false;
@@ -232,6 +465,11 @@ export class AgentServer {
232
465
  return;
233
466
  }
234
467
  s.pendingApprovals.delete(params.requestId);
468
+ // Cancel the pending timeout for THIS request (the ask path armed one).
469
+ // The legacy single-engine branch below already does this; without it
470
+ // here a decided request leaves a live timer that fires at
471
+ // APPROVAL_TIMEOUT_MS and churns a dead map entry.
472
+ this.clearApprovalTimer(params.requestId);
235
473
  resolve(params.decision);
236
474
  this.transport.send(createResponse(req.id, { ok: true }));
237
475
  return;
@@ -262,6 +500,14 @@ export class AgentServer {
262
500
  return;
263
501
  }
264
502
  s.cancel();
503
+ // s.cancel() only aborts the engine controller + drains queued turns. The
504
+ // session's pendingApprovals (askUser / browser_action / tool approvals)
505
+ // are NOT driven by the abort signal — they only settle on a client reply
506
+ // or the 5-minute APPROVAL_TIMEOUT_MS. Left alone, the awaiting tool call
507
+ // hangs until that timeout (delaying run wind-down) and the server-side
508
+ // approvalTimers entry leaks. Resolve them as cancelled now and clear the
509
+ // matching timers, mirroring the legacy path below.
510
+ this.cancelSessionApprovals(s);
265
511
  this.transport.send(createResponse(req.id, { ok: true }));
266
512
  return;
267
513
  }
@@ -278,6 +524,163 @@ export class AgentServer {
278
524
  this.clearAllApprovalTimers();
279
525
  this.transport.send(createResponse(req.id, { ok: true }));
280
526
  }
527
+ /**
528
+ * Extend a running goal's turn/budget ceilings mid-run (TODO 3.1). Routed to
529
+ * a specific session (multi-session path). Returns the resulting effective
530
+ * limits, or an error if no session / no active run.
531
+ */
532
+ handleGoalExtend(req) {
533
+ const params = (req.params ?? {});
534
+ const ext = {
535
+ addTurns: params.addTurns,
536
+ addTokenBudget: params.addTokenBudget,
537
+ addTimeBudgetMs: params.addTimeBudgetMs,
538
+ addStopBlocks: params.addStopBlocks,
539
+ };
540
+ // Multi-session path: route to the named session.
541
+ const session = this.chatManager && typeof params.sessionId === "string"
542
+ ? this.chatManager.get(params.sessionId)
543
+ : undefined;
544
+ // Legacy single-engine path (no chatManager): extend the one engine's run,
545
+ // mirroring handleCancel which also supports both paths. Without this a
546
+ // legacy host can cancel but never extend a goal.
547
+ const result = session
548
+ ? session.extendGoalRun(ext)
549
+ : this.legacyEngine
550
+ ? this.legacyEngine.extendGoalRun(ext)
551
+ : undefined;
552
+ if (result === undefined) {
553
+ // Couldn't even resolve a target (no session and no legacy engine).
554
+ if (!session && !this.legacyEngine) {
555
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, params.sessionId ? `No such session: ${params.sessionId}` : "sessionId is required"));
556
+ return;
557
+ }
558
+ }
559
+ if (!result) {
560
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "No active run to extend"));
561
+ return;
562
+ }
563
+ this.transport.send(createResponse(req.id, { ok: true, limits: result }));
564
+ }
565
+ /**
566
+ * Clear a session's persisted active goal (CC /goal clear). Routes to the
567
+ * named session, falling back to the legacy single engine (mirrors
568
+ * handleGoalExtend / handleCancel's dual path). Returns { ok, cleared }.
569
+ */
570
+ handleGoalClear(req) {
571
+ const params = (req.params ?? {});
572
+ const session = this.chatManager && typeof params.sessionId === "string"
573
+ ? this.chatManager.get(params.sessionId)
574
+ : undefined;
575
+ if (!session && !this.legacyEngine) {
576
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, params.sessionId ? `No such session: ${params.sessionId}` : "sessionId is required"));
577
+ return;
578
+ }
579
+ const cleared = session
580
+ ? session.clearGoal()
581
+ : params.sessionId
582
+ ? (this.legacyEngine.clearGoal(params.sessionId) ?? false)
583
+ : false;
584
+ this.transport.send(createResponse(req.id, { ok: true, cleared }));
585
+ }
586
+ /**
587
+ * Read a session's persisted active goal so the host can re-surface the goal
588
+ * block + Cancel button on session load. A persistent goal lives only in
589
+ * state.activeGoal and is never replayed from the transcript, so a reloaded
590
+ * (or disk-rebuilt) session has no other way to learn it. Prefers a live
591
+ * chatManager session, else falls through to a disk read (readActiveGoalFromDisk
592
+ * in worker mode, or the legacy engine's disk read in single-engine mode) — the
593
+ * bug case is an aborted/reloaded session that is NOT live. Returns { ok, goal } where goal
594
+ * is the objective string, or null when there's no goal / unknown session.
595
+ * Never errors on "no goal" — null is the normal "nothing to show" answer.
596
+ */
597
+ handleGoalGet(req) {
598
+ const params = (req.params ?? {});
599
+ if (typeof params.sessionId !== "string" || !params.sessionId) {
600
+ // Missing/empty param is InvalidParams, not SessionClosed (no session was
601
+ // ever named) — matches handleSteer/handleUnsteer/handleBackgroundShells.
602
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
603
+ return;
604
+ }
605
+ // Prefer the live session; otherwise read straight off disk. The goal we
606
+ // want to recover belongs to a session that is, by definition of this bug,
607
+ // usually NOT currently live: reopening after restart doesn't create a
608
+ // ChatSession (that only happens on a send), so chatManager.get() misses.
609
+ // In worker (chatManager) mode there is no legacyEngine, so the disk read
610
+ // MUST come from readActiveGoalFromDisk — relying on legacyEngine there
611
+ // silently returned null and the UI goal block never re-surfaced.
612
+ const live = this.chatManager ? this.chatManager.get(params.sessionId) : undefined;
613
+ const goal = live
614
+ ? live.getGoal()
615
+ : (this.readActiveGoalFromDisk?.(params.sessionId) ??
616
+ this.legacyEngine?.getGoal(params.sessionId) ??
617
+ undefined);
618
+ this.transport.send(createResponse(req.id, { ok: true, goal: goal ? goal.objective : null }));
619
+ }
620
+ /**
621
+ * Query/control a session's background shells for the desktop UI panel (TODO
622
+ * 3.2). Reads the singleton BackgroundShellManager directly (shells outlive
623
+ * the run that spawned them, so we don't need a live ChatSession). Actions:
624
+ * - (default) list → the session's shells
625
+ * - output → full retained output of one shell
626
+ * - kill → terminate one shell's process group
627
+ * All scoped by sessionId (manager enforces ownership on output/kill).
628
+ */
629
+ handleBackgroundShells(req) {
630
+ const params = (req.params ?? {});
631
+ const sessionId = params.sessionId;
632
+ if (typeof sessionId !== "string" || !sessionId) {
633
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
634
+ return;
635
+ }
636
+ const action = params.action ?? "list";
637
+ if (action === "list") {
638
+ const shells = backgroundShellManager.listForSession(sessionId);
639
+ this.transport.send(createResponse(req.id, { shells }));
640
+ return;
641
+ }
642
+ if (!params.shellId) {
643
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "shellId is required"));
644
+ return;
645
+ }
646
+ if (action === "output") {
647
+ const res = backgroundShellManager.readOutput(params.shellId, "all", sessionId);
648
+ if (!res.ok) {
649
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, res.error));
650
+ return;
651
+ }
652
+ this.transport.send(createResponse(req.id, { header: res.header, text: res.text }));
653
+ return;
654
+ }
655
+ if (action === "kill") {
656
+ void backgroundShellManager.kill(params.shellId, sessionId).then((res) => {
657
+ if (!res.ok) {
658
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, res.error));
659
+ }
660
+ else {
661
+ this.transport.send(createResponse(req.id, { ok: true }));
662
+ }
663
+ });
664
+ return;
665
+ }
666
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, `unknown action: ${action}`));
667
+ }
668
+ /**
669
+ * BackgroundWork — unified, list-only view of a session's background work
670
+ * across all three registries (shells + sub-agents + jobs) for the desktop
671
+ * background panel. Per-shell output/kill still flow through BackgroundShells
672
+ * (by shellId); this just answers "what's running in the background right now".
673
+ */
674
+ handleBackgroundWork(req) {
675
+ const params = (req.params ?? {});
676
+ const sessionId = params.sessionId;
677
+ if (typeof sessionId !== "string" || !sessionId) {
678
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
679
+ return;
680
+ }
681
+ const items = listBackgroundWorkForUI(sessionId);
682
+ this.transport.send(createResponse(req.id, { items }));
683
+ }
281
684
  // ─── CloseSession ───────────────────────────────────────────────
282
685
  handleCloseSession(req) {
283
686
  const params = (req.params ?? {});
@@ -288,6 +691,14 @@ export class AgentServer {
288
691
  if (this.chatManager) {
289
692
  this.chatManager.close(params.sessionId);
290
693
  }
694
+ // Explicit session teardown — reap that session's background shells
695
+ // (design §6 "session 被显式删除 → killSession"). This is the RPC path
696
+ // (agent/closeSession from the host on delete), distinct from the idle
697
+ // sweeper's chatManager.close() which must NOT kill (§6). Fire-and-forget.
698
+ void backgroundShellManager.killSession(params.sessionId);
699
+ // Drop this session's retained background jobs too (#2/#5): finished jobs
700
+ // are kept for the panel, so explicit teardown is where they're released.
701
+ backgroundJobRegistry.dropForSession(params.sessionId);
291
702
  this.transport.send(createResponse(req.id, { ok: true }));
292
703
  }
293
704
  // ─── Configure ──────────────────────────────────────────────────
@@ -306,15 +717,51 @@ export class AgentServer {
306
717
  if (typeof params.permissionMode === "string") {
307
718
  s.engine.setPermissionMode(params.permissionMode);
308
719
  }
720
+ // Per-session model switch — the missing piece that made model changes
721
+ // worker-global (session-isolation research §3). requestModelSwitch
722
+ // applies immediately when idle, defers to the run boundary when busy
723
+ // so it never swaps the model under a running LLM client.
724
+ if (typeof params.model === "string") {
725
+ try {
726
+ s.requestModelSwitch(params.model);
727
+ }
728
+ catch (err) {
729
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, err.message));
730
+ return;
731
+ }
732
+ }
733
+ // Hot-reload disk-default config onto this one session (layer 2).
734
+ if (params.reloadSettings === true) {
735
+ if (!this.settingsReader) {
736
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "reloadSettings is not supported by this AgentServer: no settingsReader is wired"));
737
+ return;
738
+ }
739
+ const settings = this.settingsReader();
740
+ const version = ++this.configVersion;
741
+ s.engine.refreshRuntimeConfig(diskDefaultsFrom(settings, s.engine.getEffectiveDisabledLists().disabledPlugins), version);
742
+ }
309
743
  this.transport.send(createResponse(req.id, { ok: true }));
310
744
  return;
311
745
  }
312
746
  // Global configure — delegate to legacyEngine if available,
313
747
  // or to any session's engine from chatManager for settings ops
314
748
  const engine = this.legacyEngine ?? this.anyEngine();
315
- if (params.reloadModels && engine) {
749
+ if (params.reloadModels) {
316
750
  try {
317
- engine.reloadModelPool();
751
+ const seen = new Set();
752
+ const reload = (target) => {
753
+ if (!target || seen.has(target))
754
+ return;
755
+ seen.add(target);
756
+ target.reloadModelPool();
757
+ };
758
+ reload(this.legacyEngine);
759
+ reload(this.globalQueryEngine);
760
+ if (this.chatManager) {
761
+ this.chatManager.forEachSession((s) => reload(s.engine));
762
+ }
763
+ if (seen.size === 0)
764
+ reload(engine);
318
765
  }
319
766
  catch (err) {
320
767
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
@@ -335,6 +782,48 @@ export class AgentServer {
335
782
  if (params.planMode !== undefined && engine) {
336
783
  engine.setPlanMode(params.planMode);
337
784
  }
785
+ // Config hot-reload layer 2: re-read disk settings once and push the
786
+ // disk-default config patch (+ settings-hook reload + incremental MCP
787
+ // connect) onto every live session. Parallel to reloadModels; no new
788
+ // protocol method. In-flight turns are untouched — refreshRuntimeConfig
789
+ // only mutates this.config, picked up at the next turn boundary.
790
+ if (params.reloadSettings === true) {
791
+ if (!this.settingsReader) {
792
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "reloadSettings is not supported by this AgentServer: no settingsReader is wired"));
793
+ return;
794
+ }
795
+ if (!this.chatManager) {
796
+ // Single-engine host (legacyEngine / anyEngine, no chatManager): there
797
+ // are no sessions to fan out to, but the one engine must still pick up
798
+ // the reload — otherwise we'd report ok:true while silently dropping it.
799
+ // Mirrors how reloadModels/model/planMode above act on `engine`.
800
+ if (engine) {
801
+ const settings = this.settingsReader();
802
+ engine.refreshRuntimeConfig(diskDefaultsFrom(settings, engine.getEffectiveDisabledLists().disabledPlugins), ++this.configVersion);
803
+ }
804
+ this.transport.send(createResponse(req.id, { ok: true }));
805
+ return;
806
+ }
807
+ const settings = this.settingsReader();
808
+ // The MCP merge folds each session's PROJECT capabilityOverrides (a
809
+ // project-level "on" must override the global disabledPlugins), so the
810
+ // patch is cwd-dependent — compute it per session, not once.
811
+ const perSession = [];
812
+ this.chatManager.forEachSession((s) => {
813
+ const patch = diskDefaultsFrom(settings, s.engine.getEffectiveDisabledLists().disabledPlugins);
814
+ perSession.push({ apply: (v) => s.engine.refreshRuntimeConfig(patch, v), patch });
815
+ });
816
+ // #6: content short-circuit — now keyed on ALL per-session patches, so a
817
+ // change visible to only one session (e.g. its project's overrides)
818
+ // still propagates while a true no-op skips the reloadHooks churn.
819
+ const patchJson = JSON.stringify(perSession.map((p) => p.patch));
820
+ if (patchJson !== this.lastBroadcastPatch) {
821
+ this.lastBroadcastPatch = patchJson;
822
+ const version = ++this.configVersion;
823
+ for (const p of perSession)
824
+ p.apply(version);
825
+ }
826
+ }
338
827
  this.transport.send(createResponse(req.id, { ok: true }));
339
828
  }
340
829
  // ─── Query ──────────────────────────────────────────────────────
@@ -361,12 +850,15 @@ export class AgentServer {
361
850
  case "sessions": {
362
851
  if (this.chatManager) {
363
852
  // Return the ChatSessionManager's live sessions
364
- const sessions = [...this.chatManager.sessions.entries()].map(([id, s]) => ({
365
- sessionId: id,
366
- busy: s.isBusy(),
367
- queueDepth: s.queueDepth(),
368
- lastActivityAt: s.lastActivityAt,
369
- }));
853
+ const sessions = [];
854
+ this.chatManager.forEachSession((s) => {
855
+ sessions.push({
856
+ sessionId: s.id,
857
+ busy: s.isBusy(),
858
+ queueDepth: s.queueDepth(),
859
+ lastActivityAt: s.lastActivityAt,
860
+ });
861
+ });
370
862
  this.transport.send(createResponse(req.id, { type: "sessions", data: sessions }));
371
863
  }
372
864
  else if (engine) {
@@ -393,15 +885,14 @@ export class AgentServer {
393
885
  model: config.llm.model,
394
886
  cwd: config.cwd,
395
887
  maxContextTokens: config.maxContextTokens,
396
- llm: {
397
- provider: config.llm.provider,
398
- model: config.llm.model,
399
- apiKey: config.llm.apiKey,
400
- baseUrl: config.llm.baseUrl,
401
- temperature: config.llm.temperature,
402
- maxTokens: config.llm.maxTokens,
403
- enableStreaming: config.llm.enableStreaming,
404
- },
888
+ // apiKey is redacted at this boundary: clients only get
889
+ // hasApiKey + optional apiKeyPreview. See protocol/redact.ts.
890
+ llm: redactLlmConfig(config.llm),
891
+ // Resolved feature flags (defaults merged with settings overlay)
892
+ // so the /features command can list current state.
893
+ featureFlags: engine.getFeatureFlags(),
894
+ // Effective permission rules so /permissions can list them (TODO 5.1).
895
+ permissionRules: engine.getPermissionRules(),
405
896
  },
406
897
  }));
407
898
  break;
@@ -499,7 +990,15 @@ export class AgentServer {
499
990
  cachedAt: cache ? cache.fetchedAt : undefined,
500
991
  };
501
992
  });
502
- this.transport.send(createResponse(req.id, { type: "providers", data: enriched }));
993
+ // Strip apiKey / other secret-shaped fields from each provider
994
+ // before sending. `...p` above spreads the whole provider record,
995
+ // which includes apiKey verbatim — without this redact pass, any
996
+ // protocol client could call query("providers") and harvest
997
+ // credentials. The shared redactor preserves
998
+ // presence-without-value (hasApiKey-style consumers still see the
999
+ // field shape, just with [redacted] in place of the secret).
1000
+ const safe = redactSecrets(enriched);
1001
+ this.transport.send(createResponse(req.id, { type: "providers", data: safe }));
503
1002
  }
504
1003
  catch (err) {
505
1004
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
@@ -520,8 +1019,21 @@ export class AgentServer {
520
1019
  const { key, value } = params;
521
1020
  if (!key)
522
1021
  throw new Error("key is required for config_set");
1022
+ // Refuse writes to trust-root fields via the generic config path. A
1023
+ // protocol peer (external driver, paired phone, compromised renderer)
1024
+ // could otherwise self-authorize tools or inject env/hooks/MCP servers,
1025
+ // which is equivalent to remotely bypassing workspace trust. Legit
1026
+ // writes to these go through the dedicated settings UI, not config_set;
1027
+ // provider/model edits use provider_add / models handlers below.
1028
+ if (isProtectedSettingKey(key)) {
1029
+ throw new Error(`config_set refused: "${key}" is a protected trust/permission field and cannot be written through this path.`);
1030
+ }
523
1031
  engine.updateConfig(key, value);
524
- this.transport.send(createResponse(req.id, { type: "config_set", data: { key, value } }));
1032
+ // Echo back through the same secret-aware masker as config_get so
1033
+ // a `config_set("llm.apiKey", "...")` confirmation doesn't ship
1034
+ // the new secret through the response/log path.
1035
+ const safeValue = maskSecretValue(key, value);
1036
+ this.transport.send(createResponse(req.id, { type: "config_set", data: { key, value: safeValue } }));
525
1037
  }
526
1038
  catch (err) {
527
1039
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
@@ -538,7 +1050,9 @@ export class AgentServer {
538
1050
  if (!key)
539
1051
  throw new Error("key is required for config_get");
540
1052
  const value = engine.readSetting(key);
541
- this.transport.send(createResponse(req.id, { type: "config_get", data: { key, value } }));
1053
+ // Mask secret-looking keys (apiKey/token/secret/…) at the boundary.
1054
+ const safeValue = maskSecretValue(key, value);
1055
+ this.transport.send(createResponse(req.id, { type: "config_get", data: { key, value: safeValue } }));
542
1056
  }
543
1057
  catch (err) {
544
1058
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
@@ -721,6 +1235,55 @@ export class AgentServer {
721
1235
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
722
1236
  }
723
1237
  }
1238
+ // ─── Steer ──────────────────────────────────────────────────────
1239
+ // Queue a user message into the in-flight run's turn loop (不打断). Unlike
1240
+ // Inject (appends an assistant context msg for the NEXT run), Steer feeds a
1241
+ // user message to the CURRENT run's next step. No abort, no LLM trigger by
1242
+ // itself — the running loop picks it up at its next step boundary.
1243
+ handleSteer(req) {
1244
+ const params = (req.params ?? {});
1245
+ if (!params.text || !params.sessionId) {
1246
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "text and sessionId required"));
1247
+ return;
1248
+ }
1249
+ const engine = this.chatManager
1250
+ ? this.chatManager.get(params.sessionId)?.engine
1251
+ : this.legacyEngine;
1252
+ if (!engine) {
1253
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, `No such session: ${params.sessionId}`));
1254
+ return;
1255
+ }
1256
+ try {
1257
+ engine.enqueueSteer(params.sessionId, params.text, params.id);
1258
+ this.transport.send(createResponse(req.id, { ok: true }));
1259
+ }
1260
+ catch (err) {
1261
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
1262
+ }
1263
+ }
1264
+ handleUnsteer(req) {
1265
+ const params = (req.params ?? {});
1266
+ if (!params.id || !params.sessionId) {
1267
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "id and sessionId required"));
1268
+ return;
1269
+ }
1270
+ const engine = this.chatManager
1271
+ ? this.chatManager.get(params.sessionId)?.engine
1272
+ : this.legacyEngine;
1273
+ if (!engine) {
1274
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, `No such session: ${params.sessionId}`));
1275
+ return;
1276
+ }
1277
+ try {
1278
+ // removed=false means the loop already consumed it — not an error, the
1279
+ // host just learns it couldn't be revoked (it will arrive as a bubble).
1280
+ const removed = engine.unsteer(params.sessionId, params.id);
1281
+ this.transport.send(createResponse(req.id, { ok: true, removed }));
1282
+ }
1283
+ catch (err) {
1284
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
1285
+ }
1286
+ }
724
1287
  // ─── Client Communication ───────────────────────────────────────
725
1288
  /**
726
1289
  * Ask the client to approve a tool operation (legacy single-engine path).
@@ -743,6 +1306,179 @@ export class AgentServer {
743
1306
  /**
744
1307
  * Ask the client to answer a question from the agent (legacy single-engine path).
745
1308
  */
1309
+ /**
1310
+ * Per-session AskUserQuestion for the chatManager path. Mirrors
1311
+ * requestAskUserFromClient but resolves via the SESSION's pendingApprovals
1312
+ * (the chatManager approve handler looks there, keyed by sessionId+requestId)
1313
+ * and tags the notify with sessionId so the renderer routes the question to
1314
+ * the right chat tab.
1315
+ */
1316
+ requestAskUserForSession(session, sessionId, question, opts) {
1317
+ return new Promise((resolve) => {
1318
+ const requestId = nanoid(12);
1319
+ session.pendingApprovals.set(requestId, (decision) => {
1320
+ this.clearApprovalTimer(requestId);
1321
+ const result = decision;
1322
+ if (result && typeof result === "object" && "approved" in result) {
1323
+ resolve(result.approved ? (result.answer ?? "") : (result.reason ?? "(user declined to answer)"));
1324
+ }
1325
+ else {
1326
+ // chatManager approve handler resolves with the raw decision value.
1327
+ resolve(typeof decision === "string" ? decision : "");
1328
+ }
1329
+ });
1330
+ const timer = setTimeout(() => {
1331
+ if (session.pendingApprovals.has(requestId)) {
1332
+ session.pendingApprovals.delete(requestId);
1333
+ this.approvalTimers.delete(requestId);
1334
+ resolve("(approval timed out)");
1335
+ }
1336
+ }, AgentServer.APPROVAL_TIMEOUT_MS);
1337
+ this.approvalTimers.set(requestId, timer);
1338
+ const args = { question };
1339
+ if (opts?.header !== undefined)
1340
+ args.header = opts.header;
1341
+ if (opts?.options !== undefined)
1342
+ args.options = opts.options;
1343
+ if (opts?.multiSelect !== undefined)
1344
+ args.multiSelect = opts.multiSelect;
1345
+ if (opts?.optionsOnly !== undefined)
1346
+ args.optionsOnly = opts.optionsOnly;
1347
+ this.notify(Methods.ApprovalRequest, {
1348
+ sessionId,
1349
+ requestId,
1350
+ request: {
1351
+ toolName: "__ask_user__",
1352
+ args,
1353
+ description: question,
1354
+ riskLevel: "low",
1355
+ },
1356
+ });
1357
+ });
1358
+ }
1359
+ /**
1360
+ * Build a BrowserBridge whose every method routes a browser action to the
1361
+ * client over the session's pendingApprovals channel (same as askUser). The
1362
+ * client (Electron main) drives the webview via CDP and replies with a JSON
1363
+ * result string we parse back into the typed result. On timeout / malformed
1364
+ * reply we degrade to a safe error result rather than throwing.
1365
+ */
1366
+ makeBrowserBridge(session, sessionId) {
1367
+ const call = (action, payload) => this.requestBrowserActionForSession(session, sessionId, action, payload);
1368
+ return {
1369
+ snapshot: () => call("snapshot", {}),
1370
+ click: (ref) => call("click", { ref }),
1371
+ type: (ref, text) => call("type", { ref, text }),
1372
+ navigate: (url) => call("navigate", { url }),
1373
+ scroll: (dir, amount) => call("scroll", { dir, amount }),
1374
+ readContent: () => call("readContent", {}),
1375
+ extractLinks: () => call("extractLinks", {}),
1376
+ waitForLoad: (timeoutMs) => call("waitForLoad", { timeoutMs }),
1377
+ hover: (ref) => call("hover", { ref }),
1378
+ selectOption: (ref, value) => call("selectOption", { ref, value }),
1379
+ pressKey: (key, ref) => call("pressKey", { key, ref }),
1380
+ fetchImages: (refs) => call("fetchImages", { refs }),
1381
+ screenshot: (ref) => call("screenshot", { ref }),
1382
+ listTabs: () => call("listTabs", {}),
1383
+ switchTab: (tabId) => call("switchTab", { tabId }),
1384
+ };
1385
+ }
1386
+ requestBrowserActionForSession(session, sessionId, action, payload) {
1387
+ return new Promise((resolve) => {
1388
+ const requestId = nanoid(12);
1389
+ session.pendingApprovals.set(requestId, (decision) => {
1390
+ this.clearApprovalTimer(requestId);
1391
+ // Main replies with { approved:true, answer:<json string> } (reusing the
1392
+ // ApprovalResult shape) or a raw json string. Parse → typed result.
1393
+ let raw;
1394
+ if (decision && typeof decision === "object" && "approved" in decision) {
1395
+ const r = decision;
1396
+ raw = r.approved ? r.answer : undefined;
1397
+ }
1398
+ else if (typeof decision === "string") {
1399
+ raw = decision;
1400
+ }
1401
+ if (raw === undefined) {
1402
+ resolve({ ok: false, detail: "browser action declined or unavailable" });
1403
+ return;
1404
+ }
1405
+ try {
1406
+ resolve(JSON.parse(raw));
1407
+ }
1408
+ catch {
1409
+ resolve({ ok: false, detail: "malformed browser action result" });
1410
+ }
1411
+ });
1412
+ const timer = setTimeout(() => {
1413
+ if (session.pendingApprovals.has(requestId)) {
1414
+ session.pendingApprovals.delete(requestId);
1415
+ this.approvalTimers.delete(requestId);
1416
+ resolve({ ok: false, detail: "browser action timed out" });
1417
+ }
1418
+ }, AgentServer.APPROVAL_TIMEOUT_MS);
1419
+ this.approvalTimers.set(requestId, timer);
1420
+ this.notify(Methods.ApprovalRequest, {
1421
+ sessionId,
1422
+ requestId,
1423
+ request: {
1424
+ toolName: "__browser_action__",
1425
+ args: { action, ...payload },
1426
+ description: `browser:${action}`,
1427
+ riskLevel: "low",
1428
+ },
1429
+ });
1430
+ });
1431
+ }
1432
+ /**
1433
+ * Ask the client (Electron main) to inject a cookie credential into the
1434
+ * built-in browser. Same pendingApprovals channel as browser actions; main
1435
+ * calls restoreCookiesToBrowser and replies with a JSON result string we
1436
+ * parse into { ok, count?, error? }. Degrades to ok:false on timeout/malformed.
1437
+ */
1438
+ requestCredentialInjectForSession(session, sessionId, credentialId) {
1439
+ return new Promise((resolve) => {
1440
+ const requestId = nanoid(12);
1441
+ session.pendingApprovals.set(requestId, (decision) => {
1442
+ this.clearApprovalTimer(requestId);
1443
+ let raw;
1444
+ if (decision && typeof decision === "object" && "approved" in decision) {
1445
+ const r = decision;
1446
+ raw = r.approved ? r.answer : undefined;
1447
+ }
1448
+ else if (typeof decision === "string") {
1449
+ raw = decision;
1450
+ }
1451
+ if (raw === undefined) {
1452
+ resolve({ ok: false, error: "credential inject declined or unavailable" });
1453
+ return;
1454
+ }
1455
+ try {
1456
+ resolve(JSON.parse(raw));
1457
+ }
1458
+ catch {
1459
+ resolve({ ok: false, error: "malformed credential inject result" });
1460
+ }
1461
+ });
1462
+ const timer = setTimeout(() => {
1463
+ if (session.pendingApprovals.has(requestId)) {
1464
+ session.pendingApprovals.delete(requestId);
1465
+ this.approvalTimers.delete(requestId);
1466
+ resolve({ ok: false, error: "credential inject timed out" });
1467
+ }
1468
+ }, AgentServer.APPROVAL_TIMEOUT_MS);
1469
+ this.approvalTimers.set(requestId, timer);
1470
+ this.notify(Methods.ApprovalRequest, {
1471
+ sessionId,
1472
+ requestId,
1473
+ request: {
1474
+ toolName: "__credential_action__",
1475
+ args: { action: "injectCookie", credentialId },
1476
+ description: `credential:inject:${credentialId}`,
1477
+ riskLevel: "low",
1478
+ },
1479
+ });
1480
+ });
1481
+ }
746
1482
  requestAskUserFromClient(question, opts) {
747
1483
  return new Promise((resolve) => {
748
1484
  const requestId = nanoid(12);
@@ -770,6 +1506,8 @@ export class AgentServer {
770
1506
  args.options = opts.options;
771
1507
  if (opts?.multiSelect !== undefined)
772
1508
  args.multiSelect = opts.multiSelect;
1509
+ if (opts?.optionsOnly !== undefined)
1510
+ args.optionsOnly = opts.optionsOnly;
773
1511
  this.notify(Methods.ApprovalRequest, {
774
1512
  requestId,
775
1513
  request: {
@@ -785,16 +1523,26 @@ export class AgentServer {
785
1523
  this.transport.send(createNotification(method, params));
786
1524
  }
787
1525
  /**
788
- * Get any available engine — used for global query ops when chatManager is
789
- * present but no legacyEngine was supplied. Borrows the first live session.
1526
+ * Get any available engine — used for global query/configure ops when
1527
+ * chatManager is present but no legacyEngine was supplied. Prefer a live chat
1528
+ * session; before the first user turn, lazily build a detached engine through
1529
+ * the manager's factory so global UI surfaces (/login, model manager, model
1530
+ * selector) can query models/providers without requiring a chat session first.
790
1531
  */
791
1532
  anyEngine() {
792
1533
  if (!this.chatManager)
793
1534
  return null;
794
- // Access private `sessions` map — acceptable for same-package access.
795
1535
  const sessions = this.chatManager.sessions;
796
1536
  const first = sessions.values().next().value;
797
- return first ? first.engine : null;
1537
+ if (first)
1538
+ return first.engine;
1539
+ if (this.globalQueryEngine)
1540
+ return this.globalQueryEngine;
1541
+ const factory = this.chatManager.factory;
1542
+ if (typeof factory !== "function")
1543
+ return null;
1544
+ this.globalQueryEngine = factory({});
1545
+ return this.globalQueryEngine;
798
1546
  }
799
1547
  // ─── Lifecycle ──────────────────────────────────────────────────
800
1548
  close() {
@@ -838,6 +1586,26 @@ export class AgentServer {
838
1586
  this.approvalTimers.delete(requestId);
839
1587
  }
840
1588
  }
1589
+ /**
1590
+ * Resolve all of a chat session's pending approvals as cancelled and clear
1591
+ * the matching server-side approval timers. Used by handleCancel's
1592
+ * per-session path so a Stop while a tool is awaiting approval doesn't leave
1593
+ * the tool hanging until APPROVAL_TIMEOUT_MS (and leak the timer). Each
1594
+ * requestId in the session map has a same-keyed entry in this.approvalTimers
1595
+ * (see requestAskUserForSession / makeBrowserBridge).
1596
+ */
1597
+ cancelSessionApprovals(session) {
1598
+ for (const [requestId, resolve] of session.pendingApprovals) {
1599
+ this.clearApprovalTimer(requestId);
1600
+ try {
1601
+ resolve({ approved: false, reason: "cancelled" });
1602
+ }
1603
+ catch {
1604
+ /* a resolver must never break cancel cleanup */
1605
+ }
1606
+ }
1607
+ session.pendingApprovals.clear();
1608
+ }
841
1609
  clearAllApprovalTimers() {
842
1610
  for (const timer of this.approvalTimers.values()) {
843
1611
  clearTimeout(timer);