@vantran-se/openchamber-web 1.24.4 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (367) hide show
  1. package/README.md +2 -8
  2. package/bin/lib/commands-connect-url.js +3 -8
  3. package/bin/lib/commands-tunnel.js +3 -0
  4. package/dist/assets/{CodeMirrorCommentWidgets-B4a0JkUz.js → CodeMirrorCommentWidgets-ntXIbhV2.js} +1 -1
  5. package/dist/assets/DiffView-CjxUovbM.js +4 -0
  6. package/dist/assets/FilesView-DFWD0RQR.js +17 -0
  7. package/dist/assets/{GitView-Byu2DY7s.js → GitView-BWa7e5vw.js} +4 -4
  8. package/dist/assets/{GuestAttachDialog-B808E0-d.js → GuestAttachDialog-DdOuyZzp.js} +1 -1
  9. package/dist/assets/GuestSurfacePane-DDfzid3U.js +1 -0
  10. package/dist/assets/{JsonTreeView-VW_7czo3.js → JsonTreeView-3sGwqexh.js} +1 -1
  11. package/dist/assets/LinearIssuesView-C6dpIdjw.js +1 -0
  12. package/dist/assets/MarkdownImageGallery-Q7YR_tl4.js +1 -0
  13. package/dist/assets/MarkdownRendererImpl-DxDV56bL.js +12 -0
  14. package/dist/assets/OnboardingScreen-rK4Stg__.js +1 -0
  15. package/dist/assets/{PatchDiff-DCjGhlYV.js → PatchDiff-DS-IMU1j.js} +1 -1
  16. package/dist/assets/{PierreDiffViewer-CEDZNkU2.js → PierreDiffViewer-DW23KWKZ.js} +1 -1
  17. package/dist/assets/PlanView-BCk_VobP.js +5 -0
  18. package/dist/assets/PluginPane-BPP1dbck.js +3 -0
  19. package/dist/assets/SettingsView-kMmrpn43.js +29 -0
  20. package/dist/assets/SettingsWindow-BpvD6IYQ.js +1 -0
  21. package/dist/assets/SubmoduleDiffSummary-BLttSS1X.js +1 -0
  22. package/dist/assets/ToolOutputDialog-CtqDqbyr.js +15 -0
  23. package/dist/assets/ToolPartDiffPreview-5FcKdyRy.js +8 -0
  24. package/dist/assets/{VirtualizedFileDiff-NU0FHFhT.js → VirtualizedFileDiff-C763hNCR.js} +4 -4
  25. package/dist/assets/{WalkthroughView-BZyjhqHv.js → WalkthroughView-lap5btCF.js} +2 -2
  26. package/dist/assets/appThemeRegistry-u4zw1l0B.js +1 -0
  27. package/dist/assets/{commitAndPush-BDftrjr6.js → commitAndPush-CwGJfJG-.js} +1 -1
  28. package/dist/assets/{computeVirtualFileMetrics-DlWlEM3x.js → computeVirtualFileMetrics-DW5jjX4n.js} +1 -1
  29. package/dist/assets/de-CndEg67T.js +10 -0
  30. package/dist/assets/es-DglvPlaC.js +16 -0
  31. package/dist/assets/fr-DejPCpbS.js +14 -0
  32. package/dist/assets/index-C2r64QfD.js +1 -0
  33. package/dist/assets/index-CLcM8HKx.css +1 -0
  34. package/dist/assets/index-CvLgnuNI.js +41 -0
  35. package/dist/assets/{isDiffPlainText-CO4e5J_S.js → isDiffPlainText-D47a6hMP.js} +1 -1
  36. package/dist/assets/{isFilePlainText-ue6kCou7.js → isFilePlainText-B-mGgeKl.js} +1 -1
  37. package/dist/assets/ja-CVpQawCd.js +16 -0
  38. package/dist/assets/ko-Bw446h9j.js +16 -0
  39. package/dist/assets/main-Bz4WWXbr.js +2 -0
  40. package/dist/assets/main-oreqDsOV.js +896 -0
  41. package/dist/assets/{markdownCore-CK48LG0z.js → markdownCore-D2e5sp9y.js} +4 -4
  42. package/dist/assets/miniChat-D1v2gsBI.js +2 -0
  43. package/dist/assets/mobile-DBj3lCf2.js +2 -0
  44. package/dist/assets/{patchFileDiff-D7QVlovD.js → patchFileDiff-BpeUOV91.js} +3 -3
  45. package/dist/assets/pl-BYNlPdhm.js +16 -0
  46. package/dist/assets/pt-BR-CL-Yfv9H.js +16 -0
  47. package/dist/assets/renderElectronMiniChatApp-DVtfEB2z.js +2 -0
  48. package/dist/assets/renderMobileApp-DFp9-xbG.js +25 -0
  49. package/dist/assets/runtimeEndpointReset-CsIpUep1.js +27 -0
  50. package/dist/assets/scheduledTasksApi-B5OHVpPM.js +1 -0
  51. package/dist/assets/{shikiHighlight-BPu3JiQs.js → shikiHighlight-DNm2Z_Jc.js} +1 -1
  52. package/dist/assets/{textMateThemeFromAppTheme-DWqRPo9A.js → textMateThemeFromAppTheme-DGF2kG7g.js} +1 -1
  53. package/dist/assets/tr-DzmbxLy-.js +7 -0
  54. package/dist/assets/uk-C7vQnwro.js +16 -0
  55. package/dist/assets/useAppFontEffects-Ci3FEaPf.js +7337 -0
  56. package/dist/assets/{useInlineCommentController-CgS0fnw_.js → useInlineCommentController-CuUuakrt.js} +2 -2
  57. package/dist/assets/useRootScrollLock-CQIsyERq.js +2 -0
  58. package/dist/assets/{vendor-codemirror-language-data-Bx2EHc1O.js → vendor-codemirror-language-data-CQa7_1cr.js} +2 -2
  59. package/dist/assets/vendor-cron-parser-CjvYVigk.js +2 -0
  60. package/dist/assets/vendor-opencode-client-_BAHQo4R.js +12 -0
  61. package/dist/assets/{vendor-remixicon-react-BDmGmFCA.js → vendor-remixicon-react-D6dZ0UwF.js} +1 -1
  62. package/dist/assets/{vendor-zod-CgAdXWZO.js → vendor-zod-Gea9uBlY.js} +1 -1
  63. package/dist/assets/{web-components-DxX8bZ1o.js → web-components-B6tKQyJN.js} +2 -2
  64. package/dist/assets/zh-CN-Cl0srS-z.js +16 -0
  65. package/dist/assets/zh-TW-DtII5bDb.js +16 -0
  66. package/dist/index.html +13 -7
  67. package/dist/mini-chat.html +5 -5
  68. package/dist/mobile.html +5 -5
  69. package/package.json +6 -5
  70. package/server/index.d.ts +1 -0
  71. package/server/index.js +246 -91
  72. package/server/lib/agent-tool/DOCUMENTATION.md +32 -15
  73. package/server/lib/agent-tool/runtime.js +135 -54
  74. package/server/lib/agent-tool/runtime.test.js +155 -87
  75. package/server/lib/browser-control/DOCUMENTATION.md +37 -2
  76. package/server/lib/browser-control/provider.js +217 -0
  77. package/server/lib/browser-control/provider.test.js +251 -0
  78. package/server/lib/context-obligatory/runtime.js +61 -50
  79. package/server/lib/context-obligatory/runtime.test.js +62 -162
  80. package/server/lib/event-stream/DOCUMENTATION.md +12 -1
  81. package/server/lib/event-stream/delta-coalescer.js +0 -0
  82. package/server/lib/event-stream/delta-coalescer.test.js +68 -57
  83. package/server/lib/event-stream/directory-ws-bridge.js +1 -1
  84. package/server/lib/event-stream/global-hub.js +10 -1
  85. package/server/lib/event-stream/global-hub.test.js +27 -26
  86. package/server/lib/event-stream/index.js +6 -0
  87. package/server/lib/event-stream/protocol.js +26 -19
  88. package/server/lib/event-stream/protocol.test.js +18 -7
  89. package/server/lib/event-stream/rebind.test.js +5 -5
  90. package/server/lib/event-stream/runtime.test.js +17 -15
  91. package/server/lib/event-stream/translate-v2.js +313 -0
  92. package/server/lib/event-stream/translate-v2.test.js +209 -0
  93. package/server/lib/event-stream/upstream-reader.test.js +16 -16
  94. package/server/lib/fs/DOCUMENTATION.md +4 -2
  95. package/server/lib/fs/byte-range.js +35 -0
  96. package/server/lib/fs/byte-range.test.js +31 -0
  97. package/server/lib/fs/routes.js +47 -42
  98. package/server/lib/fs/routes.test.js +123 -18
  99. package/server/lib/git/service.js +12 -0
  100. package/server/lib/github/DOCUMENTATION.md +1 -1
  101. package/server/lib/github/pr-status.js +22 -2
  102. package/server/lib/github/pr-status.test.js +27 -14
  103. package/server/lib/guests/DOCUMENTATION.md +13 -0
  104. package/server/lib/guests/catalog.js +12 -0
  105. package/server/lib/guests/catalog.test.js +33 -0
  106. package/server/lib/guests/routes.js +10 -1
  107. package/server/lib/guests/service.js +192 -14
  108. package/server/lib/guests/service.test.js +201 -1
  109. package/server/lib/guests/surface.js +630 -0
  110. package/server/lib/guests/surface.test.js +486 -0
  111. package/server/lib/markdown-image-grants/routes.js +18 -5
  112. package/server/lib/markdown-image-grants/routes.test.js +9 -6
  113. package/server/lib/message-queue/DOCUMENTATION.md +33 -22
  114. package/server/lib/message-queue/runtime.js +155 -100
  115. package/server/lib/message-queue/runtime.test.js +168 -82
  116. package/server/lib/notifications/DOCUMENTATION.md +28 -0
  117. package/server/lib/notifications/control-stream.http.test.js +68 -0
  118. package/server/lib/notifications/emitter-runtime.js +15 -0
  119. package/server/lib/notifications/emitter-runtime.test.js +32 -0
  120. package/server/lib/notifications/push-runtime.js +5 -1
  121. package/server/lib/notifications/push-runtime.test.js +2 -2
  122. package/server/lib/notifications/routes.js +1 -1
  123. package/server/lib/notifications/runtime.js +115 -26
  124. package/server/lib/notifications/template-runtime.js +13 -15
  125. package/server/lib/notifications/template-runtime.test.js +16 -9
  126. package/server/lib/notifications/trigger-runtime.test.js +187 -0
  127. package/server/lib/openchamber-control/DOCUMENTATION.md +10 -0
  128. package/server/lib/openchamber-control/actions.js +3 -2
  129. package/server/lib/openchamber-control/error.js +1 -1
  130. package/server/lib/openchamber-control/file-open.js +59 -0
  131. package/server/lib/openchamber-control/file-open.test.js +57 -0
  132. package/server/lib/openchamber-control/routes.test.js +12 -0
  133. package/server/lib/openchamber-control/service.js +102 -49
  134. package/server/lib/openchamber-control/service.test.js +84 -44
  135. package/server/lib/openchamber-sessions/archive-store.js +224 -0
  136. package/server/lib/openchamber-sessions/archive-store.test.js +187 -0
  137. package/server/lib/openchamber-sessions/opencode-client.js +22 -0
  138. package/server/lib/openchamber-sessions/routes.js +398 -328
  139. package/server/lib/openchamber-sessions/routes.test.js +777 -740
  140. package/server/lib/openchamber-sessions/session-metadata-store.js +285 -0
  141. package/server/lib/openchamber-sessions/session-metadata-store.test.js +259 -0
  142. package/server/lib/opencode/DOCUMENTATION.md +449 -29
  143. package/server/lib/opencode/agents.js +233 -327
  144. package/server/lib/opencode/auth.js +41 -44
  145. package/server/lib/opencode/auth.test.js +92 -0
  146. package/server/lib/opencode/cli-upgrade.d.ts +4 -0
  147. package/server/lib/opencode/cli-upgrade.js +18 -0
  148. package/server/lib/opencode/cli-upgrade.test.js +36 -0
  149. package/server/lib/opencode/commands.js +164 -145
  150. package/server/lib/opencode/compatibility.d.ts +16 -0
  151. package/server/lib/opencode/compatibility.js +88 -0
  152. package/server/lib/opencode/compatibility.test.js +62 -0
  153. package/server/lib/opencode/config-entity-routes.js +58 -29
  154. package/server/lib/opencode/config-entity-v2.test.js +290 -0
  155. package/server/lib/opencode/config-mutation-response.js +17 -7
  156. package/server/lib/opencode/config-mutation-response.test.js +4 -7
  157. package/server/lib/opencode/config-paths.test.js +2 -2
  158. package/server/lib/opencode/config-v2.d.ts +183 -0
  159. package/server/lib/opencode/config-v2.js +801 -0
  160. package/server/lib/opencode/config-v2.test.js +466 -0
  161. package/server/lib/opencode/credential-db.js +144 -0
  162. package/server/lib/opencode/credential-db.test.js +104 -0
  163. package/server/lib/opencode/env-runtime.js +48 -9
  164. package/server/lib/opencode/env-runtime.test.js +45 -0
  165. package/server/lib/opencode/feature-routes-runtime.js +15 -5
  166. package/server/lib/opencode/lifecycle-process.test.js +7 -4
  167. package/server/lib/opencode/lifecycle.js +85 -30
  168. package/server/lib/opencode/lifecycle.test.js +80 -8
  169. package/server/lib/opencode/login-shell-env.js +27 -0
  170. package/server/lib/opencode/managed-config-file.js +119 -0
  171. package/server/lib/opencode/managed-config-file.test.js +152 -0
  172. package/server/lib/opencode/managed-plugin-config.js +33 -13
  173. package/server/lib/opencode/mcp.js +43 -149
  174. package/server/lib/opencode/network-runtime.js +6 -7
  175. package/server/lib/opencode/openchamber-routes.test.js +2 -5
  176. package/server/lib/opencode/plugin-routes.js +3 -3
  177. package/server/lib/opencode/plugin-routes.test.js +6 -24
  178. package/server/lib/opencode/plugins.js +91 -40
  179. package/server/lib/opencode/plugins.test.js +28 -15
  180. package/server/lib/opencode/providers.js +63 -83
  181. package/server/lib/opencode/providers.test.js +128 -59
  182. package/server/lib/opencode/proxy-agent-wiring.test.js +4 -2
  183. package/server/lib/opencode/proxy.js +179 -129
  184. package/server/lib/opencode/pwa-manifest-routes.js +4 -2
  185. package/server/lib/opencode/pwa-manifest-routes.test.js +3 -2
  186. package/server/lib/opencode/response-envelope.js +14 -0
  187. package/server/lib/opencode/response-envelope.test.js +23 -0
  188. package/server/lib/opencode/routes-upgrade.test.js +73 -116
  189. package/server/lib/opencode/routes.js +121 -428
  190. package/server/lib/opencode/server-utils-runtime.js +10 -4
  191. package/server/lib/opencode/session-activity.js +78 -0
  192. package/server/lib/opencode/session-runtime.js +21 -10
  193. package/server/lib/opencode/session-runtime.test.js +9 -9
  194. package/server/lib/opencode/settings-helpers.js +9 -9
  195. package/server/lib/opencode/settings-helpers.test.js +7 -12
  196. package/server/lib/opencode/settings-registry.json +6 -8
  197. package/server/lib/opencode/settings-runtime.js +18 -1
  198. package/server/lib/opencode/settings-runtime.test.js +31 -2
  199. package/server/lib/opencode/shared.js +54 -22
  200. package/server/lib/opencode/shared.test.js +8 -4
  201. package/server/lib/opencode/shutdown-runtime.js +54 -7
  202. package/server/lib/opencode/shutdown-runtime.test.js +224 -30
  203. package/server/lib/opencode/skill-routes.js +23 -24
  204. package/server/lib/opencode/skills.js +7 -5
  205. package/server/lib/opencode/tunnel-wiring-runtime.js +2 -1
  206. package/server/lib/opencode/upgrade-capability.test.js +1 -1
  207. package/server/lib/opencode/v1-migration-topup.js +288 -0
  208. package/server/lib/opencode/v1-migration-topup.test.js +266 -0
  209. package/server/lib/opencode/v2-install.d.ts +8 -0
  210. package/server/lib/opencode/v2-install.js +158 -0
  211. package/server/lib/opencode/v2-install.test.js +124 -0
  212. package/server/lib/opencode/watcher.js +10 -3
  213. package/server/lib/opencode/watcher.test.js +17 -21
  214. package/server/lib/opencode/websearch-config.js +66 -0
  215. package/server/lib/package-manager.js +55 -68
  216. package/server/lib/package-manager.test.js +241 -75
  217. package/server/lib/permission-auto-accept/runtime.js +38 -22
  218. package/server/lib/permission-auto-accept/runtime.test.js +39 -17
  219. package/server/lib/quota/DOCUMENTATION.md +4 -0
  220. package/server/lib/quota/providers/xai.js +4 -4
  221. package/server/lib/relay/tunnel-host.js +4 -1
  222. package/server/lib/relay/tunnel-host.test.js +7 -0
  223. package/server/lib/routing/DOCUMENTATION.md +52 -21
  224. package/server/lib/routing/defaults.js +23 -6
  225. package/server/lib/routing/jev.js +17 -7
  226. package/server/lib/routing/routes.http.test.js +68 -33
  227. package/server/lib/routing/routes.js +65 -22
  228. package/server/lib/routing/runtime.js +145 -48
  229. package/server/lib/routing/runtime.test.js +79 -55
  230. package/server/lib/scheduled-tasks/DOCUMENTATION.md +4 -0
  231. package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +51 -46
  232. package/server/lib/scheduled-tasks/runtime.js +81 -74
  233. package/server/lib/scheduled-tasks/runtime.test.js +53 -1
  234. package/server/lib/session-assist/DOCUMENTATION.md +8 -3
  235. package/server/lib/session-assist/context.js +122 -46
  236. package/server/lib/session-assist/context.test.js +117 -49
  237. package/server/lib/session-assist/runtime.js +52 -22
  238. package/server/lib/session-assist/runtime.test.js +145 -173
  239. package/server/lib/session-goal/DOCUMENTATION.md +23 -15
  240. package/server/lib/session-goal/create.js +9 -12
  241. package/server/lib/session-goal/create.test.js +27 -28
  242. package/server/lib/session-goal/runtime.js +164 -77
  243. package/server/lib/session-goal/runtime.test.js +271 -519
  244. package/server/lib/session-knowledge/DOCUMENTATION.md +5 -0
  245. package/server/lib/session-knowledge/runtime.js +59 -41
  246. package/server/lib/session-knowledge/runtime.test.js +90 -20
  247. package/server/lib/small-model/DOCUMENTATION.md +154 -208
  248. package/server/lib/small-model/client.js +113 -0
  249. package/server/lib/small-model/index.js +296 -158
  250. package/server/lib/small-model/index.test.js +423 -256
  251. package/server/lib/small-model/routes.js +1 -1
  252. package/server/lib/spaces/DOCUMENTATION.md +807 -0
  253. package/server/lib/spaces/code-in-bait.js +185 -0
  254. package/server/lib/spaces/code-in.js +641 -0
  255. package/server/lib/spaces/code-in.test.js +1048 -0
  256. package/server/lib/spaces/errors.js +8 -0
  257. package/server/lib/spaces/exec-http.js +94 -0
  258. package/server/lib/spaces/gatekeeper-channel.js +202 -0
  259. package/server/lib/spaces/gatekeeper-channel.test.js +215 -0
  260. package/server/lib/spaces/gatekeeper-program.cjs +914 -0
  261. package/server/lib/spaces/gatekeeper-program.test.js +1391 -0
  262. package/server/lib/spaces/hardening.js +457 -0
  263. package/server/lib/spaces/hardening.test.js +368 -0
  264. package/server/lib/spaces/host-git.js +127 -0
  265. package/server/lib/spaces/host-git.test.js +137 -0
  266. package/server/lib/spaces/labels.js +234 -0
  267. package/server/lib/spaces/labels.test.js +219 -0
  268. package/server/lib/spaces/layout.js +184 -0
  269. package/server/lib/spaces/layout.test.js +30 -0
  270. package/server/lib/spaces/manager.js +82 -0
  271. package/server/lib/spaces/manager.test.js +114 -0
  272. package/server/lib/spaces/places/code-in.docker.live.test.js +216 -0
  273. package/server/lib/spaces/places/contract-suite.js +156 -0
  274. package/server/lib/spaces/places/contract.docker.live.test.js +7 -0
  275. package/server/lib/spaces/places/contract.test.js +6 -0
  276. package/server/lib/spaces/places/docker-engine.js +126 -0
  277. package/server/lib/spaces/places/docker-live-support.js +202 -0
  278. package/server/lib/spaces/places/docker-tools.js +186 -0
  279. package/server/lib/spaces/places/docker.js +587 -0
  280. package/server/lib/spaces/places/docker.test.js +1610 -0
  281. package/server/lib/spaces/places/escape-suite.js +1018 -0
  282. package/server/lib/spaces/places/escape.docker.live.test.js +7 -0
  283. package/server/lib/spaces/places/fake-docker.js +375 -0
  284. package/server/lib/spaces/places/memory-place.js +103 -0
  285. package/server/lib/spaces/places/packed.docker.live.test.js +77 -0
  286. package/server/lib/spaces/places/registry.js +43 -0
  287. package/server/lib/spaces/places/registry.test.js +50 -0
  288. package/server/lib/spaces/places/server.docker.live.test.js +225 -0
  289. package/server/lib/spaces/run-command.js +261 -0
  290. package/server/lib/spaces/run-command.test.js +396 -0
  291. package/server/lib/spaces/space-server.js +190 -0
  292. package/server/lib/spaces/space-server.test.js +335 -0
  293. package/server/lib/spaces/tools-filler.js +82 -0
  294. package/server/lib/spaces/tools-filler.test.js +148 -0
  295. package/server/lib/spaces/tools-pack.js +49 -0
  296. package/server/lib/spaces/tools-pack.test.js +57 -0
  297. package/server/lib/spaces/tools.js +138 -0
  298. package/server/lib/spaces/tools.test.js +153 -0
  299. package/server/lib/tts/openai-sdk.js +11 -0
  300. package/server/lib/tts/service.js +7 -5
  301. package/server/lib/tts/stt.js +3 -2
  302. package/server/lib/tunnels/DOCUMENTATION.md +2 -0
  303. package/server/lib/tunnels/routes.js +7 -0
  304. package/server/lib/tunnels/routes.test.js +55 -0
  305. package/server/lib/ui-auth/ui-auth.js +38 -2
  306. package/server/lib/ui-auth/ui-auth.test.js +42 -0
  307. package/server/lib/ui-auth/ui-passkeys.js +12 -10
  308. package/server/lib/walkthrough/DOCUMENTATION.md +10 -1
  309. package/server/lib/walkthrough/pull-request.js +61 -8
  310. package/server/lib/walkthrough/pull-request.test.js +54 -1
  311. package/server/lib/walkthrough/routes.js +20 -0
  312. package/server/lib/walkthrough/routes.test.js +19 -0
  313. package/server/opencode-proxy.test.js +190 -166
  314. package/server/opencode-worktree-readiness.test.js +5 -4
  315. package/dist/assets/DiffView-B2CD9wC8.js +0 -4
  316. package/dist/assets/FilesView-C4vMFclc.js +0 -13
  317. package/dist/assets/LinearIssuesView-EzGNCGrd.js +0 -1
  318. package/dist/assets/MarkdownImageGallery-B2YUDFaB.js +0 -1
  319. package/dist/assets/MarkdownRendererImpl-B4ikIzR3.js +0 -12
  320. package/dist/assets/OnboardingScreen-CQHBSTEi.js +0 -1
  321. package/dist/assets/PlanView-D3x-pTkZ.js +0 -5
  322. package/dist/assets/PluginPane-DEcgDQ9h.js +0 -3
  323. package/dist/assets/SettingsView-CSinuH-1.js +0 -29
  324. package/dist/assets/SettingsWindow-B5CHoJFZ.js +0 -1
  325. package/dist/assets/SubmoduleDiffSummary-oxeACCWs.js +0 -1
  326. package/dist/assets/ToolOutputDialog-tLNWmfcW.js +0 -15
  327. package/dist/assets/ToolPartDiffPreview-CflzmK-x.js +0 -8
  328. package/dist/assets/appThemeRegistry-DLicw4dH.js +0 -1
  329. package/dist/assets/de-Bro6_zyl.js +0 -10
  330. package/dist/assets/es-Bb9XQZvd.js +0 -16
  331. package/dist/assets/fr-Dgfzn-IL.js +0 -14
  332. package/dist/assets/index-CtQNmqUL.js +0 -1
  333. package/dist/assets/index-DqaMWNrC.css +0 -1
  334. package/dist/assets/index-Dqc6USl3.js +0 -40
  335. package/dist/assets/ja-BYsakGFh.js +0 -16
  336. package/dist/assets/ko-BSaQz9aN.js +0 -16
  337. package/dist/assets/main-B62_pjL5.js +0 -2
  338. package/dist/assets/main-BEdcOI2P.js +0 -896
  339. package/dist/assets/miniChat-LT8Rqofr.js +0 -2
  340. package/dist/assets/mobile-CqarEynh.js +0 -2
  341. package/dist/assets/pl-OIviPdoj.js +0 -16
  342. package/dist/assets/pt-BR-Cltg-E6s.js +0 -16
  343. package/dist/assets/renderElectronMiniChatApp-COyLTtZx.js +0 -2
  344. package/dist/assets/renderMobileApp-BC1MHg6g.js +0 -25
  345. package/dist/assets/runtimeEndpointReset-DdrazOVH.js +0 -25
  346. package/dist/assets/scheduledTasksApi-B2SWMGEU.js +0 -1
  347. package/dist/assets/tr-CQI-KPre.js +0 -7
  348. package/dist/assets/uk-CqW7R4DA.js +0 -16
  349. package/dist/assets/useAppFontEffects-ne84JBVL.js +0 -7333
  350. package/dist/assets/useRootScrollLock-Cpf1qoGp.js +0 -2
  351. package/dist/assets/vendor-cron-parser-CUcP7OfX.js +0 -2
  352. package/dist/assets/vendor-opencode-sdk-BJ73u-bH.js +0 -7
  353. package/dist/assets/zh-CN-CVZG_o_J.js +0 -16
  354. package/dist/assets/zh-TW-D5ZxzCZp.js +0 -16
  355. package/server/lib/opencode/mcp-oauth-callback.test.js +0 -111
  356. package/server/lib/routing/feature-flag.js +0 -11
  357. package/server/lib/small-model/call.js +0 -851
  358. package/server/lib/small-model/call.test.js +0 -1292
  359. package/server/lib/small-model/catalog.js +0 -13
  360. package/server/lib/small-model/resolve.js +0 -131
  361. package/server/lib/small-model/resolve.test.js +0 -197
  362. package/server/lib/small-model/runtime-providers.js +0 -164
  363. package/server/lib/small-model/runtime-providers.test.js +0 -118
  364. package/server/lib/system-prompt/DOCUMENTATION.md +0 -36
  365. package/server/lib/system-prompt/runtime.js +0 -49
  366. package/server/lib/system-prompt/runtime.test.js +0 -76
  367. package/server/lib/walkthrough/reproduce-2607.test.js +0 -148
@@ -195,6 +195,7 @@ export const registerGuestRoutes = (app, {
195
195
  resolveGitBinaryForSpawn,
196
196
  resolveOptionalProjectDirectory,
197
197
  getSmallModelService,
198
+ onGuestDeactivated = async () => false,
198
199
  }) => {
199
200
  const persistPath = extensionsPersistPath(openchamberDataDir);
200
201
  const authPath = guestAuthPersistPath(openchamberDataDir);
@@ -309,6 +310,7 @@ export const registerGuestRoutes = (app, {
309
310
  if (!isGuestPanelId(id)) {
310
311
  return res.status(404).json({ error: 'not-found' });
311
312
  }
313
+ const removed = await loadGuest(id);
312
314
  const result = await uninstallGuest(id, persistPath);
313
315
  if (!result.ok) {
314
316
  const status = result.code === 'bundled' ? 400 : 404;
@@ -316,6 +318,9 @@ export const registerGuestRoutes = (app, {
316
318
  }
317
319
  // Remove means forget: tokens, client secret, and settings go with the package.
318
320
  await forgetGuestAuth(id, authPath);
321
+ // A role this package stood in for (the agent's browser) goes back to
322
+ // the host's own, and the user is told rather than finding out mid-task.
323
+ await onGuestDeactivated({ guestId: id, guestName: removed?.name ?? id });
319
324
  res.status(204).end();
320
325
  } catch (error) {
321
326
  console.error('Failed to uninstall guest:', error);
@@ -561,7 +566,7 @@ export const registerGuestRoutes = (app, {
561
566
  res.json(result);
562
567
  } catch (error) {
563
568
  if (error instanceof GuestServiceError) {
564
- const status = error.code === 'SERVICE_FAILED' ? 502 : 400;
569
+ const status = error.code === 'SERVICE_FAILED' || error.code === 'REQUEST_FAILED' ? 502 : 400;
565
570
  return res.status(status).json({ error: error.code, message: error.message });
566
571
  }
567
572
  console.error('Failed to proxy guest service request:', error);
@@ -716,6 +721,7 @@ export const registerGuestRoutes = (app, {
716
721
  await setCapabilityGrants(guest.id, persistPath, granted, granted.length > 0 ? scope : null);
717
722
  if (granted.length === 0) {
718
723
  await stopGuestService(guest.id);
724
+ await onGuestDeactivated({ guestId: guest.id, guestName: guest.name });
719
725
  }
720
726
  // Credentials were stored for one API origin and one pair of OAuth
721
727
  // endpoints. When a newer version points the integration somewhere
@@ -745,6 +751,9 @@ export const registerGuestRoutes = (app, {
745
751
  return res.status(400).json({ error: 'invalid-request' });
746
752
  }
747
753
  await setGuestEnabled(guest.id, persistPath, parsed.data.enabled);
754
+ if (!parsed.data.enabled) {
755
+ await onGuestDeactivated({ guestId: guest.id, guestName: guest.name });
756
+ }
748
757
  const next = await loadGuest(guest.id);
749
758
  if (!next) {
750
759
  return res.status(404).json({ error: 'not-found' });
@@ -37,10 +37,39 @@ const runtimes = new Map();
37
37
  /** @type {Map<string, Promise<ServiceRuntime>>} */
38
38
  const startingByGuest = new Map();
39
39
 
40
+ /** @type {Map<string, Promise<void>>} */
41
+ const stoppingByGuest = new Map();
42
+
43
+ // Requests retain their lifecycle object across awaits. A later host start
44
+ // cannot reopen admission for work that belonged to the previous host.
45
+ let hostLifecycle = { accepting: true };
46
+
47
+ export const beginGuestServiceHost = () => {
48
+ if (runtimes.size || startingByGuest.size || stoppingByGuest.size) {
49
+ throw new Error('Guest services must finish stopping before a new host starts.');
50
+ }
51
+ hostLifecycle.accepting = false;
52
+ hostLifecycle = { accepting: true };
53
+ };
54
+
55
+ export const beginGuestServiceShutdown = () => {
56
+ hostLifecycle.accepting = false;
57
+ };
58
+
59
+ const assertGuestHostActive = (lifecycle) => {
60
+ if (!lifecycle.accepting) {
61
+ throw new GuestServiceError('The host is shutting down.', 'NO_SERVICE');
62
+ }
63
+ };
64
+
40
65
  export class GuestServiceError extends Error {
41
66
  /**
42
67
  * @param {string} message
43
- * @param {'NO_SERVICE' | 'SERVICE_FAILED' | 'BAD_PATH' | 'BAD_METHOD'} code
68
+ * @param {'NO_SERVICE' | 'SERVICE_FAILED' | 'REQUEST_FAILED' | 'BAD_PATH' | 'BAD_METHOD' | 'DISABLED' | 'CANCELLED'} code
69
+ * `SERVICE_FAILED` is a service that never became ready: nothing was sent
70
+ * to it. `REQUEST_FAILED` is a request that was sent and got no usable
71
+ * answer (timeout, dropped connection, unreadable body): the service may
72
+ * have acted on it.
44
73
  */
45
74
  constructor(message, code) {
46
75
  super(message);
@@ -175,6 +204,72 @@ const stopEpochs = new Map();
175
204
 
176
205
  const stopEpochOf = (guestId) => stopEpochs.get(guestId) ?? 0;
177
206
 
207
+ /**
208
+ * Services the host starts on its own (a provider role) have no panel whose
209
+ * closing would end them, so they end themselves: a request arms a timer, the
210
+ * next request re-arms it, and when it fires with nothing in flight the
211
+ * process goes away. The next request starts it again. A panel-driven service
212
+ * passes no `idleStopMs` and keeps today's lifetime.
213
+ * @type {Map<string, { timer: ReturnType<typeof setTimeout>, inflight: number, idleMs: number }>}
214
+ */
215
+ const idleStops = new Map();
216
+
217
+ const clearIdleStop = (guestId) => {
218
+ const entry = idleStops.get(guestId);
219
+ if (!entry) return;
220
+ clearTimeout(entry.timer);
221
+ idleStops.delete(guestId);
222
+ };
223
+
224
+ /** Long-lived consumers (a surface viewer) that keep an idle-stopping service up. */
225
+ const holds = new Map();
226
+
227
+ const armIdleStop = (guestId, idleMs) => {
228
+ const previous = idleStops.get(guestId);
229
+ const inflight = previous?.inflight ?? 0;
230
+ if (previous) clearTimeout(previous.timer);
231
+ const timer = setTimeout(() => {
232
+ const current = idleStops.get(guestId);
233
+ if (!current || current.inflight > 0 || (holds.get(guestId) ?? 0) > 0) {
234
+ // A request is running or a viewer is attached; whichever ends last
235
+ // re-arms the timer.
236
+ return;
237
+ }
238
+ idleStops.delete(guestId);
239
+ void discardRuntime(guestId);
240
+ }, idleMs);
241
+ timer.unref?.();
242
+ idleStops.set(guestId, { timer, inflight, idleMs });
243
+ };
244
+
245
+ /**
246
+ * Keeps an idle-stopping service alive while the returned release function
247
+ * has not been called. Releasing the last hold restarts the idle window.
248
+ * @param {string} guestId
249
+ */
250
+ export const holdGuestService = (guestId) => {
251
+ holds.set(guestId, (holds.get(guestId) ?? 0) + 1);
252
+ let released = false;
253
+ return () => {
254
+ if (released) return;
255
+ released = true;
256
+ const remaining = (holds.get(guestId) ?? 1) - 1;
257
+ if (remaining > 0) {
258
+ holds.set(guestId, remaining);
259
+ return;
260
+ }
261
+ holds.delete(guestId);
262
+ const entry = idleStops.get(guestId);
263
+ if (entry && runtimes.get(guestId)) armIdleStop(guestId, entry.idleMs);
264
+ };
265
+ };
266
+
267
+ const trackInflight = (guestId, delta) => {
268
+ const entry = idleStops.get(guestId);
269
+ if (!entry) return;
270
+ entry.inflight = Math.max(0, entry.inflight + delta);
271
+ };
272
+
178
273
  /**
179
274
  * A user-facing stop (Pause, Remove, withdrawn approval, socket change,
180
275
  * host quit): ends the process and cancels any request or start in flight.
@@ -182,6 +277,7 @@ const stopEpochOf = (guestId) => stopEpochs.get(guestId) ?? 0;
182
277
  */
183
278
  export const stopGuestService = async (guestId) => {
184
279
  stopEpochs.set(guestId, stopEpochOf(guestId) + 1);
280
+ clearIdleStop(guestId);
185
281
  await discardRuntime(guestId);
186
282
  };
187
283
 
@@ -192,6 +288,8 @@ export const stopGuestService = async (guestId) => {
192
288
  * @param {string} guestId
193
289
  */
194
290
  const discardRuntime = async (guestId) => {
291
+ const stopping = stoppingByGuest.get(guestId);
292
+ if (stopping) return stopping;
195
293
  const runtime = runtimes.get(guestId);
196
294
  if (!runtime) {
197
295
  return;
@@ -202,7 +300,7 @@ const discardRuntime = async (guestId) => {
202
300
  if (child.exitCode !== null || child.signalCode) {
203
301
  return;
204
302
  }
205
- await new Promise((resolve) => {
303
+ const stopped = new Promise((resolve) => {
206
304
  const timer = setTimeout(() => {
207
305
  child.kill('SIGKILL');
208
306
  resolve(undefined);
@@ -213,14 +311,24 @@ const discardRuntime = async (guestId) => {
213
311
  });
214
312
  child.kill('SIGTERM');
215
313
  });
314
+ stoppingByGuest.set(guestId, stopped);
315
+ try {
316
+ await stopped;
317
+ } finally {
318
+ stoppingByGuest.delete(guestId);
319
+ }
216
320
  };
217
321
 
218
322
  /** Test seam: the pid of a guest's running service, or `null`. */
219
323
  export const readServicePid = (guestId) => runtimes.get(guestId)?.child.pid ?? null;
220
324
 
221
325
  export const stopAllGuestServices = async () => {
222
- const ids = [...runtimes.keys()];
223
- await Promise.all(ids.map((id) => stopGuestService(id)));
326
+ const starts = [...startingByGuest.values()];
327
+ const ids = new Set([...runtimes.keys(), ...startingByGuest.keys(), ...stoppingByGuest.keys()]);
328
+ const stops = [...ids].map((id) => stopGuestService(id));
329
+ // Cancelled starts reject normally. Await their cleanup as well as children
330
+ // already registered when shutdown began.
331
+ await Promise.allSettled([...starts, ...stops]);
224
332
  };
225
333
 
226
334
  /**
@@ -266,7 +374,14 @@ const startGuestService = async ({
266
374
  socketBindings = [],
267
375
  socketOverrides = {},
268
376
  epoch,
377
+ lifecycle,
269
378
  }) => {
379
+ assertGuestHostActive(lifecycle);
380
+ // A replacement must not occupy the same guest slot while its previous
381
+ // child is still stopping; host shutdown must be able to drain both.
382
+ const previousStop = stoppingByGuest.get(guestId);
383
+ if (previousStop) await previousStop;
384
+ assertGuestHostActive(lifecycle);
270
385
  const existing = runtimes.get(guestId);
271
386
  if (existing?.status === 'ready' && existing.child.exitCode === null && !existing.child.signalCode) {
272
387
  return existing;
@@ -276,7 +391,7 @@ const startGuestService = async ({
276
391
  }
277
392
  // The request's epoch, read before its first store access: a Pause that
278
393
  // finished anywhere since then is a cancellation, spawn included.
279
- const cancelled = () => stopEpochOf(guestId) !== epoch;
394
+ const cancelled = () => !lifecycle.accepting || stopEpochOf(guestId) !== epoch;
280
395
  const stoppedError = () => new GuestServiceError('The service was stopped before it became ready.', 'NO_SERVICE');
281
396
 
282
397
  const absoluteEntry = path.resolve(packageRoot, entry);
@@ -294,6 +409,7 @@ const startGuestService = async ({
294
409
  throw stoppedError();
295
410
  }
296
411
  const port = await reserveLoopbackPort();
412
+ if (cancelled()) throw stoppedError();
297
413
  const token = crypto.randomBytes(24).toString('hex');
298
414
  const socketEnv = socketBindings.length > 0
299
415
  ? await resolveServiceSocketEnv(socketBindings, socketOverrides)
@@ -418,8 +534,15 @@ const ensureGuestService = async (params) => {
418
534
  };
419
535
 
420
536
  /**
537
+ * Starts the guest's service if needed and sends it one HTTP request, giving
538
+ * back the raw `Response`. This is the one place a host-driven caller (a
539
+ * provider role, a shared surface) or a panel proxy talks to the loopback:
540
+ * the enabled check, the grant, the spawn, the epoch, and the bearer live
541
+ * here. The caller reads the body as it needs it (text, JSON, image bytes).
542
+ *
421
543
  * @param {{
422
544
  * guestId: string,
545
+ * guestName?: string,
423
546
  * packageRoot: string,
424
547
  * service: { entry: string, permissions?: { sockets?: Array<{ id: string, candidatesByPlatform?: Partial<Record<'linux' | 'darwin' | 'win32', string[]>> }>, exec?: string[] } },
425
548
  * granted: string[],
@@ -428,9 +551,16 @@ const ensureGuestService = async (params) => {
428
551
  * path: string,
429
552
  * query?: Record<string, string>,
430
553
  * body?: string,
431
- * }} params
554
+ * accept?: string,
555
+ * timeoutMs?: number,
556
+ * idleStopMs?: number,
557
+ * signal?: AbortSignal,
558
+ * }} params `timeoutMs` and `idleStopMs` are for host-driven calls; a
559
+ * panel's `serviceRequest` keeps the SDK limits. With `idleStopMs` the
560
+ * service stops itself after that long without a request; `holdGuestService`
561
+ * suspends that while a long-lived consumer (a surface viewer) is attached.
432
562
  */
433
- export const proxyGuestServiceRequest = async ({
563
+ export const openGuestServiceRequest = async ({
434
564
  guestId,
435
565
  guestName,
436
566
  packageRoot,
@@ -441,7 +571,13 @@ export const proxyGuestServiceRequest = async ({
441
571
  path: requestPath,
442
572
  query,
443
573
  body,
574
+ accept = 'application/json',
575
+ timeoutMs = GUEST_REQUEST_TIMEOUT_MS,
576
+ idleStopMs,
577
+ signal,
444
578
  }) => {
579
+ const lifecycle = hostLifecycle;
580
+ assertGuestHostActive(lifecycle);
445
581
  if (!METHODS.has(method)) {
446
582
  throw new GuestServiceError('Unsupported request method.', 'BAD_METHOD');
447
583
  }
@@ -453,6 +589,7 @@ export const proxyGuestServiceRequest = async ({
453
589
  // enabled flag it read before the pause.
454
590
  const epoch = stopEpochOf(guestId);
455
591
  const store = await readExtensionStore(persistPath);
592
+ assertGuestHostActive(lifecycle);
456
593
  if (store.disabledGuests?.[guestId]) {
457
594
  const label = typeof guestName === 'string' && guestName.trim() ? guestName.trim() : 'This extension';
458
595
  throw new GuestServiceError(
@@ -481,9 +618,10 @@ export const proxyGuestServiceRequest = async ({
481
618
  socketBindings,
482
619
  socketOverrides,
483
620
  epoch,
621
+ lifecycle,
484
622
  });
485
623
  }
486
- if (stopEpochOf(guestId) !== epoch) {
624
+ if (!lifecycle.accepting || stopEpochOf(guestId) !== epoch) {
487
625
  if (runtimes.get(guestId) === runtime) {
488
626
  await discardRuntime(guestId);
489
627
  }
@@ -507,13 +645,24 @@ export const proxyGuestServiceRequest = async ({
507
645
 
508
646
  /** @type {Record<string, string>} */
509
647
  const headers = {
510
- Accept: 'application/json',
648
+ Accept: accept,
511
649
  [OPENCHAMBER_SERVICE_AUTH_HEADER]: `Bearer ${runtime.token}`,
512
650
  };
513
651
  if (body !== undefined && method !== 'GET') {
514
652
  headers['Content-Type'] = 'application/json';
515
653
  }
516
654
 
655
+ if (idleStopMs) {
656
+ armIdleStop(guestId, idleStopMs);
657
+ trackInflight(guestId, 1);
658
+ }
659
+ const settleIdle = () => {
660
+ if (!idleStopMs) return;
661
+ trackInflight(guestId, -1);
662
+ // Measured from the end of the last request, not its start: a long
663
+ // action must not be cut short by a timer armed before it began.
664
+ if (runtimes.get(guestId) === runtime) armIdleStop(guestId, idleStopMs);
665
+ };
517
666
  let response;
518
667
  try {
519
668
  response = await fetch(url, {
@@ -521,18 +670,47 @@ export const proxyGuestServiceRequest = async ({
521
670
  headers,
522
671
  body: method === 'GET' || body === undefined ? undefined : body,
523
672
  redirect: 'manual',
524
- signal: AbortSignal.timeout(GUEST_REQUEST_TIMEOUT_MS),
673
+ signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)]) : AbortSignal.timeout(timeoutMs),
525
674
  });
526
675
  } catch {
676
+ settleIdle();
677
+ if (signal?.aborted) {
678
+ throw new GuestServiceError('The request was cancelled.', 'CANCELLED');
679
+ }
527
680
  runtime.status = 'failed';
528
- throw new GuestServiceError('Guest service request failed.', 'SERVICE_FAILED');
681
+ throw new GuestServiceError('Guest service request failed.', 'REQUEST_FAILED');
529
682
  }
683
+ // The body is still streaming when we return; the idle window starts once
684
+ // the caller has read it (or dropped it), which is what `finished` marks.
685
+ return {
686
+ response,
687
+ finished: settleIdle,
688
+ };
689
+ };
530
690
 
531
- const text = await response.text();
691
+ /**
692
+ * Panel-shaped proxy: text answer, capped. Same params as
693
+ * `openGuestServiceRequest` plus `responseMax`.
694
+ */
695
+ export const proxyGuestServiceRequest = async ({ responseMax = GUEST_REQUEST_RESPONSE_MAX, ...params }) => {
696
+ const { response, finished } = await openGuestServiceRequest(params);
697
+ let text;
698
+ try {
699
+ text = await response.text();
700
+ } catch {
701
+ finished();
702
+ if (params.signal?.aborted) {
703
+ throw new GuestServiceError('The request was cancelled.', 'CANCELLED');
704
+ }
705
+ const runtime = runtimes.get(params.guestId);
706
+ if (runtime) runtime.status = 'failed';
707
+ throw new GuestServiceError('Guest service request failed.', 'REQUEST_FAILED');
708
+ }
709
+ finished();
532
710
  return {
533
711
  status: response.status,
534
- body: text.length <= GUEST_REQUEST_RESPONSE_MAX
712
+ body: text.length <= responseMax
535
713
  ? text
536
- : text.slice(0, GUEST_REQUEST_RESPONSE_MAX),
714
+ : text.slice(0, responseMax),
537
715
  };
538
716
  };
@@ -1,10 +1,12 @@
1
- import { afterEach, describe, expect, test } from 'bun:test';
1
+ import { afterEach, beforeEach, describe, expect, spyOn, test } from 'bun:test';
2
2
  import fs from 'node:fs/promises';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
 
6
6
  import {
7
7
  getServiceStatus,
8
+ beginGuestServiceHost,
9
+ beginGuestServiceShutdown,
8
10
  proxyGuestServiceRequest,
9
11
  readServicePid,
10
12
  stopAllGuestServices,
@@ -45,6 +47,8 @@ http.createServer((req, res) => {
45
47
  return { dir, persistPath, packageRoot };
46
48
  };
47
49
 
50
+ beforeEach(() => beginGuestServiceHost());
51
+
48
52
  afterEach(async () => {
49
53
  await stopAllGuestServices();
50
54
  });
@@ -194,6 +198,86 @@ describe('guest service proxy', () => {
194
198
  });
195
199
  });
196
200
 
201
+ describe('host-driven services', () => {
202
+ const request = (extra) => ({
203
+ guestId: 'docker',
204
+ packageRoot: extra.packageRoot,
205
+ service: { entry: 'service/main.js' },
206
+ granted: ['service'],
207
+ persistPath: extra.persistPath,
208
+ method: 'GET',
209
+ path: '/ping',
210
+ ...extra,
211
+ });
212
+
213
+ test('stops itself after the idle window and restarts on the next request', async () => {
214
+ const { dir, persistPath, packageRoot } = await writeFixture();
215
+ try {
216
+ await setCapabilityGrants('docker', persistPath, ['service']);
217
+ const first = await proxyGuestServiceRequest(request({ packageRoot, persistPath, idleStopMs: 150 }));
218
+ expect(first.status).toBe(200);
219
+ const pid = readServicePid('docker');
220
+ expect(pid).not.toBeNull();
221
+ await new Promise((resolve) => setTimeout(resolve, 400));
222
+ expect(getServiceStatus('docker')).toBe('stopped');
223
+ expect(readServicePid('docker')).toBeNull();
224
+
225
+ const second = await proxyGuestServiceRequest(request({ packageRoot, persistPath, idleStopMs: 150 }));
226
+ expect(second.status).toBe(200);
227
+ expect(readServicePid('docker')).not.toBe(pid);
228
+ } finally {
229
+ await fs.rm(dir, { recursive: true, force: true });
230
+ }
231
+ });
232
+
233
+ test('a request keeps re-arming the window, and a panel request never arms one', async () => {
234
+ const { dir, persistPath, packageRoot } = await writeFixture();
235
+ try {
236
+ await setCapabilityGrants('docker', persistPath, ['service']);
237
+ await proxyGuestServiceRequest(request({ packageRoot, persistPath, idleStopMs: 200 }));
238
+ const pid = readServicePid('docker');
239
+ for (let i = 0; i < 3; i += 1) {
240
+ await new Promise((resolve) => setTimeout(resolve, 120));
241
+ await proxyGuestServiceRequest(request({ packageRoot, persistPath, idleStopMs: 200 }));
242
+ }
243
+ expect(readServicePid('docker')).toBe(pid);
244
+
245
+ await stopGuestService('docker');
246
+ await proxyGuestServiceRequest(request({ packageRoot, persistPath }));
247
+ await new Promise((resolve) => setTimeout(resolve, 350));
248
+ expect(getServiceStatus('docker')).toBe('ready');
249
+ } finally {
250
+ await fs.rm(dir, { recursive: true, force: true });
251
+ }
252
+ });
253
+
254
+ test('a longer timeout and a bigger response cap are honoured', async () => {
255
+ const { dir, persistPath, packageRoot } = await writeFixture();
256
+ try {
257
+ await setCapabilityGrants('docker', persistPath, ['service']);
258
+ const result = await proxyGuestServiceRequest(request({ packageRoot, persistPath, timeoutMs: 45_000, responseMax: 5 }));
259
+ expect(result).toEqual({ status: 200, body: '{"pon' });
260
+ } finally {
261
+ await fs.rm(dir, { recursive: true, force: true });
262
+ }
263
+ });
264
+
265
+ test('an aborted request reports cancellation, not a failed service', async () => {
266
+ const { dir, persistPath, packageRoot } = await writeFixture();
267
+ try {
268
+ await setCapabilityGrants('docker', persistPath, ['service']);
269
+ await proxyGuestServiceRequest(request({ packageRoot, persistPath }));
270
+ const controller = new AbortController();
271
+ controller.abort();
272
+ await expect(proxyGuestServiceRequest(request({ packageRoot, persistPath, signal: controller.signal })))
273
+ .rejects.toMatchObject({ code: 'CANCELLED' });
274
+ expect(getServiceStatus('docker')).toBe('ready');
275
+ } finally {
276
+ await fs.rm(dir, { recursive: true, force: true });
277
+ }
278
+ });
279
+ });
280
+
197
281
  describe('pause during startup', () => {
198
282
  test('a stop that lands while the service is coming up wins', async () => {
199
283
  const { dir, persistPath, packageRoot } = await writeFixture();
@@ -275,3 +359,119 @@ describe('restart after the process died', () => {
275
359
  }
276
360
  });
277
361
  });
362
+
363
+ describe('host shutdown', () => {
364
+ const request = (fixture) => ({
365
+ guestId: 'docker', packageRoot: fixture.packageRoot,
366
+ service: { entry: 'service/main.js' }, granted: ['service'],
367
+ persistPath: fixture.persistPath, method: 'GET', path: '/ping',
368
+ });
369
+
370
+ test('a replacement waits for the previous child and cannot escape host shutdown', async () => {
371
+ const fixture = await writeFixture();
372
+ await fs.appendFile(path.join(fixture.packageRoot, 'service/main.js'), '\nprocess.on("SIGTERM", () => {});\n');
373
+ let pid;
374
+ let reading;
375
+ let accessing;
376
+ try {
377
+ await proxyGuestServiceRequest(request(fixture));
378
+ pid = readServicePid('docker');
379
+ const previousStop = stopGuestService('docker');
380
+ accessing = spyOn(fs, 'access');
381
+ const storeRead = Promise.withResolvers();
382
+ const readFile = fs.readFile.bind(fs);
383
+ reading = spyOn(fs, 'readFile').mockImplementation(async (...args) => {
384
+ const result = await readFile(...args);
385
+ if (args[0] === fixture.persistPath) storeRead.resolve();
386
+ return result;
387
+ });
388
+ const replacement = proxyGuestServiceRequest(request(fixture)).catch((error) => error);
389
+ await storeRead.promise;
390
+ await new Promise((resolve) => setImmediate(resolve));
391
+ expect(accessing).not.toHaveBeenCalledWith(path.join(fixture.packageRoot, 'service/main.js'));
392
+ expect(readServicePid('docker')).toBeNull();
393
+ beginGuestServiceShutdown();
394
+ const stopping = stopAllGuestServices();
395
+ process.kill(pid, 'SIGKILL');
396
+ await Promise.all([previousStop, stopping]);
397
+ expect(await replacement).toMatchObject({ code: 'NO_SERVICE' });
398
+ expect(readServicePid('docker')).toBeNull();
399
+ expect(() => process.kill(pid, 0)).toThrow();
400
+ } finally {
401
+ reading?.mockRestore();
402
+ accessing?.mockRestore();
403
+ if (pid) { try { process.kill(pid, 'SIGKILL'); } catch {} }
404
+ await stopAllGuestServices();
405
+ await fs.rm(fixture.dir, { recursive: true, force: true });
406
+ }
407
+ });
408
+
409
+ test('rejects new requests, drains a live child, and only reopens on a new host lifecycle', async () => {
410
+ const fixture = await writeFixture();
411
+ try {
412
+ expect((await proxyGuestServiceRequest(request(fixture))).status).toBe(200);
413
+ const pid = readServicePid('docker');
414
+ beginGuestServiceShutdown();
415
+ const stopping = stopAllGuestServices();
416
+ await expect(proxyGuestServiceRequest(request(fixture))).rejects.toMatchObject({ code: 'NO_SERVICE' });
417
+ await stopping;
418
+ expect(readServicePid('docker')).toBeNull();
419
+ expect(() => process.kill(pid, 0)).toThrow();
420
+ await expect(proxyGuestServiceRequest(request(fixture))).rejects.toMatchObject({ code: 'NO_SERVICE' });
421
+ beginGuestServiceHost();
422
+ expect((await proxyGuestServiceRequest(request(fixture))).status).toBe(200);
423
+ } finally {
424
+ await stopAllGuestServices();
425
+ await fs.rm(fixture.dir, { recursive: true, force: true });
426
+ }
427
+ });
428
+
429
+ test('drains a first start paused before spawn and prevents it from surviving shutdown', async () => {
430
+ const fixture = await writeFixture();
431
+ const entered = Promise.withResolvers();
432
+ const release = Promise.withResolvers();
433
+ const access = fs.access.bind(fs);
434
+ const pausedAccess = spyOn(fs, 'access').mockImplementation(async (...args) => {
435
+ if (args[0] === path.join(fixture.packageRoot, 'service/main.js')) {
436
+ entered.resolve();
437
+ await release.promise;
438
+ }
439
+ return access(...args);
440
+ });
441
+ try {
442
+ const pending = proxyGuestServiceRequest(request(fixture)).catch((error) => error);
443
+ await entered.promise;
444
+ expect(readServicePid('docker')).toBeNull();
445
+ beginGuestServiceShutdown();
446
+ let drained = false;
447
+ const stopping = stopAllGuestServices().then(() => { drained = true; });
448
+ await Promise.resolve();
449
+ expect(drained).toBe(false);
450
+ expect(() => beginGuestServiceHost()).toThrow();
451
+ release.resolve();
452
+ expect(await pending).toMatchObject({ code: 'NO_SERVICE' });
453
+ await stopping;
454
+ expect(readServicePid('docker')).toBeNull();
455
+ expect(getServiceStatus('docker')).toBe('stopped');
456
+ } finally {
457
+ release.resolve();
458
+ pausedAccess.mockRestore();
459
+ await stopAllGuestServices();
460
+ await fs.rm(fixture.dir, { recursive: true, force: true });
461
+ }
462
+ });
463
+
464
+ test('a request still reading the store cannot enter a later host lifecycle', async () => {
465
+ const fixture = await writeFixture();
466
+ try {
467
+ const pending = proxyGuestServiceRequest(request(fixture));
468
+ beginGuestServiceShutdown();
469
+ await stopAllGuestServices();
470
+ beginGuestServiceHost();
471
+ await expect(pending).rejects.toMatchObject({ code: 'NO_SERVICE' });
472
+ expect(readServicePid('docker')).toBeNull();
473
+ } finally {
474
+ await fs.rm(fixture.dir, { recursive: true, force: true });
475
+ }
476
+ });
477
+ });