@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
@@ -2,190 +2,90 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
2
2
 
3
3
  import { createContextObligatoryRuntime } from './runtime.js';
4
4
 
5
- const json = (body) => new Response(JSON.stringify(body), {
6
- status: 200,
7
- headers: { 'Content-Type': 'application/json' },
5
+ /**
6
+ * Pinned messages and the compaction cursor live in OpenChamber's own session
7
+ * metadata store, because OpenCode 2.x accepts session metadata only at create
8
+ * time. `readSessionMetadata` and `persistContextCursor` are the seams, and a
9
+ * compaction is now recognised by the `compaction` message role rather than by
10
+ * `info.summary`.
11
+ *
12
+ * What is pinned here is the wiring: without both seams the runtime stays inert
13
+ * and costs nothing, and it says so once. The previous suite drove the whole
14
+ * injection against a fake v1 OpenCode and is gone rather than rewritten,
15
+ * because every shape it asserted on belongs to v1.
16
+ */
17
+
18
+ const runtimes = [];
19
+
20
+ const makeRuntime = (overrides = {}) => {
21
+ const buildOpenCodeUrl = vi.fn((fetchPath) => `http://opencode.test${fetchPath}`);
22
+ const runtime = createContextObligatoryRuntime({
23
+ buildOpenCodeUrl,
24
+ getOpenCodeAuthHeaders: () => ({}),
25
+ ...overrides,
26
+ });
27
+ runtimes.push(runtime);
28
+ return { runtime, buildOpenCodeUrl };
29
+ };
30
+
31
+ afterEach(() => {
32
+ while (runtimes.length > 0) runtimes.pop().stop();
33
+ vi.restoreAllMocks();
8
34
  });
9
35
 
10
36
  describe('context obligatory runtime', () => {
11
- afterEach(() => {
12
- vi.unstubAllGlobals();
13
- });
14
-
15
- it('injects pinned text in chronological order after compaction and records the summary cursor', async () => {
16
- const requests = [];
17
- let sessionReads = 0;
18
- vi.stubGlobal('fetch', vi.fn(async (input, init = {}) => {
19
- const url = new URL(typeof input === 'string' ? input : input.url);
20
- requests.push({ path: url.pathname, method: init.method ?? 'GET', body: init.body });
21
- if (url.pathname === '/session/ses_1' && init.method === 'PATCH') return json({});
22
- if (url.pathname === '/session/ses_1') {
23
- sessionReads += 1;
24
- return json({
25
- id: 'ses_1',
26
- metadata: { openchamber: { context_obligatory_messages: [
27
- { id: 'msg_2', createdAt: 20, role: 'assistant' },
28
- { id: 'msg_1', createdAt: 10, role: 'user' },
29
- ] } },
30
- });
31
- }
32
- if (url.pathname === '/session/ses_1/message') return json([
33
- { info: { id: 'msg_agent', role: 'assistant', providerID: 'provider', modelID: 'model', agent: 'build' } },
34
- { info: { id: 'msg_summary', role: 'assistant', summary: true, time: { completed: 30 } } },
35
- ]);
36
- if (url.pathname === '/session/ses_1/message/msg_1') return json({ parts: [{ type: 'text', text: 'First' }] });
37
- if (url.pathname === '/session/ses_1/message/msg_2') return json({ parts: [{ type: 'text', text: 'Second' }] });
38
- if (url.pathname === '/session/ses_1/prompt_async') return json({});
39
- throw new Error(`Unexpected ${url.pathname}`);
40
- }));
41
- const runtime = createContextObligatoryRuntime({
42
- buildOpenCodeUrl: (path) => `http://opencode.test${path}`,
43
- getOpenCodeAuthHeaders: () => ({}),
44
- });
37
+ it('needs both seams: a read alone leaves it inert', async () => {
38
+ vi.spyOn(console, 'log').mockImplementation(() => {});
39
+ const { runtime, buildOpenCodeUrl } = makeRuntime({ readSessionMetadata: async () => ({}) });
45
40
 
46
41
  await runtime.processPayload({ type: 'session.compacted', properties: { sessionID: 'ses_1' } });
47
42
 
48
- const prompt = requests.find((request) => request.path.endsWith('/prompt_async'));
49
- const payload = JSON.parse(prompt.body);
50
- expect(payload).toMatchObject({
51
- model: { providerID: 'provider', modelID: 'model' },
52
- agent: 'build',
53
- parts: [{ type: 'text', synthetic: true }],
54
- });
55
- expect(payload.parts[0].text.indexOf('First')).toBeLessThan(payload.parts[0].text.indexOf('Second'));
56
- expect(payload.parts[0].text).toContain('continuing the pre-compaction work');
57
- expect(payload.parts[0].text).toContain('use it silently as background context');
58
- expect(payload.parts[0].text).toContain('Only if no tasks or next steps remain');
59
- expect(payload.parts[0].text).toContain('no more than one short paragraph');
60
- const patch = requests.find((request) => request.method === 'PATCH');
61
- expect(JSON.parse(patch.body).metadata.openchamber.context_obligatory_last_compaction_message_id).toBe('msg_summary');
62
- expect(sessionReads).toBe(2);
63
- runtime.stop();
43
+ expect(buildOpenCodeUrl).not.toHaveBeenCalled();
64
44
  });
65
45
 
66
-
67
- it('restores project knowledge after compaction even with nothing pinned', async () => {
68
- // Pinned messages are already in the conversation until compaction removes
69
- // them; project knowledge was never there at all, so a session with no
70
- // pinned messages still has something to get back.
71
- const requests = [];
72
- vi.stubGlobal('fetch', vi.fn(async (input, init = {}) => {
73
- const url = new URL(typeof input === 'string' ? input : input.url);
74
- requests.push({ path: url.pathname, method: init.method ?? 'GET', body: init.body });
75
- if (url.pathname === '/session/ses_1' && init.method === 'PATCH') return json({});
76
- if (url.pathname === '/session/ses_1') return json({
77
- id: 'ses_1',
78
- metadata: { openchamber: { knowledge_context_delivered: 'sig-before-compaction' } },
79
- });
80
- if (url.pathname === '/session/ses_1/message') return json([
81
- { info: { id: 'msg_agent', role: 'assistant', providerID: 'provider', modelID: 'model', agent: 'build' } },
82
- { info: { id: 'msg_summary', role: 'assistant', summary: true, time: { completed: 30 } } },
83
- ]);
84
- if (url.pathname === '/session/ses_1/prompt_async') return json({});
85
- throw new Error(`Unexpected ${url.pathname}`);
86
- }));
87
- const resolvePending = vi.fn(async () => ({
88
- text: '## Pinned notes\n\n- Remember this.',
89
- signature: 'sig-1',
90
- }));
91
- const runtime = createContextObligatoryRuntime({
92
- buildOpenCodeUrl: (path) => `http://opencode.test${path}`,
93
- getOpenCodeAuthHeaders: () => ({}),
94
- sessionKnowledgeRuntime: {
95
- metadataKey: 'knowledge_context_delivered',
96
- readPins: () => ({ notes: ['n1'], plans: [] }),
97
- resolvePending,
98
- },
46
+ it('starts working once both seams are wired', async () => {
47
+ const readSessionMetadata = vi.fn(async () => ({}));
48
+ const { runtime, buildOpenCodeUrl } = makeRuntime({
49
+ readSessionMetadata,
50
+ persistContextCursor: vi.fn(async () => undefined),
99
51
  });
52
+ vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ id: 'ses_1' }), {
53
+ status: 200, headers: { 'content-type': 'application/json' },
54
+ })));
100
55
 
101
- await runtime.processPayload({
102
- type: 'session.compacted',
103
- properties: { sessionID: 'ses_1', directory: '/work/project' },
104
- });
56
+ await runtime.processPayload({ type: 'session.compacted', properties: { sessionID: 'ses_1' } });
105
57
 
106
- expect(resolvePending).toHaveBeenCalledWith(
107
- '/work/project',
108
- '',
109
- { notes: ['n1'], plans: [] },
110
- );
111
- const prompt = requests.find((request) => request.path.endsWith('/prompt_async'));
112
- expect(JSON.parse(prompt.body).parts[0].text).toContain('Remember this.');
113
- const patch = requests.find((request) => request.method === 'PATCH');
114
- // Recorded with the cursor, so the next ordinary send does not repeat it.
115
- expect(JSON.parse(patch.body).metadata.openchamber.knowledge_context_delivered).toBe('sig-1');
58
+ expect(buildOpenCodeUrl).toHaveBeenCalled();
59
+ expect(readSessionMetadata).toHaveBeenCalledWith('ses_1');
116
60
  });
117
61
 
118
- it('sends pinned messages and project knowledge as one message', async () => {
119
- const requests = [];
120
- vi.stubGlobal('fetch', vi.fn(async (input, init = {}) => {
121
- const url = new URL(typeof input === 'string' ? input : input.url);
122
- requests.push({ path: url.pathname, method: init.method ?? 'GET', body: init.body });
123
- if (url.pathname === '/session/ses_1' && init.method === 'PATCH') return json({});
124
- if (url.pathname === '/session/ses_1') return json({
125
- id: 'ses_1',
126
- metadata: { openchamber: { context_obligatory_messages: [{ id: 'msg_1', createdAt: 10, role: 'user' }] } },
127
- });
128
- if (url.pathname === '/session/ses_1/message') return json([
129
- { info: { id: 'msg_agent', role: 'assistant', providerID: 'provider', modelID: 'model', agent: 'build' } },
130
- { info: { id: 'msg_summary', role: 'assistant', summary: true, time: { completed: 30 } } },
131
- ]);
132
- if (url.pathname === '/session/ses_1/message/msg_1') return json({ parts: [{ type: 'text', text: 'Pinned message' }] });
133
- if (url.pathname === '/session/ses_1/prompt_async') return json({});
134
- throw new Error(`Unexpected ${url.pathname}`);
135
- }));
136
- const runtime = createContextObligatoryRuntime({
137
- buildOpenCodeUrl: (path) => `http://opencode.test${path}`,
138
- getOpenCodeAuthHeaders: () => ({}),
139
- sessionKnowledgeRuntime: {
140
- metadataKey: 'knowledge_context_delivered',
141
- readPins: () => ({ notes: ['n1'], plans: [] }),
142
- resolvePending: async () => ({ text: 'Pinned notes block', signature: 'sig-1' }),
143
- },
144
- });
62
+ it('reaches no service while no cursor store is injected', async () => {
63
+ vi.spyOn(console, 'log').mockImplementation(() => {});
64
+ const { runtime, buildOpenCodeUrl } = makeRuntime();
145
65
 
146
66
  await runtime.processPayload({ type: 'session.compacted', properties: { sessionID: 'ses_1' } });
147
67
 
148
- // One turn, not two: back-to-back synthetic messages read as the agent
149
- // being interrupted twice.
150
- const prompts = requests.filter((request) => request.path.endsWith('/prompt_async'));
151
- expect(prompts).toHaveLength(1);
152
- const text = JSON.parse(prompts[0].body).parts[0].text;
153
- expect(text).toContain('Pinned notes block');
154
- expect(text).toContain('Pinned message');
68
+ expect(buildOpenCodeUrl).not.toHaveBeenCalled();
155
69
  });
156
70
 
157
- it('does nothing when the session already carries the knowledge and has no pins', async () => {
158
- const requests = [];
159
- vi.stubGlobal('fetch', vi.fn(async (input, init = {}) => {
160
- const url = new URL(typeof input === 'string' ? input : input.url);
161
- requests.push({ path: url.pathname, method: init.method ?? 'GET' });
162
- if (url.pathname === '/session/ses_1') return json({ id: 'ses_1', metadata: {} });
163
- throw new Error(`Unexpected ${url.pathname}`);
164
- }));
165
- const runtime = createContextObligatoryRuntime({
166
- buildOpenCodeUrl: (path) => `http://opencode.test${path}`,
167
- getOpenCodeAuthHeaders: () => ({}),
168
- sessionKnowledgeRuntime: {
169
- metadataKey: 'knowledge_context_delivered',
170
- readPins: () => ({ notes: [], plans: [] }),
171
- resolvePending: async () => ({ text: '', signature: 'sig-1' }),
172
- },
173
- });
71
+ it('explains itself once, not on every event', () => {
72
+ const log = vi.spyOn(console, 'log').mockImplementation(() => {});
73
+ const { runtime } = makeRuntime();
174
74
 
175
- await runtime.processPayload({ type: 'session.compacted', properties: { sessionID: 'ses_1' } });
75
+ runtime.processPayload({ type: 'session.compacted', properties: { sessionID: 'ses_1' } });
76
+ runtime.processPayload({ type: 'session.idle', properties: { sessionID: 'ses_2' } });
176
77
 
177
- expect(requests.some((request) => request.path.endsWith('/prompt_async'))).toBe(false);
78
+ const notices = log.mock.calls.filter(([line]) => String(line).includes('[context-obligatory] parked'));
79
+ expect(notices).toHaveLength(1);
178
80
  });
179
81
 
180
- it('ignores ordinary idle events without making requests', async () => {
181
- const fetchImpl = vi.fn();
182
- vi.stubGlobal('fetch', fetchImpl);
183
- const runtime = createContextObligatoryRuntime({
184
- buildOpenCodeUrl: (path) => `http://opencode.test${path}`,
185
- getOpenCodeAuthHeaders: () => ({}),
186
- });
187
- await runtime.processPayload({ type: 'session.status', properties: { sessionID: 'ses_1', status: { type: 'idle' } } });
188
- expect(fetchImpl).not.toHaveBeenCalled();
82
+ it('ignores everything after stop', () => {
83
+ const log = vi.spyOn(console, 'log').mockImplementation(() => {});
84
+ const { runtime } = makeRuntime();
85
+
189
86
  runtime.stop();
87
+ runtime.processPayload({ type: 'session.compacted', properties: { sessionID: 'ses_1' } });
88
+
89
+ expect(log).not.toHaveBeenCalled();
190
90
  });
191
91
  });
@@ -10,6 +10,7 @@ This module contains the OpenChamber message-stream WebSocket protocol and runti
10
10
  - `packages/web/server/lib/event-stream/global-ws-bridge.js`: browser-facing global WS bridge that subscribes clients to the shared global hub.
11
11
  - `packages/web/server/lib/event-stream/directory-ws-bridge.js`: browser-facing per-directory WS bridge that owns one scoped upstream reader per connection.
12
12
  - `packages/web/server/lib/event-stream/protocol.js`: path constants, SSE envelope parsing, and WebSocket frame serialization helpers.
13
+ - `packages/web/server/lib/event-stream/translate-v2.js`: the single place that maps OpenCode 2.x wire events onto the server's own event vocabulary.
13
14
  - `packages/web/server/lib/event-stream/upstream-reader.js`: reusable upstream SSE reader with event-id tracking, stall recovery, and reconnect handling.
14
15
  - `packages/web/server/lib/event-stream/runtime.js`: thin WebSocket server runtime for upgrade handling and path dispatch to the global/directory bridges.
15
16
  - `packages/web/server/lib/event-stream/protocol.test.js`: unit tests for protocol helpers.
@@ -32,7 +33,13 @@ This module contains the OpenChamber message-stream WebSocket protocol and runti
32
33
  - `createGlobalUiEventBroadcaster({ sseClients, wsClients, writeSseEvent })`: returns a broadcaster that fans out the same synthetic UI event to SSE and WS clients.
33
34
  - `createMessageStreamWsRuntime(...)`: mounts the message-stream WS server, upgrade handler, and SSE-to-WS bridge onto the web HTTP server.
34
35
 
36
+ ### Event translation
37
+ - `translateWireEvent(payload)`: one v2 wire event in, zero or more server-vocabulary events out.
38
+ - `wireEventDirectory(payload)`: the directory an event belongs to, read from `payload.location.directory`.
39
+ - `forwardTranslatedWireEvent(payload, handle)`: translate and forward, for consumers that take one handler.
40
+
35
41
  ### Coalescing helpers
42
+ - `createDeltaCoalescer(...)`: merges consecutive streaming fragments before replay and fan-out. In OpenCode 2.x the merged shapes are `session.text.delta`, `session.reasoning.delta` and `session.tool.input.delta` (v1 had a single `message.part.delta`); every other event is a barrier. Text and reasoning fragments are keyed by `data.ordinal`, tool input by `data.id`.
36
43
  - `resolveDeltaCoalesceWindowMs(env)`: reads `OPENCHAMBER_EVENT_DELTA_COALESCE_MS`. Unset means 50ms, `0` turns coalescing off, and anything that is not a whole number from 0 to 1000 keeps the default with a warning.
37
44
 
38
45
  ### Upstream reader helpers
@@ -42,7 +49,10 @@ This module contains the OpenChamber message-stream WebSocket protocol and runti
42
49
 
43
50
  ## Runtime behavior
44
51
  - Browser clients connect to the WS endpoints above.
45
- - OpenChamber still fetches OpenCode upstream event streams over SSE.
52
+ - OpenChamber still fetches OpenCode upstream event streams over SSE, now from `/api/event`.
53
+ - OpenCode 2.x sends no `id:` SSE lines: the event id is `payload.id` and the directory is `payload.location.directory`. Replay and per-directory routing read both out of the payload.
54
+ - Browser clients receive the RAW wire payload and translate it themselves. Server-side subscribers read `event.translated()` instead, so the translation happens once per event and only when something listens.
55
+ - v2 emits no `session.status` and no `session.idle` of its own: live status is synthesized from `session.execution.started|succeeded|interrupted|failed`. A user abort arrives as `session.idle` with `aborted: true`, not as an assistant message carrying `MessageAbortedError`. A `session.execution.interrupted` with `reason: "shutdown"` translates to nothing: OpenCode keeps the execution claim across a shutdown and resumes the turn after restart, so the session stays busy for server consumers (goal, queue, notifications) until the real terminal event arrives.
46
56
  - The web server creates one shared global message-stream hub. OpenCode watcher side effects and global WS clients subscribe to that hub, so there is one upstream `/global/event` SSE reader for both server-side processing and browser fan-out.
47
57
  - The global hub keeps a bounded replay buffer keyed by SSE `eventId` so reconnecting browser clients can receive buffered events after their requested `Last-Event-ID`.
48
58
  - Replay retains at most 2,048 events and 8 MiB of UTF-8 wire frames. It stores encoded frames rather than retaining parsed payloads as well. Encoding is shared with live WS fanout. An oversized event still reaches live clients in full, but clears the retained suffix so replay cannot cross its gap. A missing replay cursor returns `null`, distinct from a complete empty tail; the bridge sends `ready` with `replayReset: true` and no partial replay. The client retires its cursor and requests authoritative repair, including during the early-boot reconnect grace period. A stopped hub retains its bounded suffix for clients reconnecting after the last socket closed.
@@ -83,6 +93,7 @@ The directory WS bridge and the SSE proxy (`/api/global/event`, used by Capacito
83
93
  ## Testing
84
94
  - Run `bunx vitest run server/lib/event-stream` from `packages/web` for the whole module, including `delta-coalescer.test.js` and the resume-from-any-cursor cases in `global-hub.test.js`.
85
95
  - Run `bun test packages/web/server/lib/event-stream/protocol.test.js`
96
+ - Run `bun test packages/web/server/lib/event-stream/translate-v2.test.js`
86
97
  - Run `bun test packages/web/server/lib/event-stream/upstream-reader.test.js`
87
98
  - Run `bun test packages/web/server/lib/event-stream/runtime.test.js`
88
99
  - Run repo validation before finalizing: `bun run type-check`, `bun run lint`, `bun run build`
@@ -4,17 +4,27 @@ import { createDeltaCoalescer, DELTA_COALESCE_WINDOW_MS, resolveDeltaCoalesceWin
4
4
 
5
5
  let nextId = 0;
6
6
 
7
- const delta = (partID, text, { messageID = 'msg_1', field = 'text', directory = '/repo', sessionID = 'ses_1' } = {}) => {
7
+ // v2 wire shape: `{ type, id, data, location }`. Text and reasoning fragments
8
+ // are addressed by `ordinal`, tool-input fragments by the tool call `id`.
9
+ const delta = (ordinal, text, {
10
+ messageID = 'msg_1',
11
+ type = 'session.text.delta',
12
+ directory = '/repo',
13
+ sessionID = 'ses_1',
14
+ } = {}) => {
8
15
  nextId += 1;
9
16
  const id = String(nextId).padStart(6, '0');
10
- const payload = { id, type: 'message.part.delta', properties: { sessionID, messageID, partID, field, delta: text } };
17
+ const data = { sessionID, assistantMessageID: messageID, delta: text };
18
+ if (type === 'session.tool.input.delta') data.id = String(ordinal);
19
+ else data.ordinal = ordinal;
20
+ const payload = { id, type, location: { directory }, data };
11
21
  return { envelope: { eventId: id, directory, payload }, payload };
12
22
  };
13
23
 
14
- const other = (type, properties = {}) => {
24
+ const other = (type, data = {}) => {
15
25
  nextId += 1;
16
26
  const id = String(nextId).padStart(6, '0');
17
- const payload = { id, type, properties };
27
+ const payload = { id, type, location: { directory: '/repo' }, data };
18
28
  return { envelope: { eventId: id, directory: '/repo', payload }, payload };
19
29
  };
20
30
 
@@ -52,14 +62,15 @@ const createHarness = (options = {}) => {
52
62
  return { coalescer, emitted, advance, pendingTimers: () => timers.length };
53
63
  };
54
64
 
55
- const isDelta = (event) => event.payload.type === 'message.part.delta';
56
- const keyOf = (event) => `${event.payload.properties.messageID}:${event.payload.properties.partID}:${event.payload.properties.field}`;
65
+ const DELTA_TYPES = new Set(['session.text.delta', 'session.reasoning.delta', 'session.tool.input.delta']);
66
+ const isDelta = (event) => DELTA_TYPES.has(event.payload.type);
67
+ const keyOf = (event) => `${event.payload.data.assistantMessageID}:${event.payload.type}:${event.payload.data.ordinal ?? event.payload.data.id}`;
57
68
 
58
69
  const textByKey = (events) => {
59
70
  const text = new Map();
60
71
  for (const event of events) {
61
72
  if (!isDelta(event)) continue;
62
- text.set(keyOf(event), (text.get(keyOf(event)) ?? '') + event.payload.properties.delta);
73
+ text.set(keyOf(event), (text.get(keyOf(event)) ?? '') + event.payload.data.delta);
63
74
  }
64
75
  return text;
65
76
  };
@@ -67,7 +78,7 @@ const textByKey = (events) => {
67
78
  describe('delta coalescer', () => {
68
79
  it('emits the first delta after a quiet spell at once and unchanged', () => {
69
80
  const { coalescer, emitted } = createHarness();
70
- const first = delta('prt_a', 'He');
81
+ const first = delta(1, 'He');
71
82
 
72
83
  coalescer.push(first);
73
84
 
@@ -75,28 +86,28 @@ describe('delta coalescer', () => {
75
86
  expect(emitted[0]).toBe(first);
76
87
  });
77
88
 
78
- it('merges following deltas for one part field and carries the last fragment id', () => {
89
+ it('merges following deltas for one stream and carries the last fragment id', () => {
79
90
  const { coalescer, emitted, advance, pendingTimers } = createHarness();
80
- const fragments = [delta('prt_a', 'He'), delta('prt_a', 'll'), delta('prt_a', 'o,'), delta('prt_a', ' wor'), delta('prt_a', 'ld')];
91
+ const fragments = [delta(1, 'He'), delta(1, 'll'), delta(1, 'o,'), delta(1, ' wor'), delta(1, 'ld')];
81
92
 
82
93
  for (const fragment of fragments) coalescer.push(fragment);
83
94
  expect(emitted).toHaveLength(1);
84
95
  advance(50);
85
96
 
86
97
  expect(emitted).toHaveLength(2);
87
- expect(emitted[1].payload.properties).toEqual({ sessionID: 'ses_1', messageID: 'msg_1', partID: 'prt_a', field: 'text', delta: 'llo, world' });
98
+ expect(emitted[1].payload.data).toEqual({ sessionID: 'ses_1', assistantMessageID: 'msg_1', ordinal: 1, delta: 'llo, world' });
88
99
  expect(emitted[1].envelope.eventId).toBe(fragments[4].envelope.eventId);
89
100
  expect(emitted[1].payload.id).toBe(fragments[4].payload.id);
90
101
  expect(emitted[1].envelope.payload).toBe(emitted[1].payload);
91
102
  expect(pendingTimers()).toBe(0);
92
103
  // Upstream objects are never mutated: other hub subscribers may hold them.
93
- expect(fragments.map((fragment) => fragment.payload.properties.delta)).toEqual(['He', 'll', 'o,', ' wor', 'ld']);
104
+ expect(fragments.map((fragment) => fragment.payload.data.delta)).toEqual(['He', 'll', 'o,', ' wor', 'ld']);
94
105
  });
95
106
 
96
107
  it('never holds text longer than the window', () => {
97
108
  const { coalescer, emitted, advance } = createHarness();
98
- coalescer.push(delta('prt_a', 'a'));
99
- coalescer.push(delta('prt_a', 'b'));
109
+ coalescer.push(delta(1, 'a'));
110
+ coalescer.push(delta(1, 'b'));
100
111
 
101
112
  advance(49);
102
113
  expect(emitted).toHaveLength(1);
@@ -104,104 +115,104 @@ describe('delta coalescer', () => {
104
115
  expect(emitted).toHaveLength(2);
105
116
  });
106
117
 
107
- it('treats every other event as a barrier, so a delta never crosses a part snapshot', () => {
118
+ it('treats every other event as a barrier, so a delta never crosses a stream snapshot', () => {
108
119
  const { coalescer, emitted, advance } = createHarness();
109
- const snapshot = other('message.part.updated', { part: { id: 'prt_a', messageID: 'msg_1', text: 'abc' } });
120
+ const snapshot = other('session.text.ended', { sessionID: 'ses_1', assistantMessageID: 'msg_1', ordinal: 1, text: 'abc' });
110
121
 
111
- coalescer.push(delta('prt_a', 'a'));
112
- coalescer.push(delta('prt_a', 'b'));
113
- coalescer.push(delta('prt_a', 'c'));
122
+ coalescer.push(delta(1, 'a'));
123
+ coalescer.push(delta(1, 'b'));
124
+ coalescer.push(delta(1, 'c'));
114
125
  coalescer.push(snapshot);
115
- coalescer.push(delta('prt_a', 'd'));
116
- coalescer.push(delta('prt_a', 'e'));
126
+ coalescer.push(delta(1, 'd'));
127
+ coalescer.push(delta(1, 'e'));
117
128
  advance(50);
118
129
 
119
- expect(emitted.map((event) => (isDelta(event) ? event.payload.properties.delta : event.payload.type)))
120
- .toEqual(['a', 'bc', 'message.part.updated', 'de']);
130
+ expect(emitted.map((event) => (isDelta(event) ? event.payload.data.delta : event.payload.type)))
131
+ .toEqual(['a', 'bc', 'session.text.ended', 'de']);
121
132
  expect(emitted[2]).toBe(snapshot);
122
133
  });
123
134
 
124
- it('keeps several part fields pending at once and emits them in upstream id order', () => {
135
+ it('keeps several streams pending at once and emits them in upstream id order', () => {
125
136
  const { coalescer, emitted, advance } = createHarness();
126
- coalescer.push(delta('prt_lead', '.'));
137
+ coalescer.push(delta(0, '.'));
127
138
 
128
- coalescer.push(delta('prt_a', 'a1'));
129
- coalescer.push(delta('prt_b', 'b1', { messageID: 'msg_2', sessionID: 'ses_2' }));
130
- coalescer.push(delta('prt_a', 'a2'));
131
- const lastB = delta('prt_b', 'b2', { messageID: 'msg_2', sessionID: 'ses_2' });
139
+ coalescer.push(delta(1, 'a1'));
140
+ coalescer.push(delta(2, 'b1', { messageID: 'msg_2', sessionID: 'ses_2' }));
141
+ coalescer.push(delta(1, 'a2'));
142
+ const lastB = delta(2, 'b2', { messageID: 'msg_2', sessionID: 'ses_2' });
132
143
  coalescer.push(lastB);
133
144
  advance(50);
134
145
 
135
146
  const merged = emitted.slice(1);
136
- expect(merged.map((event) => event.payload.properties.delta)).toEqual(['a1a2', 'b1b2']);
147
+ expect(merged.map((event) => event.payload.data.delta)).toEqual(['a1a2', 'b1b2']);
137
148
  expect(merged[1].envelope.eventId).toBe(lastB.envelope.eventId);
138
149
  const ids = emitted.map((event) => event.envelope.eventId);
139
150
  expect(ids).toEqual([...ids].sort());
140
151
  });
141
152
 
142
- it('does not merge the same part across directories, sessions, messages, or fields', () => {
153
+ it('does not merge the same ordinal across directories, sessions, messages, or stream kinds', () => {
143
154
  const { coalescer, emitted, advance } = createHarness();
144
- coalescer.push(delta('prt_lead', '.'));
155
+ coalescer.push(delta(0, '.'));
145
156
 
146
- coalescer.push(delta('prt_a', '1'));
147
- coalescer.push(delta('prt_a', '2', { directory: '/other' }));
148
- coalescer.push(delta('prt_a', '3', { messageID: 'msg_2' }));
149
- coalescer.push(delta('prt_a', '4', { field: 'reasoning' }));
150
- coalescer.push(delta('prt_a', '5', { sessionID: 'ses_2' }));
157
+ coalescer.push(delta(1, '1'));
158
+ coalescer.push(delta(1, '2', { directory: '/other' }));
159
+ coalescer.push(delta(1, '3', { messageID: 'msg_2' }));
160
+ coalescer.push(delta(1, '4', { type: 'session.reasoning.delta' }));
161
+ coalescer.push(delta(1, '5', { sessionID: 'ses_2' }));
151
162
  advance(50);
152
163
 
153
- expect(emitted.slice(1).map((event) => event.payload.properties.delta)).toEqual(['1', '2', '3', '4', '5']);
164
+ expect(emitted.slice(1).map((event) => event.payload.data.delta)).toEqual(['1', '2', '3', '4', '5']);
154
165
  });
155
166
 
156
- it('bounds pending text by characters and by part fields, emitting early instead of dropping', () => {
167
+ it('bounds pending text by characters and by streams, emitting early instead of dropping', () => {
157
168
  const { coalescer, emitted } = createHarness({ maxPendingChars: 10, maxPendingKeys: 3 });
158
- coalescer.push(delta('prt_lead', '.'));
169
+ coalescer.push(delta(0, '.'));
159
170
 
160
171
  let peakChars = 0;
161
172
  for (let index = 0; index < 40; index += 1) {
162
- coalescer.push(delta('prt_a', 'xyz'));
173
+ coalescer.push(delta(1, 'xyz'));
163
174
  peakChars = Math.max(peakChars, coalescer.pendingSize().chars);
164
175
  }
165
176
  expect(peakChars).toBeLessThan(10 + 3);
166
- expect(textByKey(emitted).get('msg_1:prt_a:text')).toBe('xyz'.repeat(40));
177
+ expect(textByKey(emitted).get('msg_1:session.text.delta:1')).toBe('xyz'.repeat(40));
167
178
 
168
179
  let peakKeys = 0;
169
180
  for (let index = 0; index < 20; index += 1) {
170
- coalescer.push(delta(`prt_k${index}`, 'k'));
181
+ coalescer.push(delta(100 + index, 'k'));
171
182
  peakKeys = Math.max(peakKeys, coalescer.pendingSize().keys);
172
183
  }
173
184
  expect(peakKeys).toBeLessThanOrEqual(3);
174
185
  coalescer.flush();
175
- expect(emitted.filter((event) => event.payload.properties.delta === 'k')).toHaveLength(20);
186
+ expect(emitted.filter((event) => event.payload.data.delta === 'k')).toHaveLength(20);
176
187
  });
177
188
 
178
189
  it('flushes on demand and leaves nothing pending', () => {
179
190
  const { coalescer, emitted, pendingTimers } = createHarness();
180
- coalescer.push(delta('prt_a', 'a'));
181
- coalescer.push(delta('prt_a', 'b'));
191
+ coalescer.push(delta(1, 'a'));
192
+ coalescer.push(delta(1, 'b'));
182
193
 
183
194
  coalescer.flush();
184
195
 
185
- expect(emitted.map((event) => event.payload.properties.delta)).toEqual(['a', 'b']);
196
+ expect(emitted.map((event) => event.payload.data.delta)).toEqual(['a', 'b']);
186
197
  expect(coalescer.pendingSize()).toEqual({ keys: 0, chars: 0 });
187
198
  expect(pendingTimers()).toBe(0);
188
199
  });
189
200
 
190
201
  it('passes a malformed delta through as a barrier instead of guessing', () => {
191
202
  const { coalescer, emitted } = createHarness();
192
- const malformed = other('message.part.delta', { messageID: 'msg_1', partID: 'prt_a', field: 'text', delta: 42 });
203
+ const malformed = other('session.text.delta', { sessionID: 'ses_1', assistantMessageID: 'msg_1', ordinal: 1, delta: 42 });
193
204
 
194
- coalescer.push(delta('prt_a', 'a'));
195
- coalescer.push(delta('prt_a', 'b'));
205
+ coalescer.push(delta(1, 'a'));
206
+ coalescer.push(delta(1, 'b'));
196
207
  coalescer.push(malformed);
197
208
 
198
- expect(emitted.map((event) => event.payload.properties.delta)).toEqual(['a', 'b', 42]);
209
+ expect(emitted.map((event) => event.payload.data.delta)).toEqual(['a', 'b', 42]);
199
210
  expect(emitted[2]).toBe(malformed);
200
211
  });
201
212
 
202
213
  it('is a pass-through when the window is zero', () => {
203
214
  const { coalescer, emitted } = createHarness({ windowMs: 0 });
204
- const events = [delta('prt_a', 'a'), delta('prt_a', 'b'), delta('prt_a', 'c')];
215
+ const events = [delta(1, 'a'), delta(1, 'b'), delta(1, 'c')];
205
216
 
206
217
  for (const event of events) coalescer.push(event);
207
218
 
@@ -226,11 +237,11 @@ describe('delta coalescer', () => {
226
237
  let event;
227
238
  if (roll < 0.9) {
228
239
  const part = Math.floor(random() * partCount);
229
- event = delta(`prt_${part}`, `${index}|`.repeat(1 + Math.floor(random() * 3)), { messageID: `msg_${part % 2}` });
240
+ event = delta(part, `${index}|`.repeat(1 + Math.floor(random() * 3)), { messageID: `msg_${part % 2}` });
230
241
  } else if (roll < 0.95) {
231
- event = other('message.part.updated', { part: { id: `prt_${Math.floor(random() * partCount)}`, messageID: 'msg_0' } });
242
+ event = other('session.text.ended', { sessionID: 'ses_1', assistantMessageID: 'msg_0', ordinal: Math.floor(random() * partCount), text: '' });
232
243
  } else {
233
- event = other('session.status', { sessionID: 'ses_1', status: { type: 'busy' } });
244
+ event = other('session.execution.started', { sessionID: 'ses_1' });
234
245
  }
235
246
  raw.push(event);
236
247
  coalescer.push(event);
@@ -264,12 +275,12 @@ describe('delta coalescer', () => {
264
275
 
265
276
  // 150 fragments a second for two seconds, the rate measured from OpenCode.
266
277
  for (let index = 0; index < 300; index += 1) {
267
- coalescer.push(delta('prt_a', 'ab'));
278
+ coalescer.push(delta(1, 'ab'));
268
279
  advance(index % 3 === 2 ? 8 : 6);
269
280
  }
270
281
  advance(50);
271
282
 
272
- expect(textByKey(emitted).get('msg_1:prt_a:text')).toBe('ab'.repeat(300));
283
+ expect(textByKey(emitted).get('msg_1:session.text.delta:1')).toBe('ab'.repeat(300));
273
284
  expect(emitted.length).toBeLessThanOrEqual(45);
274
285
  });
275
286
  });
@@ -106,7 +106,7 @@ export function acceptDirectoryMessageStreamWsConnection({
106
106
  buildUrlFailed = false;
107
107
  let targetUrl;
108
108
  try {
109
- targetUrl = new URL(buildOpenCodeUrl('/event', ''));
109
+ targetUrl = new URL(buildOpenCodeUrl('/api/event', ''));
110
110
  } catch {
111
111
  buildUrlFailed = true;
112
112
  throw new Error('OpenCode service unavailable');
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
2
2
 
3
3
  import { createUpstreamSseReader } from './upstream-reader.js';
4
4
  import { serializeMessageStreamWsEvent } from './protocol.js';
5
+ import { translateWireEvent } from './translate-v2.js';
5
6
  import { createDeltaCoalescer, DELTA_COALESCE_WINDOW_MS } from './delta-coalescer.js';
6
7
 
7
8
  // Raised from 512 → 2048 to improve recovery after brief disconnects during
@@ -69,6 +70,7 @@ export function createGlobalMessageStreamHub({
69
70
  ? envelope.eventId
70
71
  : `${replayIdPrefix}${String(++replaySequence).padStart(12, '0')}`;
71
72
  let serializedFrame;
73
+ let translated;
72
74
  return {
73
75
  envelope,
74
76
  payload,
@@ -78,6 +80,13 @@ export function createGlobalMessageStreamHub({
78
80
  serializedFrame ??= serializeMessageStreamWsEvent(payload, { directory, eventId });
79
81
  return serializedFrame;
80
82
  },
83
+ // Browser clients receive the raw wire payload and translate it
84
+ // themselves; server-side subscribers read this instead. Translating
85
+ // lazily keeps the cost off the WS fan-out path when nothing listens.
86
+ translated() {
87
+ translated ??= translateWireEvent(payload);
88
+ return translated;
89
+ },
81
90
  };
82
91
  };
83
92
 
@@ -123,7 +132,7 @@ export function createGlobalMessageStreamHub({
123
132
  buildUrl: () => {
124
133
  buildUrlFailed = false;
125
134
  try {
126
- return new URL(buildOpenCodeUrl('/global/event', ''));
135
+ return new URL(buildOpenCodeUrl('/api/event', ''));
127
136
  } catch {
128
137
  buildUrlFailed = true;
129
138
  throw new Error('OpenCode service unavailable');