@cjhyy/code-shell-core 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (594) hide show
  1. package/README.md +36 -23
  2. package/dist/agent/agent-definition-registry.d.ts +2 -2
  3. package/dist/agent/agent-definition-registry.js +47 -10
  4. package/dist/agent/agent-definition.d.ts +5 -0
  5. package/dist/agent/agent-definition.js +14 -0
  6. package/dist/automation/index.d.ts +7 -4
  7. package/dist/automation/index.js +4 -2
  8. package/dist/automation/runner.js +3 -1
  9. package/dist/automation/scheduler.d.ts +57 -1
  10. package/dist/automation/scheduler.js +123 -23
  11. package/dist/automation/store.d.ts +6 -2
  12. package/dist/automation/store.js +7 -3
  13. package/dist/capabilities/index.d.ts +109 -0
  14. package/dist/capabilities/index.js +80 -0
  15. package/dist/capability-control/disabled-lists.d.ts +2 -1
  16. package/dist/capability-control/disabled-lists.js +3 -5
  17. package/dist/capability-control/overlay.d.ts +14 -1
  18. package/dist/capability-control/overlay.js +40 -0
  19. package/dist/capability-control/service.js +6 -4
  20. package/dist/cli/agent-server-stdio.d.ts +1 -1
  21. package/dist/cli/agent-server-stdio.js +66 -6
  22. package/dist/cli/agent-server-tcp.js +7 -3
  23. package/dist/context/compaction.d.ts +39 -3
  24. package/dist/context/compaction.js +151 -39
  25. package/dist/context/manager.d.ts +17 -1
  26. package/dist/context/manager.js +36 -1
  27. package/dist/credentials/access.d.ts +3 -1
  28. package/dist/credentials/access.js +24 -3
  29. package/dist/credentials/index.d.ts +1 -1
  30. package/dist/credentials/index.js +1 -1
  31. package/dist/credentials/oauth.d.ts +4 -1
  32. package/dist/credentials/oauth.js +14 -0
  33. package/dist/credentials/store.d.ts +15 -1
  34. package/dist/credentials/store.js +17 -4
  35. package/dist/credentials/types.d.ts +24 -0
  36. package/dist/credentials/use-credential-tool.js +15 -1
  37. package/dist/engine/auxiliary-pipeline.d.ts +40 -0
  38. package/dist/engine/auxiliary-pipeline.js +181 -0
  39. package/dist/{settings → engine}/disk-defaults.d.ts +2 -2
  40. package/dist/{settings → engine}/disk-defaults.js +5 -2
  41. package/dist/engine/dynamic-tool-defs.d.ts +4 -0
  42. package/dist/engine/dynamic-tool-defs.js +4 -0
  43. package/dist/engine/engine.d.ts +254 -195
  44. package/dist/engine/engine.js +1812 -2041
  45. package/dist/engine/file-history-hook.d.ts +11 -0
  46. package/dist/engine/file-history-hook.js +31 -0
  47. package/dist/engine/goal-judge-context.d.ts +9 -0
  48. package/dist/engine/goal-judge-context.js +234 -0
  49. package/dist/engine/injected-context-cache.d.ts +6 -0
  50. package/dist/engine/injected-context-cache.js +13 -0
  51. package/dist/engine/input-attachments.d.ts +1 -1
  52. package/dist/engine/model-facade.d.ts +3 -3
  53. package/dist/engine/model-facade.js +15 -14
  54. package/dist/engine/permission-controller.d.ts +55 -0
  55. package/dist/engine/permission-controller.js +148 -0
  56. package/dist/engine/prompt-cache-diagnostics.d.ts +58 -0
  57. package/dist/engine/prompt-cache-diagnostics.js +178 -0
  58. package/dist/engine/run-accounting.d.ts +41 -0
  59. package/dist/engine/run-accounting.js +126 -0
  60. package/dist/engine/run-context.d.ts +44 -0
  61. package/dist/engine/run-context.js +88 -0
  62. package/dist/engine/run-environment.d.ts +30 -0
  63. package/dist/engine/run-environment.js +77 -0
  64. package/dist/engine/run-finalize.d.ts +58 -0
  65. package/dist/engine/run-finalize.js +245 -0
  66. package/dist/engine/run-goal.d.ts +58 -0
  67. package/dist/engine/run-goal.js +188 -0
  68. package/dist/engine/run-image-input.d.ts +1 -1
  69. package/dist/engine/run-image-input.js +5 -3
  70. package/dist/engine/run-session-open.d.ts +41 -0
  71. package/dist/engine/run-session-open.js +186 -0
  72. package/dist/engine/run-setup.d.ts +42 -0
  73. package/dist/engine/run-setup.js +58 -0
  74. package/dist/engine/run-tooling.d.ts +125 -0
  75. package/dist/engine/run-tooling.js +283 -0
  76. package/dist/engine/run-types.d.ts +139 -0
  77. package/dist/engine/run-types.js +12 -0
  78. package/dist/engine/run-workspace.d.ts +50 -0
  79. package/dist/engine/run-workspace.js +114 -0
  80. package/dist/engine/steer-queue.d.ts +1 -1
  81. package/dist/engine/subagent-spawner.d.ts +32 -0
  82. package/dist/engine/subagent-spawner.js +287 -0
  83. package/dist/engine/tool-summary.d.ts +2 -2
  84. package/dist/engine/tool-summary.js +2 -2
  85. package/dist/engine/turn-loop.d.ts +49 -10
  86. package/dist/engine/turn-loop.js +370 -104
  87. package/dist/engine/types.d.ts +30 -3
  88. package/dist/engine/types.js +1 -1
  89. package/dist/{engine/goal.d.ts → goal/lifecycle.d.ts} +76 -4
  90. package/dist/goal/lifecycle.js +482 -0
  91. package/dist/hooks/events.d.ts +24 -10
  92. package/dist/hooks/goal-stop-hook.d.ts +17 -28
  93. package/dist/hooks/goal-stop-hook.js +110 -465
  94. package/dist/hooks/registry.js +1 -1
  95. package/dist/index.d.ts +48 -108
  96. package/dist/index.extension.d.ts +51 -0
  97. package/dist/index.extension.js +39 -0
  98. package/dist/index.internal.d.ts +86 -0
  99. package/dist/index.internal.js +92 -0
  100. package/dist/index.js +34 -123
  101. package/dist/links/cli.d.ts +48 -0
  102. package/dist/links/cli.js +617 -0
  103. package/dist/links/http.d.ts +40 -0
  104. package/dist/links/http.js +166 -0
  105. package/dist/links/index.d.ts +4 -0
  106. package/dist/links/index.js +3 -0
  107. package/dist/links/link-action-tool.d.ts +5 -0
  108. package/dist/links/link-action-tool.js +241 -0
  109. package/dist/links/providers.d.ts +8 -0
  110. package/dist/links/providers.js +665 -0
  111. package/dist/links/types.d.ts +44 -0
  112. package/dist/llm/client-base.d.ts +7 -0
  113. package/dist/llm/client-base.js +60 -0
  114. package/dist/llm/model-pool.d.ts +1 -1
  115. package/dist/llm/model-pool.js +1 -1
  116. package/dist/llm/providers/anthropic.d.ts +1 -0
  117. package/dist/llm/providers/anthropic.js +9 -0
  118. package/dist/llm/providers/openai.d.ts +1 -0
  119. package/dist/llm/providers/openai.js +17 -0
  120. package/dist/model-catalog/index.js +15 -2
  121. package/dist/model-catalog/resolve.d.ts +13 -1
  122. package/dist/model-catalog/resolve.js +37 -2
  123. package/dist/model-catalog/types.d.ts +74 -48
  124. package/dist/model-catalog/types.js +16 -5
  125. package/dist/model-catalog/upsert.d.ts +3 -1
  126. package/dist/model-catalog/upsert.js +9 -0
  127. package/dist/onboarding.d.ts +0 -1
  128. package/dist/onboarding.js +1 -22
  129. package/dist/panel-apps/bindings.d.ts +24 -0
  130. package/dist/panel-apps/bindings.js +69 -0
  131. package/dist/panel-apps/github-archive.d.ts +21 -0
  132. package/dist/panel-apps/github-archive.js +98 -0
  133. package/dist/panel-apps/index.d.ts +4 -0
  134. package/dist/panel-apps/index.js +4 -0
  135. package/dist/panel-apps/installer.d.ts +68 -0
  136. package/dist/panel-apps/installer.js +622 -0
  137. package/dist/panel-apps/manifest.d.ts +364 -0
  138. package/dist/panel-apps/manifest.js +189 -0
  139. package/dist/panel-apps/paths.d.ts +13 -0
  140. package/dist/panel-apps/paths.js +38 -0
  141. package/dist/panel-apps/registry.d.ts +51 -0
  142. package/dist/panel-apps/registry.js +99 -0
  143. package/dist/panel-apps/runtime.d.ts +80 -0
  144. package/dist/panel-apps/runtime.js +154 -0
  145. package/dist/plugins/gitOps.js +8 -1
  146. package/dist/plugins/installedPlugins.d.ts +2 -1
  147. package/dist/plugins/installedPlugins.js +146 -8
  148. package/dist/plugins/installer/codex/convertAgents.d.ts +4 -1
  149. package/dist/plugins/installer/codex/convertAgents.js +62 -8
  150. package/dist/plugins/installer/codex/convertCommands.d.ts +5 -5
  151. package/dist/plugins/installer/codex/convertCommands.js +35 -12
  152. package/dist/plugins/installer/codex/convertHooks.d.ts +10 -0
  153. package/dist/plugins/installer/codex/convertHooks.js +82 -0
  154. package/dist/plugins/installer/codex/convertSkills.js +41 -6
  155. package/dist/plugins/installer/install.d.ts +7 -1
  156. package/dist/plugins/installer/install.js +21 -87
  157. package/dist/plugins/installer/installFromArchive.d.ts +26 -4
  158. package/dist/plugins/installer/installFromArchive.js +44 -27
  159. package/dist/plugins/installer/installFromNpm.d.ts +31 -0
  160. package/dist/plugins/installer/installFromNpm.js +273 -0
  161. package/dist/plugins/installer/loadPluginAgents.js +17 -4
  162. package/dist/plugins/installer/loadPluginMcp.d.ts +14 -0
  163. package/dist/plugins/installer/loadPluginMcp.js +245 -35
  164. package/dist/plugins/installer/normalizeManifest.d.ts +13 -0
  165. package/dist/plugins/installer/normalizeManifest.js +306 -0
  166. package/dist/plugins/installer/npmTar.d.ts +13 -0
  167. package/dist/plugins/installer/npmTar.js +321 -0
  168. package/dist/plugins/installer/parseSource.d.ts +11 -0
  169. package/dist/plugins/installer/parseSource.js +48 -0
  170. package/dist/plugins/installer/preview.d.ts +101 -0
  171. package/dist/plugins/installer/preview.js +336 -0
  172. package/dist/plugins/installer/projectPluginSource.d.ts +14 -0
  173. package/dist/plugins/installer/projectPluginSource.js +140 -0
  174. package/dist/plugins/installer/types.d.ts +775 -0
  175. package/dist/plugins/installer/types.js +100 -0
  176. package/dist/plugins/installer/unzip.d.ts +11 -0
  177. package/dist/plugins/installer/unzip.js +123 -9
  178. package/dist/plugins/installer/update.d.ts +2 -2
  179. package/dist/plugins/installer/update.js +43 -4
  180. package/dist/plugins/loadPluginHooks.d.ts +19 -2
  181. package/dist/plugins/loadPluginHooks.js +97 -86
  182. package/dist/plugins/pluginAutomationTemplates.d.ts +18 -0
  183. package/dist/plugins/pluginAutomationTemplates.js +50 -0
  184. package/dist/plugins/pluginCatalog.d.ts +45 -0
  185. package/dist/plugins/pluginCatalog.js +87 -0
  186. package/dist/plugins/pluginCommandHook.d.ts +6 -0
  187. package/dist/plugins/pluginCommandHook.js +25 -8
  188. package/dist/plugins/pluginCommandsLoader.d.ts +19 -0
  189. package/dist/plugins/pluginCommandsLoader.js +181 -7
  190. package/dist/plugins/pluginContent.d.ts +10 -1
  191. package/dist/plugins/pluginContent.js +87 -30
  192. package/dist/plugins/pluginHookApproval.d.ts +30 -0
  193. package/dist/plugins/pluginHookApproval.js +160 -0
  194. package/dist/plugins/pluginHookIntegrity.d.ts +69 -0
  195. package/dist/plugins/pluginHookIntegrity.js +361 -0
  196. package/dist/plugins/pluginInstaller.js +71 -12
  197. package/dist/plugins/pluginMcpApproval.d.ts +19 -0
  198. package/dist/plugins/pluginMcpApproval.js +110 -0
  199. package/dist/plugins/pluginMcpIntegrity.d.ts +21 -0
  200. package/dist/plugins/pluginMcpIntegrity.js +74 -0
  201. package/dist/plugins/types.d.ts +30 -0
  202. package/dist/plugins/varRewrite.d.ts +9 -5
  203. package/dist/plugins/varRewrite.js +26 -17
  204. package/dist/preset/index.d.ts +11 -8
  205. package/dist/preset/index.js +62 -203
  206. package/dist/product/define.js +9 -3
  207. package/dist/product/types.d.ts +0 -2
  208. package/dist/profile/activation.d.ts +36 -0
  209. package/dist/profile/activation.js +57 -0
  210. package/dist/profile/catalog-store.d.ts +77 -0
  211. package/dist/profile/catalog-store.js +346 -0
  212. package/dist/profile/catalog.d.ts +54 -0
  213. package/dist/profile/catalog.js +152 -0
  214. package/dist/profile/index.d.ts +7 -0
  215. package/dist/profile/index.js +7 -0
  216. package/dist/profile/requirements.d.ts +76 -0
  217. package/dist/profile/requirements.js +117 -0
  218. package/dist/profile/resolve.d.ts +11 -0
  219. package/dist/profile/resolve.js +41 -0
  220. package/dist/profile/store.d.ts +17 -0
  221. package/dist/profile/store.js +167 -0
  222. package/dist/profile/types.d.ts +321 -0
  223. package/dist/profile/types.js +104 -0
  224. package/dist/prompt/composer.d.ts +46 -7
  225. package/dist/prompt/composer.js +78 -48
  226. package/dist/prompt/instruction-scanner.d.ts +6 -5
  227. package/dist/prompt/instruction-scanner.js +8 -23
  228. package/dist/prompt/section-loader.d.ts +2 -2
  229. package/dist/prompt/section-loader.js +7 -4
  230. package/dist/prompt/sections/browser.md +10 -9
  231. package/dist/prompt/sections/harness-base.md +9 -0
  232. package/dist/protocol/chat-session-manager.d.ts +64 -1
  233. package/dist/protocol/chat-session-manager.js +104 -0
  234. package/dist/protocol/chat-session.d.ts +61 -10
  235. package/dist/protocol/chat-session.js +135 -25
  236. package/dist/protocol/client.d.ts +33 -3
  237. package/dist/protocol/client.js +96 -5
  238. package/dist/protocol/index.d.ts +2 -2
  239. package/dist/protocol/index.js +2 -2
  240. package/dist/protocol/mobile-remote-types.d.ts +406 -0
  241. package/dist/protocol/mobile-remote-types.js +1 -0
  242. package/dist/protocol/server.d.ts +127 -3
  243. package/dist/protocol/server.js +1309 -182
  244. package/dist/protocol/types.d.ts +152 -34
  245. package/dist/protocol/types.js +16 -0
  246. package/dist/run/ArtifactTracker.d.ts +6 -1
  247. package/dist/run/ArtifactTracker.js +29 -21
  248. package/dist/run/EngineRunner.d.ts +2 -0
  249. package/dist/run/EngineRunner.js +1 -0
  250. package/dist/run/RunManager.d.ts +26 -0
  251. package/dist/run/RunManager.js +82 -11
  252. package/dist/run/factory.d.ts +3 -0
  253. package/dist/run/factory.js +2 -0
  254. package/dist/runtime/safe-spawn.d.ts +6 -0
  255. package/dist/runtime/safe-spawn.js +8 -13
  256. package/dist/services/auto-dream.d.ts +13 -1
  257. package/dist/services/auto-dream.js +56 -8
  258. package/dist/services/dream-consolidation.js +10 -1
  259. package/dist/services/memory-orchestrator.js +6 -2
  260. package/dist/services/oauth.js +4 -1
  261. package/dist/services/session-memory.d.ts +4 -4
  262. package/dist/services/session-memory.js +13 -10
  263. package/dist/session/memory.d.ts +26 -7
  264. package/dist/session/memory.js +79 -21
  265. package/dist/session/session-manager.d.ts +182 -28
  266. package/dist/session/session-manager.js +910 -130
  267. package/dist/session/session-message.d.ts +22 -0
  268. package/dist/session/session-message.js +1 -0
  269. package/dist/session/transcript.d.ts +46 -5
  270. package/dist/session/transcript.js +159 -3
  271. package/dist/settings/feature-flags.d.ts +40 -0
  272. package/dist/settings/feature-flags.js +40 -0
  273. package/dist/settings/manager.d.ts +39 -1
  274. package/dist/settings/manager.js +102 -41
  275. package/dist/settings/migrate-config.js +4 -2
  276. package/dist/settings/schema.d.ts +511 -177
  277. package/dist/settings/schema.js +98 -25
  278. package/dist/skills/scanner.d.ts +6 -2
  279. package/dist/skills/scanner.js +160 -9
  280. package/dist/sources/adapter.d.ts +14 -0
  281. package/dist/sources/adapter.js +7 -0
  282. package/dist/sources/adapters/local-files.d.ts +10 -0
  283. package/dist/sources/adapters/local-files.js +127 -0
  284. package/dist/sources/adapters/mcp-resource.d.ts +17 -0
  285. package/dist/sources/adapters/mcp-resource.js +41 -0
  286. package/dist/sources/adapters/mock.d.ts +3 -0
  287. package/dist/sources/adapters/mock.js +29 -0
  288. package/dist/sources/binding.d.ts +6 -0
  289. package/dist/sources/binding.js +24 -0
  290. package/dist/sources/catalog.d.ts +6 -0
  291. package/dist/sources/catalog.js +64 -0
  292. package/dist/sources/context-summary.d.ts +8 -0
  293. package/dist/sources/context-summary.js +13 -0
  294. package/dist/sources/credential-status.d.ts +2 -0
  295. package/dist/sources/credential-status.js +13 -0
  296. package/dist/sources/index.d.ts +10 -0
  297. package/dist/sources/index.js +10 -0
  298. package/dist/sources/resolve.d.ts +28 -0
  299. package/dist/sources/resolve.js +47 -0
  300. package/dist/sources/truncate-utf8.d.ts +7 -0
  301. package/dist/sources/truncate-utf8.js +18 -0
  302. package/dist/sources/types.d.ts +71 -0
  303. package/dist/sources/types.js +26 -0
  304. package/dist/testing/fetch-stub.d.ts +32 -0
  305. package/dist/testing/fetch-stub.js +24 -0
  306. package/dist/themes/image.d.ts +18 -0
  307. package/dist/themes/image.js +117 -0
  308. package/dist/themes/index.d.ts +4 -0
  309. package/dist/themes/index.js +4 -0
  310. package/dist/themes/installer.d.ts +50 -0
  311. package/dist/themes/installer.js +290 -0
  312. package/dist/themes/manifest.d.ts +102 -0
  313. package/dist/themes/manifest.js +86 -0
  314. package/dist/themes/paths.d.ts +14 -0
  315. package/dist/themes/paths.js +33 -0
  316. package/dist/tool-system/browser-bridge.d.ts +53 -11
  317. package/dist/tool-system/browser-bridge.js +8 -9
  318. package/dist/tool-system/builtin/agent-heartbeat.d.ts +2 -2
  319. package/dist/tool-system/builtin/agent-heartbeat.js +20 -6
  320. package/dist/tool-system/builtin/agent-notifications.d.ts +127 -72
  321. package/dist/tool-system/builtin/agent-notifications.js +291 -101
  322. package/dist/tool-system/builtin/agent-output-file.js +1 -3
  323. package/dist/tool-system/builtin/agent-progress.d.ts +6 -0
  324. package/dist/tool-system/builtin/agent-progress.js +83 -0
  325. package/dist/tool-system/builtin/agent-registry.d.ts +38 -2
  326. package/dist/tool-system/builtin/agent-registry.js +178 -9
  327. package/dist/tool-system/builtin/agent-transcript-translator.js +1 -1
  328. package/dist/tool-system/builtin/agent.d.ts +2 -0
  329. package/dist/tool-system/builtin/agent.js +274 -33
  330. package/dist/tool-system/builtin/background-jobs.d.ts +55 -2
  331. package/dist/tool-system/builtin/background-jobs.js +96 -4
  332. package/dist/tool-system/builtin/background-work.d.ts +6 -0
  333. package/dist/tool-system/builtin/background-work.js +8 -1
  334. package/dist/tool-system/builtin/bash.js +5 -2
  335. package/dist/tool-system/builtin/browser-tools.d.ts +6 -3
  336. package/dist/tool-system/builtin/browser-tools.js +89 -23
  337. package/dist/tool-system/builtin/edit-model-catalog.d.ts +6 -0
  338. package/dist/tool-system/builtin/edit-model-catalog.js +172 -12
  339. package/dist/tool-system/builtin/edit.js +2 -6
  340. package/dist/tool-system/builtin/file-cache.d.ts +2 -0
  341. package/dist/tool-system/builtin/file-cache.js +4 -0
  342. package/dist/tool-system/builtin/generate-image.js +17 -10
  343. package/dist/tool-system/builtin/generate-video.js +26 -7
  344. package/dist/tool-system/builtin/grep.js +2 -2
  345. package/dist/tool-system/builtin/image-uploader.js +7 -1
  346. package/dist/tool-system/builtin/index.d.ts +32 -7
  347. package/dist/tool-system/builtin/index.js +237 -167
  348. package/dist/tool-system/builtin/memory.d.ts +6 -0
  349. package/dist/tool-system/builtin/memory.js +36 -15
  350. package/dist/tool-system/builtin/panel.d.ts +9 -0
  351. package/dist/tool-system/builtin/panel.js +314 -0
  352. package/dist/tool-system/builtin/read.js +5 -1
  353. package/dist/tool-system/builtin/send-message-to-session.d.ts +6 -0
  354. package/dist/tool-system/builtin/send-message-to-session.js +67 -0
  355. package/dist/tool-system/builtin/sources.d.ts +8 -0
  356. package/dist/tool-system/builtin/sources.js +137 -0
  357. package/dist/tool-system/builtin/tool-search.js +7 -2
  358. package/dist/tool-system/builtin/update-automation-memory.js +2 -0
  359. package/dist/tool-system/builtin/web-fetch.js +17 -13
  360. package/dist/tool-system/builtin/web-search.js +1 -3
  361. package/dist/tool-system/capability-module.d.ts +93 -0
  362. package/dist/tool-system/capability-module.js +53 -0
  363. package/dist/tool-system/context.d.ts +77 -19
  364. package/dist/tool-system/executor.js +75 -50
  365. package/dist/tool-system/external-tool-exposure.d.ts +60 -0
  366. package/dist/tool-system/external-tool-exposure.js +304 -0
  367. package/dist/tool-system/mcp-manager.d.ts +11 -1
  368. package/dist/tool-system/mcp-manager.js +19 -4
  369. package/dist/tool-system/mcp-tool-policy.d.ts +9 -0
  370. package/dist/tool-system/mcp-tool-policy.js +34 -0
  371. package/dist/tool-system/panel-bridge.d.ts +53 -0
  372. package/dist/tool-system/panel-bridge.js +1 -0
  373. package/dist/tool-system/permission.d.ts +26 -0
  374. package/dist/tool-system/permission.js +46 -0
  375. package/dist/tool-system/registry.d.ts +12 -1
  376. package/dist/tool-system/registry.js +31 -7
  377. package/dist/tool-system/sandbox/index.js +3 -8
  378. package/dist/tool-system/sandbox/seatbelt.js +2 -6
  379. package/dist/tool-system/session-tool-host.d.ts +139 -0
  380. package/dist/tool-system/session-tool-host.js +167 -0
  381. package/dist/tool-system/testing/tool-registry-harness.d.ts +31 -0
  382. package/dist/tool-system/testing/tool-registry-harness.js +51 -0
  383. package/dist/tool-system/validation.d.ts +14 -7
  384. package/dist/tool-system/validation.js +701 -7
  385. package/dist/types.d.ts +186 -20
  386. package/dist/{cc-orchestrator → utils}/cwd-normalize.d.ts +1 -0
  387. package/dist/{cc-orchestrator → utils}/cwd-normalize.js +1 -0
  388. package/dist/utils/file-mutex.d.ts +65 -0
  389. package/dist/utils/file-mutex.js +145 -0
  390. package/dist/utils/json.d.ts +2 -4
  391. package/dist/utils/json.js +2 -4
  392. package/dist/utils/lockfile.d.ts +30 -1
  393. package/dist/utils/lockfile.js +2 -2
  394. package/dist/utils/secret-scrubber.d.ts +8 -0
  395. package/dist/utils/secret-scrubber.js +227 -0
  396. package/dist/utils/toolDisplay.js +1 -1
  397. package/package.json +20 -5
  398. package/THIRD_PARTY_NOTICES.md +0 -206
  399. package/dist/arena/arena.d.ts +0 -45
  400. package/dist/arena/arena.js +0 -362
  401. package/dist/arena/context/context-tools.d.ts +0 -16
  402. package/dist/arena/context/context-tools.js +0 -272
  403. package/dist/arena/context/within-root.d.ts +0 -7
  404. package/dist/arena/context/within-root.js +0 -15
  405. package/dist/arena/detect-mode.d.ts +0 -20
  406. package/dist/arena/detect-mode.js +0 -78
  407. package/dist/arena/digest-builder.d.ts +0 -25
  408. package/dist/arena/digest-builder.js +0 -120
  409. package/dist/arena/index.d.ts +0 -29
  410. package/dist/arena/index.js +0 -29
  411. package/dist/arena/iterate/convergence.d.ts +0 -25
  412. package/dist/arena/iterate/convergence.js +0 -103
  413. package/dist/arena/iterate/formats/index.d.ts +0 -22
  414. package/dist/arena/iterate/formats/index.js +0 -283
  415. package/dist/arena/iterate/index.d.ts +0 -11
  416. package/dist/arena/iterate/index.js +0 -9
  417. package/dist/arena/iterate/iterative-arena.d.ts +0 -23
  418. package/dist/arena/iterate/iterative-arena.js +0 -237
  419. package/dist/arena/iterate/parse.d.ts +0 -42
  420. package/dist/arena/iterate/parse.js +0 -123
  421. package/dist/arena/iterate/phases/argue.d.ts +0 -22
  422. package/dist/arena/iterate/phases/argue.js +0 -165
  423. package/dist/arena/iterate/phases/revise.d.ts +0 -16
  424. package/dist/arena/iterate/phases/revise.js +0 -62
  425. package/dist/arena/iterate/phases/tournament.d.ts +0 -34
  426. package/dist/arena/iterate/phases/tournament.js +0 -113
  427. package/dist/arena/iterate/tools/web-tools.d.ts +0 -13
  428. package/dist/arena/iterate/tools/web-tools.js +0 -54
  429. package/dist/arena/iterate/types.d.ts +0 -152
  430. package/dist/arena/iterate/types.js +0 -8
  431. package/dist/arena/ledger.d.ts +0 -47
  432. package/dist/arena/ledger.js +0 -159
  433. package/dist/arena/lenses/architecture.d.ts +0 -5
  434. package/dist/arena/lenses/architecture.js +0 -22
  435. package/dist/arena/lenses/engineering.d.ts +0 -5
  436. package/dist/arena/lenses/engineering.js +0 -22
  437. package/dist/arena/lenses/general.d.ts +0 -5
  438. package/dist/arena/lenses/general.js +0 -20
  439. package/dist/arena/lenses/index.d.ts +0 -16
  440. package/dist/arena/lenses/index.js +0 -47
  441. package/dist/arena/lenses/product.d.ts +0 -5
  442. package/dist/arena/lenses/product.js +0 -22
  443. package/dist/arena/model-presets.d.ts +0 -23
  444. package/dist/arena/model-presets.js +0 -44
  445. package/dist/arena/phases/adjudication.d.ts +0 -25
  446. package/dist/arena/phases/adjudication.js +0 -142
  447. package/dist/arena/phases/build-consensus.d.ts +0 -30
  448. package/dist/arena/phases/build-consensus.js +0 -85
  449. package/dist/arena/phases/claim-registry.d.ts +0 -26
  450. package/dist/arena/phases/claim-registry.js +0 -60
  451. package/dist/arena/phases/cross-review.d.ts +0 -47
  452. package/dist/arena/phases/cross-review.js +0 -224
  453. package/dist/arena/phases/debate-rounds.d.ts +0 -28
  454. package/dist/arena/phases/debate-rounds.js +0 -161
  455. package/dist/arena/phases/participant-research.d.ts +0 -39
  456. package/dist/arena/phases/participant-research.js +0 -316
  457. package/dist/arena/phases/planning-detail-expansion.d.ts +0 -39
  458. package/dist/arena/phases/planning-detail-expansion.js +0 -120
  459. package/dist/arena/planner.d.ts +0 -28
  460. package/dist/arena/planner.js +0 -312
  461. package/dist/arena/providers/docs.d.ts +0 -7
  462. package/dist/arena/providers/docs.js +0 -111
  463. package/dist/arena/providers/git.d.ts +0 -8
  464. package/dist/arena/providers/git.js +0 -174
  465. package/dist/arena/providers/index.d.ts +0 -32
  466. package/dist/arena/providers/index.js +0 -132
  467. package/dist/arena/providers/none.d.ts +0 -7
  468. package/dist/arena/providers/none.js +0 -11
  469. package/dist/arena/providers/repo.d.ts +0 -7
  470. package/dist/arena/providers/repo.js +0 -258
  471. package/dist/arena/render/session.d.ts +0 -17
  472. package/dist/arena/render/session.js +0 -190
  473. package/dist/arena/render/terminal.d.ts +0 -34
  474. package/dist/arena/render/terminal.js +0 -286
  475. package/dist/arena/strategies/discussion.d.ts +0 -25
  476. package/dist/arena/strategies/discussion.js +0 -143
  477. package/dist/arena/strategies/index.d.ts +0 -15
  478. package/dist/arena/strategies/index.js +0 -28
  479. package/dist/arena/strategies/language-wrapper.d.ts +0 -17
  480. package/dist/arena/strategies/language-wrapper.js +0 -102
  481. package/dist/arena/strategies/lens-wrapper.d.ts +0 -16
  482. package/dist/arena/strategies/lens-wrapper.js +0 -236
  483. package/dist/arena/strategies/planning.d.ts +0 -30
  484. package/dist/arena/strategies/planning.js +0 -225
  485. package/dist/arena/strategies/review.d.ts +0 -26
  486. package/dist/arena/strategies/review.js +0 -168
  487. package/dist/arena/strategies/utils.d.ts +0 -36
  488. package/dist/arena/strategies/utils.js +0 -603
  489. package/dist/arena/tools/selector.d.ts +0 -17
  490. package/dist/arena/tools/selector.js +0 -61
  491. package/dist/arena/transitions.d.ts +0 -53
  492. package/dist/arena/transitions.js +0 -97
  493. package/dist/arena/types.d.ts +0 -518
  494. package/dist/arena/types.js +0 -27
  495. package/dist/cc-orchestrator/agent-adapter.d.ts +0 -54
  496. package/dist/cc-orchestrator/agent-adapter.js +0 -143
  497. package/dist/cc-orchestrator/cc-capability.d.ts +0 -19
  498. package/dist/cc-orchestrator/cc-capability.js +0 -53
  499. package/dist/cc-orchestrator/codex-session-discovery.d.ts +0 -24
  500. package/dist/cc-orchestrator/codex-session-discovery.js +0 -191
  501. package/dist/cc-orchestrator/codex-session-history.d.ts +0 -38
  502. package/dist/cc-orchestrator/codex-session-history.js +0 -247
  503. package/dist/cc-orchestrator/external-agent-bindings.d.ts +0 -27
  504. package/dist/cc-orchestrator/external-agent-bindings.js +0 -150
  505. package/dist/cc-orchestrator/external-agent-changes.d.ts +0 -19
  506. package/dist/cc-orchestrator/external-agent-changes.js +0 -231
  507. package/dist/cc-orchestrator/external-agent-driver.d.ts +0 -19
  508. package/dist/cc-orchestrator/external-agent-driver.js +0 -284
  509. package/dist/cc-orchestrator/external-agent-session-store.d.ts +0 -23
  510. package/dist/cc-orchestrator/external-agent-session-store.js +0 -146
  511. package/dist/cc-orchestrator/index.d.ts +0 -8
  512. package/dist/cc-orchestrator/index.js +0 -8
  513. package/dist/cc-orchestrator/relevance-judge.d.ts +0 -15
  514. package/dist/cc-orchestrator/relevance-judge.js +0 -29
  515. package/dist/cc-orchestrator/session-discovery.d.ts +0 -46
  516. package/dist/cc-orchestrator/session-discovery.js +0 -125
  517. package/dist/cc-orchestrator/session-history.d.ts +0 -54
  518. package/dist/cc-orchestrator/session-history.js +0 -150
  519. package/dist/cron/cron-runtime.d.ts +0 -2
  520. package/dist/cron/cron-runtime.js +0 -2
  521. package/dist/cron/cron-store.d.ts +0 -2
  522. package/dist/cron/cron-store.js +0 -2
  523. package/dist/cron/scheduler.d.ts +0 -7
  524. package/dist/cron/scheduler.js +0 -7
  525. package/dist/engine/goal.js +0 -206
  526. package/dist/external-agents/config.d.ts +0 -2
  527. package/dist/external-agents/config.js +0 -15
  528. package/dist/external-agents/types.d.ts +0 -31
  529. package/dist/git/parse-log.d.ts +0 -13
  530. package/dist/git/parse-log.js +0 -21
  531. package/dist/git/utils.d.ts +0 -49
  532. package/dist/git/utils.js +0 -161
  533. package/dist/git/worktree/crud.d.ts +0 -72
  534. package/dist/git/worktree/crud.js +0 -235
  535. package/dist/git/worktree/diff.d.ts +0 -14
  536. package/dist/git/worktree/diff.js +0 -82
  537. package/dist/git/worktree/git-exec.d.ts +0 -7
  538. package/dist/git/worktree/git-exec.js +0 -87
  539. package/dist/git/worktree/index.d.ts +0 -5
  540. package/dist/git/worktree/index.js +0 -5
  541. package/dist/git/worktree/query.d.ts +0 -43
  542. package/dist/git/worktree/query.js +0 -128
  543. package/dist/git/worktree/slug.d.ts +0 -11
  544. package/dist/git/worktree/slug.js +0 -58
  545. package/dist/git/worktree.d.ts +0 -1
  546. package/dist/git/worktree.js +0 -5
  547. package/dist/lsp/client.d.ts +0 -41
  548. package/dist/lsp/client.js +0 -186
  549. package/dist/lsp/manager.d.ts +0 -40
  550. package/dist/lsp/manager.js +0 -155
  551. package/dist/lsp/root-path.d.ts +0 -9
  552. package/dist/lsp/root-path.js +0 -12
  553. package/dist/lsp/servers.d.ts +0 -16
  554. package/dist/lsp/servers.js +0 -60
  555. package/dist/prompt/sections/coding.md +0 -35
  556. package/dist/quota/credentials.d.ts +0 -3
  557. package/dist/quota/credentials.js +0 -80
  558. package/dist/quota/index.d.ts +0 -36
  559. package/dist/quota/index.js +0 -155
  560. package/dist/quota/types.d.ts +0 -48
  561. package/dist/quota/types.js +0 -13
  562. package/dist/review/review-prompt.d.ts +0 -28
  563. package/dist/review/review-prompt.js +0 -81
  564. package/dist/state.d.ts +0 -160
  565. package/dist/state.js +0 -267
  566. package/dist/tool-system/builtin/apply-patch/applier.d.ts +0 -26
  567. package/dist/tool-system/builtin/apply-patch/applier.js +0 -308
  568. package/dist/tool-system/builtin/apply-patch/backup-targets.d.ts +0 -10
  569. package/dist/tool-system/builtin/apply-patch/backup-targets.js +0 -30
  570. package/dist/tool-system/builtin/apply-patch/index.d.ts +0 -20
  571. package/dist/tool-system/builtin/apply-patch/index.js +0 -105
  572. package/dist/tool-system/builtin/apply-patch/parser.d.ts +0 -17
  573. package/dist/tool-system/builtin/apply-patch/parser.js +0 -212
  574. package/dist/tool-system/builtin/apply-patch/seek-sequence.d.ts +0 -18
  575. package/dist/tool-system/builtin/apply-patch/seek-sequence.js +0 -123
  576. package/dist/tool-system/builtin/apply-patch/types.d.ts +0 -49
  577. package/dist/tool-system/builtin/apply-patch/types.js +0 -13
  578. package/dist/tool-system/builtin/arena.d.ts +0 -31
  579. package/dist/tool-system/builtin/arena.js +0 -416
  580. package/dist/tool-system/builtin/brief.d.ts +0 -6
  581. package/dist/tool-system/builtin/brief.js +0 -41
  582. package/dist/tool-system/builtin/check-quota.d.ts +0 -15
  583. package/dist/tool-system/builtin/check-quota.js +0 -34
  584. package/dist/tool-system/builtin/drive-claude-code.d.ts +0 -51
  585. package/dist/tool-system/builtin/drive-claude-code.js +0 -698
  586. package/dist/tool-system/builtin/lsp.d.ts +0 -7
  587. package/dist/tool-system/builtin/lsp.js +0 -144
  588. package/dist/tool-system/builtin/notebook-edit.d.ts +0 -7
  589. package/dist/tool-system/builtin/notebook-edit.js +0 -127
  590. package/dist/tool-system/builtin/worktree.d.ts +0 -11
  591. package/dist/tool-system/builtin/worktree.js +0 -363
  592. /package/dist/{external-agents → links}/types.js +0 -0
  593. /package/dist/{engine/session-usage.d.ts → session/usage.d.ts} +0 -0
  594. /package/dist/{engine/session-usage.js → session/usage.js} +0 -0
@@ -15,10 +15,9 @@
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
+ import { isProtectedSettingKey, SettingsManager } from "../settings/manager.js";
19
19
  import { getApprovalRouter, getInteractiveApprovalBackend, } from "../tool-system/permission.js";
20
- import { getArenaStatus } from "../tool-system/builtin/arena.js";
21
- import { agentNotificationBus, notificationQueue, buildNotificationMessage, } from "../tool-system/builtin/agent-notifications.js";
20
+ import { agentNotificationBus, notificationQueue, buildNotificationMessage, notificationEnvelopeToLegacyStreamEvent, } from "../tool-system/builtin/agent-notifications.js";
22
21
  import { backgroundShellManager } from "../runtime/background-shell.js";
23
22
  import { backgroundJobRegistry } from "../tool-system/builtin/background-jobs.js";
24
23
  import { listBackgroundWorkForUI } from "../tool-system/builtin/background-work.js";
@@ -27,6 +26,8 @@ import { nanoid } from "nanoid";
27
26
  import { assertSafeSessionId, SessionManager } from "../session/session-manager.js";
28
27
  import { redactLlmConfig, maskSecretValue } from "./redact.js";
29
28
  import { redactSecrets } from "../logging/sanitize-messages.js";
29
+ import { computeEffectiveDisabledLists } from "../capability-control/disabled-lists.js";
30
+ import { describePluginCommands, expandPluginCommandBody, scanPluginCommands, MAX_PLUGIN_COMMAND_ARGUMENT_CHARS, } from "../plugins/pluginCommandsLoader.js";
30
31
  function isValidRunAttachment(value) {
31
32
  if (!value || typeof value !== "object")
32
33
  return false;
@@ -48,6 +49,30 @@ function runInputError(params) {
48
49
  if (params.task.trim().length === 0 && !hasAttachment) {
49
50
  return "task or a valid attachment is required";
50
51
  }
52
+ if (params.displayText !== undefined &&
53
+ (typeof params.displayText !== "string" ||
54
+ params.displayText.trim().length === 0 ||
55
+ params.displayText.length > 20_000)) {
56
+ return "displayText must be a non-empty string up to 20000 characters";
57
+ }
58
+ if (params.injected !== undefined && typeof params.injected !== "boolean") {
59
+ return "injected must be a boolean";
60
+ }
61
+ if (params.behaviorMode !== undefined &&
62
+ (typeof params.behaviorMode !== "string" || params.behaviorMode.length === 0)) {
63
+ return `invalid behavior mode: ${String(params.behaviorMode)}`;
64
+ }
65
+ if (params.kind !== undefined && (typeof params.kind !== "string" || params.kind.length === 0)) {
66
+ return `invalid session kind: ${String(params.kind)}`;
67
+ }
68
+ if (params.profileParams !== undefined &&
69
+ (!params.profileParams ||
70
+ typeof params.profileParams !== "object" ||
71
+ Array.isArray(params.profileParams))) {
72
+ return "profileParams must be an object";
73
+ }
74
+ // Domain-specific run-param validation (e.g. behavior-mode vocabularies and
75
+ // extension-owned param shapes) lives in ExtensionModule.validateRunParams.
51
76
  return null;
52
77
  }
53
78
  const COMPACT_STREAM_STRATEGIES = new Set([
@@ -57,14 +82,146 @@ const COMPACT_STREAM_STRATEGIES = new Set([
57
82
  "snip",
58
83
  "emergency",
59
84
  "compacted",
85
+ // Range archival (archive_range query) emits its boundary through this map;
86
+ // without "range" here it would be silently downgraded to "compacted".
87
+ "range",
60
88
  ]);
61
89
  function toCompactStreamStrategy(strategy) {
62
90
  return COMPACT_STREAM_STRATEGIES.has(strategy)
63
91
  ? strategy
64
92
  : "compacted";
65
93
  }
94
+ const INTERNAL_PENDING_TOOLS = new Set([
95
+ "__browser_action__",
96
+ "__credential_action__",
97
+ "__workspace_action__",
98
+ ]);
99
+ function internalCancellation(failure, reason) {
100
+ return { __internalCancelled: true, failure, reason };
101
+ }
102
+ function asInternalCancellation(value) {
103
+ return value &&
104
+ typeof value === "object" &&
105
+ value.__internalCancelled === true
106
+ ? value
107
+ : undefined;
108
+ }
109
+ /** Human-facing tail appended to a host-loopback failure detail. */
110
+ const HOST_LOOPBACK_FAILURE_DETAIL = {
111
+ denied: "declined by the user",
112
+ cancelled: "cancelled because the turn was stopped",
113
+ session_closed: "cancelled because the session closed",
114
+ owner_lost: "unavailable because the approving client disconnected",
115
+ timed_out: "timed out waiting for the host",
116
+ malformed: "returned a malformed result",
117
+ };
118
+ /**
119
+ * Cap on host-supplied failure text forwarded to the model.
120
+ *
121
+ * The rest of the Panel path is carefully bounded (512KB JSON results, 500-char
122
+ * tool descriptions, ≤16 tools); the failure path must be too. Host text reaches
123
+ * the model verbatim, so an unbounded echo is both a context-budget hazard and an
124
+ * injection surface. Collapsed to one line so a multi-line payload can't fake
125
+ * structure in the tool result.
126
+ */
127
+ const MAX_HOST_LOOPBACK_DETAIL_CHARS = 500;
128
+ function boundedHostDetail(text) {
129
+ const oneLine = text.replace(/\s+/gu, " ").trim();
130
+ return oneLine.length <= MAX_HOST_LOOPBACK_DETAIL_CHARS
131
+ ? oneLine
132
+ : `${oneLine.slice(0, MAX_HOST_LOOPBACK_DETAIL_CHARS)}…`;
133
+ }
134
+ /**
135
+ * Recover the classified failure detail a host-loopback request settled with, if
136
+ * any. Bridges call this before falling back to "malformed result": a cancelled /
137
+ * denied / timed-out request is a KNOWN terminal state, and mislabelling it as
138
+ * malformed blames the host for something the user (or a closing session) did.
139
+ */
140
+ function hostLoopbackDetail(result) {
141
+ if (!result || typeof result !== "object")
142
+ return undefined;
143
+ const candidate = result;
144
+ // Take the human-readable text FIRST, and do NOT require `failure` to be
145
+ // present. Genuine Desktop replies are `{ok:false, panelId?, detail}` and carry
146
+ // no `failure` key at all (see AgentPanelHostResult) — gating on `failure`
147
+ // discarded every real host error and relabelled it "malformed result", which
148
+ // is exactly the mislabelling this whole change exists to prevent.
149
+ // The bridges also disagree on the key: panel uses `error`, browser `detail`.
150
+ if (typeof candidate.error === "string" && candidate.error) {
151
+ return boundedHostDetail(candidate.error);
152
+ }
153
+ if (typeof candidate.detail === "string" && candidate.detail) {
154
+ return boundedHostDetail(candidate.detail);
155
+ }
156
+ // Classification only — never the raw string. `failure` arrives from
157
+ // host-parsed JSON, so echoing an unrecognized value verbatim would let the
158
+ // host inject arbitrary unbounded text into a tool result.
159
+ if (typeof candidate.failure === "string") {
160
+ return HOST_LOOPBACK_FAILURE_DETAIL[candidate.failure];
161
+ }
162
+ return undefined;
163
+ }
164
+ /**
165
+ * Normalize what a host-loopback resolver received into either the parsed host
166
+ * payload or a classified failure. Shared by all four bridges so their terminal
167
+ * semantics can't drift apart.
168
+ *
169
+ * Accepts `{approved:true, answer:<json>}` (the ApprovalResult shape main replies
170
+ * with), a bare JSON string, or an InternalPendingCancellation sentinel.
171
+ */
172
+ function parseHostLoopbackDecision(decision, label) {
173
+ const cancelled = asInternalCancellation(decision);
174
+ if (cancelled) {
175
+ return {
176
+ ok: false,
177
+ failure: cancelled.failure,
178
+ detail: `${label} ${HOST_LOOPBACK_FAILURE_DETAIL[cancelled.failure]}`,
179
+ };
180
+ }
181
+ let raw;
182
+ if (decision && typeof decision === "object" && "approved" in decision) {
183
+ const result = decision;
184
+ raw = result.approved ? result.answer : undefined;
185
+ }
186
+ else if (typeof decision === "string") {
187
+ raw = decision;
188
+ }
189
+ if (raw === undefined) {
190
+ return {
191
+ ok: false,
192
+ failure: "denied",
193
+ detail: `${label} ${HOST_LOOPBACK_FAILURE_DETAIL.denied}`,
194
+ };
195
+ }
196
+ try {
197
+ return { ok: true, value: JSON.parse(raw) };
198
+ }
199
+ catch {
200
+ // The host answered but garbled it. That is NOT a denial — blaming the user
201
+ // for a host serialization bug sends the model looking in the wrong place.
202
+ return {
203
+ ok: false,
204
+ failure: "malformed",
205
+ detail: `${label} ${HOST_LOOPBACK_FAILURE_DETAIL.malformed}`,
206
+ };
207
+ }
208
+ }
209
+ function safeApprovalToolName(toolName) {
210
+ const firstLine = toolName.split(/\r?\n/, 1)[0]?.trim() ?? "";
211
+ if (/(?:sk|api|token|secret)[-_][a-z0-9_-]{6,}/i.test(firstLine))
212
+ return "工具";
213
+ return (firstLine
214
+ .replace(/[^\p{L}\p{N}_.:@/-]+/gu, " ")
215
+ .trim()
216
+ .slice(0, 40) || "工具");
217
+ }
66
218
  export class AgentServer {
67
- chatManager;
219
+ /** Host-supplied manager; requests route through the `chatManager` getter. */
220
+ baseChatManager;
221
+ /** Identity resolution hook; null → single-manager behavior (today's path). */
222
+ resolveIdentity;
223
+ /** Lazily-derived managers keyed by identity (non-base identities only). */
224
+ identityManagers = new Map();
68
225
  legacyEngine;
69
226
  globalQueryEngine = null;
70
227
  transport;
@@ -72,13 +229,24 @@ export class AgentServer {
72
229
  settingsReader;
73
230
  /** Disk-only active-goal reader for agent/goalGet on a non-live session. */
74
231
  readActiveGoalFromDisk;
232
+ updateActiveGoalOnDisk;
233
+ clearActiveGoalOnDisk;
75
234
  workspaceBridgeEnabled;
235
+ panelBridgeEnabled;
76
236
  connectionId;
77
237
  strictApprovalRouting;
78
238
  approvalRouter;
79
239
  approvalConnectionUnregister = null;
80
240
  disconnected = false;
81
241
  pendingApprovalTargets = new Map();
242
+ /** Extension modules this server consults for protocol-level hooks. */
243
+ extensionModules;
244
+ /** Protocol lifecycle observers created by extension modules (registration order). */
245
+ protocolObservers = [];
246
+ /** Extension-registered protocol method/query aliases (compat channel). */
247
+ protocolQueryHandlers = new Map();
248
+ /** Union of extension hiddenSessionKinds, excluded from generic session lists. */
249
+ hiddenSessionKinds;
82
250
  /**
83
251
  * Last per-session EngineConfigSlice supplied by agent/run. Kept even after
84
252
  * idle eviction so background-completion wakeups can rebuild the ChatSession
@@ -88,6 +256,8 @@ export class AgentServer {
88
256
  lastSliceBySession = new Map();
89
257
  /** Lazy disk reader for cold wakeup rehydrate when this process lacks a slice. */
90
258
  diskSessionReader = null;
259
+ /** Sessions dir for diskSessionReader; undefined → default sessions root. */
260
+ sessionDiskRoot;
91
261
  /**
92
262
  * Monotonic config-reload version, bumped per reloadSettings request so each
93
263
  * Engine.refreshRuntimeConfig can drop out-of-order (stale) deliveries (Q5).
@@ -129,12 +299,40 @@ export class AgentServer {
129
299
  */
130
300
  bgAgentBusUnsubscribe = null;
131
301
  wakeupsInFlight = new Set();
302
+ /**
303
+ * Effective session manager for the connection this server serves. Without
304
+ * a resolveIdentity hook this is exactly the host-supplied manager —
305
+ * today's single-manager behavior, byte-for-byte. With the hook, requests
306
+ * route through a lazily-created per-identity manager, so the same
307
+ * sessionId under two identities resolves to two isolated ChatSessions and
308
+ * a connection can only list/stream sessions of its own identity.
309
+ */
310
+ get chatManager() {
311
+ if (!this.baseChatManager || !this.resolveIdentity)
312
+ return this.baseChatManager;
313
+ const identity = this.resolveIdentity({ connectionId: this.connectionId });
314
+ if (identity === this.baseChatManager.identity)
315
+ return this.baseChatManager;
316
+ let manager = this.identityManagers.get(identity);
317
+ if (!manager) {
318
+ // forIdentity validates the identity as a safe path segment and throws
319
+ // on garbage — the request that triggered routing then fails closed.
320
+ manager = this.baseChatManager.forIdentity(identity);
321
+ this.identityManagers.set(identity, manager);
322
+ }
323
+ return manager;
324
+ }
132
325
  constructor(options) {
133
- this.chatManager = options.chatManager ?? null;
326
+ this.baseChatManager = options.chatManager ?? null;
327
+ this.resolveIdentity = options.resolveIdentity ?? null;
328
+ this.sessionDiskRoot = options.sessionDiskRoot;
134
329
  this.legacyEngine = options.engine ?? null;
135
330
  this.settingsReader = options.settingsReader ?? null;
136
331
  this.readActiveGoalFromDisk = options.readActiveGoalFromDisk ?? null;
332
+ this.updateActiveGoalOnDisk = options.updateActiveGoalOnDisk ?? null;
333
+ this.clearActiveGoalOnDisk = options.clearActiveGoalOnDisk ?? null;
137
334
  this.workspaceBridgeEnabled = options.workspaceBridge === true;
335
+ this.panelBridgeEnabled = options.panelBridge === true;
138
336
  this.connectionId = options.connectionId ?? "legacy-agent-server";
139
337
  this.strictApprovalRouting = options.connectionId !== undefined;
140
338
  this.approvalRouter = options.approvalRouter ?? getApprovalRouter();
@@ -142,10 +340,45 @@ export class AgentServer {
142
340
  this.approvalRouter.deregister(this.connectionId, "legacy approval host replaced");
143
341
  }
144
342
  getInteractiveApprovalBackend().clearPromptFn();
145
- if (!this.chatManager && !this.legacyEngine) {
343
+ if (!this.baseChatManager && !this.legacyEngine) {
146
344
  throw new Error("AgentServer: either chatManager or engine must be supplied");
147
345
  }
148
346
  this.transport = options.transport;
347
+ // Protocol lifecycle observers. Each configured extension module may
348
+ // attach one; the server calls them at every lifecycle hook point and
349
+ // isolates per-observer failures. Core default-loads the pet extension
350
+ // until the pet domain moves out of core (see AgentServerOptions).
351
+ this.extensionModules = options.extensionModules ?? [];
352
+ this.hiddenSessionKinds = [
353
+ ...new Set(this.extensionModules.flatMap((module) => module.hiddenSessionKinds ?? [])),
354
+ ];
355
+ const observerHost = {
356
+ getLiveSessionSnapshot: () => this.chatManager?.getLiveSessionSnapshot().sessions ?? [],
357
+ projectionGeneration: () => this.workerGeneration(),
358
+ getSessionKind: (sessionId) => {
359
+ const session = this.chatManager?.get(sessionId);
360
+ if (!session)
361
+ return undefined;
362
+ return session.engine
363
+ .getSessionManager?.()
364
+ .readSessionKind?.(sessionId);
365
+ },
366
+ isTransportDisconnected: () => this.disconnected,
367
+ notify: (method, params) => this.notify(method, params),
368
+ registerQuery: (type, handler) => {
369
+ this.protocolQueryHandlers.set(type, handler);
370
+ },
371
+ };
372
+ for (const module of this.extensionModules) {
373
+ if (!module.createProtocolObserver)
374
+ continue;
375
+ try {
376
+ this.protocolObservers.push(module.createProtocolObserver(observerHost));
377
+ }
378
+ catch (err) {
379
+ logger.warn(`protocol observer init failed for extension ${module.id}: ${err.message}`);
380
+ }
381
+ }
149
382
  // Wire up incoming messages
150
383
  this.transport.onMessage((msg) => {
151
384
  if (isRequest(msg)) {
@@ -177,8 +410,11 @@ export class AgentServer {
177
410
  // polls (background-jobs.ts). The bus now guarantees a real sessionId
178
411
  // (no more legacy-bucket coercion to ""), so we forward whatever sid it
179
412
  // hands us.
180
- this.bgAgentBusUnsubscribe = agentNotificationBus.subscribe((sessionId, event) => {
181
- this.notify(Methods.StreamEvent, { sessionId, event });
413
+ this.bgAgentBusUnsubscribe = agentNotificationBus.subscribe((envelope) => {
414
+ const sessionId = envelope.to.sessionId;
415
+ const event = notificationEnvelopeToLegacyStreamEvent(envelope);
416
+ if (event)
417
+ this.notify(Methods.StreamEvent, { sessionId, event });
182
418
  // Background work that finishes while the session is IDLE (a
183
419
  // run_in_background Bash like a download, a background sub-agent, or a
184
420
  // video poll — the engine no longer parks on any of them) would otherwise
@@ -190,7 +426,8 @@ export class AgentServer {
190
426
  // (trigger B) drains it at end-of-turn instead. A never-exiting dev server
191
427
  // emits no completion, so it never wakes anything (no task/service
192
428
  // classification needed).
193
- this.maybeWakeIdleSession(sessionId);
429
+ if (envelope.kind === "result")
430
+ this.maybeWakeIdleSession(sessionId);
194
431
  });
195
432
  // Notify client we're ready
196
433
  this.notify(Methods.Status, { status: "ready" });
@@ -339,8 +576,8 @@ export class AgentServer {
339
576
  if (cached)
340
577
  return { ...cached };
341
578
  if (!this.diskSessionReader)
342
- this.diskSessionReader = new SessionManager();
343
- const cwd = this.diskSessionReader.readCwd(sessionId);
579
+ this.diskSessionReader = new SessionManager(this.sessionDiskRoot);
580
+ const cwd = this.diskSessionReader.readSessionMainRoot(sessionId);
344
581
  if (!cwd)
345
582
  return null;
346
583
  return {
@@ -357,9 +594,88 @@ export class AgentServer {
357
594
  session.engine.setAskUser((question, opts) => this.requestAskUserForSession(session, sid, question, opts));
358
595
  session.engine.setBrowserBridge(this.makeBrowserBridge(session, sid));
359
596
  session.engine.setInjectCredential((credentialId, credentialScope) => this.requestCredentialInjectForSession(session, sid, credentialId, credentialScope));
597
+ session.engine.setSessionMessageRouter((input) => this.routeSessionMessage(input));
360
598
  if (this.workspaceBridgeEnabled && typeof session.engine.setWorkspaceBridge === "function") {
361
599
  session.engine.setWorkspaceBridge(this.makeWorkspaceBridge(session, sid));
362
600
  }
601
+ if (this.panelBridgeEnabled && typeof session.engine.setPanelBridge === "function") {
602
+ session.engine.setPanelBridge(this.makePanelBridge(session, sid));
603
+ }
604
+ }
605
+ /** Queue a model-sent message as an ordinary user turn in another Session. */
606
+ async routeSessionMessage(input) {
607
+ const manager = this.chatManager;
608
+ if (!manager)
609
+ throw new Error("cross-Session messaging requires a multi-session host");
610
+ const targetId = input.target.sessionId;
611
+ if (manager.isUnavailable(targetId)) {
612
+ throw new Error(`target Session is closing or closed: ${targetId}`);
613
+ }
614
+ const sourceSlice = this.lastSliceBySession.get(input.sourceSessionId);
615
+ const targetSlice = {
616
+ cwd: input.target.workspaceRoot,
617
+ projectTrusted: sourceSlice?.projectTrusted ?? false,
618
+ };
619
+ const targetAlreadyExists = manager.sessionExistsOnDisk(targetId, targetSlice);
620
+ const targetSession = await manager.getOrCreate(targetId, targetSlice);
621
+ const approvalRegistration = this.approvalRouter.register(targetId, this.connectionId);
622
+ if (!approvalRegistration.ok) {
623
+ throw new Error(`target Session ${targetId} is owned by another connection`);
624
+ }
625
+ this.rememberSessionSlice(targetId, targetSlice);
626
+ this.observeSessionAttached(targetId, targetSession.lastActivityAt);
627
+ this.wireInteractiveSession(targetSession, targetId);
628
+ const userMessageEvent = {
629
+ type: "session_user_message",
630
+ text: input.message,
631
+ };
632
+ this.observeSessionStream(targetId, userMessageEvent);
633
+ this.notify(Methods.StreamEvent, { sessionId: targetId, event: userMessageEvent });
634
+ const run = targetSession.enqueueTurn(input.message, {
635
+ cwd: input.target.workspaceRoot,
636
+ // A planned Session needs its renderer-selected initial profile. Once a
637
+ // Session exists, omitting this field makes Engine use the target's own
638
+ // persisted binding, so a stale source catalog cannot switch it back.
639
+ workspaceProfile: targetAlreadyExists ? undefined : input.target.workspaceProfile,
640
+ sessionMessageTargets: input.catalog,
641
+ onStream: (event) => {
642
+ this.observeSessionStream(targetId, event);
643
+ this.notify(Methods.StreamEvent, { sessionId: targetId, event });
644
+ },
645
+ approvalRouter: this.approvalRouter,
646
+ });
647
+ // The turn itself must NOT block the sender — a member's work can take
648
+ // minutes and the lead has more to dispatch. But a turn that fails to *start*
649
+ // (most often: the target's digital human is not installed, so run-setup
650
+ // throws "Workspace profile ... is unavailable") used to be reported only to
651
+ // the log while SendMessageToSession still answered "has queued the turn".
652
+ // A real lead then waited two hours and re-sent, never learning the reason.
653
+ // So: surface an immediate failure to the caller, keep a slow turn detached.
654
+ let startupError;
655
+ const tracked = run
656
+ .then(() => this.maybeWakeIdleSession(targetId))
657
+ .catch((error) => {
658
+ startupError = error;
659
+ logger.warn("session_message.turn_failed", {
660
+ sourceSessionId: input.sourceSessionId,
661
+ targetSessionId: targetId,
662
+ error: error instanceof Error ? error.message : String(error),
663
+ });
664
+ this.notify(Methods.StreamEvent, {
665
+ sessionId: targetId,
666
+ event: {
667
+ type: "error",
668
+ error: error instanceof Error ? error.message : "cross-Session message failed",
669
+ },
670
+ });
671
+ });
672
+ // One macrotask is enough for a synchronous-throw path to settle; anything
673
+ // still running by then is genuine work and stays detached.
674
+ await Promise.race([tracked, new Promise((resolve) => setTimeout(resolve, 0))]);
675
+ if (startupError) {
676
+ throw startupError instanceof Error ? startupError : new Error(String(startupError));
677
+ }
678
+ void tracked;
363
679
  }
364
680
  // ─── Request Dispatch ───────────────────────────────────────────
365
681
  async handleRequest(req) {
@@ -368,7 +684,7 @@ export class AgentServer {
368
684
  await this.handleRun(req);
369
685
  break;
370
686
  case Methods.ForkSession:
371
- this.handleForkSession(req);
687
+ await this.handleForkSession(req);
372
688
  break;
373
689
  case Methods.Approve:
374
690
  this.handleApprove(req);
@@ -397,9 +713,18 @@ export class AgentServer {
397
713
  case Methods.ReleaseWorkspace:
398
714
  this.handleReleaseWorkspace(req);
399
715
  break;
716
+ case Methods.SetWorkspace:
717
+ this.handleSetWorkspace(req);
718
+ break;
400
719
  case Methods.GoalExtend:
401
720
  this.handleGoalExtend(req);
402
721
  break;
722
+ case Methods.GoalUpdate:
723
+ await this.handleGoalUpdate(req);
724
+ break;
725
+ case Methods.GoalDelete:
726
+ this.handleGoalClear(req, true);
727
+ break;
403
728
  case Methods.GoalClear:
404
729
  this.handleGoalClear(req);
405
730
  break;
@@ -412,31 +737,210 @@ export class AgentServer {
412
737
  case Methods.BackgroundWork:
413
738
  this.handleBackgroundWork(req);
414
739
  break;
740
+ case Methods.PluginCommandsList:
741
+ this.handlePluginCommandsList(req);
742
+ break;
743
+ case Methods.PluginCommandExpand:
744
+ this.handlePluginCommandExpand(req);
745
+ break;
746
+ // Compat channel: the wire method name is retained, but the handler is
747
+ // whatever extension registered it (the pet extension by default).
748
+ case Methods.GetPetProjectionSnapshot:
749
+ await this.handleExtensionProtocolMethod(req);
750
+ break;
415
751
  default:
416
752
  this.transport.send(createErrorResponse(req.id, ErrorCodes.MethodNotFound, `Unknown method: ${req.method}`));
417
753
  }
418
754
  }
755
+ pluginCommandContext(cwd) {
756
+ if (typeof cwd !== "string" ||
757
+ cwd.trim().length === 0 ||
758
+ cwd.length > 4_096 ||
759
+ cwd.includes("\0")) {
760
+ return { error: "cwd must be a non-empty string of at most 4096 characters" };
761
+ }
762
+ const disabledPluginNames = new Set(computeEffectiveDisabledLists(new SettingsManager(cwd, "full"), cwd).disabledPlugins);
763
+ return { cwd, disabledPluginNames };
764
+ }
765
+ handlePluginCommandsList(req) {
766
+ const params = (req.params ?? {});
767
+ const context = this.pluginCommandContext(params.cwd);
768
+ if ("error" in context) {
769
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, context.error));
770
+ return;
771
+ }
772
+ try {
773
+ this.transport.send(createResponse(req.id, {
774
+ commands: describePluginCommands(scanPluginCommands(), context.disabledPluginNames),
775
+ }));
776
+ }
777
+ catch (error) {
778
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, error.message));
779
+ }
780
+ }
781
+ handlePluginCommandExpand(req) {
782
+ const params = (req.params ?? {});
783
+ const context = this.pluginCommandContext(params.cwd);
784
+ if ("error" in context) {
785
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, context.error));
786
+ return;
787
+ }
788
+ if (typeof params.name !== "string" ||
789
+ params.name.length === 0 ||
790
+ params.name.length > 512 ||
791
+ params.name.includes("\0")) {
792
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "name must be a non-empty string of at most 512 characters"));
793
+ return;
794
+ }
795
+ const rawArguments = params.rawArguments ?? "";
796
+ if (typeof rawArguments !== "string" ||
797
+ rawArguments.length > MAX_PLUGIN_COMMAND_ARGUMENT_CHARS) {
798
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, `rawArguments must be a string of at most ${MAX_PLUGIN_COMMAND_ARGUMENT_CHARS} characters`));
799
+ return;
800
+ }
801
+ const command = scanPluginCommands().find((candidate) => candidate.name === params.name && !context.disabledPluginNames.has(candidate.pluginName));
802
+ if (!command) {
803
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, `plugin command is unavailable: ${params.name}`));
804
+ return;
805
+ }
806
+ try {
807
+ this.transport.send(createResponse(req.id, {
808
+ prompt: expandPluginCommandBody(command.body, rawArguments),
809
+ }));
810
+ }
811
+ catch (error) {
812
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, error.message));
813
+ }
814
+ }
815
+ /** Route a protocol method to its extension-registered handler, if any. */
816
+ async handleExtensionProtocolMethod(req) {
817
+ const handler = this.protocolQueryHandlers.get(req.method);
818
+ if (!handler) {
819
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.MethodNotFound, `Unknown method: ${req.method}`));
820
+ return;
821
+ }
822
+ try {
823
+ const data = await handler((req.params ?? {}));
824
+ this.transport.send(createResponse(req.id, data));
825
+ }
826
+ catch (err) {
827
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
828
+ }
829
+ }
830
+ // ─── Protocol observers ─────────────────────────────────────────
831
+ // Domain-agnostic lifecycle dispatch. Observers run in registration order
832
+ // and a throwing observer never breaks the protocol path or its peers.
833
+ forEachObserver(hook, fn) {
834
+ for (const observer of this.protocolObservers) {
835
+ try {
836
+ fn(observer);
837
+ }
838
+ catch (err) {
839
+ logger.warn(`protocol observer ${hook} hook failed: ${err.message}`);
840
+ }
841
+ }
842
+ }
843
+ observeSessionAttached(sessionId, lastActivityAt) {
844
+ this.forEachObserver("onSessionAttached", (observer) => observer.onSessionAttached?.(sessionId, lastActivityAt));
845
+ }
846
+ observeSessionStream(sessionId, event) {
847
+ this.forEachObserver("onSessionStream", (observer) => observer.onSessionStream?.(sessionId, event));
848
+ }
849
+ observeRunBoundary(sessionId, phase) {
850
+ this.forEachObserver("onRunBoundary", (observer) => observer.onRunBoundary?.(sessionId, phase));
851
+ }
852
+ /** Observers may replace approval metadata (e.g. surfaceable overrides). */
853
+ observeApprovalCreated(metadata) {
854
+ for (const observer of this.protocolObservers) {
855
+ try {
856
+ const next = observer.onApprovalCreated?.(metadata);
857
+ if (next)
858
+ metadata = next;
859
+ }
860
+ catch (err) {
861
+ logger.warn(`protocol observer onApprovalCreated hook failed: ${err.message}`);
862
+ }
863
+ }
864
+ return metadata;
865
+ }
866
+ observeApprovalTransition(metadata, status) {
867
+ this.forEachObserver("onApprovalTransition", (observer) => observer.onApprovalTransition?.(metadata, status));
868
+ }
869
+ observeSessionClosed(sessionId) {
870
+ this.forEachObserver("onSessionClosed", (observer) => observer.onSessionClosed?.(sessionId));
871
+ }
872
+ observeServerClose() {
873
+ this.forEachObserver("onServerClose", (observer) => observer.onServerClose?.());
874
+ }
875
+ /** Generic run-param shape checks plus each extension's domain validation. */
876
+ validateRunInput(params) {
877
+ const generic = runInputError(params);
878
+ if (generic)
879
+ return generic;
880
+ for (const module of this.extensionModules) {
881
+ if (!module.validateRunParams)
882
+ continue;
883
+ const error = module.validateRunParams(params);
884
+ if (error)
885
+ return error;
886
+ }
887
+ return null;
888
+ }
419
889
  // ─── Run ────────────────────────────────────────────────────────
420
- handleForkSession(req) {
890
+ async handleForkSession(req) {
421
891
  const params = req.params ?? {};
422
892
  const sourceSessionId = typeof params.sourceSessionId === "string" ? params.sourceSessionId : "";
423
893
  const targetSessionId = typeof params.targetSessionId === "string" ? params.targetSessionId : undefined;
424
894
  const throughEventId = typeof params.throughEventId === "string" ? params.throughEventId : undefined;
425
- if (!sourceSessionId || params.mode !== "full") {
426
- this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "forkSession requires sourceSessionId and mode=full"));
895
+ const fromEventId = typeof params.fromEventId === "string" ? params.fromEventId : undefined;
896
+ const toEventId = typeof params.toEventId === "string" ? params.toEventId : undefined;
897
+ const mode = params.mode;
898
+ const isSideFork = params.forkKind === "side";
899
+ if (!sourceSessionId || (mode !== "full" && mode !== "summary")) {
900
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "forkSession requires sourceSessionId and mode=full|summary"));
427
901
  return;
428
902
  }
429
- if (params.targetSessionId !== undefined && typeof params.targetSessionId !== "string") {
430
- this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "targetSessionId must be a string"));
903
+ if (params.targetSessionId !== undefined &&
904
+ (typeof params.targetSessionId !== "string" || params.targetSessionId.length === 0)) {
905
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "targetSessionId must be a non-empty string"));
431
906
  return;
432
907
  }
433
908
  if (params.throughEventId !== undefined && typeof params.throughEventId !== "string") {
434
909
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "throughEventId must be a string"));
435
910
  return;
436
911
  }
912
+ if (mode === "summary" && (!fromEventId || !toEventId)) {
913
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "summary fork requires fromEventId and toEventId"));
914
+ return;
915
+ }
916
+ if (mode === "summary" &&
917
+ (params.throughEventId !== undefined ||
918
+ params.forkKind !== undefined ||
919
+ params.quickChatClaimId !== undefined)) {
920
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "summary fork only accepts fromEventId/toEventId range fields"));
921
+ return;
922
+ }
923
+ if (mode === "full" && (params.fromEventId !== undefined || params.toEventId !== undefined)) {
924
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "full fork does not accept summary range fields"));
925
+ return;
926
+ }
927
+ if (mode === "full" &&
928
+ params.quickChatClaimId !== undefined &&
929
+ typeof params.quickChatClaimId !== "string") {
930
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "quickChatClaimId must be a string"));
931
+ return;
932
+ }
933
+ if (params.forkKind !== undefined && !isSideFork) {
934
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "forkKind must be side when present"));
935
+ return;
936
+ }
937
+ if (isSideFork && throughEventId !== undefined) {
938
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "side fork uses the last completed turn and cannot specify throughEventId"));
939
+ return;
940
+ }
437
941
  try {
438
942
  assertSafeSessionId(sourceSessionId);
439
- if (targetSessionId)
943
+ if (targetSessionId !== undefined)
440
944
  assertSafeSessionId(targetSessionId);
441
945
  }
442
946
  catch (err) {
@@ -446,12 +950,30 @@ export class AgentServer {
446
950
  let source;
447
951
  try {
448
952
  source = this.chatManager?.get(sourceSessionId);
953
+ if (!source && this.chatManager) {
954
+ if (this.chatManager.isUnavailable(sourceSessionId)) {
955
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, `Session is closing or closed: ${sourceSessionId}`));
956
+ return;
957
+ }
958
+ source =
959
+ (await this.chatManager.getOrCreatePersisted(sourceSessionId, this.lastSliceBySession.get(sourceSessionId), mode === "summary")) ?? undefined;
960
+ if (source) {
961
+ this.rememberSessionSlice(sourceSessionId, {
962
+ ...this.lastSliceBySession.get(sourceSessionId),
963
+ cwd: source.engine.getConfig().cwd,
964
+ });
965
+ }
966
+ }
449
967
  }
450
968
  catch (err) {
451
969
  this.transport.send(createErrorResponse(req.id, ErrorCodes.Overloaded, err instanceof Error ? err.message : String(err)));
452
970
  return;
453
971
  }
454
- const engine = source?.engine ?? this.legacyEngine ?? this.anyEngine();
972
+ if (!isSideFork && source && (source.isBusy() || source.queueDepth() > 0)) {
973
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.Overloaded, "source session is still producing or has queued turns"));
974
+ return;
975
+ }
976
+ const engine = source?.engine ?? this.legacyEngine;
455
977
  if (!engine || !engine.sessionExistsOnDisk(sourceSessionId)) {
456
978
  this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionNotFound, `Session not found: ${sourceSessionId}`));
457
979
  return;
@@ -461,7 +983,56 @@ export class AgentServer {
461
983
  return;
462
984
  }
463
985
  try {
464
- const result = engine.forkSession(sourceSessionId, { targetSessionId, throughEventId });
986
+ if (mode === "summary") {
987
+ const sourceRange = { fromEventId: fromEventId, toEventId: toEventId };
988
+ const packageAndPublish = async (signal) => {
989
+ const selected = engine.selectContextPackage(sourceSessionId, sourceRange);
990
+ const packaged = await engine.summarizeContextPackage(selected.messages, signal, sourceSessionId);
991
+ const result = engine.createSummaryFork(sourceSessionId, {
992
+ targetSessionId,
993
+ ...sourceRange,
994
+ summary: packaged.summary,
995
+ sourceEventCount: selected.sourceEventCount,
996
+ estimatedTokens: packaged.estimatedTokens,
997
+ });
998
+ return { selected, packaged, result };
999
+ };
1000
+ let packagedResult;
1001
+ if (source) {
1002
+ packagedResult = await source.runExclusive(packageAndPublish);
1003
+ }
1004
+ else {
1005
+ if (this.running)
1006
+ throw new Error("source session is still producing or has queued turns");
1007
+ this.running = true;
1008
+ this.abortController = new AbortController();
1009
+ try {
1010
+ packagedResult = await packageAndPublish(this.abortController.signal);
1011
+ }
1012
+ finally {
1013
+ this.abortController = null;
1014
+ this.running = false;
1015
+ }
1016
+ }
1017
+ const { packaged, result } = packagedResult;
1018
+ const workspace = result.bundle.state.workspace ?? {
1019
+ root: result.bundle.state.cwd,
1020
+ kind: "main",
1021
+ };
1022
+ this.transport.send(createResponse(req.id, {
1023
+ sessionId: result.bundle.state.sessionId,
1024
+ mode: "summary",
1025
+ summary: packaged.summary,
1026
+ sourceRange,
1027
+ estimatedTokens: packaged.estimatedTokens,
1028
+ forkedFrom: result.lineage,
1029
+ workspace,
1030
+ }));
1031
+ return;
1032
+ }
1033
+ const result = engine.forkSession(sourceSessionId, isSideFork
1034
+ ? { targetSessionId, snapshotMode: "completed", ephemeral: true }
1035
+ : { targetSessionId, throughEventId });
465
1036
  const workspace = result.bundle.state.workspace ?? {
466
1037
  root: result.bundle.state.cwd,
467
1038
  kind: "main",
@@ -478,9 +1049,11 @@ export class AgentServer {
478
1049
  const message = err instanceof Error ? err.message : String(err);
479
1050
  const code = /not found/i.test(message)
480
1051
  ? ErrorCodes.SessionNotFound
481
- : /already exists|invalid|cursor|metadata|unfinished|orphaned|unsupported|malformed/i.test(message)
482
- ? ErrorCodes.InvalidParams
483
- : ErrorCodes.InternalError;
1052
+ : /still producing|queued turns|busy/i.test(message)
1053
+ ? ErrorCodes.Overloaded
1054
+ : /already exists|invalid|cursor|metadata|unfinished|orphaned|unsupported|malformed/i.test(message)
1055
+ ? ErrorCodes.InvalidParams
1056
+ : ErrorCodes.InternalError;
484
1057
  this.transport.send(createErrorResponse(req.id, code, message));
485
1058
  }
486
1059
  }
@@ -499,7 +1072,7 @@ export class AgentServer {
499
1072
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
500
1073
  return;
501
1074
  }
502
- const inputError = runInputError(params);
1075
+ const inputError = this.validateRunInput(params);
503
1076
  if (inputError) {
504
1077
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, inputError));
505
1078
  return;
@@ -512,7 +1085,7 @@ export class AgentServer {
512
1085
  : undefined,
513
1086
  };
514
1087
  if (params.requireExisting === true && !cm.get(params.sessionId)) {
515
- let existsOnDisk = false;
1088
+ let existsOnDisk;
516
1089
  try {
517
1090
  existsOnDisk = cm.sessionExistsOnDisk(params.sessionId, sessionConfig);
518
1091
  }
@@ -536,6 +1109,7 @@ export class AgentServer {
536
1109
  return;
537
1110
  }
538
1111
  const sid = params.sessionId;
1112
+ this.observeSessionAttached(sid, session.lastActivityAt);
539
1113
  const approvalRegistration = this.approvalRouter.register(sid, this.connectionId);
540
1114
  if (!approvalRegistration.ok) {
541
1115
  this.transport.send(createErrorResponse(req.id, ErrorCodes.Overloaded, `Session ${sid} is owned by another connection`));
@@ -560,27 +1134,56 @@ export class AgentServer {
560
1134
  // these host callbacks must be installed after every create/recreate.
561
1135
  this.wireInteractiveSession(session, sid);
562
1136
  try {
1137
+ const displayText = typeof params.displayText === "string" ? params.displayText.trim() : "";
1138
+ if (displayText) {
1139
+ const userMessageEvent = {
1140
+ type: "session_user_message",
1141
+ text: displayText,
1142
+ ...(typeof params.clientMessageId === "string"
1143
+ ? { clientMessageId: params.clientMessageId }
1144
+ : {}),
1145
+ };
1146
+ this.observeSessionStream(sid, userMessageEvent);
1147
+ this.notify(Methods.StreamEvent, { sessionId: sid, event: userMessageEvent });
1148
+ }
563
1149
  const run = session.enqueueTurn(params.task, {
564
1150
  cwd: params.cwd,
1151
+ displayText: displayText || undefined,
1152
+ injected: params.injected === true,
565
1153
  attachments: Array.isArray(params.attachments) ? params.attachments : undefined,
566
1154
  goal: typeof params.goal === "string" ||
567
1155
  (params.goal != null && typeof params.goal === "object")
568
1156
  ? params.goal
569
1157
  : undefined,
570
- onStream: (event) => this.notify(Methods.StreamEvent, { sessionId: sid, event }),
1158
+ onStream: (event) => {
1159
+ this.observeSessionStream(sid, event);
1160
+ this.notify(Methods.StreamEvent, { sessionId: sid, event });
1161
+ },
571
1162
  clientMessageId: typeof params.clientMessageId === "string" ? params.clientMessageId : undefined,
572
1163
  permissionMode: params.permissionMode,
573
1164
  planMode: params.planMode,
1165
+ behaviorMode: params.behaviorMode,
1166
+ profileParams: params.profileParams,
1167
+ workspaceProfile: params.workspaceProfile,
1168
+ sessionBrief: params.sessionBrief,
1169
+ sessionMessageTargets: params.sessionMessageTargets,
1170
+ petRuntimeContext: params.petRuntimeContext,
1171
+ petWorkspaces: params.petWorkspaces,
1172
+ kind: params.kind,
574
1173
  approvalRouter: this.approvalRouter,
575
1174
  });
576
1175
  this.notify(Methods.RunAccepted, { requestId: req.id, sessionId: sid });
1176
+ this.observeRunBoundary(sid, "start");
577
1177
  const result = await run;
1178
+ this.observeRunBoundary(sid, "end");
578
1179
  const runResult = {
579
1180
  text: result.text,
580
1181
  reason: result.reason,
581
1182
  sessionId: result.sessionId ?? sid,
582
1183
  turnCount: result.turnCount,
583
1184
  usage: result.usage,
1185
+ ...(result.extensions ? { extensions: result.extensions } : {}),
1186
+ petWorkDelegation: result.petWorkDelegation,
584
1187
  };
585
1188
  this.transport.send(createResponse(req.id, runResult));
586
1189
  // Run-boundary re-check (trigger B): the session is idle now. If a
@@ -591,6 +1194,7 @@ export class AgentServer {
591
1194
  this.maybeWakeIdleSession(sid);
592
1195
  }
593
1196
  catch (err) {
1197
+ this.observeRunBoundary(sid, "error");
594
1198
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
595
1199
  // Even on a failed run the session goes idle — re-check so a background
596
1200
  // completion that landed during the failed run isn't orphaned.
@@ -603,7 +1207,7 @@ export class AgentServer {
603
1207
  return;
604
1208
  }
605
1209
  const params = (req.params ?? {});
606
- const inputError = runInputError(params);
1210
+ const inputError = this.validateRunInput(params);
607
1211
  if (inputError) {
608
1212
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, inputError));
609
1213
  return;
@@ -656,6 +1260,16 @@ export class AgentServer {
656
1260
  // `signal.aborted` below it's already gone.
657
1261
  const runController = this.abortController;
658
1262
  try {
1263
+ const displayText = typeof params.displayText === "string" ? params.displayText.trim() : "";
1264
+ if (displayText) {
1265
+ streamToClient({
1266
+ type: "session_user_message",
1267
+ text: displayText,
1268
+ ...(typeof params.clientMessageId === "string"
1269
+ ? { clientMessageId: params.clientMessageId }
1270
+ : {}),
1271
+ });
1272
+ }
659
1273
  this.notify(Methods.RunAccepted, {
660
1274
  requestId: req.id,
661
1275
  sessionId: params.sessionId ?? "",
@@ -663,6 +1277,8 @@ export class AgentServer {
663
1277
  const result = await this.legacyEngine.run(params.task, {
664
1278
  cwd: params.cwd,
665
1279
  sessionId: params.sessionId,
1280
+ displayText: displayText || undefined,
1281
+ injected: params.injected === true,
666
1282
  signal: runController.signal,
667
1283
  onStream: streamToClient,
668
1284
  clientMessageId: typeof params.clientMessageId === "string" ? params.clientMessageId : undefined,
@@ -671,6 +1287,16 @@ export class AgentServer {
671
1287
  (params.goal != null && typeof params.goal === "object")
672
1288
  ? params.goal
673
1289
  : undefined,
1290
+ permissionMode: params.permissionMode,
1291
+ planMode: params.planMode,
1292
+ behaviorMode: params.behaviorMode,
1293
+ profileParams: params.profileParams,
1294
+ workspaceProfile: params.workspaceProfile,
1295
+ sessionBrief: params.sessionBrief,
1296
+ sessionMessageTargets: params.sessionMessageTargets,
1297
+ petRuntimeContext: params.petRuntimeContext,
1298
+ petWorkspaces: params.petWorkspaces,
1299
+ kind: params.kind,
674
1300
  });
675
1301
  const runResult = {
676
1302
  text: result.text,
@@ -678,6 +1304,8 @@ export class AgentServer {
678
1304
  sessionId: result.sessionId,
679
1305
  turnCount: result.turnCount,
680
1306
  usage: result.usage,
1307
+ ...(result.extensions ? { extensions: result.extensions } : {}),
1308
+ petWorkDelegation: result.petWorkDelegation,
681
1309
  };
682
1310
  this.transport.send(createResponse(req.id, runResult));
683
1311
  }
@@ -735,18 +1363,19 @@ export class AgentServer {
735
1363
  this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, `No such session: ${params.sessionId}`));
736
1364
  return;
737
1365
  }
738
- const resolve = s.pendingApprovals.get(params.requestId);
739
- if (!resolve) {
1366
+ const entry = s.pendingApprovals.get(params.requestId);
1367
+ if (!entry) {
740
1368
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, `No pending approval for session ${params.sessionId}: ${params.requestId}`));
741
1369
  return;
742
1370
  }
743
1371
  s.pendingApprovals.delete(params.requestId);
1372
+ this.observeApprovalTransition(entry.metadata, "resolved");
744
1373
  this.pendingApprovalTargets.delete(params.requestId);
745
1374
  // Cancel the pending timeout for requests that arm one (browser actions,
746
1375
  // credential injection, and tool approvals). AskUserQuestion does not use
747
1376
  // a timeout; this is harmless for those request ids.
748
1377
  this.clearApprovalTimer(params.requestId);
749
- resolve(params.decision);
1378
+ entry.resolve(params.decision);
750
1379
  this.transport.send(createResponse(req.id, { ok: true }));
751
1380
  return;
752
1381
  }
@@ -838,37 +1467,260 @@ export class AgentServer {
838
1467
  }
839
1468
  this.transport.send(createResponse(req.id, { ok: true, limits: result }));
840
1469
  }
1470
+ /** Reject destructive Goal controls owned by a different TCP connection. */
1471
+ authorizeGoalControl(req, sessionId) {
1472
+ if (!this.strictApprovalRouting)
1473
+ return true;
1474
+ const owner = this.approvalRouter.current(sessionId);
1475
+ if (!owner || owner.connectionId === this.connectionId)
1476
+ return true;
1477
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.Overloaded, `Session ${sessionId} is owned by another connection`));
1478
+ return false;
1479
+ }
1480
+ /** Start a fully rebuilt Goal run for an idle or goal-less in-flight session. */
1481
+ async queueGoalResumeTurn(sessionId, goal, existing) {
1482
+ if (!this.chatManager)
1483
+ return null;
1484
+ let session = existing;
1485
+ if (!session) {
1486
+ // Let ChatSessionManager probe through its configured Engine factory so
1487
+ // custom sessionStorageDir roots stay aligned with the host. The generic
1488
+ // background-wakeup helper's default SessionManager probe is not safe for
1489
+ // this cold-resume path.
1490
+ session =
1491
+ (await this.chatManager.getOrCreatePersisted(sessionId, {
1492
+ projectTrusted: false,
1493
+ })) ?? undefined;
1494
+ if (!session)
1495
+ return null;
1496
+ this.wireInteractiveSession(session, sessionId);
1497
+ }
1498
+ if (session.engine.isHeadless())
1499
+ return null;
1500
+ return {
1501
+ completion: session.enqueueGoalResumeTurn(goal, {
1502
+ onStream: (event) => this.notify(Methods.StreamEvent, { sessionId, event }),
1503
+ approvalRouter: this.approvalRouter,
1504
+ }),
1505
+ };
1506
+ }
1507
+ reconcileFailedGoalResume(sessionId, goal, claimedOwner, error) {
1508
+ const patch = {
1509
+ paused: true,
1510
+ expectedGoalId: goal.goalId,
1511
+ expectedRevision: goal.revision,
1512
+ };
1513
+ const live = this.chatManager?.get(sessionId);
1514
+ const rolledBack = live
1515
+ ? live.updateGoal(patch)
1516
+ : this.updateActiveGoalOnDisk?.(sessionId, patch);
1517
+ const authoritative = rolledBack ?? live?.getGoal() ?? this.readActiveGoalFromDisk?.(sessionId);
1518
+ if (authoritative) {
1519
+ this.notify(Methods.StreamEvent, {
1520
+ sessionId,
1521
+ event: {
1522
+ type: "goal_updated",
1523
+ goalId: authoritative.goalId,
1524
+ revision: authoritative.revision,
1525
+ objective: authoritative.objective,
1526
+ paused: authoritative.paused === true,
1527
+ },
1528
+ });
1529
+ }
1530
+ if (rolledBack && claimedOwner && this.approvalRouter.matches(claimedOwner)) {
1531
+ this.approvalRouter.release(sessionId, this.connectionId, "goal resume turn was not queued");
1532
+ }
1533
+ logger.warn("goal.resume_turn_not_queued", {
1534
+ sessionId,
1535
+ goalId: goal.goalId,
1536
+ revision: goal.revision,
1537
+ error: error instanceof Error ? error.message : String(error),
1538
+ });
1539
+ this.notify(Methods.StreamEvent, {
1540
+ sessionId,
1541
+ event: {
1542
+ type: "error",
1543
+ error: rolledBack
1544
+ ? "Goal resume could not start; the Goal was paused again"
1545
+ : "Goal resume could not start; Goal state changed before rollback",
1546
+ },
1547
+ });
1548
+ }
1549
+ /** Edit or pause/resume a persisted goal without replacing its identity. */
1550
+ async handleGoalUpdate(req) {
1551
+ const params = (req.params ?? {});
1552
+ if (typeof params.sessionId !== "string" || !params.sessionId) {
1553
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
1554
+ return;
1555
+ }
1556
+ if (params.objective === undefined && params.paused === undefined) {
1557
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "objective or paused is required"));
1558
+ return;
1559
+ }
1560
+ if (params.objective !== undefined && typeof params.objective !== "string") {
1561
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "objective must be a string"));
1562
+ return;
1563
+ }
1564
+ if (params.objective !== undefined && !params.objective.trim()) {
1565
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "objective must not be blank"));
1566
+ return;
1567
+ }
1568
+ if (params.paused !== undefined && typeof params.paused !== "boolean") {
1569
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "paused must be boolean"));
1570
+ return;
1571
+ }
1572
+ if (typeof params.expectedGoalId !== "string" || !params.expectedGoalId) {
1573
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "expectedGoalId is required and must be a non-empty string"));
1574
+ return;
1575
+ }
1576
+ if (typeof params.expectedRevision !== "number" ||
1577
+ !Number.isInteger(params.expectedRevision) ||
1578
+ params.expectedRevision < 1) {
1579
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "expectedRevision is required and must be a positive integer"));
1580
+ return;
1581
+ }
1582
+ const patch = {
1583
+ objective: params.objective,
1584
+ paused: params.paused,
1585
+ expectedGoalId: params.expectedGoalId,
1586
+ expectedRevision: params.expectedRevision,
1587
+ };
1588
+ const session = this.chatManager?.get(params.sessionId);
1589
+ const beforeGoal = session?.getGoal?.() ??
1590
+ this.legacyEngine?.getGoal(params.sessionId) ??
1591
+ this.readActiveGoalFromDisk?.(params.sessionId);
1592
+ const resumeInPlace = session?.canResumeGoalInPlace?.() === true;
1593
+ // `paused:false` is also an explicit kick for an unpaused-but-idle Goal
1594
+ // left behind by ESC, a model failure, or worker restart. A live Goal run
1595
+ // consumes the update in place and must not enqueue a duplicate turn.
1596
+ const driveRequested = !!beforeGoal && params.paused === false && !resumeInPlace;
1597
+ if (!this.authorizeGoalControl(req, params.sessionId))
1598
+ return;
1599
+ if (driveRequested && !this.chatManager) {
1600
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "Goal resume requires a multi-session host that can schedule a continuation turn"));
1601
+ return;
1602
+ }
1603
+ let claimedResumeOwner = null;
1604
+ if (this.strictApprovalRouting &&
1605
+ driveRequested &&
1606
+ !this.approvalRouter.current(params.sessionId)) {
1607
+ const registration = this.approvalRouter.register(params.sessionId, this.connectionId);
1608
+ if (!registration.ok) {
1609
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.Overloaded, `Session ${params.sessionId} is owned by another connection`));
1610
+ return;
1611
+ }
1612
+ claimedResumeOwner = registration.target;
1613
+ }
1614
+ const goal = session
1615
+ ? session.updateGoal(patch)
1616
+ : this.legacyEngine
1617
+ ? this.legacyEngine.updateGoal(params.sessionId, patch)
1618
+ : this.updateActiveGoalOnDisk?.(params.sessionId, patch);
1619
+ if (!goal) {
1620
+ if (claimedResumeOwner && this.approvalRouter.matches(claimedResumeOwner)) {
1621
+ this.approvalRouter.release(params.sessionId, this.connectionId, "goal resume CAS did not apply");
1622
+ }
1623
+ // A missing/stale goal is an ordinary optimistic-concurrency miss, not a
1624
+ // malformed request. Keep it in the result channel so Desktop, TUI and
1625
+ // the no-worker fallback share one contract.
1626
+ this.transport.send(createResponse(req.id, { ok: true, updated: false }));
1627
+ return;
1628
+ }
1629
+ this.notify(Methods.StreamEvent, {
1630
+ sessionId: params.sessionId,
1631
+ event: {
1632
+ type: "goal_updated",
1633
+ ...(goal.goalId ? { goalId: goal.goalId } : {}),
1634
+ ...(goal.revision ? { revision: goal.revision } : {}),
1635
+ objective: goal.objective,
1636
+ paused: goal.paused === true,
1637
+ },
1638
+ });
1639
+ this.transport.send(createResponse(req.id, {
1640
+ ok: true,
1641
+ updated: true,
1642
+ goal: goal.objective,
1643
+ ...(goal.goalId ? { goalId: goal.goalId } : {}),
1644
+ ...(goal.revision ? { revision: goal.revision } : {}),
1645
+ paused: goal.paused === true,
1646
+ }));
1647
+ if (driveRequested) {
1648
+ void this.queueGoalResumeTurn(params.sessionId, goal, session)
1649
+ .then((ticket) => {
1650
+ if (!ticket) {
1651
+ this.reconcileFailedGoalResume(params.sessionId, goal, claimedResumeOwner, "session is unavailable or headless");
1652
+ return;
1653
+ }
1654
+ void ticket.completion.catch((err) => {
1655
+ this.reconcileFailedGoalResume(params.sessionId, goal, claimedResumeOwner, err);
1656
+ });
1657
+ })
1658
+ .catch((err) => {
1659
+ this.reconcileFailedGoalResume(params.sessionId, goal, claimedResumeOwner, err);
1660
+ });
1661
+ }
1662
+ }
841
1663
  /**
842
1664
  * Clear a session's persisted active goal (CC /goal clear). Routes to the
843
1665
  * named session, falling back to the legacy single engine (mirrors
844
1666
  * handleGoalExtend / handleCancel's dual path). Returns { ok, cleared }.
845
1667
  */
846
- handleGoalClear(req) {
1668
+ handleGoalClear(req, deleteAlias = false) {
847
1669
  const params = (req.params ?? {});
1670
+ if (typeof params.sessionId !== "string" || !params.sessionId) {
1671
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
1672
+ return;
1673
+ }
1674
+ if (deleteAlias &&
1675
+ (typeof params.expectedGoalId !== "string" ||
1676
+ !params.expectedGoalId ||
1677
+ typeof params.expectedRevision !== "number" ||
1678
+ !Number.isInteger(params.expectedRevision) ||
1679
+ params.expectedRevision < 1)) {
1680
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "goalDelete requires expectedGoalId and a positive expectedRevision"));
1681
+ return;
1682
+ }
1683
+ if (!this.authorizeGoalControl(req, params.sessionId))
1684
+ return;
1685
+ const expected = deleteAlias
1686
+ ? { goalId: params.expectedGoalId, revision: params.expectedRevision }
1687
+ : undefined;
848
1688
  const session = this.chatManager && typeof params.sessionId === "string"
849
1689
  ? this.chatManager.get(params.sessionId)
850
1690
  : undefined;
851
- if (!session && !this.legacyEngine) {
1691
+ if (!session && !this.legacyEngine && !this.clearActiveGoalOnDisk) {
852
1692
  this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, params.sessionId ? `No such session: ${params.sessionId}` : "sessionId is required"));
853
1693
  return;
854
1694
  }
1695
+ const clearingGoal = session
1696
+ ? session.getGoal?.()
1697
+ : params.sessionId
1698
+ ? (this.legacyEngine?.getGoal(params.sessionId) ??
1699
+ this.readActiveGoalFromDisk?.(params.sessionId))
1700
+ : undefined;
855
1701
  const cleared = session
856
- ? session.clearGoal()
1702
+ ? session.clearGoal(expected)
857
1703
  : params.sessionId
858
- ? (this.legacyEngine.clearGoal(params.sessionId) ?? false)
1704
+ ? (this.legacyEngine?.clearGoal(params.sessionId, expected) ??
1705
+ this.clearActiveGoalOnDisk?.(params.sessionId, expected) ??
1706
+ false)
859
1707
  : false;
860
1708
  if (cleared && typeof params.sessionId === "string" && params.sessionId.length > 0) {
861
1709
  this.notify(Methods.StreamEvent, {
862
1710
  sessionId: params.sessionId,
863
- event: { type: "goal_cleared" },
1711
+ event: {
1712
+ type: "goal_cleared",
1713
+ ...(clearingGoal?.goalId ? { goalId: clearingGoal.goalId } : {}),
1714
+ ...(clearingGoal?.revision ? { revision: clearingGoal.revision } : {}),
1715
+ },
864
1716
  });
865
1717
  }
866
- this.transport.send(createResponse(req.id, { ok: true, cleared }));
1718
+ this.transport.send(createResponse(req.id, deleteAlias ? { ok: true, deleted: cleared } : { ok: true, cleared }));
867
1719
  }
868
1720
  /**
869
1721
  * Read a session's persisted active goal so the host can re-surface the goal
870
1722
  * block + Cancel button on session load. A persistent goal lives only in
871
- * state.activeGoal and is never replayed from the transcript, so a reloaded
1723
+ * state.goalLifecycle and is never replayed from the transcript, so a reloaded
872
1724
  * (or disk-rebuilt) session has no other way to learn it. Prefers a live
873
1725
  * chatManager session, else falls through to a disk read (readActiveGoalFromDisk
874
1726
  * in worker mode, or the legacy engine's disk read in single-engine mode) — the
@@ -897,7 +1749,13 @@ export class AgentServer {
897
1749
  : (this.readActiveGoalFromDisk?.(params.sessionId) ??
898
1750
  this.legacyEngine?.getGoal(params.sessionId) ??
899
1751
  undefined);
900
- this.transport.send(createResponse(req.id, { ok: true, goal: goal ? goal.objective : null }));
1752
+ this.transport.send(createResponse(req.id, {
1753
+ ok: true,
1754
+ goal: goal ? goal.objective : null,
1755
+ ...(goal?.goalId ? { goalId: goal.goalId } : {}),
1756
+ ...(goal?.revision ? { revision: goal.revision } : {}),
1757
+ paused: goal?.paused === true,
1758
+ }));
901
1759
  }
902
1760
  /**
903
1761
  * Query/control a session's background shells for the desktop UI panel (TODO
@@ -974,10 +1832,11 @@ export class AgentServer {
974
1832
  if (this.chatManager) {
975
1833
  const session = this.chatManager.get(params.sessionId);
976
1834
  if (session) {
977
- this.cancelSessionApprovals(session, "session closed");
1835
+ this.cancelSessionApprovals(session, "session closed", "cancelled", "session_closed");
978
1836
  }
979
1837
  this.approvalRouter.release(params.sessionId, this.connectionId, "session closed");
980
1838
  await this.chatManager.close(params.sessionId);
1839
+ this.observeSessionClosed(params.sessionId);
981
1840
  }
982
1841
  // Explicit session teardown — reap that session's background shells
983
1842
  // (design §6 "session 被显式删除 → killSession"). This is the RPC path
@@ -1012,6 +1871,33 @@ export class AgentServer {
1012
1871
  const workspace = engine?.releaseSessionWorkspace?.(params.sessionId) ?? null;
1013
1872
  this.transport.send(createResponse(req.id, { ok: true, workspace }));
1014
1873
  }
1874
+ handleSetWorkspace(req) {
1875
+ const params = (req.params ?? {});
1876
+ if (typeof params.sessionId !== "string" || params.sessionId.length === 0) {
1877
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
1878
+ return;
1879
+ }
1880
+ if (!params.workspace ||
1881
+ typeof params.workspace !== "object" ||
1882
+ typeof params.workspace.root !== "string" ||
1883
+ params.workspace.root.length === 0 ||
1884
+ (params.workspace.kind !== "main" && params.workspace.kind !== "worktree")) {
1885
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "valid workspace is required"));
1886
+ return;
1887
+ }
1888
+ const engine = this.chatManager
1889
+ ? this.chatManager.get(params.sessionId)?.engine
1890
+ : this.legacyEngine;
1891
+ if (!engine) {
1892
+ this.transport.send(createResponse(req.id, { ok: true, workspace: null }));
1893
+ return;
1894
+ }
1895
+ const workspace = engine.setSessionWorkspace?.(params.sessionId, params.workspace);
1896
+ this.transport.send(createResponse(req.id, {
1897
+ ok: workspace !== undefined && workspace !== null,
1898
+ workspace: workspace ?? null,
1899
+ }));
1900
+ }
1015
1901
  // ─── Configure ──────────────────────────────────────────────────
1016
1902
  handleConfigure(req) {
1017
1903
  const params = (req.params ?? {});
@@ -1177,6 +2063,51 @@ export class AgentServer {
1177
2063
  this.transport.send(createResponse(req.id, { ok: true }));
1178
2064
  }
1179
2065
  // ─── Query ──────────────────────────────────────────────────────
2066
+ /**
2067
+ * Resolve the Engine that owns a session-scoped query (compact, archive_range,
2068
+ * …). With a chatManager: materialize a persisted-but-not-live session on
2069
+ * demand, or borrow any live engine when no sessionId is given. Sends the
2070
+ * appropriate error response and returns null when the session is
2071
+ * closing/closed, unknown, or no engine is available; `label` names the query
2072
+ * in the "no engine" message. Domain-neutral — no query-specific literals.
2073
+ */
2074
+ async resolveEngineForSessionQuery(req, sessionId, fallbackEngine, label) {
2075
+ let resolved = fallbackEngine;
2076
+ if (this.chatManager) {
2077
+ if (sessionId) {
2078
+ if (this.chatManager.isUnavailable(sessionId)) {
2079
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, `Session is closing or closed: ${sessionId}`));
2080
+ return null;
2081
+ }
2082
+ let session = this.chatManager.get(sessionId);
2083
+ if (!session) {
2084
+ const probeEngine = this.anyEngine();
2085
+ if (!probeEngine?.sessionExistsOnDisk(sessionId)) {
2086
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionNotFound, `Session not found: ${sessionId}`));
2087
+ return null;
2088
+ }
2089
+ try {
2090
+ session = await this.chatManager.getOrCreate(sessionId, {
2091
+ cwd: probeEngine.getSessionManager().readSessionMainRoot(sessionId),
2092
+ });
2093
+ }
2094
+ catch (err) {
2095
+ this.transport.send(createErrorResponse(req.id, err.code ?? ErrorCodes.InternalError, err.message));
2096
+ return null;
2097
+ }
2098
+ }
2099
+ resolved = session.engine;
2100
+ }
2101
+ else {
2102
+ resolved = this.anyEngine();
2103
+ }
2104
+ }
2105
+ if (!resolved) {
2106
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, `No engine available for ${label} query`));
2107
+ return null;
2108
+ }
2109
+ return resolved;
2110
+ }
1180
2111
  async handleQuery(req) {
1181
2112
  const params = (req.params ?? {});
1182
2113
  // For query operations we prefer the legacyEngine; if absent borrow any
@@ -1199,20 +2130,14 @@ export class AgentServer {
1199
2130
  }
1200
2131
  case "sessions": {
1201
2132
  if (this.chatManager) {
1202
- // Return the ChatSessionManager's live sessions
1203
- const sessions = [];
1204
- this.chatManager.forEachSession((s) => {
1205
- sessions.push({
1206
- sessionId: s.id,
1207
- busy: s.isBusy(),
1208
- queueDepth: s.queueDepth(),
1209
- lastActivityAt: s.lastActivityAt,
1210
- });
1211
- });
2133
+ // Pet projection and the legacy query share this one live-state builder.
2134
+ const { sessions } = this.chatManager.getLiveSessionSnapshot();
1212
2135
  this.transport.send(createResponse(req.id, { type: "sessions", data: sessions }));
1213
2136
  }
1214
2137
  else if (engine) {
1215
- const sessions = engine.getSessionManager().list();
2138
+ const sessions = engine
2139
+ .getSessionManager()
2140
+ .list(undefined, { excludeKinds: this.hiddenSessionKinds });
1216
2141
  this.transport.send(createResponse(req.id, { type: "sessions", data: sessions }));
1217
2142
  }
1218
2143
  else {
@@ -1274,40 +2199,9 @@ export class AgentServer {
1274
2199
  const compactSessionId = typeof params.sessionId === "string" && params.sessionId.length > 0
1275
2200
  ? params.sessionId
1276
2201
  : undefined;
1277
- let compactEngine = engine;
1278
- if (this.chatManager) {
1279
- if (compactSessionId) {
1280
- if (this.chatManager.isUnavailable(compactSessionId)) {
1281
- this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, `Session is closing or closed: ${compactSessionId}`));
1282
- return;
1283
- }
1284
- let session = this.chatManager.get(compactSessionId);
1285
- if (!session) {
1286
- const probeEngine = this.anyEngine();
1287
- if (!probeEngine?.sessionExistsOnDisk(compactSessionId)) {
1288
- this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionNotFound, `Session not found: ${compactSessionId}`));
1289
- return;
1290
- }
1291
- try {
1292
- session = await this.chatManager.getOrCreate(compactSessionId, {
1293
- cwd: probeEngine.getSessionManager().readCwd(compactSessionId),
1294
- });
1295
- }
1296
- catch (err) {
1297
- this.transport.send(createErrorResponse(req.id, err.code ?? ErrorCodes.InternalError, err.message));
1298
- return;
1299
- }
1300
- }
1301
- compactEngine = session.engine;
1302
- }
1303
- else {
1304
- compactEngine = this.anyEngine();
1305
- }
1306
- }
1307
- if (!compactEngine) {
1308
- this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, "No engine available for compact query"));
2202
+ const compactEngine = await this.resolveEngineForSessionQuery(req, compactSessionId, engine, "compact");
2203
+ if (!compactEngine)
1309
2204
  return;
1310
- }
1311
2205
  try {
1312
2206
  const result = await compactEngine.forceCompact(compactSessionId);
1313
2207
  if (result.before > result.after) {
@@ -1332,6 +2226,45 @@ export class AgentServer {
1332
2226
  }
1333
2227
  break;
1334
2228
  }
2229
+ case "archive_range": {
2230
+ const archiveSessionId = typeof params.sessionId === "string" && params.sessionId.length > 0
2231
+ ? params.sessionId
2232
+ : undefined;
2233
+ const start = Number(params.start);
2234
+ const end = Number(params.end);
2235
+ if (!archiveSessionId || !Number.isFinite(start) || !Number.isFinite(end)) {
2236
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "archive_range requires sessionId, start and end"));
2237
+ return;
2238
+ }
2239
+ const archiveEngine = await this.resolveEngineForSessionQuery(req, archiveSessionId, engine, "archive_range");
2240
+ if (!archiveEngine)
2241
+ return;
2242
+ try {
2243
+ const result = await archiveEngine.archiveTurnRange(archiveSessionId, { start, end });
2244
+ if (result.before > result.after) {
2245
+ const event = {
2246
+ type: "context_compact",
2247
+ // Range archival has its own tier; "range" must be in
2248
+ // COMPACT_STREAM_STRATEGIES or it degrades to "compacted".
2249
+ strategy: toCompactStreamStrategy("range"),
2250
+ before: result.before,
2251
+ after: result.after,
2252
+ };
2253
+ this.notify(Methods.StreamEvent, {
2254
+ sessionId: archiveSessionId,
2255
+ event,
2256
+ });
2257
+ }
2258
+ this.transport.send(createResponse(req.id, {
2259
+ type: "archive_range",
2260
+ data: result,
2261
+ }));
2262
+ }
2263
+ catch (err) {
2264
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
2265
+ }
2266
+ break;
2267
+ }
1335
2268
  case "models": {
1336
2269
  if (!engine) {
1337
2270
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, "No engine available for models query"));
@@ -1400,11 +2333,6 @@ export class AgentServer {
1400
2333
  }
1401
2334
  break;
1402
2335
  }
1403
- case "arena_status": {
1404
- const status = getArenaStatus();
1405
- this.transport.send(createResponse(req.id, { type: "arena_status", data: status }));
1406
- break;
1407
- }
1408
2336
  case "config_set": {
1409
2337
  if (!engine) {
1410
2338
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, "No engine available for config_set"));
@@ -1593,8 +2521,16 @@ export class AgentServer {
1593
2521
  }
1594
2522
  break;
1595
2523
  }
1596
- default:
2524
+ default: {
2525
+ const capability = engine
2526
+ ? await engine.queryCapability(params.type, params)
2527
+ : { handled: false };
2528
+ if (capability.handled) {
2529
+ this.transport.send(createResponse(req.id, { type: params.type, data: capability.data }));
2530
+ break;
2531
+ }
1597
2532
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, `Unknown query type: ${params.type}`));
2533
+ }
1598
2534
  }
1599
2535
  }
1600
2536
  // ─── Inject ─────────────────────────────────────────────────────
@@ -1708,9 +2644,21 @@ export class AgentServer {
1708
2644
  return;
1709
2645
  }
1710
2646
  if (session) {
1711
- session.pendingApprovals.set(requestId, (decision) => {
1712
- resolve(decision);
1713
- });
2647
+ const internal = INTERNAL_PENDING_TOOLS.has(request.toolName);
2648
+ const toolName = safeApprovalToolName(request.toolName);
2649
+ this.registerSessionApproval(session, {
2650
+ sessionId,
2651
+ requestId,
2652
+ routeGeneration: effectiveRoute.generation,
2653
+ workerGeneration: this.workerGeneration(),
2654
+ kind: internal ? "internal" : "tool_approval",
2655
+ title: internal ? "内部操作等待" : `等待批准 ${toolName}`,
2656
+ toolName,
2657
+ riskLevel: request.riskLevel,
2658
+ createdAt: Date.now(),
2659
+ expiresAt: Date.now() + AgentServer.APPROVAL_TIMEOUT_MS,
2660
+ surfaceable: !internal,
2661
+ }, (decision) => resolve(decision));
1714
2662
  }
1715
2663
  else {
1716
2664
  this.pendingApprovals.set(requestId, resolve);
@@ -1719,7 +2667,12 @@ export class AgentServer {
1719
2667
  const timer = setTimeout(() => {
1720
2668
  const pending = session?.pendingApprovals ?? this.pendingApprovals;
1721
2669
  if (pending.has(requestId)) {
1722
- pending.delete(requestId);
2670
+ if (session) {
2671
+ this.takeSessionApproval(session, requestId, "expired");
2672
+ }
2673
+ else {
2674
+ pending.delete(requestId);
2675
+ }
1723
2676
  this.pendingApprovalTargets.delete(requestId);
1724
2677
  this.approvalTimers.delete(requestId);
1725
2678
  resolve({ approved: false, reason: "approval timed out" });
@@ -1753,7 +2706,17 @@ export class AgentServer {
1753
2706
  requestAskUserForSession(session, sessionId, question, opts) {
1754
2707
  return new Promise((resolve) => {
1755
2708
  const requestId = nanoid(12);
1756
- session.pendingApprovals.set(requestId, (decision) => {
2709
+ const routeEnvelope = this.approvalRouteEnvelope(sessionId, requestId);
2710
+ this.registerSessionApproval(session, {
2711
+ sessionId,
2712
+ requestId,
2713
+ routeGeneration: "generation" in routeEnvelope ? routeEnvelope.generation : undefined,
2714
+ workerGeneration: this.workerGeneration(),
2715
+ kind: "ask_user",
2716
+ title: "需要用户回答",
2717
+ createdAt: Date.now(),
2718
+ surfaceable: true,
2719
+ }, (decision) => {
1757
2720
  this.clearApprovalTimer(requestId);
1758
2721
  const result = decision;
1759
2722
  if (result && typeof result === "object" && "approved" in result) {
@@ -1776,7 +2739,7 @@ export class AgentServer {
1776
2739
  if (opts?.optionsOnly !== undefined)
1777
2740
  args.optionsOnly = opts.optionsOnly;
1778
2741
  this.notify(Methods.ApprovalRequest, {
1779
- ...this.approvalRouteEnvelope(sessionId, requestId),
2742
+ ...routeEnvelope,
1780
2743
  request: {
1781
2744
  toolName: "__ask_user__",
1782
2745
  args,
@@ -1801,12 +2764,11 @@ export class AgentServer {
1801
2764
  }
1802
2765
  if (goalActive) {
1803
2766
  const timer = setTimeout(() => {
1804
- const resolveFn = session.pendingApprovals.get(requestId);
1805
- if (resolveFn) {
1806
- session.pendingApprovals.delete(requestId);
2767
+ const entry = this.takeSessionApproval(session, requestId, "expired");
2768
+ if (entry) {
1807
2769
  this.pendingApprovalTargets.delete(requestId);
1808
2770
  this.approvalTimers.delete(requestId);
1809
- resolveFn("(用户未在限定时间内回答;目标运行中请自行做出合理假设并继续推进。)");
2771
+ entry.resolve("(用户未在限定时间内回答;目标运行中请自行做出合理假设并继续推进。)");
1810
2772
  // Tell every client the ask is resolved (nobody answered) so the
1811
2773
  // stale AskUserQuestion card is dismissed instead of lingering as
1812
2774
  // if still waiting. Reuses the same envelope shape the desktop main
@@ -1833,7 +2795,7 @@ export class AgentServer {
1833
2795
  type: (ref, text) => call("type", { ref, text }),
1834
2796
  navigate: (url) => call("navigate", { url }),
1835
2797
  scroll: (dir, amount) => call("scroll", { dir, amount }),
1836
- readContent: () => call("readContent", {}),
2798
+ readContent: (options) => call("readContent", { ...(options ?? {}) }),
1837
2799
  extractLinks: () => call("extractLinks", {}),
1838
2800
  waitForLoad: (timeoutMs) => call("waitForLoad", { timeoutMs }),
1839
2801
  hover: (ref) => call("hover", { ref }),
@@ -1848,40 +2810,31 @@ export class AgentServer {
1848
2810
  requestBrowserActionForSession(session, sessionId, action, payload) {
1849
2811
  return new Promise((resolve) => {
1850
2812
  const requestId = nanoid(12);
1851
- session.pendingApprovals.set(requestId, (decision) => {
2813
+ const routeEnvelope = this.approvalRouteEnvelope(sessionId, requestId);
2814
+ this.registerSessionApproval(session, this.internalPendingMetadata(sessionId, requestId, routeEnvelope, "__browser_action__"), (decision) => {
1852
2815
  this.clearApprovalTimer(requestId);
1853
2816
  // Main replies with { approved:true, answer:<json string> } (reusing the
1854
2817
  // ApprovalResult shape) or a raw json string. Parse → typed result.
1855
- let raw;
1856
- if (decision && typeof decision === "object" && "approved" in decision) {
1857
- const r = decision;
1858
- raw = r.approved ? r.answer : undefined;
1859
- }
1860
- else if (typeof decision === "string") {
1861
- raw = decision;
1862
- }
1863
- if (raw === undefined) {
1864
- resolve({ ok: false, detail: "browser action declined or unavailable" });
1865
- return;
1866
- }
1867
- try {
1868
- resolve(JSON.parse(raw));
1869
- }
1870
- catch {
1871
- resolve({ ok: false, detail: "malformed browser action result" });
1872
- }
2818
+ const parsed = parseHostLoopbackDecision(decision, "browser action");
2819
+ resolve(parsed.ok
2820
+ ? parsed.value
2821
+ : { ok: false, failure: parsed.failure, detail: parsed.detail });
1873
2822
  });
1874
2823
  const timer = setTimeout(() => {
1875
2824
  if (session.pendingApprovals.has(requestId)) {
1876
- session.pendingApprovals.delete(requestId);
2825
+ this.takeSessionApproval(session, requestId, "expired");
1877
2826
  this.pendingApprovalTargets.delete(requestId);
1878
2827
  this.approvalTimers.delete(requestId);
1879
- resolve({ ok: false, detail: "browser action timed out" });
2828
+ resolve({
2829
+ ok: false,
2830
+ failure: "timed_out",
2831
+ detail: `browser action ${HOST_LOOPBACK_FAILURE_DETAIL.timed_out}`,
2832
+ });
1880
2833
  }
1881
2834
  }, AgentServer.APPROVAL_TIMEOUT_MS);
1882
2835
  this.approvalTimers.set(requestId, timer);
1883
2836
  this.notify(Methods.ApprovalRequest, {
1884
- ...this.approvalRouteEnvelope(sessionId, requestId),
2837
+ ...routeEnvelope,
1885
2838
  request: {
1886
2839
  toolName: "__browser_action__",
1887
2840
  args: { action, ...payload },
@@ -1900,38 +2853,28 @@ export class AgentServer {
1900
2853
  requestCredentialInjectForSession(session, sessionId, credentialId, credentialScope = "full") {
1901
2854
  return new Promise((resolve) => {
1902
2855
  const requestId = nanoid(12);
1903
- session.pendingApprovals.set(requestId, (decision) => {
2856
+ const routeEnvelope = this.approvalRouteEnvelope(sessionId, requestId);
2857
+ this.registerSessionApproval(session, this.internalPendingMetadata(sessionId, requestId, routeEnvelope, "__credential_action__"), (decision) => {
1904
2858
  this.clearApprovalTimer(requestId);
1905
- let raw;
1906
- if (decision && typeof decision === "object" && "approved" in decision) {
1907
- const r = decision;
1908
- raw = r.approved ? r.answer : undefined;
1909
- }
1910
- else if (typeof decision === "string") {
1911
- raw = decision;
1912
- }
1913
- if (raw === undefined) {
1914
- resolve({ ok: false, error: "credential inject declined or unavailable" });
1915
- return;
1916
- }
1917
- try {
1918
- resolve(JSON.parse(raw));
1919
- }
1920
- catch {
1921
- resolve({ ok: false, error: "malformed credential inject result" });
1922
- }
2859
+ const parsed = parseHostLoopbackDecision(decision, "credential inject");
2860
+ resolve(parsed.ok
2861
+ ? parsed.value
2862
+ : { ok: false, error: parsed.detail });
1923
2863
  });
1924
2864
  const timer = setTimeout(() => {
1925
2865
  if (session.pendingApprovals.has(requestId)) {
1926
- session.pendingApprovals.delete(requestId);
2866
+ this.takeSessionApproval(session, requestId, "expired");
1927
2867
  this.pendingApprovalTargets.delete(requestId);
1928
2868
  this.approvalTimers.delete(requestId);
1929
- resolve({ ok: false, error: "credential inject timed out" });
2869
+ resolve({
2870
+ ok: false,
2871
+ error: `credential inject ${HOST_LOOPBACK_FAILURE_DETAIL.timed_out}`,
2872
+ });
1930
2873
  }
1931
2874
  }, AgentServer.APPROVAL_TIMEOUT_MS);
1932
2875
  this.approvalTimers.set(requestId, timer);
1933
2876
  this.notify(Methods.ApprovalRequest, {
1934
- ...this.approvalRouteEnvelope(sessionId, requestId),
2877
+ ...routeEnvelope,
1935
2878
  request: {
1936
2879
  toolName: "__credential_action__",
1937
2880
  args: { action: "injectCookie", credentialId, credentialScope },
@@ -1946,46 +2889,145 @@ export class AgentServer {
1946
2889
  switch: (target) => this.requestWorkspaceSwitchForSession(session, sessionId, target),
1947
2890
  };
1948
2891
  }
2892
+ makePanelBridge(session, sessionId) {
2893
+ return {
2894
+ list: async () => {
2895
+ const result = (await this.requestPanelActionForSession(session, sessionId, "list", {}));
2896
+ if (result?.ok === true && Array.isArray(result.panels)) {
2897
+ return {
2898
+ items: result.panels,
2899
+ };
2900
+ }
2901
+ // Never report a failed discovery as an empty host. "(no panels
2902
+ // available)" is an affirmative claim that makes the model give up.
2903
+ return { items: [], failed: hostLoopbackDetail(result) ?? "panel list failed" };
2904
+ },
2905
+ open: async (panelId) => {
2906
+ const result = (await this.requestPanelActionForSession(session, sessionId, "open", {
2907
+ panelId,
2908
+ }));
2909
+ // Gate on SUCCESS, not merely on shape. A real failure reply is
2910
+ // `{ok:false, panelId, detail}` (AgentPanelHost) — it satisfies a
2911
+ // shape-only `result?.panelId` check and would return verbatim, skipping
2912
+ // both the classification recovery and the length bound below. Every
2913
+ // failure must fall through here.
2914
+ if (result?.ok === true && result?.panelId)
2915
+ return result;
2916
+ // A classified terminal failure (cancelled / denied / timed out) must
2917
+ // survive this normalization — reporting it as "malformed" would tell the
2918
+ // model the HOST misbehaved when in fact the user stopped the turn.
2919
+ return {
2920
+ ok: false,
2921
+ panelId: result?.panelId ?? panelId,
2922
+ detail: hostLoopbackDetail(result) ?? "panel host returned a malformed result",
2923
+ };
2924
+ },
2925
+ tools: async (panelId) => {
2926
+ const result = (await this.requestPanelActionForSession(session, sessionId, "tools", {
2927
+ panelId,
2928
+ }));
2929
+ if (result?.ok === true && Array.isArray(result.tools)) {
2930
+ return {
2931
+ items: result.tools,
2932
+ };
2933
+ }
2934
+ return { items: [], failed: hostLoopbackDetail(result) ?? "panel tools query failed" };
2935
+ },
2936
+ invoke: async (panelId, toolName, args) => {
2937
+ const result = (await this.requestPanelActionForSession(session, sessionId, "invoke", {
2938
+ panelId,
2939
+ toolName,
2940
+ arguments: args,
2941
+ }));
2942
+ // Success-gated for the same reason as `open` above. Note a real failure
2943
+ // reply carries `panelId` but usually NOT `toolName` (AgentPanelHost emits
2944
+ // `{ok:false, panelId, detail}`), so the shape check alone was less
2945
+ // frequently bypassed here than for `open` — but `ok === true` is the
2946
+ // property that actually matters, and it also keeps a Panel App's raw
2947
+ // `error.message` from reaching the model unbounded.
2948
+ if (result?.ok === true && result?.panelId && result?.toolName)
2949
+ return result;
2950
+ return {
2951
+ ok: false,
2952
+ panelId: result?.panelId ?? panelId,
2953
+ toolName: result?.toolName ?? toolName,
2954
+ detail: hostLoopbackDetail(result) ?? "panel host returned a malformed result",
2955
+ };
2956
+ },
2957
+ };
2958
+ }
2959
+ requestPanelActionForSession(session, sessionId, action, payload) {
2960
+ return new Promise((resolve) => {
2961
+ const requestId = nanoid(12);
2962
+ const routeEnvelope = this.approvalRouteEnvelope(sessionId, requestId);
2963
+ this.registerSessionApproval(session, this.internalPendingMetadata(sessionId, requestId, routeEnvelope, "__panel_action__"), (decision) => {
2964
+ this.clearApprovalTimer(requestId);
2965
+ const parsed = parseHostLoopbackDecision(decision, "panel action");
2966
+ resolve(parsed.ok ? parsed.value : { ok: false, failure: parsed.failure, error: parsed.detail });
2967
+ });
2968
+ const timer = setTimeout(() => {
2969
+ if (session.pendingApprovals.has(requestId)) {
2970
+ this.takeSessionApproval(session, requestId, "expired");
2971
+ this.pendingApprovalTargets.delete(requestId);
2972
+ this.approvalTimers.delete(requestId);
2973
+ resolve({
2974
+ ok: false,
2975
+ failure: "timed_out",
2976
+ error: `panel action ${HOST_LOOPBACK_FAILURE_DETAIL.timed_out}`,
2977
+ });
2978
+ }
2979
+ }, AgentServer.APPROVAL_TIMEOUT_MS);
2980
+ this.approvalTimers.set(requestId, timer);
2981
+ this.notify(Methods.ApprovalRequest, {
2982
+ ...routeEnvelope,
2983
+ request: {
2984
+ toolName: "__panel_action__",
2985
+ args: { action, ...payload },
2986
+ description: `panel:${action}`,
2987
+ riskLevel: "low",
2988
+ },
2989
+ });
2990
+ });
2991
+ }
1949
2992
  requestWorkspaceSwitchForSession(session, sessionId, target) {
1950
2993
  return new Promise((resolve, reject) => {
1951
2994
  const requestId = nanoid(12);
1952
- session.pendingApprovals.set(requestId, (decision) => {
2995
+ const routeEnvelope = this.approvalRouteEnvelope(sessionId, requestId);
2996
+ this.registerSessionApproval(session, this.internalPendingMetadata(sessionId, requestId, routeEnvelope, "__workspace_action__"), (decision) => {
1953
2997
  this.clearApprovalTimer(requestId);
1954
- let raw;
1955
- if (decision && typeof decision === "object" && "approved" in decision) {
1956
- const r = decision;
1957
- raw = r.approved ? r.answer : undefined;
1958
- }
1959
- else if (typeof decision === "string") {
1960
- raw = decision;
1961
- }
1962
- if (raw === undefined) {
1963
- reject(new Error("workspace switch declined or unavailable"));
2998
+ const outcome = parseHostLoopbackDecision(decision, "workspace switch");
2999
+ if (!outcome.ok) {
3000
+ reject(new Error(outcome.detail));
1964
3001
  return;
1965
3002
  }
1966
- try {
1967
- const parsed = JSON.parse(raw);
1968
- if ("ok" in parsed && parsed.ok === false) {
1969
- reject(new Error(parsed.error ?? "workspace switch failed"));
1970
- return;
1971
- }
1972
- resolve(parsed);
3003
+ const parsed = outcome.value;
3004
+ // A workspace is always an object. Reject any other JSON shape rather
3005
+ // than resolving it: `null` / a bare number / a string would otherwise
3006
+ // be handed to setSessionWorkspace as if the switch had succeeded,
3007
+ // rebasing the session onto a non-workspace. (Previously a `"ok" in
3008
+ // parsed` TypeError happened to be caught and turned into a rejection;
3009
+ // this states the requirement instead of relying on that.)
3010
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
3011
+ reject(new Error(`workspace switch ${HOST_LOOPBACK_FAILURE_DETAIL.malformed}`));
3012
+ return;
1973
3013
  }
1974
- catch {
1975
- reject(new Error("malformed workspace switch result"));
3014
+ if ("ok" in parsed && parsed.ok === false) {
3015
+ reject(new Error(parsed.error ?? "workspace switch failed"));
3016
+ return;
1976
3017
  }
3018
+ resolve(parsed);
1977
3019
  });
1978
3020
  const timer = setTimeout(() => {
1979
3021
  if (session.pendingApprovals.has(requestId)) {
1980
- session.pendingApprovals.delete(requestId);
3022
+ this.takeSessionApproval(session, requestId, "expired");
1981
3023
  this.pendingApprovalTargets.delete(requestId);
1982
3024
  this.approvalTimers.delete(requestId);
1983
- reject(new Error("workspace switch timed out"));
3025
+ reject(new Error(`workspace switch ${HOST_LOOPBACK_FAILURE_DETAIL.timed_out}`));
1984
3026
  }
1985
3027
  }, AgentServer.APPROVAL_TIMEOUT_MS);
1986
3028
  this.approvalTimers.set(requestId, timer);
1987
3029
  this.notify(Methods.ApprovalRequest, {
1988
- ...this.approvalRouteEnvelope(sessionId, requestId),
3030
+ ...routeEnvelope,
1989
3031
  request: {
1990
3032
  toolName: "__workspace_action__",
1991
3033
  args: { action: "switch", target },
@@ -2059,6 +3101,7 @@ export class AgentServer {
2059
3101
  }
2060
3102
  // ─── Lifecycle ──────────────────────────────────────────────────
2061
3103
  close() {
3104
+ this.observeServerClose();
2062
3105
  this.disconnect("server closing");
2063
3106
  // Detach the bg-agent bus subscription first so a final flurry of
2064
3107
  // completions during shutdown can't race the `shutdown` status
@@ -2073,11 +3116,17 @@ export class AgentServer {
2073
3116
  }
2074
3117
  this.bgAgentBusUnsubscribe = null;
2075
3118
  }
2076
- if (this.chatManager) {
2077
- this.chatManager.forEachSession((session) => {
2078
- this.cancelSessionApprovals(session, "server closing");
2079
- });
2080
- this.chatManager.closeAll();
3119
+ // Close the host-supplied manager plus every identity-derived manager
3120
+ // this server lazily created (the map is empty without a resolveIdentity
3121
+ // hook, so the default path closes exactly the base manager as before).
3122
+ if (this.baseChatManager) {
3123
+ const managers = [this.baseChatManager, ...this.identityManagers.values()];
3124
+ for (const manager of managers) {
3125
+ manager.forEachSession((session) => {
3126
+ this.cancelSessionApprovals(session, "server closing", "cancelled", "session_closed");
3127
+ });
3128
+ manager.closeAll();
3129
+ }
2081
3130
  }
2082
3131
  // Legacy path cleanup
2083
3132
  if (this.abortController) {
@@ -2108,6 +3157,47 @@ export class AgentServer {
2108
3157
  this.approvalRouter.deregister(this.connectionId, reason);
2109
3158
  }
2110
3159
  }
3160
+ /** Resolver-free metadata view for extension projections and protocol snapshots. */
3161
+ getPendingDecisionSnapshot() {
3162
+ const entries = [];
3163
+ this.forEachObserver("snapshotPendingDecisions", (observer) => {
3164
+ const snapshot = observer.snapshotPendingDecisions?.();
3165
+ if (snapshot)
3166
+ entries.push(...snapshot);
3167
+ });
3168
+ return entries;
3169
+ }
3170
+ /** Host lifecycle generation stamped onto pending-approval metadata. */
3171
+ workerGeneration() {
3172
+ return this.chatManager?.getLiveSessionSnapshot().generation ?? 0;
3173
+ }
3174
+ registerSessionApproval(session, metadata, resolve) {
3175
+ metadata = this.observeApprovalCreated(metadata);
3176
+ session.pendingApprovals.set(metadata.requestId, { resolve, metadata });
3177
+ }
3178
+ takeSessionApproval(session, requestId, status) {
3179
+ const entry = session.pendingApprovals.get(requestId);
3180
+ if (!entry)
3181
+ return undefined;
3182
+ session.pendingApprovals.delete(requestId);
3183
+ this.observeApprovalTransition(entry.metadata, status);
3184
+ return entry;
3185
+ }
3186
+ internalPendingMetadata(sessionId, requestId, route, toolName) {
3187
+ const createdAt = Date.now();
3188
+ return {
3189
+ sessionId,
3190
+ requestId,
3191
+ routeGeneration: "generation" in route ? route.generation : undefined,
3192
+ workerGeneration: this.workerGeneration(),
3193
+ kind: "internal",
3194
+ title: "内部操作等待",
3195
+ toolName,
3196
+ createdAt,
3197
+ expiresAt: createdAt + AgentServer.APPROVAL_TIMEOUT_MS,
3198
+ surfaceable: false,
3199
+ };
3200
+ }
2111
3201
  clearApprovalTimer(requestId) {
2112
3202
  const timer = this.approvalTimers.get(requestId);
2113
3203
  if (timer) {
@@ -2122,15 +3212,52 @@ export class AgentServer {
2122
3212
  * the tool hanging. Bounded request types have same-keyed timer entries;
2123
3213
  * AskUserQuestion does not.
2124
3214
  */
2125
- cancelSessionApprovals(session, reason = "cancelled") {
2126
- for (const [requestId, resolve] of session.pendingApprovals) {
2127
- this.clearApprovalTimer(requestId);
2128
- this.pendingApprovalTargets.delete(requestId);
2129
- try {
2130
- resolve({ approved: false, reason });
2131
- }
2132
- catch {
2133
- /* a resolver must never break cancel cleanup */
3215
+ cancelSessionApprovals(session, reason = "cancelled", status = "cancelled", failure = "cancelled") {
3216
+ // Two kinds of entry share this map and they must NOT settle the same way.
3217
+ //
3218
+ // - kind "tool_approval": a real permission prompt. `{approved:false}` is
3219
+ // the correct terminal value — the tool call does not proceed.
3220
+ // - kind "internal": an in-flight host-loopback request (Panel / Browser /
3221
+ // workspace / credential). `{approved:false}` here is a lie: it makes a
3222
+ // Stop or a session close indistinguishable from the user pressing Deny,
3223
+ // and the model is told its Panel operation was refused. These get a
3224
+ // cancellation sentinel carrying the real cause instead.
3225
+ //
3226
+ // Internal entries settle FIRST: they need no user interaction and have a
3227
+ // determinate outcome, and draining them before the map is emptied keeps
3228
+ // every resolver reachable.
3229
+ //
3230
+ // Drain in a loop rather than over one snapshot. A resolver — or the
3231
+ // observeApprovalTransition hook called synchronously below — may register a
3232
+ // NEW pending entry while we are cancelling. Iterating a snapshot and then
3233
+ // clear()ing would delete that newcomer without ever settling it, leaving its
3234
+ // awaiting tool hung until an outer timeout (its own timer is already gone).
3235
+ // The `settled` guard keeps this terminating even if a resolver re-registers
3236
+ // the same requestId, and the iteration cap stops a pathological resolver
3237
+ // that registers a fresh id every time from spinning forever.
3238
+ const settled = new Set();
3239
+ for (let pass = 0; session.pendingApprovals.size > 0 && pass < 1000; pass += 1) {
3240
+ const entries = [...session.pendingApprovals].filter(([id]) => !settled.has(id));
3241
+ if (entries.length === 0)
3242
+ break;
3243
+ const ordered = [
3244
+ ...entries.filter(([, entry]) => entry.metadata.kind === "internal"),
3245
+ ...entries.filter(([, entry]) => entry.metadata.kind !== "internal"),
3246
+ ];
3247
+ for (const [requestId, entry] of ordered) {
3248
+ settled.add(requestId);
3249
+ session.pendingApprovals.delete(requestId);
3250
+ this.clearApprovalTimer(requestId);
3251
+ this.pendingApprovalTargets.delete(requestId);
3252
+ this.observeApprovalTransition(entry.metadata, status);
3253
+ try {
3254
+ entry.resolve(entry.metadata.kind === "internal"
3255
+ ? internalCancellation(failure, reason)
3256
+ : { approved: false, reason });
3257
+ }
3258
+ catch {
3259
+ /* a resolver must never break cancel cleanup */
3260
+ }
2134
3261
  }
2135
3262
  }
2136
3263
  session.pendingApprovals.clear();
@@ -2139,7 +3266,7 @@ export class AgentServer {
2139
3266
  for (const target of targets) {
2140
3267
  const session = this.chatManager?.get(target.sessionId);
2141
3268
  if (session) {
2142
- this.cancelSessionApprovals(session, reason);
3269
+ this.cancelSessionApprovals(session, reason, "owner-lost", "owner_lost");
2143
3270
  continue;
2144
3271
  }
2145
3272
  for (const [requestId, pending] of this.pendingApprovalTargets) {