@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
@@ -1,23 +1,29 @@
1
1
  import express from 'express';
2
2
  import request from 'supertest';
3
- import { afterEach, describe, expect, it, vi } from 'vitest';
3
+ import { describe, expect, it, vi } from 'vitest';
4
4
 
5
5
  import { registerRoutingPromptRewrite, registerRoutingRoutes } from './routes.js';
6
6
 
7
7
  /**
8
- * The prompt rewrite sits in front of the generic OpenCode proxy, which
9
- * replays `req.body` when a middleware parsed it. These tests mount the
10
- * rewrite ahead of a stand-in proxy that records what it would forward.
8
+ * The send rewrite sits in front of the generic OpenCode proxy, which replays
9
+ * `req.body` when a middleware parsed it. These tests mount the rewrite ahead
10
+ * of a stand-in proxy that records what it would forward.
11
11
  */
12
- const createApp = ({ flag = '1', resolvePromptBody } = {}) => {
13
- process.env.OPENCHAMBER_ROUTING_ENABLE = flag;
12
+ const createApp = ({ routeSend, autoSessions = new Set() } = {}) => {
14
13
  const forwarded = [];
15
14
  const runtime = {
16
- resolvePromptBody: resolvePromptBody ?? vi.fn(async (body) => {
17
- if (body.model?.modelID === 'auto') body.model = { providerID: 'openai', modelID: 'gpt-6-astra' };
18
- return null;
15
+ noteModelSelection: vi.fn((sessionId, model) => {
16
+ const auto = model?.providerID === 'openchamber' && model?.id === 'auto';
17
+ if (auto) autoSessions.add(sessionId);
18
+ else autoSessions.delete(sessionId);
19
+ return auto;
19
20
  }),
20
- describe: async () => ({ available: true, autoReady: true, tokenPresent: true, config: null, builtins: [] }),
21
+ isAutoSession: (sessionId) => autoSessions.has(sessionId),
22
+ routeSend: routeSend ?? vi.fn(async ({ body }) => {
23
+ if (typeof body?.command === 'string') body.model = { providerID: 'openai', id: 'gpt-6-astra' };
24
+ return {};
25
+ }),
26
+ describe: async () => ({ available: true, autoReady: true, tokenPresent: true, jevSource: 'typesafe', config: null, builtins: [] }),
21
27
  heldPermissions: () => [],
22
28
  updateConfig: vi.fn(async () => ({ available: true })),
23
29
  setToken: vi.fn(async () => ({ available: true, tokenPresent: true })),
@@ -43,37 +49,70 @@ const createApp = ({ flag = '1', resolvePromptBody } = {}) => {
43
49
  return { app, runtime, forwarded };
44
50
  };
45
51
 
46
- afterEach(() => {
47
- delete process.env.OPENCHAMBER_ROUTING_ENABLE;
48
- });
49
-
50
- describe('routing prompt rewrite', () => {
51
- it('rewrites the Auto sentinel on prompt_async and passes the directory along', async () => {
52
+ describe('routing send rewrite', () => {
53
+ it('swallows the Auto sentinel on a model switch instead of forwarding it', async () => {
52
54
  const { app, runtime, forwarded } = createApp();
53
55
  await request(app)
54
- .post('/api/session/s1/prompt_async?directory=%2Frepo')
55
- .send({ model: { providerID: 'openchamber', modelID: 'auto' }, parts: [{ type: 'text', text: 'hi' }] })
56
+ .post('/api/session/s1/model?directory=%2Frepo')
57
+ .send({ model: { providerID: 'openchamber', id: 'auto' } })
58
+ .expect(204);
59
+ expect(forwarded).toEqual([]);
60
+ expect(runtime.noteModelSelection).toHaveBeenCalledWith('s1', { providerID: 'openchamber', id: 'auto' }, '/repo');
61
+ expect(runtime.isAutoSession('s1')).toBe(true);
62
+ });
63
+
64
+ it('drops the Auto sentinel from a session create so OpenCode never stores it', async () => {
65
+ const { app, forwarded, runtime } = createApp();
66
+ await request(app)
67
+ .post('/api/session?directory=%2Frepo')
68
+ .send({ title: 'btw', model: { providerID: 'openchamber', id: 'auto' }, agent: 'build' })
56
69
  .expect(204);
57
- expect(forwarded).toEqual([{ path: '/session/s1/prompt_async', parsed: true, body: { model: { providerID: 'openai', modelID: 'gpt-6-astra' }, parts: [{ type: 'text', text: 'hi' }] } }]);
58
- expect(runtime.resolvePromptBody).toHaveBeenCalledWith(expect.anything(), { sessionId: 's1', directory: '/repo' });
70
+ expect(forwarded).toEqual([{ path: '/session', parsed: true, body: { title: 'btw', agent: 'build' } }]);
71
+ expect(runtime.noteModelSelection).not.toHaveBeenCalled();
72
+
73
+ await request(app).post('/api/session').send({ model: { providerID: 'anthropic', id: 'claude-opus-5' } }).expect(204);
74
+ expect(forwarded[1]).toEqual({ path: '/session', parsed: true, body: { model: { providerID: 'anthropic', id: 'claude-opus-5' } } });
59
75
  });
60
76
 
61
- it('leaves the stream untouched when the flag is off or the body is not JSON', async () => {
62
- const off = createApp({ flag: '' });
63
- await request(off.app).post('/api/session/s1/prompt').send({ model: { providerID: 'openchamber', modelID: 'auto' } }).expect(204);
64
- expect(off.forwarded[0].parsed).toBe(false);
65
- expect(off.runtime.resolvePromptBody).not.toHaveBeenCalled();
77
+ it('forwards a real model switch untouched and takes the session off Auto', async () => {
78
+ const { app, forwarded, runtime } = createApp();
79
+ await request(app).post('/api/session/s1/model').send({ model: { providerID: 'openchamber', id: 'auto' } }).expect(204);
80
+ await request(app).post('/api/session/s1/model').send({ model: { providerID: 'anthropic', id: 'claude-opus-5' } }).expect(204);
81
+ expect(forwarded).toEqual([{ path: '/session/s1/model', parsed: true, body: { model: { providerID: 'anthropic', id: 'claude-opus-5' } } }]);
82
+ expect(runtime.isAutoSession('s1')).toBe(false);
83
+ });
66
84
 
67
- const text = createApp();
85
+ it('routes a prompt in an Auto session and passes the directory along', async () => {
86
+ const autoSessions = new Set(['s1']);
87
+ const { app, runtime, forwarded } = createApp({ autoSessions });
88
+ await request(app)
89
+ .post('/api/session/s1/prompt?directory=%2Frepo')
90
+ .send({ text: 'hi' })
91
+ .expect(204);
92
+ expect(forwarded).toEqual([{ path: '/session/s1/prompt', parsed: true, body: { text: 'hi' } }]);
93
+ expect(runtime.routeSend).toHaveBeenCalledWith({ sessionId: 's1', directory: '/repo', body: { text: 'hi' } });
94
+ });
95
+
96
+ it('leaves a send in a session that is not on Auto unread', async () => {
97
+ const { app, runtime, forwarded } = createApp();
98
+ await request(app).post('/api/session/s1/prompt').send({ text: 'hi' }).expect(204);
99
+ expect(runtime.routeSend).not.toHaveBeenCalled();
100
+ expect(forwarded[0].parsed).toBe(false);
101
+ });
102
+
103
+ it('leaves the stream untouched when the body is not JSON', async () => {
104
+ const text = createApp({ autoSessions: new Set(['s1']) });
68
105
  await request(text.app).post('/api/session/s1/command').set('content-type', 'text/plain').send('raw').expect(204);
69
106
  expect(text.forwarded[0]).toEqual({ path: '/session/s1/command', parsed: false, raw: 'raw' });
70
107
  });
71
108
 
72
- it('answers with the runtime error instead of forwarding an unresolved sentinel', async () => {
109
+
110
+ it('answers with the runtime error instead of forwarding an unroutable send', async () => {
73
111
  const { app, forwarded } = createApp({
74
- resolvePromptBody: vi.fn(async () => { throw Object.assign(new Error('no fallback'), { status: 400 }); }),
112
+ autoSessions: new Set(['s1']),
113
+ routeSend: vi.fn(async () => { throw Object.assign(new Error('no fallback'), { status: 400 }); }),
75
114
  });
76
- const response = await request(app).post('/api/session/s1/prompt_async').send({ model: { providerID: 'openchamber', modelID: 'auto' } });
115
+ const response = await request(app).post('/api/session/s1/prompt').send({ text: 'hi' });
77
116
  expect(response.status).toBe(400);
78
117
  expect(response.body).toEqual({ error: 'no fallback' });
79
118
  expect(forwarded).toEqual([]);
@@ -93,8 +132,4 @@ describe('routing routes', () => {
93
132
  expect(runtime.clearToken).toHaveBeenCalled();
94
133
  });
95
134
 
96
- it('is absent without the feature flag', async () => {
97
- const { app } = createApp({ flag: '' });
98
- await request(app).put('/api/routing/token').send({ token: 'x' }).expect(404);
99
- });
100
135
  });
@@ -2,17 +2,27 @@
2
2
  * `/api/routing` — configuration and the Jev key. Normal authenticated
3
3
  * OpenChamber routes: do not add them to browser URL-token allowlists.
4
4
  *
5
- * `/api/session/:id/{prompt_async,prompt,command}` — the rewrite that turns
6
- * `openchamber/auto` into a real model before the generic OpenCode proxy
7
- * forwards the request. Registered ahead of the proxy; it parses the JSON body
8
- * only while Auto can actually be selected, so a build without the flag pays
9
- * nothing on the send path.
5
+ * The send-path rewrite, registered ahead of the generic OpenCode proxy. In
6
+ * OpenCode 2.x a prompt body carries no model: the client switches the session
7
+ * first (`POST /session/:id/model`) and sends afterwards. So the rewrite is in
8
+ * two halves:
9
+ *
10
+ * - `POST /session/:id/model` with `openchamber/auto` never reaches OpenCode,
11
+ * which has no such provider. It marks the session as routed and answers as
12
+ * a switch would.
13
+ * - `POST /session/:id/{prompt,command}` in a routed session asks Jev on the
14
+ * request text and switches the session onto the chosen model and agent
15
+ * before the prompt is forwarded.
16
+ *
17
+ * Both parse the JSON body only for JSON requests, so a streamed or non-JSON
18
+ * upload reaches the proxy untouched.
10
19
  */
11
20
  import express from 'express';
12
- import { isRoutingFeatureAvailable } from './feature-flag.js';
21
+ import { isAutoModel } from './defaults.js';
13
22
 
14
- const AUTO_SESSION_PATHS = [
15
- '/api/session/:sessionId/prompt_async',
23
+ const CREATE_PATH = '/api/session';
24
+ const MODEL_PATH = '/api/session/:sessionId/model';
25
+ const SEND_PATHS = [
16
26
  '/api/session/:sessionId/prompt',
17
27
  '/api/session/:sessionId/command',
18
28
  ];
@@ -23,12 +33,9 @@ const sendError = (res, error) => {
23
33
  };
24
34
 
25
35
  export function registerRoutingRoutes(app, runtime) {
26
- const unavailable = (res) => res.status(404).json({ error: 'Routing is not available in this build' });
27
-
28
36
  app.get('/api/routing', async (_req, res) => {
29
37
  try {
30
38
  const state = await runtime.describe();
31
- if (!state.available) return unavailable(res);
32
39
  res.json({ ...state, heldPermissions: runtime.heldPermissions() });
33
40
  } catch (error) {
34
41
  sendError(res, error);
@@ -36,7 +43,6 @@ export function registerRoutingRoutes(app, runtime) {
36
43
  });
37
44
 
38
45
  app.put('/api/routing', express.json({ limit: '256kb' }), async (req, res) => {
39
- if (!isRoutingFeatureAvailable()) return unavailable(res);
40
46
  try {
41
47
  res.json(await runtime.updateConfig(req.body?.config));
42
48
  } catch (error) {
@@ -45,7 +51,6 @@ export function registerRoutingRoutes(app, runtime) {
45
51
  });
46
52
 
47
53
  app.put('/api/routing/token', express.json({ limit: '16kb' }), async (req, res) => {
48
- if (!isRoutingFeatureAvailable()) return unavailable(res);
49
54
  try {
50
55
  res.json(await runtime.setToken(req.body?.token));
51
56
  } catch (error) {
@@ -54,7 +59,6 @@ export function registerRoutingRoutes(app, runtime) {
54
59
  });
55
60
 
56
61
  app.delete('/api/routing/token', async (_req, res) => {
57
- if (!isRoutingFeatureAvailable()) return unavailable(res);
58
62
  try {
59
63
  res.json(await runtime.clearToken());
60
64
  } catch (error) {
@@ -65,17 +69,56 @@ export function registerRoutingRoutes(app, runtime) {
65
69
 
66
70
  export function registerRoutingPromptRewrite(app, runtime) {
67
71
  const parseJson = express.json({ limit: '50mb' });
68
- app.post(AUTO_SESSION_PATHS, (req, res, next) => {
69
- if (!isRoutingFeatureAvailable()) return next();
72
+
73
+ /** Parses the body only for JSON requests; anything else stays a stream. */
74
+ const withParsedBody = (handler) => (req, res, next) => {
70
75
  const contentType = String(req.headers['content-type'] ?? '').toLowerCase();
71
76
  if (!contentType.includes('application/json')) return next();
72
77
  parseJson(req, res, (parseError) => {
73
78
  if (parseError) return next(parseError);
74
- const url = new URL(req.url, 'http://localhost');
75
- const directory = url.searchParams.get('directory') || req.get('x-opencode-directory') || undefined;
76
- runtime.resolvePromptBody(req.body, { sessionId: req.params.sessionId, directory })
77
- .then(() => next())
78
- .catch((error) => sendError(res, error));
79
+ handler(req, res, next);
79
80
  });
80
- });
81
+ };
82
+
83
+ const directoryOf = (req) => {
84
+ const url = new URL(req.url, 'http://localhost');
85
+ return url.searchParams.get('directory') || req.get('x-opencode-directory') || undefined;
86
+ };
87
+
88
+ // Session creation is the other v2 request that carries a model: flows that
89
+ // know their first turn's selection create the session on it (btw forks,
90
+ // auto review, fusion, extension starts). OpenCode accepts the sentinel at
91
+ // creation and only fails the first prompt with "provider.no-route", so the
92
+ // model is dropped here and the session starts on OpenCode's default. No
93
+ // session id exists yet to mark; the first send resends the sentinel through
94
+ // the model switch below because the session's record never matches Auto.
95
+ app.post(CREATE_PATH, withParsedBody((req, _res, next) => {
96
+ if (!isAutoModel(req.body?.model)) return next();
97
+ delete req.body.model;
98
+ next();
99
+ }));
100
+
101
+ app.post(MODEL_PATH, withParsedBody((req, res, next) => {
102
+ const directory = directoryOf(req);
103
+ // Swallowed, not forwarded: OpenCode has no `openchamber` provider, and the
104
+ // real model is only known once the request text arrives.
105
+ if (runtime.noteModelSelection(req.params.sessionId, req.body?.model, directory)) return res.status(204).end();
106
+ next();
107
+ }));
108
+
109
+ // Whether the session is routed is known from the URL alone, so an ordinary
110
+ // send is never read: it reaches the proxy as the stream it arrived as, the
111
+ // way it did before routing existed.
112
+ const routedSendsOnly = (handler) => {
113
+ const parsed = withParsedBody(handler);
114
+ return (req, res, next) => (runtime.isAutoSession(req.params.sessionId) ? parsed(req, res, next) : next());
115
+ };
116
+
117
+ app.post(SEND_PATHS, routedSendsOnly((req, res, next) => {
118
+ const sessionId = req.params.sessionId;
119
+ const directory = directoryOf(req);
120
+ runtime.routeSend({ sessionId, directory, body: req.body })
121
+ .then(() => next())
122
+ .catch((error) => sendError(res, error));
123
+ }));
81
124
  }
@@ -1,16 +1,21 @@
1
1
  /**
2
- * Owns Jev routing at runtime: whether Auto is ready, rewriting a prompt body
3
- * that names the `openchamber/auto` model, and the safety net consulted before
4
- * a permission is auto-accepted. Every failure path keeps the user's own
2
+ * Owns Jev routing at runtime: whether Auto is ready, which model and agent a
3
+ * send that selected `openchamber/auto` runs on, and the safety net consulted
4
+ * before a permission is auto-accepted. Every failure path keeps the user's own
5
5
  * behaviour: a prompt goes to the fallback model, a permission is accepted as
6
6
  * auto-accept would have, and the UI is told why.
7
+ *
8
+ * OpenCode 2.x holds the model and the agent on the session, switched by their
9
+ * own calls, and a prompt body carries only the user's text. So Auto is a
10
+ * per-session state here: the sentinel arrives on `POST /session/:id/model`
11
+ * and is swallowed, and every prompt in that session is routed until a real
12
+ * model is selected.
7
13
  */
8
- import { createOpencodeClient } from '@opencode-ai/sdk/v2';
14
+ import { OpenCode } from '@opencode/client';
9
15
  import { z } from 'zod';
10
- import { isRoutingFeatureAvailable } from './feature-flag.js';
11
- import { BUILTIN_CATEGORIES, isAutoModel } from './defaults.js';
16
+ import { AUTO_MODEL_REF, BUILTIN_CATEGORIES, isAutoModel } from './defaults.js';
12
17
  import { createRoutingStore, parseEffectiveConfig } from './store.js';
13
- import { buildPermissionRequest, buildRoutingRequest, createJevClient, decidePermission, decideRouting } from './jev.js';
18
+ import { buildPermissionRequest, buildRoutingRequest, createJevClient, decidePermission, decideRouting, jevEndpoint } from './jev.js';
14
19
  import { loadRoutingHistory } from './history.js';
15
20
 
16
21
  const HISTORY_TIMEOUT_MS = 2500;
@@ -19,25 +24,31 @@ const PERMISSION_DECISION_TTL_MS = 15 * 60 * 1000;
19
24
 
20
25
  const errorMessage = (error) => (error instanceof Error ? error.message : String(error));
21
26
 
22
- const textPartSchema = z.object({ type: z.literal('text'), text: z.string(), synthetic: z.boolean().optional() });
23
- const commandBodySchema = z.object({ command: z.string(), arguments: z.string().optional() });
24
- const promptBodySchema = z.object({ parts: z.array(z.unknown()).optional() });
27
+ // v2's command body (`session.command` in the protocol) names the command in
28
+ // `name` and carries its arguments in `text`; a prompt body has no `name`.
29
+ const commandBodySchema = z.object({ name: z.string().trim().min(1), text: z.string().nullish() });
30
+ // v2 sends one user turn as flat text; the context the composer attached went
31
+ // ahead of it as synthetic messages, which are never the request being routed.
32
+ const promptBodySchema = z.object({ text: z.string().nullish() });
25
33
 
26
- /** The user's words for this send: text parts the composer authored, or the slash command. */
34
+ /** The user's words for this send: the prompt text, or the slash command. */
27
35
  export const requestTextOf = (body) => {
28
36
  const command = commandBodySchema.safeParse(body);
29
37
  if (command.success) {
30
- const args = command.data.arguments?.trim();
31
- return `/${command.data.command}${args ? ` ${args}` : ''}`;
38
+ const args = command.data.text?.trim();
39
+ return `/${command.data.name}${args ? ` ${args}` : ''}`;
32
40
  }
33
41
  const prompt = promptBodySchema.safeParse(body);
34
- const parts = prompt.success ? prompt.data.parts ?? [] : [];
35
- return parts
36
- .map((part) => textPartSchema.safeParse(part))
37
- .filter((part) => part.success && !part.data.synthetic)
38
- .map((part) => part.data.text)
39
- .join('\n\n')
40
- .trim();
42
+ return (prompt.success ? prompt.data.text ?? '' : '').trim();
43
+ };
44
+
45
+ const agentBodySchema = z.object({ agent: z.string().trim().min(1) });
46
+
47
+ /** OpenChamber stores a model as `{ providerID, modelID }`; v2 wants a `Model.Ref`. */
48
+ const toModelRef = (model, variant) => {
49
+ const ref = { providerID: model.providerID, id: model.modelID };
50
+ if (variant) ref.variant = variant;
51
+ return ref;
41
52
  };
42
53
 
43
54
  export function createRoutingRuntime({
@@ -51,6 +62,14 @@ export function createRoutingRuntime({
51
62
  now = Date.now,
52
63
  }) {
53
64
  const permissionDecisions = new Map();
65
+ // Sessions the user put on Auto. The sentinel never reaches OpenCode, so
66
+ // nothing upstream remembers the choice for us.
67
+ // TODO(v2): this is process memory. A server restart drops the mark and the
68
+ // session silently runs on whatever model it was last switched to while the
69
+ // composer still shows Auto. Either persist it next to `routing.json` or have
70
+ // the client resend the sentinel with every send.
71
+ const autoSessions = new Map();
72
+ const AUTO_SESSION_LIMIT = 1000;
54
73
 
55
74
  const broadcast = (type, properties) => {
56
75
  try {
@@ -64,63 +83,82 @@ export function createRoutingRuntime({
64
83
 
65
84
  /** What the client needs to decide whether to offer Auto and what the settings page shows. */
66
85
  const describe = async () => {
67
- const available = isRoutingFeatureAvailable();
68
- if (!available) return { available: false, autoReady: false, tokenPresent: false, config: null, builtins: [] };
69
86
  const [config, token] = await Promise.all([store.readConfig(), store.readToken()]);
70
87
  const tokenPresent = Boolean(token);
71
- const autoReady = config.enabled && tokenPresent && Boolean(config.fallback) && enabledCategories(config).length >= 2;
88
+ // A key is not a precondition: without one Jev answers through the free
89
+ // model zen serves, so Auto only needs a fallback and two categories.
90
+ const autoReady = config.enabled && Boolean(config.fallback) && enabledCategories(config).length >= 2;
72
91
  // Built-in text travels with the config so "Reset" in Settings restores the shipped wording.
73
- return { available, autoReady, tokenPresent, config, builtins: BUILTIN_CATEGORIES };
92
+ // `available` stays in the payload for the client: a runtime without an
93
+ // OpenChamber server (VS Code) answers 404 and reads it as false.
94
+ return { available: true, autoReady, tokenPresent, jevSource: jevEndpoint(token).source, config, builtins: BUILTIN_CATEGORIES };
74
95
  };
75
96
 
76
97
  const publishUpdated = async () => {
77
98
  const state = await describe();
78
- broadcast('openchamber:routing.updated', { available: state.available, autoReady: state.autoReady, tokenPresent: state.tokenPresent });
99
+ broadcast('openchamber:routing.updated', {
100
+ available: state.available, autoReady: state.autoReady, tokenPresent: state.tokenPresent, jevSource: state.jevSource,
101
+ });
79
102
  return state;
80
103
  };
81
104
 
105
+ const openCodeClient = (directory) => {
106
+ const headers = { ...getOpenCodeAuthHeaders() };
107
+ // v2 scopes by header and rejects non-ASCII header values.
108
+ const scope = z.string().trim().min(1).safeParse(directory);
109
+ if (scope.success) headers['x-opencode-directory'] = encodeURIComponent(scope.data);
110
+ return OpenCode.make({ baseUrl: buildOpenCodeUrl('/', '').replace(/\/$/, ''), headers });
111
+ };
112
+
82
113
  const readHistory = async ({ sessionId, directory }) => {
83
- const baseUrl = buildOpenCodeUrl('/', '').replace(/\/$/, '');
84
- const client = createOpencodeClient({ baseUrl, headers: getOpenCodeAuthHeaders(), throwOnError: true });
114
+ const client = openCodeClient(directory);
85
115
  const signal = AbortSignal.timeout(HISTORY_TIMEOUT_MS);
86
116
  return loadRoutingHistory({
87
117
  signal,
88
- readPage: (page) => client.session.messages({ sessionID: sessionId, directory, ...page }, { signal }),
118
+ // v2 pages a session's messages newest first and returns `{ data, cursor }`.
119
+ readPage: ({ limit, cursor }) => client.message.list(
120
+ { sessionID: sessionId, limit, ...(cursor ? { cursor } : { order: 'desc' }) },
121
+ { signal },
122
+ ),
89
123
  });
90
124
  };
91
125
 
92
126
  // A category without a model of its own means "the fallback pair"; a variant
93
127
  // only travels with the model it was chosen for.
94
- const applyChoice = (body, config, choice) => {
128
+ const chooseSelection = (config, choice, composerAgent) => {
95
129
  const own = Boolean(choice?.model);
96
130
  const model = own ? choice.model : config.fallback.model;
97
131
  const variant = own ? choice.variant : config.fallback.variant;
98
- // Keep the wire shape the route uses: a string on /command, an object on the prompt routes.
99
- body.model = z.string().safeParse(body.model).success
100
- ? `${model.providerID}/${model.modelID}`
101
- : { providerID: model.providerID, modelID: model.modelID };
102
- if (variant) body.variant = variant;
103
- else delete body.variant;
104
- if (choice?.agent) body.agent = choice.agent;
105
- return { providerID: model.providerID, modelID: model.modelID, variant: variant ?? null, agent: choice?.agent ?? null };
132
+ return {
133
+ model: toModelRef(model, variant),
134
+ // A category agent replaces the composer's; an empty one keeps it.
135
+ agent: choice?.agent || composerAgent || null,
136
+ decision: { providerID: model.providerID, modelID: model.modelID, variant: variant ?? null, agent: choice?.agent ?? null },
137
+ };
106
138
  };
107
139
 
108
140
  /**
109
- * Rewrites `body.model` in place when it is the Auto sentinel. Returns the
110
- * decision that was applied, or null when the body named a real model.
141
+ * Resolves one send that named the Auto sentinel. Returns the model and
142
+ * agent the send must use, or null when a real model was selected.
143
+ *
144
+ * v2 carries neither model nor agent in a prompt body — they are session
145
+ * state, switched by their own calls — so the caller applies the selection
146
+ * (`applySessionSelection`, or its own switch calls) instead of the runtime
147
+ * rewriting a body in place the way v1 allowed.
148
+ *
111
149
  * Throws only when Auto cannot be honoured at all (no fallback configured):
112
150
  * the sentinel must never reach OpenCode.
113
151
  */
114
- const resolvePromptBody = async (body, { sessionId, directory }) => {
115
- if (!isAutoModel(body?.model)) return null;
152
+ const resolveAutoSelection = async ({ sessionId, directory, model, agent, requestText }) => {
153
+ if (!isAutoModel(model)) return null;
116
154
  const state = await describe();
117
155
  const config = state.config;
118
156
  if (!config?.fallback) {
119
157
  throw Object.assign(new Error('Auto routing is selected but no fallback model is configured'), { status: 400 });
120
158
  }
121
159
  const decision = { sessionId, at: now(), category: null, confidence: 0, reason: 'not-ready', ms: 0 };
160
+ let selection;
122
161
  if (state.autoReady) {
123
- const request = requestTextOf(body);
124
162
  let history = [];
125
163
  try {
126
164
  history = await readHistory({ sessionId, directory });
@@ -129,23 +167,69 @@ export function createRoutingRuntime({
129
167
  }
130
168
  try {
131
169
  const token = await store.readToken();
170
+ const request = (requestText ?? '').trim();
132
171
  const { answers, ms } = await jev.ask(buildRoutingRequest({ categories: enabledCategories(config), history, request }), token);
133
172
  const result = decideRouting(answers.category, { categories: enabledCategories(config), minConfidence: config.minConfidence });
134
173
  decision.category = result.category?.id ?? null;
135
174
  decision.confidence = result.confidence;
136
175
  decision.reason = result.reason;
137
176
  decision.ms = ms;
138
- Object.assign(decision, applyChoice(body, config, result.category));
177
+ selection = chooseSelection(config, result.category, agent);
139
178
  } catch (error) {
140
179
  decision.reason = 'error';
141
180
  decision.error = errorMessage(error);
142
- Object.assign(decision, applyChoice(body, config, null));
181
+ selection = chooseSelection(config, null, agent);
143
182
  }
144
183
  } else {
145
- Object.assign(decision, applyChoice(body, config, null));
184
+ selection = chooseSelection(config, null, agent);
146
185
  }
186
+ Object.assign(decision, selection.decision);
147
187
  broadcast('openchamber:routing.decision', decision);
148
- return decision;
188
+ return { model: selection.model, agent: selection.agent, decision };
189
+ };
190
+
191
+ /**
192
+ * `POST /session/:id/model` with the sentinel puts the session on Auto;
193
+ * with any real model it takes it off again.
194
+ */
195
+ const noteModelSelection = (sessionId, model, directory) => {
196
+ if (!sessionId) return false;
197
+ if (!isAutoModel(model)) {
198
+ autoSessions.delete(sessionId);
199
+ return false;
200
+ }
201
+ autoSessions.delete(sessionId);
202
+ autoSessions.set(sessionId, { directory: directory ?? null, at: now() });
203
+ while (autoSessions.size > AUTO_SESSION_LIMIT) autoSessions.delete(autoSessions.keys().next().value);
204
+ return true;
205
+ };
206
+
207
+ const isAutoSession = (sessionId) => Boolean(sessionId) && autoSessions.has(sessionId);
208
+
209
+ /** Switches the session onto a resolved selection, the way a v2 send does. */
210
+ const applySessionSelection = async (sessionId, directory, selection) => {
211
+ const client = openCodeClient(directory);
212
+ await client.session.switchModel({ sessionID: sessionId, model: selection.model });
213
+ if (selection.agent) await client.session.switchAgent({ sessionID: sessionId, agent: selection.agent });
214
+ };
215
+
216
+ /**
217
+ * One send in a routed session: asks Jev on the request text, switches the
218
+ * session onto the answer, and keeps the body in step with it.
219
+ */
220
+ const routeSend = async ({ sessionId, directory, body }) => {
221
+ const resolved = await resolveAutoSelection({
222
+ sessionId,
223
+ directory,
224
+ model: AUTO_MODEL_REF,
225
+ agent: agentBodySchema.safeParse(body).data?.agent ?? null,
226
+ requestText: requestTextOf(body),
227
+ });
228
+ if (!resolved) return null;
229
+ // v2 prompt and command bodies carry neither model nor agent: switching
230
+ // the session is the whole application of the decision.
231
+ await applySessionSelection(sessionId, directory, resolved);
232
+ return resolved.decision;
149
233
  };
150
234
 
151
235
  /**
@@ -158,7 +242,7 @@ export function createRoutingRuntime({
158
242
  const cached = permissionDecisions.get(permission.id);
159
243
  if (cached && now() - cached.at < PERMISSION_DECISION_TTL_MS) return cached.result;
160
244
  const state = await describe();
161
- if (!state.available || !state.config?.enabled || !state.config.safetyNet.enabled || !state.tokenPresent) return { action: 'accept' };
245
+ if (!state.config?.enabled || !state.config.safetyNet.enabled) return { action: 'accept' };
162
246
  let result;
163
247
  try {
164
248
  const token = await store.readToken();
@@ -213,5 +297,18 @@ export function createRoutingRuntime({
213
297
  return held;
214
298
  };
215
299
 
216
- return { describe, resolvePromptBody, evaluatePermission, forgetPermission, heldPermissions, updateConfig, setToken, clearToken };
300
+ return {
301
+ describe,
302
+ noteModelSelection,
303
+ isAutoSession,
304
+ resolveAutoSelection,
305
+ applySessionSelection,
306
+ routeSend,
307
+ evaluatePermission,
308
+ forgetPermission,
309
+ heldPermissions,
310
+ updateConfig,
311
+ setToken,
312
+ clearToken,
313
+ };
217
314
  }