@vantran-se/openchamber-web 1.24.4 → 2.0.2

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-B82Oye3V.js} +1 -1
  5. package/dist/assets/DiffView-DJLmWn6Z.js +4 -0
  6. package/dist/assets/FilesView-DjksmZRo.js +17 -0
  7. package/dist/assets/{GitView-Byu2DY7s.js → GitView-DiUsUIgr.js} +4 -4
  8. package/dist/assets/{GuestAttachDialog-B808E0-d.js → GuestAttachDialog-CYwWYLr3.js} +1 -1
  9. package/dist/assets/GuestSurfacePane-CmctyRI4.js +1 -0
  10. package/dist/assets/{JsonTreeView-VW_7czo3.js → JsonTreeView-DOHHcbgJ.js} +1 -1
  11. package/dist/assets/LinearIssuesView-DaqnylcV.js +1 -0
  12. package/dist/assets/MarkdownImageGallery-HJNvWt7L.js +1 -0
  13. package/dist/assets/MarkdownRendererImpl-nDZRGg2I.js +12 -0
  14. package/dist/assets/OnboardingScreen-C2B5Bsof.js +1 -0
  15. package/dist/assets/{PatchDiff-DCjGhlYV.js → PatchDiff-C4FLBjpt.js} +1 -1
  16. package/dist/assets/{PierreDiffViewer-CEDZNkU2.js → PierreDiffViewer-C_1S9c9n.js} +1 -1
  17. package/dist/assets/PlanView-_eZvb-G5.js +5 -0
  18. package/dist/assets/PluginPane-sM8fcZIV.js +3 -0
  19. package/dist/assets/SettingsView-BRfr46hb.js +29 -0
  20. package/dist/assets/SettingsWindow-CcUDwQHg.js +1 -0
  21. package/dist/assets/SubmoduleDiffSummary-CrN1ZGwl.js +1 -0
  22. package/dist/assets/ToolOutputDialog-Cij4jI09.js +15 -0
  23. package/dist/assets/ToolPartDiffPreview-BXzwQ-JY.js +8 -0
  24. package/dist/assets/{VirtualizedFileDiff-NU0FHFhT.js → VirtualizedFileDiff-DSWTLJNH.js} +4 -4
  25. package/dist/assets/{WalkthroughView-BZyjhqHv.js → WalkthroughView-CpUUgY8H.js} +2 -2
  26. package/dist/assets/appThemeRegistry-B5tHTkR8.js +1 -0
  27. package/dist/assets/{commitAndPush-BDftrjr6.js → commitAndPush-D7D8qV8p.js} +1 -1
  28. package/dist/assets/{computeVirtualFileMetrics-DlWlEM3x.js → computeVirtualFileMetrics-cq9uQ30i.js} +1 -1
  29. package/dist/assets/de-CUO1-KqF.js +10 -0
  30. package/dist/assets/es-BlbC6sGJ.js +16 -0
  31. package/dist/assets/fr-D2nwe_0W.js +14 -0
  32. package/dist/assets/index-BQOoYIOu.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-D0obJ2p8.js} +1 -1
  36. package/dist/assets/{isFilePlainText-ue6kCou7.js → isFilePlainText-DB3_rjyW.js} +1 -1
  37. package/dist/assets/ja-CA7vnDsZ.js +16 -0
  38. package/dist/assets/ko-Chy6a9l-.js +16 -0
  39. package/dist/assets/main-B9JFK3vF.js +896 -0
  40. package/dist/assets/main-Bhiqnrhd.js +2 -0
  41. package/dist/assets/{markdownCore-CK48LG0z.js → markdownCore-tfqOFaBd.js} +4 -4
  42. package/dist/assets/miniChat-B0UOL5zA.js +2 -0
  43. package/dist/assets/mobile-BJppFIEo.js +2 -0
  44. package/dist/assets/{patchFileDiff-D7QVlovD.js → patchFileDiff-Db6KuwCP.js} +3 -3
  45. package/dist/assets/pl-CTR5A_1P.js +16 -0
  46. package/dist/assets/pt-BR-BE1O1KEx.js +16 -0
  47. package/dist/assets/renderElectronMiniChatApp-Yz5sfoFh.js +2 -0
  48. package/dist/assets/renderMobileApp-QQ-yB8aY.js +25 -0
  49. package/dist/assets/runtimeEndpointReset-DKDdzRzm.js +27 -0
  50. package/dist/assets/scheduledTasksApi-C5sCuNqf.js +1 -0
  51. package/dist/assets/{shikiHighlight-BPu3JiQs.js → shikiHighlight-xURoo5t_.js} +1 -1
  52. package/dist/assets/{textMateThemeFromAppTheme-DWqRPo9A.js → textMateThemeFromAppTheme-BZLGuYTU.js} +1 -1
  53. package/dist/assets/tr-Bh5vK6cW.js +7 -0
  54. package/dist/assets/uk-CLDkZFy_.js +16 -0
  55. package/dist/assets/useAppFontEffects-BxrtCnfR.js +7337 -0
  56. package/dist/assets/{useInlineCommentController-CgS0fnw_.js → useInlineCommentController-CeXuR-SR.js} +2 -2
  57. package/dist/assets/useRootScrollLock-DgWxezc1.js +2 -0
  58. package/dist/assets/{vendor-codemirror-language-data-Bx2EHc1O.js → vendor-codemirror-language-data-DOuaQUZ0.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-63H1h21j.js} +2 -2
  64. package/dist/assets/zh-CN-DODgcmYj.js +16 -0
  65. package/dist/assets/zh-TW-ByNfIwLA.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,203 +1,175 @@
1
- import { createServer } from 'node:http';
2
1
  import { afterEach, describe, expect, it, vi } from 'vitest';
3
2
  import { createSessionAssistRuntime } from './runtime.js';
4
3
 
5
- const resources = [];
6
- const message = (id, role, text, extra = {}) => ({
7
- info: { id, role, parentID: 'user', finish: 'stop', time: { completed: 1 }, providerID: 'test-provider', modelID: 'test-model', ...extra },
8
- parts: [{ type: 'text', text }],
9
- });
10
- const output = (recap = 'Зміни готові', suggestion = '') => ({ text: JSON.stringify({ recap, suggestion }), providerID: 'test-provider', modelID: 'test-model' });
11
- const pause = () => new Promise((resolve) => setTimeout(resolve, 15));
12
-
13
- async function fixture(generate = async () => output()) {
14
- const state = {
15
- messages: [message('user', 'user', 'Виправ помилку'), message('answer', 'assistant', 'Виправлено')],
16
- session: { id: 'session', directory: '/project', time: {}, metadata: { external: 'keep', openchamber: { note: 'keep' } } },
17
- targets: { recap: true, suggestion: true },
18
- gets: 0, failFresh: false, patches: [], requests: [], calls: [],
19
- };
20
- const server = createServer(async (request, response) => {
21
- const url = new URL(request.url, 'http://localhost');
22
- state.requests.push({ method: request.method, directory: url.searchParams.get('directory'), limit: url.searchParams.get('limit'), auth: request.headers['x-test-auth'] });
23
- response.setHeader('content-type', 'application/json');
24
- if (request.method === 'PATCH') {
25
- const chunks = [];
26
- for await (const chunk of request) chunks.push(chunk);
27
- state.patches.push(JSON.parse(Buffer.concat(chunks).toString()));
28
- response.end(JSON.stringify(state.session));
29
- } else if (url.pathname.endsWith('/message')) {
30
- response.end(JSON.stringify(url.searchParams.get('limit') === '1' ? state.messages.slice(-1) : state.messages));
31
- } else {
32
- state.gets++;
33
- response.statusCode = state.failFresh && state.gets > 1 ? 500 : 200;
34
- response.end(JSON.stringify(state.session));
35
- }
4
+ /**
5
+ * The recap and the suggestion live in OpenChamber's own session metadata
6
+ * store, because OpenCode 2.x accepts session metadata only at create time.
7
+ * `persistSessionAssist` is the seam. What is pinned here is the wiring: no
8
+ * store means no work and no cost, and injecting one turns generation back on.
9
+ *
10
+ * The previous suite drove the whole generation through a fake v1 OpenCode
11
+ * server. It is gone rather than rewritten: every shape it asserted on
12
+ * (`message.parts`, `info.parentID`, `info.summary`, `PATCH /session/{id}`)
13
+ * belongs to v1, so keeping it green would prove nothing about v2. The reader
14
+ * itself is covered by `context.test.js`.
15
+ */
16
+
17
+ const runtimes = [];
18
+
19
+ const makeRuntime = (overrides = {}) => {
20
+ const buildOpenCodeUrl = vi.fn(() => 'http://127.0.0.1:1/');
21
+ const getOpenCodeAuthHeaders = vi.fn(() => ({}));
22
+ const getSmallModelService = vi.fn(async () => {
23
+ throw new Error('the small model must not be consulted while assist is parked');
36
24
  });
37
- await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
38
- const base = `http://127.0.0.1:${server.address().port}`;
39
- state.base = base;
40
25
  const runtime = createSessionAssistRuntime({
41
- buildOpenCodeUrl: (route) => state.base + route,
42
- getOpenCodeAuthHeaders: () => ({ 'x-test-auth': 'fixture' }),
43
- getTargets: () => state.targets,
26
+ buildOpenCodeUrl,
27
+ getOpenCodeAuthHeaders,
28
+ getSmallModelService,
29
+ getTargets: () => ({ recap: true, suggestion: true }),
44
30
  quietMs: 1,
45
- getSmallModelService: async () => ({
46
- describeSmallModel: async () => ({ inputCharBudget: 64_000 }),
47
- generateSmallModelText: async (args) => { state.calls.push(args); return generate(args, state); },
48
- }),
31
+ ...overrides,
49
32
  });
50
- resources.push({ runtime, server });
51
- const status = (type) => runtime.processPayload({ type: 'session.status', properties: { sessionID: 'session', status: { type } } }, '/project');
52
- return { state, runtime, status };
53
- }
33
+ runtimes.push(runtime);
34
+ return { runtime, buildOpenCodeUrl, getOpenCodeAuthHeaders, getSmallModelService };
35
+ };
54
36
 
55
- afterEach(async () => {
56
- for (const { runtime, server } of resources.splice(0)) {
57
- runtime.stop();
58
- server.closeAllConnections();
59
- await new Promise((resolve) => server.close(resolve));
60
- }
37
+ const idle = (sessionId = 'ses_1') => ({
38
+ type: 'session.status',
39
+ properties: { sessionID: sessionId, status: { type: 'idle' } },
40
+ });
41
+
42
+ afterEach(() => {
43
+ while (runtimes.length > 0) runtimes.pop().stop();
61
44
  vi.restoreAllMocks();
62
45
  });
63
46
 
64
47
  describe('session assist runtime', () => {
65
- it('uses bounded authenticated SDK reads and preserves metadata with an empty suggestion', async () => {
66
- const { state, status } = await fixture(async (_args, current) => {
67
- current.session.metadata.openchamber.concurrent = 'new';
68
- return output();
69
- });
70
- status('idle');
71
- await vi.waitFor(() => expect(state.patches).toHaveLength(1));
72
- expect(state.requests.every((r) => r.directory === '/project' && r.auth === 'fixture')).toBe(true);
73
- expect(state.requests.filter((r) => r.limit).map((r) => r.limit)).toEqual(['50', '1']);
74
- expect(state.calls[0]).toMatchObject({ restrictToPreferredProvider: true, onOverflow: 'error', preferredProviderID: 'test-provider', preferredModelID: 'test-model', sessionID: 'session' });
75
- expect(state.patches[0].metadata).toMatchObject({ external: 'keep', openchamber: {
76
- note: 'keep', concurrent: 'new', assist: { recap: 'Зміни готові', suggestion: '', forMessageID: 'answer' },
77
- } });
48
+ it('does no work and reaches no service while no assist store is injected', async () => {
49
+ vi.spyOn(console, 'log').mockImplementation(() => {});
50
+ const { runtime, buildOpenCodeUrl, getSmallModelService } = makeRuntime();
51
+
52
+ runtime.processPayload(idle());
53
+ await new Promise((resolve) => setTimeout(resolve, 20));
54
+
55
+ expect(buildOpenCodeUrl).not.toHaveBeenCalled();
56
+ expect(getSmallModelService).not.toHaveBeenCalled();
78
57
  });
79
58
 
80
- it('does no work with both settings off and skips child, archived, or reverted sessions', async () => {
81
- const { state, status } = await fixture();
82
- state.targets = { recap: false, suggestion: false };
83
- status('idle');
84
- await pause();
85
- expect(state.requests).toHaveLength(0);
86
- state.targets.recap = true;
87
- state.session.parentID = 'parent';
88
- status('idle');
89
- await vi.waitFor(() => expect(state.gets).toBe(1));
90
- delete state.session.parentID;
91
- state.session.revert = { messageID: 'user' };
92
- status('idle');
93
- await vi.waitFor(() => expect(state.gets).toBe(2));
94
- delete state.session.revert;
95
- state.session.time.archived = 1;
96
- status('idle');
97
- await vi.waitFor(() => expect(state.gets).toBe(3));
98
- expect(state.calls).toHaveLength(0);
59
+ it('explains itself once, not on every idle session', async () => {
60
+ const log = vi.spyOn(console, 'log').mockImplementation(() => {});
61
+ const { runtime } = makeRuntime();
62
+
63
+ runtime.processPayload(idle('ses_1'));
64
+ runtime.processPayload(idle('ses_2'));
65
+ runtime.processPayload(idle('ses_3'));
66
+
67
+ const notices = log.mock.calls.filter(([line]) => String(line).includes('[session-assist] parked'));
68
+ expect(notices).toHaveLength(1);
99
69
  });
100
70
 
101
- it('keeps recent context for recap-only and performs no write for an empty suggestion-only result', async () => {
102
- const { state, status } = await fixture();
103
- state.targets.suggestion = false;
104
- state.messages.unshift(message('previous-user', 'user', 'Попередня задача'), message('previous-answer', 'assistant', 'Зміст зробленого', { parentID: 'previous-user' }));
105
- status('idle');
106
- await vi.waitFor(() => expect(state.patches).toHaveLength(1));
107
- expect(state.calls[0].prompt).toContain('Зміст зробленого');
108
- expect(state.calls[0].system).not.toContain('suggestion');
109
- state.targets = { recap: false, suggestion: true };
110
- status('idle');
111
- await vi.waitFor(() => expect(state.calls).toHaveLength(2));
112
- await pause();
113
- expect(state.patches).toHaveLength(1);
71
+ it('ignores everything after stop', async () => {
72
+ const log = vi.spyOn(console, 'log').mockImplementation(() => {});
73
+ const { runtime } = makeRuntime();
74
+
75
+ runtime.stop();
76
+ runtime.processPayload(idle());
77
+
78
+ expect(log).not.toHaveBeenCalled();
114
79
  });
115
80
 
116
- it('does not write a stale result when the tail moves during generation', async () => {
117
- const { state, status } = await fixture(async (_args, current) => {
118
- current.messages.push(message('new-user', 'user', 'Нова задача'));
119
- return output();
81
+ it('leaves an archived session alone: no context is loaded and no model is called', async () => {
82
+ const persistSessionAssist = vi.fn(async () => undefined);
83
+ const getSmallModelService = vi.fn(async () => {
84
+ throw new Error('the small model must not be consulted for an archived session');
85
+ });
86
+ const fetchMock = vi.fn(async (input) => {
87
+ const url = new URL(String(input));
88
+ const body = url.pathname === '/api/session/ses_1'
89
+ ? { location: { directory: '/repo' }, data: { id: 'ses_1', location: { directory: '/repo' } } }
90
+ : { location: { directory: '/repo' }, data: { data: [], cursor: null } };
91
+ return new Response(JSON.stringify(body), { status: 200, headers: { 'content-type': 'application/json' } });
92
+ });
93
+ vi.stubGlobal('fetch', fetchMock);
94
+ const { runtime } = makeRuntime({
95
+ persistSessionAssist,
96
+ getSmallModelService,
97
+ buildOpenCodeUrl: (fetchPath) => `http://opencode.test${fetchPath}`,
98
+ isSessionArchived: async (sessionId) => sessionId === 'ses_1',
120
99
  });
121
- status('idle');
122
- await vi.waitFor(() => expect(state.requests.some((r) => r.limit === '1')).toBe(true));
123
- await pause();
124
- expect(state.patches).toHaveLength(0);
125
- });
126
100
 
127
- it('does not fall back to stale metadata when the fresh read fails', async () => {
128
- const warning = vi.spyOn(console, 'warn').mockImplementation(() => {});
129
- const { state, status } = await fixture();
130
- state.failFresh = true;
131
- status('idle');
132
- await vi.waitFor(() => expect(warning).toHaveBeenCalled());
133
- expect(state.gets).toBe(2);
134
- expect(state.patches).toHaveLength(0);
135
- });
101
+ runtime.processPayload(idle());
102
+ await new Promise((resolve) => setTimeout(resolve, 40));
136
103
 
137
- it('cancels old work and retains an expired newer idle timer until it can run', async () => {
138
- const releases = [];
139
- const { state, status } = await fixture(() => new Promise((resolve) => releases.push(resolve)));
140
- status('idle');
141
- await vi.waitFor(() => expect(releases).toHaveLength(1));
142
- status('busy');
143
- expect(state.calls[0].signal.aborted).toBe(true);
144
- state.messages.push(message('next-user', 'user', 'Далі'), message('next-answer', 'assistant', 'Готово', { parentID: 'next-user' }));
145
- status('idle');
146
- await pause();
147
- releases[0](output('Старий результат'));
148
- await vi.waitFor(() => expect(releases).toHaveLength(2));
149
- expect(state.patches).toHaveLength(0);
150
- releases[1](output('Новий результат'));
151
- await vi.waitFor(() => expect(state.patches).toHaveLength(1));
152
- expect(state.patches[0].metadata.openchamber.assist).toMatchObject({ recap: 'Новий результат', forMessageID: 'next-answer' });
104
+ // The session record was read (that is where the parent/revert checks
105
+ // live), then the archive check stopped everything else.
106
+ expect(fetchMock.mock.calls.map(([input]) => new URL(String(input)).pathname)).toEqual(['/api/session/ses_1']);
107
+ expect(getSmallModelService).not.toHaveBeenCalled();
108
+ expect(persistSessionAssist).not.toHaveBeenCalled();
109
+ vi.unstubAllGlobals();
153
110
  });
154
111
 
155
- it('aborts on stop and honors settings switched off during generation', async () => {
156
- let release;
157
- const { state, runtime, status } = await fixture(() => new Promise((resolve) => { release = resolve; }));
158
- status('idle');
159
- await vi.waitFor(() => expect(state.calls).toHaveLength(1));
160
- runtime.stop();
161
- expect(state.calls[0].signal.aborted).toBe(true);
162
- release(output());
163
- await pause();
164
- expect(state.patches).toHaveLength(0);
165
- const second = await fixture(async (_args, current) => {
166
- current.targets = { recap: false, suggestion: false };
167
- return output();
112
+ it('generates and saves an assist for a turn that v2 closed with an idle marker', async () => {
113
+ vi.spyOn(console, 'log').mockImplementation(() => {});
114
+ const persistSessionAssist = vi.fn(async () => undefined);
115
+ const listLimits = [];
116
+ // Newest first, the way OpenCode serves it: the idle marker sits above
117
+ // the answer, and a model switch the user made afterwards above that.
118
+ const records = [
119
+ { id: 'msg_switch', type: 'model-switched', model: { providerID: 'p', id: 'm' } },
120
+ { id: 'msg_idle', type: 'idle', outcome: 'succeeded' },
121
+ { id: 'msg_a', type: 'assistant', content: [{ type: 'text', text: 'All done.' }], finish: 'stop', time: { completed: 2 }, model: { providerID: 'p', id: 'm' } },
122
+ { id: 'msg_u', type: 'user', text: 'Do the thing' },
123
+ ];
124
+ const fetchMock = vi.fn(async (input) => {
125
+ const url = new URL(String(input));
126
+ let body;
127
+ // `session.get` answers `{ data }`, which the client unwraps; the message
128
+ // page is `{ data, cursor }` and reaches the reader as is.
129
+ if (url.pathname === '/api/session/ses_1') body = { data: { id: 'ses_1', location: { directory: '/repo' } } };
130
+ else if (url.pathname === '/api/session/ses_1/message') {
131
+ listLimits.push(Number(url.searchParams.get('limit')));
132
+ body = { data: records.slice(0, Number(url.searchParams.get('limit'))), cursor: {} };
133
+ } else throw new Error(`unexpected ${url.pathname}`);
134
+ return new Response(JSON.stringify(body), { status: 200, headers: { 'content-type': 'application/json' } });
168
135
  });
169
- second.status('idle');
170
- await vi.waitFor(() => expect(second.state.gets).toBe(2));
171
- await pause();
172
- expect(second.state.patches).toHaveLength(0);
173
- });
136
+ vi.stubGlobal('fetch', fetchMock);
137
+ const { runtime } = makeRuntime({
138
+ persistSessionAssist,
139
+ buildOpenCodeUrl: (fetchPath) => `http://opencode.test${fetchPath}`,
140
+ isSessionArchived: async () => false,
141
+ getSmallModelService: async () => ({
142
+ describeSmallModel: async () => ({ inputCharBudget: 20_000 }),
143
+ generateSmallModelText: async () => ({ text: '{"recap":"Did the thing.","suggestion":"Verify it."}' }),
144
+ }),
145
+ });
146
+
147
+ runtime.processPayload(idle());
148
+ for (let i = 0; i < 20 && persistSessionAssist.mock.calls.length === 0; i += 1) {
149
+ await new Promise((resolve) => setTimeout(resolve, 10));
150
+ }
174
151
 
175
- it('ignores historical user updates but cancels a new request during generation', async () => {
176
- let release;
177
- const { state, runtime, status } = await fixture(() => new Promise((resolve) => { release = resolve; }));
178
- status('idle');
179
- await vi.waitFor(() => expect(state.calls).toHaveLength(1));
180
- const userUpdate = (created) => runtime.processPayload({ type: 'message.updated', properties: {
181
- info: { id: 'user', sessionID: 'session', role: 'user', time: { created } },
182
- } });
183
- userUpdate(1);
184
- expect(state.calls[0].signal.aborted).toBe(false);
185
- userUpdate(Date.now());
186
- expect(state.calls[0].signal.aborted).toBe(true);
187
- release(output());
188
- await pause();
189
- expect(state.patches).toHaveLength(0);
152
+ expect(persistSessionAssist).toHaveBeenCalledTimes(1);
153
+ expect(persistSessionAssist.mock.calls[0][2]).toMatchObject({ recap: 'Did the thing.', suggestion: 'Verify it.', forMessageID: 'msg_a' });
154
+ // The pre-write re-check must see past the idle marker as well.
155
+ expect(listLimits.at(-1)).toBeGreaterThan(2);
156
+ vi.unstubAllGlobals();
190
157
  });
191
158
 
192
- it('rejects endpoint changes before writing instead of carrying a session into a new runtime', async () => {
193
- const warning = vi.spyOn(console, 'warn').mockImplementation(() => {});
194
- const { state, status } = await fixture(async (_args, current) => {
195
- current.base = 'http://unreachable.invalid';
196
- return output();
159
+ it('arms generation again as soon as a store is injected', async () => {
160
+ const persistSessionAssist = vi.fn(async () => undefined);
161
+ const getSmallModelService = vi.fn(async () => {
162
+ throw new Error('stop here: the transport is what this test observes');
197
163
  });
198
- status('idle');
199
- await vi.waitFor(() => expect(warning).toHaveBeenCalled());
200
- expect(state.patches).toHaveLength(0);
201
- expect(state.gets).toBe(1);
164
+ const buildOpenCodeUrl = vi.fn(() => 'http://127.0.0.1:1/');
165
+ const { runtime } = makeRuntime({ persistSessionAssist, getSmallModelService, buildOpenCodeUrl });
166
+
167
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
168
+ runtime.processPayload(idle());
169
+ await new Promise((resolve) => setTimeout(resolve, 30));
170
+
171
+ // The idle timer fired and the generation path ran, which is what the gate
172
+ // above suppresses.
173
+ expect(buildOpenCodeUrl).toHaveBeenCalled();
202
174
  });
203
175
  });
@@ -81,13 +81,19 @@ before touching the filesystem). Rationale: metadata rides every
81
81
  a goal on an idle session emits no status transition.
82
82
  3. On fire (`tick`), gated by the `sessionGoalEnabled` setting:
83
83
  - fetch session (skip sub-agent sessions), require an `active` goal;
84
- - authoritative live-activity check after the quiet window: re-read the
85
- session status map, bail if the parent resumed, then list direct child
86
- sessions and bail while any child is `busy`/`retry`. A background
87
- subagent leaves its parent idle, then injects its result into the parent
88
- when done; that parent `busy` → `idle` cycle re-arms the loop without
89
- polling. Status/children fetch failure is unknown, not empty, so it skips
90
- the audit and retries after another quiet window;
84
+ - authoritative live-activity check after the quiet window: re-read
85
+ `/api/session/active`, bail if the parent resumed; then list the
86
+ parent's subagent sessions through `GET /api/session?parentID=` (cursor
87
+ paged) and bail while any of them is active. A status or children fetch
88
+ failure is unknown, not empty, so it skips the audit and retries after
89
+ another quiet window;
90
+ both reads live in `../opencode/session-activity.js`, shared with the
91
+ notification runtime;
92
+ - messages come from `/api/session/:id/message` as v2's flat records
93
+ (`type`, `content[]`, `model`, `finish`, `tokens`); `toLoopMessage`
94
+ projects them into the `{ info, parts }` view the rest of the tick reads,
95
+ and a completed `compaction` record plays v1's `summary: true` assistant
96
+ turn. Other plumbing roles are dropped from the view;
91
97
  - quiescence check via the message tail (trailing user message or
92
98
  unfinished assistant reply → bail; the next idle transition re-arms);
93
99
  - token accounting as a SNAPSHOT of the latest completed assistant turn:
@@ -143,9 +149,10 @@ before touching the filesystem). Rationale: metadata rides every
143
149
  loop blind to the turn cap;
144
150
  - continue: persist accounting + `turnsUsed` first (a crash after the
145
151
  write just waits for the next idle tick; the reverse could double-send),
146
- re-check the tail, then `POST /session/:id/prompt_async` with the
147
- continuation prompt using the last assistant message's
148
- provider/model/agent — the goal spends the session's own subscription.
152
+ re-check the tail, then `POST /api/session/:id/prompt` with the
153
+ continuation prompt. v2 keeps the model and agent on the session, so the
154
+ prompt runs on what the session already uses; nothing is re-selected —
155
+ the goal spends the session's own subscription.
149
156
  4. Settling (`complete`/`blocked`/`budgetLimited`) fires the injected
150
157
  `emitGoalNotification` so the user hears about it even with the UI closed:
151
158
  desktop + UI broadcast + the standard push fanout (web-push with full
@@ -190,10 +197,11 @@ sees only that final turn, so the report is its evidence.
190
197
 
191
198
  Scheduled tasks can run as goals: `execution.goalEnabled` (+ optional
192
199
  `execution.goalTokenBudget`) on a task makes the scheduled-tasks runtime
193
- stamp `metadata.openchamber.goal` onto the fresh session (objective = the
194
- expanded task prompt, or the argument-expanded command template for a slash
195
- command) and attach the goal-mode intro part to normal prompts.
196
- The loop here picks it up from session events like any other goal.
200
+ write the goal into OpenChamber's session metadata store through the
201
+ `persistSessionGoal` seam `server/index.js` hands it (objective = the expanded
202
+ task prompt; v2 `CommandInfo` carries no template, so a slash command's
203
+ objective is its raw invocation) and attach the goal-mode intro part to normal
204
+ prompts. The store write is what arms the loop (`notifyGoalChanged`).
197
205
 
198
206
  ## CLI-created goals
199
207
 
@@ -220,7 +228,7 @@ ordering used by create and scheduled goals.
220
228
  - Web-server feature: VS Code (extension-only) renders goal state via
221
229
  `session.updated` but does not run the loop.
222
230
  - A goal on a session with no assistant reply yet starts after the first
223
- user exchange completes (no provider/model to continue with before that).
231
+ user exchange completes (there is no reply to audit before that).
224
232
  - `tokensUsed` only counts completed assistant messages seen within the
225
233
  40-message fetch window per tick; extremely long busy stretches between
226
234
  idles undercount (acceptable: budget is a guardrail, not billing).
@@ -56,6 +56,7 @@ export const createSessionGoal = async ({
56
56
  providerID,
57
57
  modelID,
58
58
  onWarning,
59
+ persistSessionGoal,
59
60
  }) => {
60
61
  const warn = (message, error) => {
61
62
  if (typeof onWarning === 'function') {
@@ -98,17 +99,13 @@ export const createSessionGoal = async ({
98
99
  createdAt: now,
99
100
  updatedAt: now,
100
101
  };
101
- const url = new URL(`${baseUrl}/session/${encodeURIComponent(sessionID)}`);
102
- url.searchParams.set('directory', directory);
103
- const response = await fetch(url.toString(), {
104
- method: 'PATCH',
105
- headers: {
106
- ...authHeaders,
107
- 'content-type': 'application/json',
108
- accept: 'application/json',
109
- },
110
- body: JSON.stringify({ metadata: { openchamber: { goal } } }),
111
- });
112
- if (!response.ok) throw new Error(`goal metadata patch failed (${response.status})`);
102
+ // OpenCode 2.x accepts session metadata only at create time, so the goal
103
+ // record lives in OpenChamber's own store next to the objective text.
104
+ void baseUrl;
105
+ void authHeaders;
106
+ if (typeof persistSessionGoal !== 'function') {
107
+ throw new Error('goal mode needs a session metadata store to save the goal in');
108
+ }
109
+ await persistSessionGoal(sessionID, directory, goal);
113
110
  return goal;
114
111
  };
@@ -20,8 +20,9 @@ describe('session goal creation', () => {
20
20
  generateSmallModelTextMock.mockReset().mockResolvedValue({ text: '' });
21
21
  });
22
22
 
23
- it('writes the objective before patching active goal metadata', async () => {
24
- const fetchMock = vi.fn(async () => ({ ok: true }));
23
+ it('saves the goal in OpenChamber\'s own store, never through OpenCode', async () => {
24
+ const fetchMock = vi.fn();
25
+ const persistSessionGoal = vi.fn(async () => undefined);
25
26
  const originalFetch = globalThis.fetch;
26
27
  globalThis.fetch = fetchMock;
27
28
  try {
@@ -34,43 +35,41 @@ describe('session goal creation', () => {
34
35
  tokenBudget: 200_000,
35
36
  providerID: 'openai',
36
37
  modelID: 'gpt-5.5',
38
+ persistSessionGoal,
37
39
  });
38
40
 
39
41
  expect(writeObjectiveMock).toHaveBeenCalledWith('ses_123', 'Finish and verify the migration');
40
- expect(writeObjectiveMock.mock.invocationCallOrder[0]).toBeLessThan(fetchMock.mock.invocationCallOrder[0]);
41
42
  expect(goal).toMatchObject({ objective: '', objectiveFile: true, status: 'active', tokenBudget: 200_000 });
42
- expect(fetchMock).toHaveBeenCalledWith(
43
- 'http://opencode.test/session/ses_123?directory=%2Frepo%2Fapp',
44
- expect.objectContaining({ method: 'PATCH' }),
45
- );
43
+ expect(persistSessionGoal).toHaveBeenCalledWith('ses_123', '/repo/app', goal);
44
+ // v2 has no session-metadata route; nothing may be sent to OpenCode.
45
+ expect(fetchMock).not.toHaveBeenCalled();
46
46
  } finally {
47
47
  globalThis.fetch = originalFetch;
48
48
  }
49
49
  });
50
50
 
51
- it('falls back to inline metadata when objective storage fails', async () => {
51
+ it('falls back to an inline objective when the objective file cannot be written', async () => {
52
52
  writeObjectiveMock.mockRejectedValueOnce(new Error('disk unavailable'));
53
- const fetchMock = vi.fn(async () => ({ ok: true }));
54
- const originalFetch = globalThis.fetch;
55
- globalThis.fetch = fetchMock;
56
- try {
57
- await createSessionGoal({
58
- baseUrl: 'http://opencode.test',
59
- authHeaders: {},
60
- sessionID: 'ses_123',
61
- directory: '/repo/app',
62
- objective: 'Finish the migration',
63
- onWarning: vi.fn(),
64
- });
53
+ const persistSessionGoal = vi.fn(async () => undefined);
65
54
 
66
- const payload = JSON.parse(fetchMock.mock.calls[0][1].body);
67
- expect(payload.metadata.openchamber.goal).toMatchObject({
68
- objective: 'Finish the migration',
69
- objectiveFile: false,
70
- });
71
- } finally {
72
- globalThis.fetch = originalFetch;
73
- }
55
+ await createSessionGoal({
56
+ sessionID: 'ses_123',
57
+ directory: '/repo/app',
58
+ objective: 'Finish the migration',
59
+ onWarning: vi.fn(),
60
+ persistSessionGoal,
61
+ });
62
+
63
+ const [, , goal] = persistSessionGoal.mock.calls[0];
64
+ expect(goal).toMatchObject({ objective: 'Finish the migration', objectiveFile: false });
65
+ });
66
+
67
+ it('refuses to report a goal it has nowhere to save', async () => {
68
+ await expect(createSessionGoal({
69
+ sessionID: 'ses_123',
70
+ directory: '/repo/app',
71
+ objective: 'Finish the migration',
72
+ })).rejects.toThrow(/needs a session metadata store/);
74
73
  });
75
74
 
76
75
  it('builds the same goal intro with an optional budget', () => {