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

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 (396) hide show
  1. package/README.md +13 -10
  2. package/dist/agent/agent-definition-registry.d.ts +3 -0
  3. package/dist/agent/agent-definition-registry.js +3 -1
  4. package/dist/agent/agent-definition.d.ts +15 -0
  5. package/dist/agent/agent-definition.js +27 -3
  6. package/dist/arena/context/context-tools.js +47 -4
  7. package/dist/arena/ledger.js +9 -1
  8. package/dist/arena/strategies/utils.d.ts +1 -4
  9. package/dist/arena/strategies/utils.js +7 -84
  10. package/dist/automation/cron-expr.d.ts +10 -0
  11. package/dist/automation/cron-expr.js +23 -5
  12. package/dist/automation/runner.d.ts +21 -4
  13. package/dist/automation/runner.js +48 -12
  14. package/dist/automation/scheduler.d.ts +38 -0
  15. package/dist/automation/scheduler.js +89 -12
  16. package/dist/automation/store.js +5 -2
  17. package/dist/capability-control/disabled-lists.d.ts +25 -0
  18. package/dist/capability-control/disabled-lists.js +57 -0
  19. package/dist/capability-control/overlay.d.ts +15 -0
  20. package/dist/capability-control/overlay.js +27 -0
  21. package/dist/cc-orchestrator/agent-adapter.d.ts +50 -0
  22. package/dist/cc-orchestrator/agent-adapter.js +133 -0
  23. package/dist/cc-orchestrator/cc-capability.d.ts +19 -0
  24. package/dist/cc-orchestrator/cc-capability.js +53 -0
  25. package/dist/cc-orchestrator/codex-session-discovery.d.ts +24 -0
  26. package/dist/cc-orchestrator/codex-session-discovery.js +191 -0
  27. package/dist/cc-orchestrator/codex-session-history.d.ts +25 -0
  28. package/dist/cc-orchestrator/codex-session-history.js +187 -0
  29. package/dist/cc-orchestrator/external-agent-changes.d.ts +19 -0
  30. package/dist/cc-orchestrator/external-agent-changes.js +214 -0
  31. package/dist/cc-orchestrator/external-agent-driver.d.ts +18 -0
  32. package/dist/cc-orchestrator/external-agent-driver.js +69 -0
  33. package/dist/cc-orchestrator/index.d.ts +8 -0
  34. package/dist/cc-orchestrator/index.js +8 -0
  35. package/dist/cc-orchestrator/relevance-judge.d.ts +15 -0
  36. package/dist/cc-orchestrator/relevance-judge.js +29 -0
  37. package/dist/cc-orchestrator/session-discovery.d.ts +46 -0
  38. package/dist/cc-orchestrator/session-discovery.js +125 -0
  39. package/dist/cc-orchestrator/session-history.d.ts +19 -0
  40. package/dist/cc-orchestrator/session-history.js +67 -0
  41. package/dist/cli/agent-server-stdio.d.ts +15 -1
  42. package/dist/cli/agent-server-stdio.js +115 -17
  43. package/dist/cli/agent-server-tcp.js +40 -26
  44. package/dist/context/compaction.d.ts +86 -0
  45. package/dist/context/compaction.js +279 -0
  46. package/dist/context/manager.d.ts +18 -0
  47. package/dist/context/manager.js +181 -48
  48. package/dist/context/token-counter.js +13 -0
  49. package/dist/cost-tracker.js +5 -61
  50. package/dist/credentials/cipher.d.ts +49 -0
  51. package/dist/credentials/cipher.js +45 -0
  52. package/dist/credentials/cookie-jar.d.ts +24 -0
  53. package/dist/credentials/cookie-jar.js +40 -0
  54. package/dist/credentials/index.d.ts +7 -0
  55. package/dist/credentials/index.js +5 -0
  56. package/dist/credentials/inject-credential-tool.d.ts +20 -0
  57. package/dist/credentials/inject-credential-tool.js +130 -0
  58. package/dist/credentials/store.d.ts +72 -0
  59. package/dist/credentials/store.js +184 -0
  60. package/dist/credentials/types.d.ts +56 -0
  61. package/dist/credentials/use-credential-tool.d.ts +29 -0
  62. package/dist/credentials/use-credential-tool.js +205 -0
  63. package/dist/credentials/use-gate.d.ts +56 -0
  64. package/dist/credentials/use-gate.js +52 -0
  65. package/dist/data/model-metadata.d.ts +77 -0
  66. package/dist/data/model-metadata.js +56 -0
  67. package/dist/data/model-metadata.json +216 -0
  68. package/dist/data/openrouter-models.d.ts +18 -7
  69. package/dist/data/openrouter-models.js +35 -8
  70. package/dist/engine/aux-key.d.ts +10 -0
  71. package/dist/engine/aux-key.js +11 -0
  72. package/dist/engine/dynamic-tool-defs.d.ts +19 -0
  73. package/dist/engine/dynamic-tool-defs.js +36 -0
  74. package/dist/engine/engine.d.ts +280 -141
  75. package/dist/engine/engine.js +1193 -245
  76. package/dist/engine/friendly-error.d.ts +18 -0
  77. package/dist/engine/friendly-error.js +63 -0
  78. package/dist/engine/goal.d.ts +145 -0
  79. package/dist/engine/goal.js +144 -0
  80. package/dist/engine/image-policy.d.ts +13 -0
  81. package/dist/engine/image-policy.js +24 -0
  82. package/dist/engine/model-connections-pool.d.ts +17 -0
  83. package/dist/engine/model-connections-pool.js +67 -0
  84. package/dist/engine/model-facade.d.ts +10 -0
  85. package/dist/engine/model-facade.js +15 -0
  86. package/dist/engine/patch-orphaned-tools.js +3 -0
  87. package/dist/engine/query.js +2 -0
  88. package/dist/engine/resolve-llm-config.d.ts +16 -0
  89. package/dist/engine/resolve-llm-config.js +44 -0
  90. package/dist/engine/runtime.d.ts +2 -0
  91. package/dist/engine/runtime.js +27 -1
  92. package/dist/engine/sandbox-cache-key.d.ts +10 -0
  93. package/dist/engine/sandbox-cache-key.js +9 -0
  94. package/dist/engine/sandbox-config.d.ts +31 -0
  95. package/dist/engine/sandbox-config.js +38 -0
  96. package/dist/engine/session-usage.d.ts +31 -0
  97. package/dist/engine/session-usage.js +81 -0
  98. package/dist/engine/steer-queue.d.ts +33 -0
  99. package/dist/engine/steer-queue.js +26 -0
  100. package/dist/engine/streaming-tool-queue.d.ts +12 -0
  101. package/dist/engine/streaming-tool-queue.js +50 -10
  102. package/dist/engine/turn-loop.d.ts +94 -4
  103. package/dist/engine/turn-loop.js +432 -54
  104. package/dist/engine/types.d.ts +175 -0
  105. package/dist/engine/types.js +13 -0
  106. package/dist/external-agents/config.d.ts +2 -0
  107. package/dist/external-agents/config.js +15 -0
  108. package/dist/external-agents/types.d.ts +31 -0
  109. package/dist/external-agents/types.js +1 -0
  110. package/dist/git/utils.d.ts +12 -0
  111. package/dist/git/utils.js +38 -6
  112. package/dist/git/worktree.d.ts +48 -0
  113. package/dist/git/worktree.js +86 -10
  114. package/dist/hooks/goal-stop-hook.d.ts +28 -0
  115. package/dist/hooks/goal-stop-hook.js +186 -9
  116. package/dist/hooks/registry.d.ts +8 -0
  117. package/dist/hooks/registry.js +19 -0
  118. package/dist/hooks/shell-runner.js +10 -21
  119. package/dist/index.d.ts +51 -7
  120. package/dist/index.js +62 -5
  121. package/dist/llm/capabilities/param-specs.d.ts +14 -0
  122. package/dist/llm/capabilities/param-specs.js +62 -0
  123. package/dist/llm/capabilities/rules.js +5 -1
  124. package/dist/llm/capabilities/types.d.ts +10 -0
  125. package/dist/llm/client-base.d.ts +28 -1
  126. package/dist/llm/client-base.js +119 -13
  127. package/dist/llm/model-cache.js +4 -2
  128. package/dist/llm/model-pool.d.ts +21 -0
  129. package/dist/llm/model-pool.js +21 -1
  130. package/dist/llm/provider-auth.d.ts +41 -0
  131. package/dist/llm/provider-auth.js +76 -0
  132. package/dist/llm/provider-catalog.d.ts +4 -0
  133. package/dist/llm/providers/anthropic.js +64 -14
  134. package/dist/llm/providers/openai.d.ts +36 -0
  135. package/dist/llm/providers/openai.js +257 -51
  136. package/dist/llm/reasoning-setting.d.ts +3 -3
  137. package/dist/llm/reasoning-setting.js +9 -1
  138. package/dist/llm/stream-watchdog.js +5 -1
  139. package/dist/llm/token-counter.js +9 -2
  140. package/dist/llm/types.d.ts +4 -0
  141. package/dist/logging/sanitize-messages.js +22 -0
  142. package/dist/lsp/manager.d.ts +1 -1
  143. package/dist/lsp/manager.js +40 -10
  144. package/dist/model-catalog/builtin.d.ts +12 -0
  145. package/dist/model-catalog/builtin.js +412 -0
  146. package/dist/model-catalog/gen-connections.d.ts +20 -0
  147. package/dist/model-catalog/gen-connections.js +28 -0
  148. package/dist/model-catalog/index.d.ts +41 -0
  149. package/dist/model-catalog/index.js +90 -0
  150. package/dist/model-catalog/params.d.ts +20 -0
  151. package/dist/model-catalog/params.js +45 -0
  152. package/dist/model-catalog/resolve.d.ts +48 -0
  153. package/dist/model-catalog/resolve.js +33 -0
  154. package/dist/model-catalog/save-entry.d.ts +32 -0
  155. package/dist/model-catalog/save-entry.js +104 -0
  156. package/dist/model-catalog/types.d.ts +561 -0
  157. package/dist/model-catalog/types.js +93 -0
  158. package/dist/model-catalog/upsert.d.ts +9 -0
  159. package/dist/model-catalog/upsert.js +8 -0
  160. package/dist/onboarding.d.ts +12 -82
  161. package/dist/onboarding.js +61 -326
  162. package/dist/plugins/gitOps.d.ts +19 -0
  163. package/dist/plugins/gitOps.js +73 -4
  164. package/dist/plugins/installer/checkUpdate.d.ts +16 -0
  165. package/dist/plugins/installer/checkUpdate.js +32 -0
  166. package/dist/plugins/installer/codex/convertCommands.d.ts +19 -0
  167. package/dist/plugins/installer/codex/convertCommands.js +46 -0
  168. package/dist/plugins/installer/codex/convertMcp.d.ts +5 -2
  169. package/dist/plugins/installer/codex/convertMcp.js +45 -5
  170. package/dist/plugins/installer/install.js +25 -1
  171. package/dist/plugins/installer/installFromArchive.d.ts +43 -0
  172. package/dist/plugins/installer/installFromArchive.js +134 -0
  173. package/dist/plugins/installer/installFromSource.js +8 -2
  174. package/dist/plugins/installer/loadPluginAgents.js +6 -2
  175. package/dist/plugins/installer/loadPluginMcp.d.ts +9 -2
  176. package/dist/plugins/installer/loadPluginMcp.js +35 -2
  177. package/dist/plugins/installer/pruneDisabled.d.ts +24 -0
  178. package/dist/plugins/installer/pruneDisabled.js +73 -0
  179. package/dist/plugins/installer/types.d.ts +5 -2
  180. package/dist/plugins/installer/types.js +1 -0
  181. package/dist/plugins/installer/uninstall.js +4 -0
  182. package/dist/plugins/installer/unzip.d.ts +14 -0
  183. package/dist/plugins/installer/unzip.js +82 -0
  184. package/dist/plugins/installer/update.d.ts +14 -0
  185. package/dist/plugins/installer/update.js +53 -21
  186. package/dist/plugins/loadPluginHooks.d.ts +47 -1
  187. package/dist/plugins/loadPluginHooks.js +73 -1
  188. package/dist/plugins/marketplaceManager.d.ts +7 -0
  189. package/dist/plugins/marketplaceManager.js +20 -0
  190. package/dist/plugins/pluginCommandHook.js +4 -18
  191. package/dist/plugins/pluginContent.d.ts +30 -0
  192. package/dist/plugins/pluginContent.js +83 -0
  193. package/dist/plugins/pluginInstaller.d.ts +13 -0
  194. package/dist/plugins/pluginInstaller.js +42 -8
  195. package/dist/plugins/schemas.js +1 -0
  196. package/dist/plugins/types.d.ts +6 -0
  197. package/dist/preset/index.d.ts +11 -1
  198. package/dist/preset/index.js +115 -6
  199. package/dist/product/types.d.ts +1 -1
  200. package/dist/prompt/composer.d.ts +32 -0
  201. package/dist/prompt/composer.js +75 -21
  202. package/dist/prompt/instruction-scanner.js +5 -3
  203. package/dist/prompt/section-loader.js +1 -0
  204. package/dist/prompt/sections/base.md +2 -0
  205. package/dist/prompt/sections/browser.md +10 -0
  206. package/dist/prompt/sections/coding.md +4 -0
  207. package/dist/protocol/chat-session-manager.d.ts +9 -2
  208. package/dist/protocol/chat-session-manager.js +38 -0
  209. package/dist/protocol/chat-session.d.ts +69 -1
  210. package/dist/protocol/chat-session.js +102 -4
  211. package/dist/protocol/client.d.ts +31 -0
  212. package/dist/protocol/client.js +44 -0
  213. package/dist/protocol/server.d.ts +120 -7
  214. package/dist/protocol/server.js +753 -51
  215. package/dist/protocol/transport.js +3 -2
  216. package/dist/protocol/types.d.ts +60 -0
  217. package/dist/protocol/types.js +16 -0
  218. package/dist/quota/credentials.d.ts +3 -0
  219. package/dist/quota/credentials.js +80 -0
  220. package/dist/quota/index.d.ts +36 -0
  221. package/dist/quota/index.js +155 -0
  222. package/dist/quota/types.d.ts +48 -0
  223. package/dist/quota/types.js +13 -0
  224. package/dist/review/review-prompt.d.ts +28 -0
  225. package/dist/review/review-prompt.js +81 -0
  226. package/dist/run/FileRunStore.js +8 -3
  227. package/dist/run/RunApprovalBackend.js +25 -5
  228. package/dist/run/RunManager.d.ts +12 -0
  229. package/dist/run/RunManager.js +35 -0
  230. package/dist/run/factory.d.ts +1 -1
  231. package/dist/runtime/background-shell.d.ts +139 -0
  232. package/dist/runtime/background-shell.js +509 -0
  233. package/dist/runtime/output-clean.d.ts +24 -0
  234. package/dist/runtime/output-clean.js +41 -0
  235. package/dist/runtime/ring-file.d.ts +64 -0
  236. package/dist/runtime/ring-file.js +174 -0
  237. package/dist/runtime/safe-spawn.js +98 -39
  238. package/dist/runtime/spawn-common.d.ts +159 -0
  239. package/dist/runtime/spawn-common.js +404 -0
  240. package/dist/runtime/truncate-output.d.ts +22 -0
  241. package/dist/runtime/truncate-output.js +49 -0
  242. package/dist/runtime/utf8-cut.d.ts +11 -0
  243. package/dist/runtime/utf8-cut.js +23 -0
  244. package/dist/services/auto-dream.d.ts +4 -0
  245. package/dist/services/auto-dream.js +26 -26
  246. package/dist/services/diagnostics.d.ts +1 -2
  247. package/dist/services/diagnostics.js +12 -7
  248. package/dist/services/extract-memories.d.ts +14 -1
  249. package/dist/services/extract-memories.js +45 -6
  250. package/dist/services/memory-orchestrator.d.ts +21 -0
  251. package/dist/services/memory-orchestrator.js +125 -50
  252. package/dist/services/session-memory.js +24 -12
  253. package/dist/session/file-history.d.ts +124 -1
  254. package/dist/session/file-history.js +222 -6
  255. package/dist/session/memory.d.ts +116 -2
  256. package/dist/session/memory.js +250 -28
  257. package/dist/session/session-manager.d.ts +40 -0
  258. package/dist/session/session-manager.js +119 -3
  259. package/dist/session/simple-diff.d.ts +23 -0
  260. package/dist/session/simple-diff.js +84 -0
  261. package/dist/session/transcript.d.ts +29 -1
  262. package/dist/session/transcript.js +56 -2
  263. package/dist/session/undo-target.d.ts +67 -0
  264. package/dist/session/undo-target.js +144 -0
  265. package/dist/settings/disk-defaults.d.ts +9 -2
  266. package/dist/settings/disk-defaults.js +11 -2
  267. package/dist/settings/feature-flags.d.ts +64 -0
  268. package/dist/settings/feature-flags.js +61 -0
  269. package/dist/settings/manager.d.ts +68 -1
  270. package/dist/settings/manager.js +266 -29
  271. package/dist/settings/migrate-config.d.ts +45 -0
  272. package/dist/settings/migrate-config.js +125 -0
  273. package/dist/settings/schema-export.d.ts +25 -0
  274. package/dist/settings/schema-export.js +38 -0
  275. package/dist/settings/schema.d.ts +1186 -771
  276. package/dist/settings/schema.js +268 -97
  277. package/dist/skills/scanner.d.ts +9 -0
  278. package/dist/skills/scanner.js +30 -2
  279. package/dist/stt/resolve-transcribe.d.ts +31 -0
  280. package/dist/stt/resolve-transcribe.js +108 -0
  281. package/dist/stt/transcribe.d.ts +51 -0
  282. package/dist/stt/transcribe.js +65 -0
  283. package/dist/tool-system/browser-bridge.d.ts +226 -0
  284. package/dist/tool-system/browser-bridge.js +163 -0
  285. package/dist/tool-system/builtin/agent-heartbeat.d.ts +49 -0
  286. package/dist/tool-system/builtin/agent-heartbeat.js +89 -0
  287. package/dist/tool-system/builtin/agent-notifications.d.ts +12 -3
  288. package/dist/tool-system/builtin/agent-notifications.js +9 -3
  289. package/dist/tool-system/builtin/agent-output-file.d.ts +38 -0
  290. package/dist/tool-system/builtin/agent-output-file.js +72 -0
  291. package/dist/tool-system/builtin/agent-registry.d.ts +12 -0
  292. package/dist/tool-system/builtin/agent-registry.js +8 -0
  293. package/dist/tool-system/builtin/agent.d.ts +21 -1
  294. package/dist/tool-system/builtin/agent.js +489 -42
  295. package/dist/tool-system/builtin/apply-patch/applier.js +66 -8
  296. package/dist/tool-system/builtin/apply-patch/backup-targets.d.ts +10 -0
  297. package/dist/tool-system/builtin/apply-patch/backup-targets.js +30 -0
  298. package/dist/tool-system/builtin/apply-patch/index.js +0 -15
  299. package/dist/tool-system/builtin/background-jobs.d.ts +76 -0
  300. package/dist/tool-system/builtin/background-jobs.js +124 -0
  301. package/dist/tool-system/builtin/background-shell-tools.d.ts +20 -0
  302. package/dist/tool-system/builtin/background-shell-tools.js +108 -0
  303. package/dist/tool-system/builtin/background-work.d.ts +67 -0
  304. package/dist/tool-system/builtin/background-work.js +86 -0
  305. package/dist/tool-system/builtin/bash-output-style.d.ts +32 -0
  306. package/dist/tool-system/builtin/bash-output-style.js +40 -0
  307. package/dist/tool-system/builtin/bash.d.ts +5 -2
  308. package/dist/tool-system/builtin/bash.js +101 -64
  309. package/dist/tool-system/builtin/browser-tools.d.ts +33 -0
  310. package/dist/tool-system/builtin/browser-tools.js +312 -0
  311. package/dist/tool-system/builtin/cancel-goal.d.ts +31 -0
  312. package/dist/tool-system/builtin/cancel-goal.js +64 -0
  313. package/dist/tool-system/builtin/check-quota.d.ts +15 -0
  314. package/dist/tool-system/builtin/check-quota.js +34 -0
  315. package/dist/tool-system/builtin/config.js +7 -0
  316. package/dist/tool-system/builtin/cron.d.ts +7 -0
  317. package/dist/tool-system/builtin/cron.js +64 -4
  318. package/dist/tool-system/builtin/drive-claude-code.d.ts +30 -0
  319. package/dist/tool-system/builtin/drive-claude-code.js +157 -0
  320. package/dist/tool-system/builtin/edit-model-catalog.d.ts +3 -0
  321. package/dist/tool-system/builtin/edit-model-catalog.js +104 -0
  322. package/dist/tool-system/builtin/edit.js +20 -16
  323. package/dist/tool-system/builtin/eol.d.ts +29 -0
  324. package/dist/tool-system/builtin/eol.js +37 -0
  325. package/dist/tool-system/builtin/file-cache.d.ts +6 -0
  326. package/dist/tool-system/builtin/file-cache.js +8 -0
  327. package/dist/tool-system/builtin/generate-image.d.ts +35 -0
  328. package/dist/tool-system/builtin/generate-image.js +278 -50
  329. package/dist/tool-system/builtin/generate-video.d.ts +55 -0
  330. package/dist/tool-system/builtin/generate-video.js +364 -0
  331. package/dist/tool-system/builtin/glob.js +0 -7
  332. package/dist/tool-system/builtin/grep.d.ts +9 -0
  333. package/dist/tool-system/builtin/grep.js +106 -11
  334. package/dist/tool-system/builtin/image-providers.d.ts +86 -0
  335. package/dist/tool-system/builtin/image-providers.js +190 -0
  336. package/dist/tool-system/builtin/image-uploader.d.ts +33 -0
  337. package/dist/tool-system/builtin/image-uploader.js +74 -0
  338. package/dist/tool-system/builtin/index.d.ts +10 -2
  339. package/dist/tool-system/builtin/index.js +251 -27
  340. package/dist/tool-system/builtin/mcp-tools.js +23 -3
  341. package/dist/tool-system/builtin/memory.js +45 -7
  342. package/dist/tool-system/builtin/notebook-edit.js +0 -7
  343. package/dist/tool-system/builtin/powershell.js +8 -2
  344. package/dist/tool-system/builtin/read.js +10 -10
  345. package/dist/tool-system/builtin/repl.js +4 -1
  346. package/dist/tool-system/builtin/skill.js +9 -0
  347. package/dist/tool-system/builtin/sleep.js +8 -2
  348. package/dist/tool-system/builtin/tool-search.js +25 -7
  349. package/dist/tool-system/builtin/video-providers.d.ts +154 -0
  350. package/dist/tool-system/builtin/video-providers.js +235 -0
  351. package/dist/tool-system/builtin/web-fetch.js +12 -2
  352. package/dist/tool-system/builtin/web-search.js +21 -7
  353. package/dist/tool-system/builtin/worktree.d.ts +2 -1
  354. package/dist/tool-system/builtin/worktree.js +31 -4
  355. package/dist/tool-system/builtin/write.js +0 -6
  356. package/dist/tool-system/context.d.ts +164 -6
  357. package/dist/tool-system/executor.d.ts +3 -1
  358. package/dist/tool-system/executor.js +197 -90
  359. package/dist/tool-system/investigation-guard.js +1 -1
  360. package/dist/tool-system/mcp-manager.d.ts +42 -4
  361. package/dist/tool-system/mcp-manager.js +203 -20
  362. package/dist/tool-system/mcp-stdio-diagnostics.d.ts +9 -0
  363. package/dist/tool-system/mcp-stdio-diagnostics.js +93 -0
  364. package/dist/tool-system/path-policy.d.ts +5 -0
  365. package/dist/tool-system/path-policy.js +307 -8
  366. package/dist/tool-system/permission.d.ts +39 -2
  367. package/dist/tool-system/permission.js +283 -70
  368. package/dist/tool-system/plan-mode-allowlist.d.ts +13 -2
  369. package/dist/tool-system/plan-mode-allowlist.js +24 -2
  370. package/dist/tool-system/registry.d.ts +1 -0
  371. package/dist/tool-system/registry.js +16 -2
  372. package/dist/tool-system/sandbox/index.d.ts +8 -0
  373. package/dist/tool-system/sandbox/index.js +7 -2
  374. package/dist/tool-system/sandbox/off.js +7 -1
  375. package/dist/tool-system/validate-tool-metadata.d.ts +36 -0
  376. package/dist/tool-system/validate-tool-metadata.js +63 -0
  377. package/dist/types.d.ts +252 -7
  378. package/dist/updater.js +20 -9
  379. package/dist/utils/envUtils.d.ts +0 -9
  380. package/dist/utils/envUtils.js +3 -28
  381. package/dist/utils/exec.d.ts +48 -0
  382. package/dist/utils/exec.js +154 -0
  383. package/dist/utils/json.d.ts +12 -0
  384. package/dist/utils/json.js +92 -0
  385. package/dist/utils/theme.d.ts +1 -1
  386. package/dist/utils/theme.js +1 -1
  387. package/dist/utils/toolDisplay.js +0 -1
  388. package/package.json +13 -7
  389. package/dist/agent/coordinator.d.ts +0 -49
  390. package/dist/agent/coordinator.js +0 -77
  391. package/dist/settings/manager.test.js +0 -73
  392. package/dist/tool-system/builtin/remote-trigger.d.ts +0 -6
  393. package/dist/tool-system/builtin/remote-trigger.js +0 -54
  394. package/dist/tool-system/builtin/send-message.d.ts +0 -6
  395. package/dist/tool-system/builtin/send-message.js +0 -47
  396. /package/dist/{settings/manager.test.d.ts → credentials/types.js} +0 -0
@@ -15,12 +15,30 @@
15
15
  */
16
16
  import { Methods, ErrorCodes, createResponse, createErrorResponse, createNotification, isRequest, } from "./types.js";
17
17
  import { diskDefaultsFrom } from "../engine/engine.js";
18
+ import { isProtectedSettingKey } from "../settings/manager.js";
18
19
  import { setInteractiveApprovalFn } from "../tool-system/permission.js";
19
20
  import { getArenaStatus } from "../tool-system/builtin/arena.js";
20
- 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";
21
26
  import { nanoid } from "nanoid";
22
27
  import { redactLlmConfig, maskSecretValue } from "./redact.js";
23
28
  import { redactSecrets } from "../logging/sanitize-messages.js";
29
+ const COMPACT_STREAM_STRATEGIES = new Set([
30
+ "micro",
31
+ "summary",
32
+ "window",
33
+ "snip",
34
+ "emergency",
35
+ "compacted",
36
+ ]);
37
+ function toCompactStreamStrategy(strategy) {
38
+ return COMPACT_STREAM_STRATEGIES.has(strategy)
39
+ ? strategy
40
+ : "compacted";
41
+ }
24
42
  export class AgentServer {
25
43
  chatManager;
26
44
  legacyEngine;
@@ -28,6 +46,8 @@ export class AgentServer {
28
46
  transport;
29
47
  /** Fresh-disk settings reader for config hot-reload; null when not wired. */
30
48
  settingsReader;
49
+ /** Disk-only active-goal reader for agent/goalGet on a non-live session. */
50
+ readActiveGoalFromDisk;
31
51
  /**
32
52
  * Monotonic config-reload version, bumped per reloadSettings request so each
33
53
  * Engine.refreshRuntimeConfig can drop out-of-order (stale) deliveries (Q5).
@@ -64,6 +84,7 @@ export class AgentServer {
64
84
  this.chatManager = options.chatManager ?? null;
65
85
  this.legacyEngine = options.engine ?? null;
66
86
  this.settingsReader = options.settingsReader ?? null;
87
+ this.readActiveGoalFromDisk = options.readActiveGoalFromDisk ?? null;
67
88
  if (!this.chatManager && !this.legacyEngine) {
68
89
  throw new Error("AgentServer: either chatManager or engine must be supplied");
69
90
  }
@@ -93,15 +114,121 @@ export class AgentServer {
93
114
  // B2.2 — forward background sub-agent completion events through the
94
115
  // protocol so Desktop / SDK / remote AgentClients see them too. The
95
116
  // bus is fed from `NotificationQueue.enqueue`, so this single
96
- // subscription covers every enqueue site (today: only agent.ts). The
97
- // bus now guarantees a real sessionId (no more legacy-bucket coercion
98
- // to ""), so we forward whatever sid it hands us.
117
+ // subscription covers every enqueue site background sub-agents
118
+ // (agent.ts), background shells (background-shell.ts), and video/job
119
+ // polls (background-jobs.ts). The bus now guarantees a real sessionId
120
+ // (no more legacy-bucket coercion to ""), so we forward whatever sid it
121
+ // hands us.
99
122
  this.bgAgentBusUnsubscribe = agentNotificationBus.subscribe((sessionId, event) => {
100
123
  this.notify(Methods.StreamEvent, { sessionId, event });
124
+ // Background work that finishes while the session is IDLE (a
125
+ // run_in_background Bash like a download, a background sub-agent, or a
126
+ // video poll — the engine no longer parks on any of them) would otherwise
127
+ // leave its completion sitting in the queue until the user manually sends.
128
+ // Wake the session with one run carrying the notification so the model
129
+ // reads "download complete" and continues on its own (the persisted goal
130
+ // is judged that turn). If the work finishes while a run is still in
131
+ // flight, the idle guard below skips it and the run-boundary re-check
132
+ // (trigger B) drains it at end-of-turn instead. A never-exiting dev server
133
+ // emits no completion, so it never wakes anything (no task/service
134
+ // classification needed).
135
+ this.maybeWakeIdleSession(sessionId);
101
136
  });
102
137
  // Notify client we're ready
103
138
  this.notify(Methods.Status, { status: "ready" });
104
139
  }
140
+ /**
141
+ * Wake an IDLE chatManager session that has pending background-completion
142
+ * notifications, by enqueueing one turn whose task is the drained
143
+ * notification(s). The model then sees the completion and continues; the
144
+ * session's persisted goal (if any) is judged on that turn.
145
+ *
146
+ * Guards:
147
+ * - chatManager path only (the legacy single-engine / headless path drives
148
+ * its own loop and has no idle-session-resume concept).
149
+ * - Session must exist AND be idle. If it's busy, we do nothing: the in-flight
150
+ * run's end-of-turn `drainAll` already collects every pending notification,
151
+ * so a second run would be redundant — and `enqueueTurn` while busy would
152
+ * queue a spurious extra turn.
153
+ * - We `drainAll` exactly here and feed the items into the woken turn. This
154
+ * also merges a burst of near-simultaneous completions into one wakeup:
155
+ * the first drains all currently-pending items; subsequent bus events for
156
+ * the same session find it busy (or find an empty queue) and no-op.
157
+ *
158
+ * INVARIANT (the burst-merge correctness depends on it): the merge only
159
+ * holds because (a) the bus fans out to subscribers SYNCHRONOUSLY, and (b)
160
+ * `enqueueTurn` sets the session's `active` (→ isBusy()===true) SYNCHRONOUSLY
161
+ * before its first await. So when bus events #2..N arrive — still on the same
162
+ * synchronous fan-out as #1 — they already see the session as busy and no-op.
163
+ * If either path is ever made async (e.g. an await is added inside
164
+ * enqueueTurn before `active` is set, or the bus starts deferring delivery),
165
+ * this degrades into N concurrent wakeups for N completions. Keep both
166
+ * synchronous, or replace this comment's assumption with an explicit
167
+ * "wakeup in flight" guard flag.
168
+ */
169
+ maybeWakeIdleSession(sessionId) {
170
+ if (!this.chatManager)
171
+ return;
172
+ const session = this.chatManager.get(sessionId);
173
+ if (!session || session.isBusy())
174
+ return;
175
+ // Headless / automation runs are one-shot: the caller takes result.text and
176
+ // is gone, so there's no consumer for a woken continuation turn. Headless
177
+ // already drained its background sub-agents inside engine.run before
178
+ // returning; any remaining queued notification (video/shell) must NOT spin
179
+ // an orphan turn. Only the interactive path auto-continues.
180
+ if (session.engine.isHeadless())
181
+ return;
182
+ // Don't resurrect a session the user just Stopped: cancel() leaves it idle
183
+ // (active=null) so isBusy() reads false, but auto-running a fresh turn here
184
+ // would defeat the Stop. The flag clears the moment the user sends again.
185
+ if (session.wasCancelledSinceLastTurn())
186
+ return;
187
+ const pending = notificationQueue.drainAll(sessionId);
188
+ if (pending.length === 0)
189
+ return;
190
+ const task = `<system-reminder>\n${buildNotificationMessage(pending)}\n</system-reminder>`;
191
+ void session
192
+ .enqueueTurn(task, {
193
+ // Synthetic notification, not the user's own input: persisted with an
194
+ // `injected` flag so a disk rebuild doesn't render it as a phantom user
195
+ // bubble (the live UI shows only the woken assistant's reply).
196
+ injected: true,
197
+ onStream: (event) => this.notify(Methods.StreamEvent, { sessionId, event }),
198
+ })
199
+ .catch((err) => {
200
+ // A wakeup turn failing must not crash the bus fan-out. The drained
201
+ // notifications are already in the transcript via the run's messages;
202
+ // log and move on.
203
+ logger.warn("bg_wakeup.turn_failed", {
204
+ sessionId,
205
+ error: err.message,
206
+ });
207
+ // Belt-and-braces, mirroring the send() path's run().then(clear-busy):
208
+ // the renderer set the composer "working" spinner on this run's
209
+ // session_started, and clears it on turn_complete/error. A failure
210
+ // BEFORE the turn-loop runs (e.g. a setup error) emits neither, which
211
+ // would leave the spinner stuck. Emit a terminal `error` (NOT a
212
+ // turn_complete) so the renderer clears busy AND a woken automation
213
+ // session's runStatus flips to "failed" rather than being mislabeled
214
+ // "completed". If the turn-loop already emitted its own `error`, this is
215
+ // a harmless duplicate (busy already cleared, status already failed).
216
+ this.notify(Methods.StreamEvent, {
217
+ sessionId,
218
+ event: { type: "error", error: err?.message ?? "background wakeup failed" },
219
+ });
220
+ })
221
+ .finally(() => {
222
+ // Run-boundary re-check (trigger B): the woken summarize turn may have
223
+ // spawned NEW background work (e.g. goal "generate 2 videos" → after #1
224
+ // completes, this turn submits #2). When #2 finishes its notification
225
+ // arrives while we're idle again — but if it arrived DURING this turn
226
+ // (busy), trigger A skipped it. Re-checking at the run boundary drains
227
+ // anything that landed while busy, chaining wakeups until the queue is
228
+ // truly empty. Replaces the old engine for(;;) outer loop.
229
+ this.maybeWakeIdleSession(sessionId);
230
+ });
231
+ }
105
232
  // ─── Request Dispatch ───────────────────────────────────────────
106
233
  async handleRequest(req) {
107
234
  switch (req.method) {
@@ -123,9 +250,30 @@ export class AgentServer {
123
250
  case Methods.Inject:
124
251
  this.handleInject(req);
125
252
  break;
253
+ case Methods.Steer:
254
+ this.handleSteer(req);
255
+ break;
256
+ case Methods.Unsteer:
257
+ this.handleUnsteer(req);
258
+ break;
126
259
  case Methods.CloseSession:
127
260
  this.handleCloseSession(req);
128
261
  break;
262
+ case Methods.GoalExtend:
263
+ this.handleGoalExtend(req);
264
+ break;
265
+ case Methods.GoalClear:
266
+ this.handleGoalClear(req);
267
+ break;
268
+ case Methods.GoalGet:
269
+ this.handleGoalGet(req);
270
+ break;
271
+ case Methods.BackgroundShells:
272
+ this.handleBackgroundShells(req);
273
+ break;
274
+ case Methods.BackgroundWork:
275
+ this.handleBackgroundWork(req);
276
+ break;
129
277
  default:
130
278
  this.transport.send(createErrorResponse(req.id, ErrorCodes.MethodNotFound, `Unknown method: ${req.method}`));
131
279
  }
@@ -155,6 +303,7 @@ export class AgentServer {
155
303
  session = cm.getOrCreate(params.sessionId, {
156
304
  permissionMode: params.permissionMode,
157
305
  cwd: params.cwd,
306
+ projectTrusted: params.projectTrusted,
158
307
  });
159
308
  }
160
309
  catch (err) {
@@ -162,6 +311,28 @@ export class AgentServer {
162
311
  this.transport.send(createErrorResponse(req.id, code, err.message));
163
312
  return;
164
313
  }
314
+ // `requireExisting`: reject if the target session isn't on disk rather than
315
+ // running the prompt against a freshly-created blank session. A cron
316
+ // "continue this conversation" job whose session the user deleted must fail
317
+ // loudly here (SessionNotFound) so the scheduler can auto-disable it,
318
+ // instead of silently executing with no transcript/goal/context.
319
+ if (params.requireExisting === true && !session.engine.sessionExistsOnDisk(params.sessionId)) {
320
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionNotFound, `session ${params.sessionId} does not exist`));
321
+ return;
322
+ }
323
+ if (params.model !== undefined) {
324
+ if (typeof params.model !== "string" || params.model.length === 0) {
325
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "model must be a non-empty string"));
326
+ return;
327
+ }
328
+ try {
329
+ session.requestModelSwitch(params.model);
330
+ }
331
+ catch (err) {
332
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, err.message));
333
+ return;
334
+ }
335
+ }
165
336
  if (typeof params.planMode === "boolean") {
166
337
  session.engine.setPlanMode(params.planMode);
167
338
  }
@@ -176,6 +347,15 @@ export class AgentServer {
176
347
  // headless engines (no human to answer).
177
348
  if (!session.engine.isHeadless()) {
178
349
  session.engine.setAskUser((question, opts) => this.requestAskUserForSession(session, sid, question, opts));
350
+ // Browser automation bridge: each method routes a browser action to the
351
+ // client (Electron main drives the webview via CDP) over the SAME
352
+ // request/response channel as askUser (pendingApprovals + requestId).
353
+ // Browser actions still keep their own bounded timeout; AskUserQuestion
354
+ // waits for a real answer or Stop/cancel.
355
+ session.engine.setBrowserBridge(this.makeBrowserBridge(session, sid));
356
+ // Cookie→browser injection (InjectCredential tool): same cross-process
357
+ // channel; main restores the cookie jar into the built-in browser.
358
+ session.engine.setInjectCredential((credentialId) => this.requestCredentialInjectForSession(session, sid, credentialId));
179
359
  }
180
360
  try {
181
361
  const result = await session.enqueueTurn(params.task, {
@@ -185,6 +365,7 @@ export class AgentServer {
185
365
  ? params.goal
186
366
  : undefined,
187
367
  onStream: (event) => this.notify(Methods.StreamEvent, { sessionId: sid, event }),
368
+ clientMessageId: typeof params.clientMessageId === "string" ? params.clientMessageId : undefined,
188
369
  });
189
370
  const runResult = {
190
371
  text: result.text,
@@ -194,9 +375,18 @@ export class AgentServer {
194
375
  usage: result.usage,
195
376
  };
196
377
  this.transport.send(createResponse(req.id, runResult));
378
+ // Run-boundary re-check (trigger B): the session is idle now. If a
379
+ // background task (shell/video) completed DURING this run, its bus event
380
+ // fired while we were busy and trigger A skipped it — drain it now and
381
+ // wake a continuation turn. Interactive path only; headless already
382
+ // drained its sub-agents inside engine.run before returning.
383
+ this.maybeWakeIdleSession(sid);
197
384
  }
198
385
  catch (err) {
199
386
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
387
+ // Even on a failed run the session goes idle — re-check so a background
388
+ // completion that landed during the failed run isn't orphaned.
389
+ this.maybeWakeIdleSession(sid);
200
390
  }
201
391
  }
202
392
  async handleRunLegacy(req) {
@@ -224,6 +414,19 @@ export class AgentServer {
224
414
  // Engine.setPermissionMode now keeps this.permissionMode + this.planMode
225
415
  // in sync and tools read them via ToolContext.permissionMode/planMode.
226
416
  }
417
+ if (params.model !== undefined) {
418
+ if (typeof params.model !== "string" || params.model.length === 0) {
419
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "model must be a non-empty string"));
420
+ return;
421
+ }
422
+ try {
423
+ this.legacyEngine.switchModel(params.model);
424
+ }
425
+ catch (err) {
426
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, err.message));
427
+ return;
428
+ }
429
+ }
227
430
  this.running = true;
228
431
  this.abortController = new AbortController();
229
432
  this.notify(Methods.Status, { status: "running" });
@@ -242,6 +445,7 @@ export class AgentServer {
242
445
  sessionId: params.sessionId,
243
446
  signal: runController.signal,
244
447
  onStream: streamToClient,
448
+ clientMessageId: typeof params.clientMessageId === "string" ? params.clientMessageId : undefined,
245
449
  goal: typeof params.goal === "string" ||
246
450
  (params.goal != null && typeof params.goal === "object")
247
451
  ? params.goal
@@ -289,7 +493,10 @@ export class AgentServer {
289
493
  // ─── Approve ────────────────────────────────────────────────────
290
494
  handleApprove(req) {
291
495
  const params = (req.params ?? {});
292
- // ChatSessionManager path: look up per-session pendingApprovals
496
+ // ChatSessionManager path: approvals are scoped by (sessionId, requestId).
497
+ // Never fall back from a session-tagged response to the legacy global map:
498
+ // a stale/misrouted UI response must fail closed instead of resolving a
499
+ // pending prompt from another session.
293
500
  if (this.chatManager && typeof params.sessionId === "string") {
294
501
  const s = this.chatManager.get(params.sessionId);
295
502
  if (!s) {
@@ -298,10 +505,14 @@ export class AgentServer {
298
505
  }
299
506
  const resolve = s.pendingApprovals.get(params.requestId);
300
507
  if (!resolve) {
301
- this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, `No pending approval: ${params.requestId}`));
508
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, `No pending approval for session ${params.sessionId}: ${params.requestId}`));
302
509
  return;
303
510
  }
304
511
  s.pendingApprovals.delete(params.requestId);
512
+ // Cancel the pending timeout for requests that arm one (browser actions,
513
+ // credential injection, and tool approvals). AskUserQuestion does not use
514
+ // a timeout; this is harmless for those request ids.
515
+ this.clearApprovalTimer(params.requestId);
305
516
  resolve(params.decision);
306
517
  this.transport.send(createResponse(req.id, { ok: true }));
307
518
  return;
@@ -332,6 +543,13 @@ export class AgentServer {
332
543
  return;
333
544
  }
334
545
  s.cancel();
546
+ // s.cancel() only aborts the engine controller + drains queued turns. The
547
+ // session's pendingApprovals (askUser / browser_action / tool approvals)
548
+ // are NOT driven directly by the abort signal. Left alone, an awaiting
549
+ // AskUserQuestion would now wait forever, while bounded request types
550
+ // would wait until APPROVAL_TIMEOUT_MS. Resolve them as cancelled now and
551
+ // clear any matching timers, mirroring the legacy path below.
552
+ this.cancelSessionApprovals(s);
335
553
  this.transport.send(createResponse(req.id, { ok: true }));
336
554
  return;
337
555
  }
@@ -348,6 +566,163 @@ export class AgentServer {
348
566
  this.clearAllApprovalTimers();
349
567
  this.transport.send(createResponse(req.id, { ok: true }));
350
568
  }
569
+ /**
570
+ * Extend a running goal's turn/budget ceilings mid-run (TODO 3.1). Routed to
571
+ * a specific session (multi-session path). Returns the resulting effective
572
+ * limits, or an error if no session / no active run.
573
+ */
574
+ handleGoalExtend(req) {
575
+ const params = (req.params ?? {});
576
+ const ext = {
577
+ addTurns: params.addTurns,
578
+ addTokenBudget: params.addTokenBudget,
579
+ addTimeBudgetMs: params.addTimeBudgetMs,
580
+ addStopBlocks: params.addStopBlocks,
581
+ };
582
+ // Multi-session path: route to the named session.
583
+ const session = this.chatManager && typeof params.sessionId === "string"
584
+ ? this.chatManager.get(params.sessionId)
585
+ : undefined;
586
+ // Legacy single-engine path (no chatManager): extend the one engine's run,
587
+ // mirroring handleCancel which also supports both paths. Without this a
588
+ // legacy host can cancel but never extend a goal.
589
+ const result = session
590
+ ? session.extendGoalRun(ext)
591
+ : this.legacyEngine
592
+ ? this.legacyEngine.extendGoalRun(ext)
593
+ : undefined;
594
+ if (result === undefined) {
595
+ // Couldn't even resolve a target (no session and no legacy engine).
596
+ if (!session && !this.legacyEngine) {
597
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, params.sessionId ? `No such session: ${params.sessionId}` : "sessionId is required"));
598
+ return;
599
+ }
600
+ }
601
+ if (!result) {
602
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "No active run to extend"));
603
+ return;
604
+ }
605
+ this.transport.send(createResponse(req.id, { ok: true, limits: result }));
606
+ }
607
+ /**
608
+ * Clear a session's persisted active goal (CC /goal clear). Routes to the
609
+ * named session, falling back to the legacy single engine (mirrors
610
+ * handleGoalExtend / handleCancel's dual path). Returns { ok, cleared }.
611
+ */
612
+ handleGoalClear(req) {
613
+ const params = (req.params ?? {});
614
+ const session = this.chatManager && typeof params.sessionId === "string"
615
+ ? this.chatManager.get(params.sessionId)
616
+ : undefined;
617
+ if (!session && !this.legacyEngine) {
618
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, params.sessionId ? `No such session: ${params.sessionId}` : "sessionId is required"));
619
+ return;
620
+ }
621
+ const cleared = session
622
+ ? session.clearGoal()
623
+ : params.sessionId
624
+ ? (this.legacyEngine.clearGoal(params.sessionId) ?? false)
625
+ : false;
626
+ this.transport.send(createResponse(req.id, { ok: true, cleared }));
627
+ }
628
+ /**
629
+ * Read a session's persisted active goal so the host can re-surface the goal
630
+ * block + Cancel button on session load. A persistent goal lives only in
631
+ * state.activeGoal and is never replayed from the transcript, so a reloaded
632
+ * (or disk-rebuilt) session has no other way to learn it. Prefers a live
633
+ * chatManager session, else falls through to a disk read (readActiveGoalFromDisk
634
+ * in worker mode, or the legacy engine's disk read in single-engine mode) — the
635
+ * bug case is an aborted/reloaded session that is NOT live. Returns { ok, goal } where goal
636
+ * is the objective string, or null when there's no goal / unknown session.
637
+ * Never errors on "no goal" — null is the normal "nothing to show" answer.
638
+ */
639
+ handleGoalGet(req) {
640
+ const params = (req.params ?? {});
641
+ if (typeof params.sessionId !== "string" || !params.sessionId) {
642
+ // Missing/empty param is InvalidParams, not SessionClosed (no session was
643
+ // ever named) — matches handleSteer/handleUnsteer/handleBackgroundShells.
644
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
645
+ return;
646
+ }
647
+ // Prefer the live session; otherwise read straight off disk. The goal we
648
+ // want to recover belongs to a session that is, by definition of this bug,
649
+ // usually NOT currently live: reopening after restart doesn't create a
650
+ // ChatSession (that only happens on a send), so chatManager.get() misses.
651
+ // In worker (chatManager) mode there is no legacyEngine, so the disk read
652
+ // MUST come from readActiveGoalFromDisk — relying on legacyEngine there
653
+ // silently returned null and the UI goal block never re-surfaced.
654
+ const live = this.chatManager ? this.chatManager.get(params.sessionId) : undefined;
655
+ const goal = live
656
+ ? live.getGoal()
657
+ : (this.readActiveGoalFromDisk?.(params.sessionId) ??
658
+ this.legacyEngine?.getGoal(params.sessionId) ??
659
+ undefined);
660
+ this.transport.send(createResponse(req.id, { ok: true, goal: goal ? goal.objective : null }));
661
+ }
662
+ /**
663
+ * Query/control a session's background shells for the desktop UI panel (TODO
664
+ * 3.2). Reads the singleton BackgroundShellManager directly (shells outlive
665
+ * the run that spawned them, so we don't need a live ChatSession). Actions:
666
+ * - (default) list → the session's shells
667
+ * - output → full retained output of one shell
668
+ * - kill → terminate one shell's process group
669
+ * All scoped by sessionId (manager enforces ownership on output/kill).
670
+ */
671
+ handleBackgroundShells(req) {
672
+ const params = (req.params ?? {});
673
+ const sessionId = params.sessionId;
674
+ if (typeof sessionId !== "string" || !sessionId) {
675
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
676
+ return;
677
+ }
678
+ const action = params.action ?? "list";
679
+ if (action === "list") {
680
+ const shells = backgroundShellManager.listForSession(sessionId);
681
+ this.transport.send(createResponse(req.id, { shells }));
682
+ return;
683
+ }
684
+ if (!params.shellId) {
685
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "shellId is required"));
686
+ return;
687
+ }
688
+ if (action === "output") {
689
+ const res = backgroundShellManager.readOutput(params.shellId, "all", sessionId);
690
+ if (!res.ok) {
691
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, res.error));
692
+ return;
693
+ }
694
+ this.transport.send(createResponse(req.id, { header: res.header, text: res.text }));
695
+ return;
696
+ }
697
+ if (action === "kill") {
698
+ void backgroundShellManager.kill(params.shellId, sessionId).then((res) => {
699
+ if (!res.ok) {
700
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, res.error));
701
+ }
702
+ else {
703
+ this.transport.send(createResponse(req.id, { ok: true }));
704
+ }
705
+ });
706
+ return;
707
+ }
708
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, `unknown action: ${action}`));
709
+ }
710
+ /**
711
+ * BackgroundWork — unified, list-only view of a session's background work
712
+ * across all three registries (shells + sub-agents + jobs) for the desktop
713
+ * background panel. Per-shell output/kill still flow through BackgroundShells
714
+ * (by shellId); this just answers "what's running in the background right now".
715
+ */
716
+ handleBackgroundWork(req) {
717
+ const params = (req.params ?? {});
718
+ const sessionId = params.sessionId;
719
+ if (typeof sessionId !== "string" || !sessionId) {
720
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
721
+ return;
722
+ }
723
+ const items = listBackgroundWorkForUI(sessionId);
724
+ this.transport.send(createResponse(req.id, { items }));
725
+ }
351
726
  // ─── CloseSession ───────────────────────────────────────────────
352
727
  handleCloseSession(req) {
353
728
  const params = (req.params ?? {});
@@ -356,8 +731,20 @@ export class AgentServer {
356
731
  return;
357
732
  }
358
733
  if (this.chatManager) {
734
+ const session = this.chatManager.get(params.sessionId);
735
+ if (session) {
736
+ this.cancelSessionApprovals(session, "session closed");
737
+ }
359
738
  this.chatManager.close(params.sessionId);
360
739
  }
740
+ // Explicit session teardown — reap that session's background shells
741
+ // (design §6 "session 被显式删除 → killSession"). This is the RPC path
742
+ // (agent/closeSession from the host on delete), distinct from the idle
743
+ // sweeper's chatManager.close() which must NOT kill (§6). Fire-and-forget.
744
+ void backgroundShellManager.killSession(params.sessionId);
745
+ // Drop this session's retained background jobs too (#2/#5): finished jobs
746
+ // are kept for the panel, so explicit teardown is where they're released.
747
+ backgroundJobRegistry.dropForSession(params.sessionId);
361
748
  this.transport.send(createResponse(req.id, { ok: true }));
362
749
  }
363
750
  // ─── Configure ──────────────────────────────────────────────────
@@ -368,7 +755,11 @@ export class AgentServer {
368
755
  const sid = params.sessionId;
369
756
  const s = this.chatManager.get(sid);
370
757
  if (!s) {
371
- this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, `No such session: ${sid}`));
758
+ // Session not found (already cleaned by idle sweeper, or never created).
759
+ // Don't create one just for configure — let the subsequent run() do it
760
+ // with proper per-session config. Return OK since there's nothing to
761
+ // configure on a non-existent session.
762
+ this.transport.send(createResponse(req.id, { ok: true }));
372
763
  return;
373
764
  }
374
765
  if (typeof params.planMode === "boolean")
@@ -376,13 +767,32 @@ export class AgentServer {
376
767
  if (typeof params.permissionMode === "string") {
377
768
  s.engine.setPermissionMode(params.permissionMode);
378
769
  }
770
+ if (params.clearModels) {
771
+ this.chatManager.runtime.clearModels();
772
+ }
773
+ if (params.reloadModels) {
774
+ try {
775
+ this.chatManager.runtime.reloadModelsFromSettings();
776
+ }
777
+ catch (err) {
778
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
779
+ return;
780
+ }
781
+ }
379
782
  // Per-session model switch — the missing piece that made model changes
380
783
  // worker-global (session-isolation research §3). requestModelSwitch
381
784
  // applies immediately when idle, defers to the run boundary when busy
382
785
  // so it never swaps the model under a running LLM client.
383
786
  if (typeof params.model === "string") {
384
787
  try {
385
- s.requestModelSwitch(params.model);
788
+ const entry = s.requestModelSwitch(params.model);
789
+ this.transport.send(createResponse(req.id, {
790
+ ok: true,
791
+ model: entry.model,
792
+ key: entry.key,
793
+ maxContextTokens: entry.maxContextTokens,
794
+ }));
795
+ return;
386
796
  }
387
797
  catch (err) {
388
798
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, err.message));
@@ -397,7 +807,7 @@ export class AgentServer {
397
807
  }
398
808
  const settings = this.settingsReader();
399
809
  const version = ++this.configVersion;
400
- s.engine.refreshRuntimeConfig(diskDefaultsFrom(settings), version);
810
+ s.engine.refreshRuntimeConfig(diskDefaultsFrom(settings, s.engine.getEffectiveDisabledLists().disabledPlugins), version);
401
811
  }
402
812
  this.transport.send(createResponse(req.id, { ok: true }));
403
813
  return;
@@ -405,9 +815,33 @@ export class AgentServer {
405
815
  // Global configure — delegate to legacyEngine if available,
406
816
  // or to any session's engine from chatManager for settings ops
407
817
  const engine = this.legacyEngine ?? this.anyEngine();
408
- if (params.reloadModels && engine) {
818
+ if (params.clearModels) {
819
+ if (this.chatManager) {
820
+ this.chatManager.runtime.clearModels();
821
+ }
822
+ else {
823
+ this.legacyEngine?.getModelPool().clear();
824
+ this.globalQueryEngine?.getModelPool().clear();
825
+ }
826
+ }
827
+ if (params.reloadModels) {
409
828
  try {
410
- engine.reloadModelPool();
829
+ if (this.chatManager) {
830
+ this.chatManager.runtime.reloadModelsFromSettings();
831
+ }
832
+ else {
833
+ const seen = new Set();
834
+ const reload = (target) => {
835
+ if (!target || seen.has(target))
836
+ return;
837
+ seen.add(target);
838
+ target.reloadModelPool();
839
+ };
840
+ reload(this.legacyEngine);
841
+ reload(this.globalQueryEngine);
842
+ if (seen.size === 0)
843
+ reload(engine);
844
+ }
411
845
  }
412
846
  catch (err) {
413
847
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
@@ -417,7 +851,12 @@ export class AgentServer {
417
851
  if (params.model !== undefined && engine) {
418
852
  try {
419
853
  const entry = engine.switchModel(params.model);
420
- this.transport.send(createResponse(req.id, { ok: true, model: entry.model, key: entry.key }));
854
+ this.transport.send(createResponse(req.id, {
855
+ ok: true,
856
+ model: entry.model,
857
+ key: entry.key,
858
+ maxContextTokens: entry.maxContextTokens,
859
+ }));
421
860
  return;
422
861
  }
423
862
  catch (err) {
@@ -445,23 +884,29 @@ export class AgentServer {
445
884
  // Mirrors how reloadModels/model/planMode above act on `engine`.
446
885
  if (engine) {
447
886
  const settings = this.settingsReader();
448
- engine.refreshRuntimeConfig(diskDefaultsFrom(settings), ++this.configVersion);
887
+ engine.refreshRuntimeConfig(diskDefaultsFrom(settings, engine.getEffectiveDisabledLists().disabledPlugins), ++this.configVersion);
449
888
  }
450
889
  this.transport.send(createResponse(req.id, { ok: true }));
451
890
  return;
452
891
  }
453
892
  const settings = this.settingsReader();
454
- const patch = diskDefaultsFrom(settings);
455
- // #6: content short-circuit. If the disk-default patch is byte-identical
456
- // to the last one we broadcast, the disk config that matters to running
457
- // sessions hasn't changed — skip the whole forEachSession (and the per-
458
- // session reloadHooks churn it triggers). Still return ok; a genuine
459
- // change differs in JSON and propagates normally.
460
- const patchJson = JSON.stringify(patch);
893
+ // The MCP merge folds each session's PROJECT capabilityOverrides (a
894
+ // project-level "on" must override the global disabledPlugins), so the
895
+ // patch is cwd-dependent compute it per session, not once.
896
+ const perSession = [];
897
+ this.chatManager.forEachSession((s) => {
898
+ const patch = diskDefaultsFrom(settings, s.engine.getEffectiveDisabledLists().disabledPlugins);
899
+ perSession.push({ apply: (v) => s.engine.refreshRuntimeConfig(patch, v), patch });
900
+ });
901
+ // #6: content short-circuit — now keyed on ALL per-session patches, so a
902
+ // change visible to only one session (e.g. its project's overrides)
903
+ // still propagates while a true no-op skips the reloadHooks churn.
904
+ const patchJson = JSON.stringify(perSession.map((p) => p.patch));
461
905
  if (patchJson !== this.lastBroadcastPatch) {
462
906
  this.lastBroadcastPatch = patchJson;
463
907
  const version = ++this.configVersion;
464
- this.chatManager.forEachSession((s) => s.engine.refreshRuntimeConfig(patch, version));
908
+ for (const p of perSession)
909
+ p.apply(version);
465
910
  }
466
911
  }
467
912
  this.transport.send(createResponse(req.id, { ok: true }));
@@ -528,6 +973,11 @@ export class AgentServer {
528
973
  // apiKey is redacted at this boundary: clients only get
529
974
  // hasApiKey + optional apiKeyPreview. See protocol/redact.ts.
530
975
  llm: redactLlmConfig(config.llm),
976
+ // Resolved feature flags (defaults merged with settings overlay)
977
+ // so the /features command can list current state.
978
+ featureFlags: engine.getFeatureFlags(),
979
+ // Effective permission rules so /permissions can list them (TODO 5.1).
980
+ permissionRules: engine.getPermissionRules(),
531
981
  },
532
982
  }));
533
983
  break;
@@ -556,12 +1006,53 @@ export class AgentServer {
556
1006
  break;
557
1007
  }
558
1008
  case "compact": {
559
- if (!engine) {
1009
+ const compactSessionId = typeof params.sessionId === "string" && params.sessionId.length > 0
1010
+ ? params.sessionId
1011
+ : undefined;
1012
+ let compactEngine = engine;
1013
+ if (this.chatManager) {
1014
+ if (compactSessionId) {
1015
+ let session = this.chatManager.get(compactSessionId);
1016
+ if (!session) {
1017
+ const probeEngine = this.anyEngine();
1018
+ if (!probeEngine?.sessionExistsOnDisk(compactSessionId)) {
1019
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionNotFound, `Session not found: ${compactSessionId}`));
1020
+ return;
1021
+ }
1022
+ try {
1023
+ session = this.chatManager.getOrCreate(compactSessionId, {
1024
+ cwd: probeEngine.getSessionManager().readCwd(compactSessionId),
1025
+ });
1026
+ }
1027
+ catch (err) {
1028
+ this.transport.send(createErrorResponse(req.id, err.code ?? ErrorCodes.InternalError, err.message));
1029
+ return;
1030
+ }
1031
+ }
1032
+ compactEngine = session.engine;
1033
+ }
1034
+ else {
1035
+ compactEngine = this.anyEngine();
1036
+ }
1037
+ }
1038
+ if (!compactEngine) {
560
1039
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, "No engine available for compact query"));
561
1040
  return;
562
1041
  }
563
1042
  try {
564
- const result = engine.forceCompact();
1043
+ const result = await compactEngine.forceCompact(compactSessionId);
1044
+ if (result.before > result.after) {
1045
+ const event = {
1046
+ type: "context_compact",
1047
+ strategy: toCompactStreamStrategy(result.strategy),
1048
+ before: result.before,
1049
+ after: result.after,
1050
+ };
1051
+ this.notify(Methods.StreamEvent, {
1052
+ sessionId: compactSessionId ?? "",
1053
+ event,
1054
+ });
1055
+ }
565
1056
  this.transport.send(createResponse(req.id, {
566
1057
  type: "compact",
567
1058
  data: result,
@@ -654,6 +1145,15 @@ export class AgentServer {
654
1145
  const { key, value } = params;
655
1146
  if (!key)
656
1147
  throw new Error("key is required for config_set");
1148
+ // Refuse writes to trust-root fields via the generic config path. A
1149
+ // protocol peer (external driver, paired phone, compromised renderer)
1150
+ // could otherwise self-authorize tools or inject env/hooks/MCP servers,
1151
+ // which is equivalent to remotely bypassing workspace trust. Legit
1152
+ // writes to these go through the dedicated settings UI, not config_set;
1153
+ // provider/model edits use provider_add / models handlers below.
1154
+ if (isProtectedSettingKey(key)) {
1155
+ throw new Error(`config_set refused: "${key}" is a protected trust/permission field and cannot be written through this path.`);
1156
+ }
657
1157
  engine.updateConfig(key, value);
658
1158
  // Echo back through the same secret-aware masker as config_get so
659
1159
  // a `config_set("llm.apiKey", "...")` confirmation doesn't ship
@@ -861,6 +1361,55 @@ export class AgentServer {
861
1361
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
862
1362
  }
863
1363
  }
1364
+ // ─── Steer ──────────────────────────────────────────────────────
1365
+ // Queue a user message into the in-flight run's turn loop (不打断). Unlike
1366
+ // Inject (appends an assistant context msg for the NEXT run), Steer feeds a
1367
+ // user message to the CURRENT run's next step. No abort, no LLM trigger by
1368
+ // itself — the running loop picks it up at its next step boundary.
1369
+ handleSteer(req) {
1370
+ const params = (req.params ?? {});
1371
+ if (!params.text || !params.sessionId) {
1372
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "text and sessionId required"));
1373
+ return;
1374
+ }
1375
+ const engine = this.chatManager
1376
+ ? this.chatManager.get(params.sessionId)?.engine
1377
+ : this.legacyEngine;
1378
+ if (!engine) {
1379
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, `No such session: ${params.sessionId}`));
1380
+ return;
1381
+ }
1382
+ try {
1383
+ const result = engine.enqueueSteer(params.sessionId, params.text, params.id, params.clientMessageId);
1384
+ this.transport.send(createResponse(req.id, { ok: true, ...result }));
1385
+ }
1386
+ catch (err) {
1387
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
1388
+ }
1389
+ }
1390
+ handleUnsteer(req) {
1391
+ const params = (req.params ?? {});
1392
+ if (!params.id || !params.sessionId) {
1393
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "id and sessionId required"));
1394
+ return;
1395
+ }
1396
+ const engine = this.chatManager
1397
+ ? this.chatManager.get(params.sessionId)?.engine
1398
+ : this.legacyEngine;
1399
+ if (!engine) {
1400
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, `No such session: ${params.sessionId}`));
1401
+ return;
1402
+ }
1403
+ try {
1404
+ // removed=false means the loop already consumed it — not an error, the
1405
+ // host just learns it couldn't be revoked (it will arrive as a bubble).
1406
+ const removed = engine.unsteer(params.sessionId, params.id);
1407
+ this.transport.send(createResponse(req.id, { ok: true, removed }));
1408
+ }
1409
+ catch (err) {
1410
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
1411
+ }
1412
+ }
864
1413
  // ─── Client Communication ───────────────────────────────────────
865
1414
  /**
866
1415
  * Ask the client to approve a tool operation (legacy single-engine path).
@@ -868,27 +1417,42 @@ export class AgentServer {
868
1417
  requestApprovalFromClient(request) {
869
1418
  return new Promise((resolve) => {
870
1419
  const requestId = nanoid(12);
871
- this.pendingApprovals.set(requestId, resolve);
1420
+ const sessionId = typeof request.sessionId === "string" ? request.sessionId : undefined;
1421
+ const session = this.chatManager && sessionId ? this.chatManager.get(sessionId) : undefined;
1422
+ if (this.chatManager && sessionId && !session) {
1423
+ resolve({ approved: false, reason: `session closed: ${sessionId}` });
1424
+ return;
1425
+ }
1426
+ if (session) {
1427
+ session.pendingApprovals.set(requestId, (decision) => {
1428
+ resolve(decision);
1429
+ });
1430
+ }
1431
+ else {
1432
+ this.pendingApprovals.set(requestId, resolve);
1433
+ }
872
1434
  const timer = setTimeout(() => {
873
- if (this.pendingApprovals.has(requestId)) {
874
- this.pendingApprovals.delete(requestId);
1435
+ const pending = session?.pendingApprovals ?? this.pendingApprovals;
1436
+ if (pending.has(requestId)) {
1437
+ pending.delete(requestId);
875
1438
  this.approvalTimers.delete(requestId);
876
1439
  resolve({ approved: false, reason: "approval timed out" });
877
1440
  }
878
1441
  }, AgentServer.APPROVAL_TIMEOUT_MS);
879
1442
  this.approvalTimers.set(requestId, timer);
880
- this.notify(Methods.ApprovalRequest, { requestId, request });
1443
+ this.notify(Methods.ApprovalRequest, {
1444
+ ...(sessionId ? { sessionId } : {}),
1445
+ requestId,
1446
+ request,
1447
+ });
881
1448
  });
882
1449
  }
883
1450
  /**
884
- * Ask the client to answer a question from the agent (legacy single-engine path).
885
- */
886
- /**
887
- * Per-session AskUserQuestion for the chatManager path. Mirrors
888
- * requestAskUserFromClient but resolves via the SESSION's pendingApprovals
889
- * (the chatManager approve handler looks there, keyed by sessionId+requestId)
890
- * and tags the notify with sessionId so the renderer routes the question to
891
- * the right chat tab.
1451
+ * Per-session AskUserQuestion for the chatManager path. Resolves via the
1452
+ * SESSION's pendingApprovals (the chatManager approve handler looks there,
1453
+ * keyed by sessionId+requestId) and tags the notify with sessionId so the
1454
+ * renderer routes the question to the right chat tab. This intentionally has
1455
+ * no wall-clock timeout; Stop/cancel drains the pending ask.
892
1456
  */
893
1457
  requestAskUserForSession(session, sessionId, question, opts) {
894
1458
  return new Promise((resolve) => {
@@ -904,14 +1468,6 @@ export class AgentServer {
904
1468
  resolve(typeof decision === "string" ? decision : "");
905
1469
  }
906
1470
  });
907
- const timer = setTimeout(() => {
908
- if (session.pendingApprovals.has(requestId)) {
909
- session.pendingApprovals.delete(requestId);
910
- this.approvalTimers.delete(requestId);
911
- resolve("(approval timed out)");
912
- }
913
- }, AgentServer.APPROVAL_TIMEOUT_MS);
914
- this.approvalTimers.set(requestId, timer);
915
1471
  const args = { question };
916
1472
  if (opts?.header !== undefined)
917
1473
  args.header = opts.header;
@@ -919,6 +1475,8 @@ export class AgentServer {
919
1475
  args.options = opts.options;
920
1476
  if (opts?.multiSelect !== undefined)
921
1477
  args.multiSelect = opts.multiSelect;
1478
+ if (opts?.optionsOnly !== undefined)
1479
+ args.optionsOnly = opts.optionsOnly;
922
1480
  this.notify(Methods.ApprovalRequest, {
923
1481
  sessionId,
924
1482
  requestId,
@@ -931,6 +1489,134 @@ export class AgentServer {
931
1489
  });
932
1490
  });
933
1491
  }
1492
+ /**
1493
+ * Build a BrowserBridge whose every method routes a browser action to the
1494
+ * client over the session's pendingApprovals channel (same as askUser). The
1495
+ * client (Electron main) drives the webview via CDP and replies with a JSON
1496
+ * result string we parse back into the typed result. On timeout / malformed
1497
+ * reply we degrade to a safe error result rather than throwing.
1498
+ */
1499
+ makeBrowserBridge(session, sessionId) {
1500
+ const call = (action, payload) => this.requestBrowserActionForSession(session, sessionId, action, payload);
1501
+ return {
1502
+ snapshot: () => call("snapshot", {}),
1503
+ click: (ref) => call("click", { ref }),
1504
+ type: (ref, text) => call("type", { ref, text }),
1505
+ navigate: (url) => call("navigate", { url }),
1506
+ scroll: (dir, amount) => call("scroll", { dir, amount }),
1507
+ readContent: () => call("readContent", {}),
1508
+ extractLinks: () => call("extractLinks", {}),
1509
+ waitForLoad: (timeoutMs) => call("waitForLoad", { timeoutMs }),
1510
+ hover: (ref) => call("hover", { ref }),
1511
+ selectOption: (ref, value) => call("selectOption", { ref, value }),
1512
+ pressKey: (key, ref) => call("pressKey", { key, ref }),
1513
+ fetchImages: (refs) => call("fetchImages", { refs }),
1514
+ screenshot: (ref) => call("screenshot", { ref }),
1515
+ listTabs: () => call("listTabs", {}),
1516
+ switchTab: (tabId) => call("switchTab", { tabId }),
1517
+ };
1518
+ }
1519
+ requestBrowserActionForSession(session, sessionId, action, payload) {
1520
+ return new Promise((resolve) => {
1521
+ const requestId = nanoid(12);
1522
+ session.pendingApprovals.set(requestId, (decision) => {
1523
+ this.clearApprovalTimer(requestId);
1524
+ // Main replies with { approved:true, answer:<json string> } (reusing the
1525
+ // ApprovalResult shape) or a raw json string. Parse → typed result.
1526
+ let raw;
1527
+ if (decision && typeof decision === "object" && "approved" in decision) {
1528
+ const r = decision;
1529
+ raw = r.approved ? r.answer : undefined;
1530
+ }
1531
+ else if (typeof decision === "string") {
1532
+ raw = decision;
1533
+ }
1534
+ if (raw === undefined) {
1535
+ resolve({ ok: false, detail: "browser action declined or unavailable" });
1536
+ return;
1537
+ }
1538
+ try {
1539
+ resolve(JSON.parse(raw));
1540
+ }
1541
+ catch {
1542
+ resolve({ ok: false, detail: "malformed browser action result" });
1543
+ }
1544
+ });
1545
+ const timer = setTimeout(() => {
1546
+ if (session.pendingApprovals.has(requestId)) {
1547
+ session.pendingApprovals.delete(requestId);
1548
+ this.approvalTimers.delete(requestId);
1549
+ resolve({ ok: false, detail: "browser action timed out" });
1550
+ }
1551
+ }, AgentServer.APPROVAL_TIMEOUT_MS);
1552
+ this.approvalTimers.set(requestId, timer);
1553
+ this.notify(Methods.ApprovalRequest, {
1554
+ sessionId,
1555
+ requestId,
1556
+ request: {
1557
+ toolName: "__browser_action__",
1558
+ args: { action, ...payload },
1559
+ description: `browser:${action}`,
1560
+ riskLevel: "low",
1561
+ },
1562
+ });
1563
+ });
1564
+ }
1565
+ /**
1566
+ * Ask the client (Electron main) to inject a cookie credential into the
1567
+ * built-in browser. Same pendingApprovals channel as browser actions; main
1568
+ * calls restoreCookiesToBrowser and replies with a JSON result string we
1569
+ * parse into { ok, count?, error? }. Degrades to ok:false on timeout/malformed.
1570
+ */
1571
+ requestCredentialInjectForSession(session, sessionId, credentialId) {
1572
+ return new Promise((resolve) => {
1573
+ const requestId = nanoid(12);
1574
+ session.pendingApprovals.set(requestId, (decision) => {
1575
+ this.clearApprovalTimer(requestId);
1576
+ let raw;
1577
+ if (decision && typeof decision === "object" && "approved" in decision) {
1578
+ const r = decision;
1579
+ raw = r.approved ? r.answer : undefined;
1580
+ }
1581
+ else if (typeof decision === "string") {
1582
+ raw = decision;
1583
+ }
1584
+ if (raw === undefined) {
1585
+ resolve({ ok: false, error: "credential inject declined or unavailable" });
1586
+ return;
1587
+ }
1588
+ try {
1589
+ resolve(JSON.parse(raw));
1590
+ }
1591
+ catch {
1592
+ resolve({ ok: false, error: "malformed credential inject result" });
1593
+ }
1594
+ });
1595
+ const timer = setTimeout(() => {
1596
+ if (session.pendingApprovals.has(requestId)) {
1597
+ session.pendingApprovals.delete(requestId);
1598
+ this.approvalTimers.delete(requestId);
1599
+ resolve({ ok: false, error: "credential inject timed out" });
1600
+ }
1601
+ }, AgentServer.APPROVAL_TIMEOUT_MS);
1602
+ this.approvalTimers.set(requestId, timer);
1603
+ this.notify(Methods.ApprovalRequest, {
1604
+ sessionId,
1605
+ requestId,
1606
+ request: {
1607
+ toolName: "__credential_action__",
1608
+ args: { action: "injectCookie", credentialId },
1609
+ description: `credential:inject:${credentialId}`,
1610
+ riskLevel: "low",
1611
+ },
1612
+ });
1613
+ });
1614
+ }
1615
+ /**
1616
+ * Ask the client to answer a question from the agent (legacy single-engine
1617
+ * path). This intentionally has no wall-clock timeout; Stop/cancel drains
1618
+ * the pending ask.
1619
+ */
934
1620
  requestAskUserFromClient(question, opts) {
935
1621
  return new Promise((resolve) => {
936
1622
  const requestId = nanoid(12);
@@ -943,14 +1629,6 @@ export class AgentServer {
943
1629
  resolve(result.reason ?? "(user declined to answer)");
944
1630
  }
945
1631
  });
946
- const timer = setTimeout(() => {
947
- if (this.pendingApprovals.has(requestId)) {
948
- this.pendingApprovals.delete(requestId);
949
- this.approvalTimers.delete(requestId);
950
- resolve("(approval timed out)");
951
- }
952
- }, AgentServer.APPROVAL_TIMEOUT_MS);
953
- this.approvalTimers.set(requestId, timer);
954
1632
  const args = { question };
955
1633
  if (opts?.header !== undefined)
956
1634
  args.header = opts.header;
@@ -958,6 +1636,8 @@ export class AgentServer {
958
1636
  args.options = opts.options;
959
1637
  if (opts?.multiSelect !== undefined)
960
1638
  args.multiSelect = opts.multiSelect;
1639
+ if (opts?.optionsOnly !== undefined)
1640
+ args.optionsOnly = opts.optionsOnly;
961
1641
  this.notify(Methods.ApprovalRequest, {
962
1642
  requestId,
963
1643
  request: {
@@ -1010,6 +1690,9 @@ export class AgentServer {
1010
1690
  this.bgAgentBusUnsubscribe = null;
1011
1691
  }
1012
1692
  if (this.chatManager) {
1693
+ this.chatManager.forEachSession((session) => {
1694
+ this.cancelSessionApprovals(session, "server closing");
1695
+ });
1013
1696
  this.chatManager.closeAll();
1014
1697
  }
1015
1698
  // Legacy path cleanup
@@ -1036,6 +1719,25 @@ export class AgentServer {
1036
1719
  this.approvalTimers.delete(requestId);
1037
1720
  }
1038
1721
  }
1722
+ /**
1723
+ * Resolve all of a chat session's pending approvals as cancelled and clear
1724
+ * any matching server-side approval timers. Used by handleCancel's
1725
+ * per-session path so a Stop while a tool is awaiting approval doesn't leave
1726
+ * the tool hanging. Bounded request types have same-keyed timer entries;
1727
+ * AskUserQuestion does not.
1728
+ */
1729
+ cancelSessionApprovals(session, reason = "cancelled") {
1730
+ for (const [requestId, resolve] of session.pendingApprovals) {
1731
+ this.clearApprovalTimer(requestId);
1732
+ try {
1733
+ resolve({ approved: false, reason });
1734
+ }
1735
+ catch {
1736
+ /* a resolver must never break cancel cleanup */
1737
+ }
1738
+ }
1739
+ session.pendingApprovals.clear();
1740
+ }
1039
1741
  clearAllApprovalTimers() {
1040
1742
  for (const timer of this.approvalTimers.values()) {
1041
1743
  clearTimeout(timer);