@vantran-se/openchamber-web 2.1.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (442) hide show
  1. package/README.md +32 -24
  2. package/bin/cli.js +1 -3
  3. package/bin/cli.test.js +40 -16
  4. package/bin/lib/DOCUMENTATION.md +6 -5
  5. package/bin/lib/cli-args.js +6 -1
  6. package/bin/lib/cli-args.test.js +34 -0
  7. package/bin/lib/cli-lifecycle.js +0 -1
  8. package/bin/lib/cli-network.js +8 -2
  9. package/bin/lib/cli-process.js +1 -1
  10. package/bin/lib/cli-settings-accessors.test.js +21 -13
  11. package/bin/lib/cli-startup.js +17 -11
  12. package/bin/lib/cli-startup.test.js +92 -2
  13. package/bin/lib/commands-connect-url.js +8 -1
  14. package/bin/lib/commands-schedule.js +8 -2
  15. package/bin/lib/commands-serve.js +15 -21
  16. package/bin/lib/commands-update.js +12 -24
  17. package/bin/lib/commands-update.test.js +14 -46
  18. package/dist/assets/DiffView-Cv9MSXl1.js +4 -0
  19. package/dist/assets/FilePreviewCommentMenu-D4kLseHj.js +4 -0
  20. package/dist/assets/FilesView-BVoD8I2x.js +14 -0
  21. package/dist/assets/{GitView-DzuzhIPx.js → GitView-Bzb8p-ew.js} +4 -4
  22. package/dist/assets/GuestSurfacePane-Crdbvsxo.js +1 -0
  23. package/dist/assets/InProgressOperationBanner-CwMjNQgk.js +2 -0
  24. package/dist/assets/{JsonTreeView-B_EPRjzy.js → JsonTreeView-DRKHZfsn.js} +1 -1
  25. package/dist/assets/MarkdownImageGallery-DZtqT1Zn.js +1 -0
  26. package/dist/assets/MarkdownRendererImpl-CqG7HCNd.js +10 -0
  27. package/dist/assets/OnboardingScreen-DMIxwYK5.js +1 -0
  28. package/dist/assets/{PatchDiff-6yh1csza.js → PatchDiff-CUcCg1jk.js} +1 -1
  29. package/dist/assets/PierreDiffViewer-CnnR-dnD.js +228 -0
  30. package/dist/assets/PlanView-RCBadcY7.js +5 -0
  31. package/dist/assets/SettingsView-CjBjZFRO.js +30 -0
  32. package/dist/assets/SettingsWindow-DyM7QuhI.js +1 -0
  33. package/dist/assets/SubmoduleDiffSummary-BWIfgMED.js +1 -0
  34. package/dist/assets/ToolOutputDialog-Q0P3FuAi.js +15 -0
  35. package/dist/assets/ToolPartDiffPreview-p7qR9FpU.js +8 -0
  36. package/dist/assets/{VirtualizedFileDiff-BTJVRkyo.js → VirtualizedFileDiff-CFpjL8Ns.js} +3 -3
  37. package/dist/assets/{Virtualizer-xCEYz34p.js → Virtualizer-C0IsclZn.js} +1 -1
  38. package/dist/assets/WalkthroughView-DI31zD02.js +4 -0
  39. package/dist/assets/appThemeRegistry-B9gkFq9i.js +1 -0
  40. package/dist/assets/{computeVirtualFileMetrics-CmPe29DM.js → computeVirtualFileMetrics-C2WM6JC2.js} +2 -2
  41. package/dist/assets/de-YKfk5MmC.js +14 -0
  42. package/dist/assets/es-_gwT0kr-.js +20 -0
  43. package/dist/assets/fileTreeStatus-D438yi8M.js +1 -0
  44. package/dist/assets/fr-DqzvXmIm.js +18 -0
  45. package/dist/assets/index-BLaHKdtx.js +1 -0
  46. package/dist/assets/index-BkMXRrVQ.css +1 -0
  47. package/dist/assets/index-DxNa7xYB.js +65 -0
  48. package/dist/assets/initializeSharedPreferences-Bck5EAWP.js +1 -0
  49. package/dist/assets/{isDiffPlainText-CQmDCnEv.js → isDiffPlainText-Dgbw8VKk.js} +1 -1
  50. package/dist/assets/{isFilePlainText-CYBUzW4I.js → isFilePlainText-_ZS5UYRj.js} +1 -1
  51. package/dist/assets/ja-B-GFYhQE.js +20 -0
  52. package/dist/assets/ko-mzksfqDC.js +20 -0
  53. package/dist/assets/main-C5Xx8dn_.js +2 -0
  54. package/dist/assets/main-CfBSQM1H.js +990 -0
  55. package/dist/assets/markdownCore-BmWwW1ll.js +7 -0
  56. package/dist/assets/miniChat-mnKwy4vQ.js +2 -0
  57. package/dist/assets/mobile-DeEQd0wZ.js +2 -0
  58. package/dist/assets/nl-CWQr6NSq.js +20 -0
  59. package/dist/assets/{parseDiffFromFile-Cy0PNxun.js → parseDiffFromFile-BUgn-T9p.js} +2 -2
  60. package/dist/assets/patchFileDiff-Dt8LhgBh.js +13 -0
  61. package/dist/assets/pl-UmRcDUX2.js +20 -0
  62. package/dist/assets/pt-BR-BSLybvVF.js +20 -0
  63. package/dist/assets/relocateRestoredSession-B5E9xb3T.js +1 -0
  64. package/dist/assets/renderElectronMiniChatApp-C0a9A5_J.js +2 -0
  65. package/dist/assets/renderMobileApp-BNncg3Pk.js +25 -0
  66. package/dist/assets/runtimeEndpointReset-CZ5aafpl.js +62 -0
  67. package/dist/assets/{selectionMarkdown-TkYk5h9l.js → selectionMarkdown-CUN3wUld.js} +9 -9
  68. package/dist/assets/{sessionOwnership-Bs1xbRfB.js → sessionOwnership-BSuxCEhb.js} +1 -1
  69. package/dist/assets/shikiHighlight-DHDx-6eM.js +1 -0
  70. package/dist/assets/space-apply-DM_2pRW7.js +1 -0
  71. package/dist/assets/space-close-Bpomt8z0.js +1 -0
  72. package/dist/assets/{surface-B6n7wJtt.js → surface-C-jQaGnL.js} +1 -1
  73. package/dist/assets/{textMateThemeFromAppTheme-CDlKffWz.js → textMateThemeFromAppTheme-DlsTphbY.js} +1 -1
  74. package/dist/assets/tr-gaTrX_HK.js +11 -0
  75. package/dist/assets/uk-CnJF6ykl.js +20 -0
  76. package/dist/assets/{useAppFontEffects-DCqCVGlT.js → useAppFontEffects-DGlvOqP_.js} +194 -233
  77. package/dist/assets/useInlineCommentController-kMN4Wg_D.js +2 -0
  78. package/dist/assets/{useRootScrollLock-BowwS8kq.js → useRootScrollLock-DMPpvpNP.js} +2 -2
  79. package/dist/assets/{vendor-base-ui-DX96QhTP.js → vendor-base-ui-Det7U81h.js} +1 -1
  80. package/dist/assets/{vendor-codemirror-autocomplete-DW1FQz9_.js → vendor-codemirror-autocomplete-BSHQHbKu.js} +1 -1
  81. package/dist/assets/{vendor-codemirror-commands-DpmhalwN.js → vendor-codemirror-commands-C4G_dPWe.js} +1 -1
  82. package/dist/assets/{vendor-codemirror-lang-angular-BqExbN1M.js → vendor-codemirror-lang-angular-B6LbBGHB.js} +1 -1
  83. package/dist/assets/{vendor-codemirror-lang-cpp-DZA36HfS.js → vendor-codemirror-lang-cpp-BWWXBx7W.js} +1 -1
  84. package/dist/assets/{vendor-codemirror-lang-css-B07blnnZ.js → vendor-codemirror-lang-css-Cayfb29q.js} +1 -1
  85. package/dist/assets/{vendor-codemirror-lang-go-BM8K9D1-.js → vendor-codemirror-lang-go-BoKlCNKw.js} +1 -1
  86. package/dist/assets/{vendor-codemirror-lang-html-DaH9adfE.js → vendor-codemirror-lang-html-BQWerHsJ.js} +1 -1
  87. package/dist/assets/{vendor-codemirror-lang-java-CsdC0viz.js → vendor-codemirror-lang-java-DWX7Fq_K.js} +1 -1
  88. package/dist/assets/{vendor-codemirror-lang-javascript-CpGYXGuo.js → vendor-codemirror-lang-javascript-DbpEcZM0.js} +2 -2
  89. package/dist/assets/{vendor-codemirror-lang-jinja-BBeGzXw5.js → vendor-codemirror-lang-jinja-D1JIJRA7.js} +1 -1
  90. package/dist/assets/{vendor-codemirror-lang-json-58T70Hqs.js → vendor-codemirror-lang-json-BzgckzmB.js} +1 -1
  91. package/dist/assets/{vendor-codemirror-lang-less-BI-xCA59.js → vendor-codemirror-lang-less-B9EpDXFi.js} +1 -1
  92. package/dist/assets/{vendor-codemirror-lang-liquid-zEcMa-zy.js → vendor-codemirror-lang-liquid-9U7kRbSf.js} +1 -1
  93. package/dist/assets/{vendor-codemirror-lang-markdown-BLKnlLP4.js → vendor-codemirror-lang-markdown-D2TEBIvZ.js} +1 -1
  94. package/dist/assets/{vendor-codemirror-lang-php-9f35M83-.js → vendor-codemirror-lang-php-BT6sDVzE.js} +1 -1
  95. package/dist/assets/vendor-codemirror-lang-python-jAMtYiA9.js +3 -0
  96. package/dist/assets/{vendor-codemirror-lang-rust-MNDL_jZQ.js → vendor-codemirror-lang-rust-CVO56EiL.js} +1 -1
  97. package/dist/assets/{vendor-codemirror-lang-sass-p9epsSNm.js → vendor-codemirror-lang-sass-DZlOoUWu.js} +1 -1
  98. package/dist/assets/{vendor-codemirror-lang-sql-Bqzh0EXX.js → vendor-codemirror-lang-sql-fZ6t1DPw.js} +1 -1
  99. package/dist/assets/{vendor-codemirror-lang-vue-BnNUWV-p.js → vendor-codemirror-lang-vue-DxbqrLiE.js} +1 -1
  100. package/dist/assets/{vendor-codemirror-lang-wast-C2a-oLmk.js → vendor-codemirror-lang-wast-DyU1Gt-I.js} +1 -1
  101. package/dist/assets/vendor-codemirror-lang-xml-C9a_K5_p.js +1 -0
  102. package/dist/assets/{vendor-codemirror-lang-yaml-C0sjswB0.js → vendor-codemirror-lang-yaml-Cptq8l0e.js} +1 -1
  103. package/dist/assets/{vendor-codemirror-language-ByBn5zIW.js → vendor-codemirror-language-DufXoQvE.js} +2 -2
  104. package/dist/assets/vendor-codemirror-language-data-CtwSWKtX.js +2 -0
  105. package/dist/assets/{vendor-codemirror-search-ib-sHD5W.js → vendor-codemirror-search-CvXM_pZg.js} +1 -1
  106. package/dist/assets/{vendor-codemirror-state-G3UuR-be.js → vendor-codemirror-state-Dek9sMtw.js} +1 -1
  107. package/dist/assets/{vendor-codemirror-view-Bniqp7p5.js → vendor-codemirror-view-1e0FQ6Wn.js} +1 -1
  108. package/dist/assets/vendor-framer-motion-DxDVhOLc.js +9 -0
  109. package/dist/assets/vendor-katex-CuQvj9-L.js +257 -0
  110. package/dist/assets/vendor-katex-DdZNUVNG.css +1 -0
  111. package/dist/assets/{vendor-motion-dom-DcCgeG69.js → vendor-motion-dom-HXPOMxb7.js} +1 -1
  112. package/dist/assets/{vendor-opencode-client-DHzhmLwV.js → vendor-opencode-client-BmFcUkbQ.js} +1 -1
  113. package/dist/assets/{vendor-replit-codemirror-vim-DkzqxN2H.js → vendor-replit-codemirror-vim-BlJRn-cW.js} +2 -2
  114. package/dist/assets/{vendor-tanstack-react-virtual-H_JmXOFV.js → vendor-tanstack-react-virtual-CvuLsk7D.js} +1 -1
  115. package/dist/assets/{vendor-tanstack-virtual-core-Dxc8hSTB.js → vendor-tanstack-virtual-core-DPaNcJcT.js} +1 -1
  116. package/dist/assets/zh-CN-t2vigSB1.js +20 -0
  117. package/dist/assets/zh-TW-jAgqBokB.js +20 -0
  118. package/dist/index.html +6 -6
  119. package/dist/mini-chat.html +6 -6
  120. package/dist/mobile.html +6 -6
  121. package/package.json +8 -5
  122. package/server/index.js +80 -36
  123. package/server/lib/agent-memory/actions.js +1 -6
  124. package/server/lib/agent-memory/routes.js +1 -2
  125. package/server/lib/agent-memory/runtime.js +1 -8
  126. package/server/lib/agent-tool/DOCUMENTATION.md +7 -3
  127. package/server/lib/agent-tool/runtime.js +3 -6
  128. package/server/lib/agent-tool/runtime.test.js +0 -2
  129. package/server/lib/browser-control/DOCUMENTATION.md +6 -0
  130. package/server/lib/changelog/update-notes.js +1 -1
  131. package/server/lib/context-obligatory/runtime.js +1 -2
  132. package/server/lib/dev-tunnel/DOCUMENTATION.md +8 -0
  133. package/server/lib/dev-tunnel/client.js +21 -10
  134. package/server/lib/dev-tunnel/tunnel.test.js +34 -0
  135. package/server/lib/dispatch-results/runtime.js +1 -1
  136. package/server/lib/enterprise-mode.d.ts +1 -0
  137. package/server/lib/enterprise-mode.js +13 -2
  138. package/server/lib/enterprise-mode.test.js +21 -0
  139. package/server/lib/event-stream/DOCUMENTATION.md +2 -2
  140. package/server/lib/event-stream/index.js +1 -3
  141. package/server/lib/event-stream/translate-v2.js +1 -9
  142. package/server/lib/event-stream/translate-v2.test.js +0 -11
  143. package/server/lib/fs/DOCUMENTATION.md +5 -2
  144. package/server/lib/fs/routes.js +62 -15
  145. package/server/lib/fs/routes.test.js +278 -48
  146. package/server/lib/fs/workspace-file-names.js +104 -0
  147. package/server/lib/fs/workspace-file-names.test.js +62 -0
  148. package/server/lib/git/DOCUMENTATION.md +17 -5
  149. package/server/lib/git/contributor-provenance-storage.js +9 -6
  150. package/server/lib/git/contributor-provenance-storage.test.js +29 -0
  151. package/server/lib/git/credential-broker.js +1 -1
  152. package/server/lib/git/credential-resolver.js +1 -4
  153. package/server/lib/git/credentials.js +1 -0
  154. package/server/lib/git/discovery-endpoint.js +1 -1
  155. package/server/lib/git/identity-provisioning.js +2 -1
  156. package/server/lib/git/identity-storage.js +3 -4
  157. package/server/lib/git/lfs-discovery.js +14 -14
  158. package/server/lib/git/lfs-discovery.test.js +16 -6
  159. package/server/lib/git/network-operation-plan.js +50 -62
  160. package/server/lib/git/network-operation-plan.test.js +103 -1
  161. package/server/lib/git/network-operation-registry.js +4 -6
  162. package/server/lib/git/network-operation-storage.js +4 -6
  163. package/server/lib/git/network-operations.js +12 -4
  164. package/server/lib/git/network-operations.test.js +19 -6
  165. package/server/lib/git/redaction.js +2 -1
  166. package/server/lib/git/repository-credential-runtime.js +5 -3
  167. package/server/lib/git/repository-credential-runtime.test.js +22 -0
  168. package/server/lib/git/routes.js +31 -137
  169. package/server/lib/git/routes.test.js +2 -2
  170. package/server/lib/git/service.js +143 -27
  171. package/server/lib/git/service.test.js +215 -1
  172. package/server/lib/git/ssh-credential-storage.js +1 -4
  173. package/server/lib/git/submodule-discovery.js +1 -1
  174. package/server/lib/git/worktree-bootstrap-storage.js +3 -4
  175. package/server/lib/git/worktree-bootstrap-storage.test.js +15 -0
  176. package/server/lib/git/worktree-creation.js +194 -0
  177. package/server/lib/git/worktree-creation.test.js +155 -0
  178. package/server/lib/github/DOCUMENTATION.md +11 -6
  179. package/server/lib/github/auth.js +2 -4
  180. package/server/lib/github/device-flow.js +2 -2
  181. package/server/lib/github/gh-cli-credential.js +1 -0
  182. package/server/lib/github/octokit.js +31 -0
  183. package/server/lib/github/pr-status.js +35 -18
  184. package/server/lib/github/pr-status.test.js +271 -3
  185. package/server/lib/github/reference-search.js +102 -13
  186. package/server/lib/github/reference-search.test.js +48 -7
  187. package/server/lib/github/routes.auth.test.js +18 -0
  188. package/server/lib/github/routes.js +360 -6
  189. package/server/lib/github/routes.mutations.test.js +239 -0
  190. package/server/lib/github/routes.test.js +134 -2
  191. package/server/lib/gitlab/DOCUMENTATION.md +5 -1
  192. package/server/lib/gitlab/auth-storage.test.js +1 -0
  193. package/server/lib/gitlab/mappers.js +30 -1
  194. package/server/lib/gitlab/resources.js +230 -6
  195. package/server/lib/gitlab/resources.test.js +179 -0
  196. package/server/lib/gitlab/routes.js +200 -6
  197. package/server/lib/gitlab/routes.test.js +186 -2
  198. package/server/lib/gitlab/validation.js +1 -5
  199. package/server/lib/guests/DOCUMENTATION.md +1 -1
  200. package/server/lib/guests/oauth.js +2 -2
  201. package/server/lib/guests/service.js +1 -1
  202. package/server/lib/guests/sockets.js +1 -1
  203. package/server/lib/injected-env.d.ts +8 -0
  204. package/server/lib/injected-env.js +50 -0
  205. package/server/lib/injected-env.test.js +56 -0
  206. package/server/lib/linear/DOCUMENTATION.md +5 -6
  207. package/server/lib/linear/issues.js +62 -4
  208. package/server/lib/linear/issues.test.js +68 -0
  209. package/server/lib/linear/oauth.js +5 -5
  210. package/server/lib/linear/parse.js +2 -9
  211. package/server/lib/message-queue/DOCUMENTATION.md +23 -1
  212. package/server/lib/message-queue/runtime.js +85 -12
  213. package/server/lib/message-queue/runtime.test.js +172 -2
  214. package/server/lib/notifications/apns-runtime.js +5 -6
  215. package/server/lib/notifications/apns-runtime.test.js +20 -4
  216. package/server/lib/openchamber-control/DOCUMENTATION.md +19 -1
  217. package/server/lib/openchamber-control/actions.js +2 -1
  218. package/server/lib/openchamber-control/service.js +119 -20
  219. package/server/lib/openchamber-control/service.test.js +166 -0
  220. package/server/lib/openchamber-sessions/archive-store.js +1 -6
  221. package/server/lib/openchamber-sessions/routes.js +47 -12
  222. package/server/lib/openchamber-sessions/routes.test.js +65 -2
  223. package/server/lib/openchamber-sessions/session-metadata-store.js +1 -6
  224. package/server/lib/opencode/DOCUMENTATION.md +22 -8
  225. package/server/lib/opencode/agents.js +62 -0
  226. package/server/lib/opencode/auth-state-runtime.js +3 -1
  227. package/server/lib/opencode/auth.d.ts +1 -1
  228. package/server/lib/opencode/auth.js +8 -0
  229. package/server/lib/opencode/auth.test.js +31 -0
  230. package/server/lib/opencode/config-entity-routes.js +16 -0
  231. package/server/lib/opencode/config-entity-v2.test.js +16 -1
  232. package/server/lib/opencode/config-paths.test.js +2 -2
  233. package/server/lib/opencode/config-v2.js +2 -4
  234. package/server/lib/opencode/feature-routes-runtime.js +89 -49
  235. package/server/lib/opencode/lifecycle.js +41 -21
  236. package/server/lib/opencode/lifecycle.test.js +50 -2
  237. package/server/lib/opencode/model-discovery.js +1 -1
  238. package/server/lib/opencode/model-discovery.test.js +17 -0
  239. package/server/lib/opencode/models-metadata.js +0 -2
  240. package/server/lib/opencode/openchamber-routes.test.js +3 -3
  241. package/server/lib/opencode/plugins.js +6 -9
  242. package/server/lib/opencode/provider-source-route.test.js +101 -3
  243. package/server/lib/opencode/providers-disabled.test.js +59 -0
  244. package/server/lib/opencode/providers.js +34 -0
  245. package/server/lib/opencode/proxy.js +11 -3
  246. package/server/lib/opencode/routes.js +40 -2
  247. package/server/lib/opencode/settings-helpers.js +27 -1
  248. package/server/lib/opencode/settings-helpers.test.js +19 -7
  249. package/server/lib/opencode/settings-registry.json +27 -16
  250. package/server/lib/opencode/settings-runtime.test.js +18 -0
  251. package/server/lib/opencode/shared-service-runtime.js +23 -104
  252. package/server/lib/opencode/shared.js +77 -46
  253. package/server/lib/opencode/shared.test.js +12 -12
  254. package/server/lib/opencode/shutdown-runtime.test.js +2 -3
  255. package/server/lib/opencode/skill-routes.js +123 -38
  256. package/server/lib/opencode/skill-routes.test.js +201 -0
  257. package/server/lib/opencode/skills.js +81 -34
  258. package/server/lib/opencode/skills.test.js +176 -21
  259. package/server/lib/opencode/startup-pipeline-runtime.js +6 -3
  260. package/server/lib/opencode/startup-pipeline-runtime.test.js +2 -1
  261. package/server/lib/opencode/watcher.test.js +0 -54
  262. package/server/lib/outbound-proxy.js +52 -0
  263. package/server/lib/outbound-proxy.test.js +55 -0
  264. package/server/lib/package-manager.js +23 -17
  265. package/server/lib/package-manager.test.js +5 -18
  266. package/server/lib/permission-auto-accept/DOCUMENTATION.md +1 -1
  267. package/server/lib/permission-auto-accept/runtime.js +19 -47
  268. package/server/lib/permission-auto-accept/runtime.test.js +2 -2
  269. package/server/lib/project-context/DOCUMENTATION.md +7 -1
  270. package/server/lib/project-context/routes.js +1 -2
  271. package/server/lib/project-context/runtime.js +12 -14
  272. package/server/lib/project-context/runtime.test.js +78 -2
  273. package/server/lib/projects/project-config.js +30 -15
  274. package/server/lib/projects/project-config.test.js +30 -0
  275. package/server/lib/projects/project-setup.js +1 -2
  276. package/server/lib/projects/routes.js +1 -2
  277. package/server/lib/quota/DOCUMENTATION.md +35 -7
  278. package/server/lib/quota/index.js +1 -0
  279. package/server/lib/quota/providers/claude/index.js +1 -1
  280. package/server/lib/quota/providers/cline-pass.js +1 -1
  281. package/server/lib/quota/providers/exe-dev.js +1 -1
  282. package/server/lib/quota/providers/google/index.js +1 -1
  283. package/server/lib/quota/providers/hyper.js +1 -1
  284. package/server/lib/quota/providers/kimi.js +116 -36
  285. package/server/lib/quota/providers/kimi.test.js +128 -3
  286. package/server/lib/quota/providers/opencode-go.js +144 -21
  287. package/server/lib/quota/providers/opencode-go.test.js +229 -6
  288. package/server/lib/quota/providers/openrouter.js +1 -1
  289. package/server/lib/quota/providers/zenmux.js +1 -1
  290. package/server/lib/quota/utils/auth.js +1 -0
  291. package/server/lib/quota/utils/transformers.js +2 -6
  292. package/server/lib/relay/DOCUMENTATION.md +18 -3
  293. package/server/lib/relay/e2ee.js +7 -7
  294. package/server/lib/relay/host-client.js +84 -13
  295. package/server/lib/relay/host-client.test.js +201 -1
  296. package/server/lib/relay/identity.js +6 -29
  297. package/server/lib/relay/identity.test.js +18 -28
  298. package/server/lib/relay/key-store.js +141 -0
  299. package/server/lib/relay/key-store.test.js +138 -0
  300. package/server/lib/relay/service.js +8 -8
  301. package/server/lib/relay/service.test.js +10 -5
  302. package/server/lib/relay/signing-key.js +13 -36
  303. package/server/lib/relay/tunnel-codec.js +10 -10
  304. package/server/lib/route-module-exports.test.js +22 -0
  305. package/server/lib/routing/DOCUMENTATION.md +23 -3
  306. package/server/lib/routing/cache.js +87 -0
  307. package/server/lib/routing/defaults.js +11 -0
  308. package/server/lib/routing/history.js +12 -4
  309. package/server/lib/routing/runtime.js +39 -3
  310. package/server/lib/routing/runtime.test.js +81 -2
  311. package/server/lib/routing/store.js +12 -1
  312. package/server/lib/routing/store.test.js +13 -0
  313. package/server/lib/scheduled-tasks/DOCUMENTATION.md +59 -1
  314. package/server/lib/scheduled-tasks/existing-session.js +119 -0
  315. package/server/lib/scheduled-tasks/existing-session.test.js +350 -0
  316. package/server/lib/scheduled-tasks/loops.js +1 -8
  317. package/server/lib/scheduled-tasks/routes.js +8 -8
  318. package/server/lib/scheduled-tasks/runtime.js +103 -13
  319. package/server/lib/scheduled-tasks/runtime.test.js +48 -0
  320. package/server/lib/scheduled-tasks/service.js +33 -11
  321. package/server/lib/scheduled-tasks/service.test.js +128 -2
  322. package/server/lib/scheduled-tasks/session-defaults.js +27 -0
  323. package/server/lib/scheduled-tasks/session-defaults.test.js +19 -0
  324. package/server/lib/session-assist/context.js +1 -0
  325. package/server/lib/session-folders/routes.js +2 -2
  326. package/server/lib/session-goal/objectives.js +1 -1
  327. package/server/lib/session-knowledge/routes.js +2 -2
  328. package/server/lib/session-knowledge/runtime.js +1 -2
  329. package/server/lib/session-work/DOCUMENTATION.md +39 -14
  330. package/server/lib/session-work/questions.js +31 -2
  331. package/server/lib/session-work/runtime.js +22 -13
  332. package/server/lib/session-work/runtime.test.js +43 -26
  333. package/server/lib/session-work/state.js +10 -4
  334. package/server/lib/session-work/state.test.js +8 -1
  335. package/server/lib/shared/DOCUMENTATION.md +89 -0
  336. package/server/lib/shared/guards.js +53 -0
  337. package/server/lib/shared/guards.test.js +112 -0
  338. package/server/lib/skills-catalog/DOCUMENTATION.md +10 -8
  339. package/server/lib/skills-catalog/cache.js +3 -2
  340. package/server/lib/skills-catalog/curated-sources.js +5 -1
  341. package/server/lib/skills-catalog/git.js +107 -20
  342. package/server/lib/skills-catalog/git.test.js +78 -0
  343. package/server/lib/skills-catalog/install.js +21 -12
  344. package/server/lib/skills-catalog/scan.js +25 -15
  345. package/server/lib/skills-catalog/source.js +26 -1
  346. package/server/lib/skills-catalog/source.test.js +27 -0
  347. package/server/lib/source-control/DOCUMENTATION.md +2 -1
  348. package/server/lib/source-control/audit-storage.js +7 -6
  349. package/server/lib/source-control/audit-storage.test.js +11 -0
  350. package/server/lib/source-control/binding-service.js +2 -5
  351. package/server/lib/source-control/change-request-worktree.js +90 -0
  352. package/server/lib/source-control/change-request-worktree.test.js +121 -0
  353. package/server/lib/source-control/mutation-executor.js +1 -1
  354. package/server/lib/source-control/mutation-storage.js +11 -5
  355. package/server/lib/source-control/provider-instance.js +1 -2
  356. package/server/lib/source-control/repository-identity.js +1 -1
  357. package/server/lib/source-control/routes.js +14 -2
  358. package/server/lib/spaces/DOCUMENTATION.md +42 -17
  359. package/server/lib/spaces/command-stream.test.js +15 -0
  360. package/server/lib/spaces/dispatcher.js +7 -0
  361. package/server/lib/spaces/dispatcher.test.js +17 -0
  362. package/server/lib/spaces/hardening.js +2 -2
  363. package/server/lib/spaces/hardening.test.js +1 -1
  364. package/server/lib/spaces/host.js +2 -0
  365. package/server/lib/spaces/journey.js +66 -5
  366. package/server/lib/spaces/journey.test.js +95 -4
  367. package/server/lib/spaces/layout.js +1 -1
  368. package/server/lib/spaces/places/docker-disk.js +38 -14
  369. package/server/lib/spaces/places/docker-disk.test.js +80 -1
  370. package/server/lib/spaces/places/docker.js +35 -11
  371. package/server/lib/spaces/places/fake-docker.js +22 -3
  372. package/server/lib/spaces/places/memory-place.js +3 -0
  373. package/server/lib/spaces/places/packed.docker.live.test.js +1 -1
  374. package/server/lib/spaces/routes.js +5 -0
  375. package/server/lib/spaces/routes.test.js +4 -2
  376. package/server/lib/spaces/run-command.js +9 -2
  377. package/server/lib/spaces/tools.js +4 -5
  378. package/server/lib/spaces/tools.test.js +2 -2
  379. package/server/lib/terminal/runtime.js +18 -6
  380. package/server/lib/terminal/runtime.test.js +66 -0
  381. package/server/lib/tts/index.js +3 -0
  382. package/server/lib/tts/service.js +4 -2
  383. package/server/lib/walkthrough/DOCUMENTATION.md +3 -1
  384. package/server/lib/walkthrough/index.js +2 -3
  385. package/server/lib/walkthrough/pr-source.test.js +22 -0
  386. package/server/lib/walkthrough/sources.js +6 -1
  387. package/server/opencode-proxy.test.js +127 -0
  388. package/bin/lib/commands-startup.test.js +0 -15
  389. package/dist/assets/CodeMirrorCommentWidgets-CVDSjcrP.js +0 -1
  390. package/dist/assets/DiffView-Ct7vL3fD.js +0 -4
  391. package/dist/assets/FilesView-DT0uEYgT.js +0 -17
  392. package/dist/assets/GuestSurfacePane-DVvk1VPu.js +0 -1
  393. package/dist/assets/LinearIssuesView-C10MwA9h.js +0 -1
  394. package/dist/assets/MarkdownImageGallery-BbN_K_3d.js +0 -1
  395. package/dist/assets/MarkdownRendererImpl-BLt2Tkk9.js +0 -10
  396. package/dist/assets/OnboardingScreen-Bgp5A3WB.js +0 -1
  397. package/dist/assets/PierreDiffViewer-CWJLj-S-.js +0 -210
  398. package/dist/assets/PlanView-DsRWnsOI.js +0 -5
  399. package/dist/assets/PublishDialog-CCeKloRu.js +0 -2
  400. package/dist/assets/SettingsView-BuPPsNPp.js +0 -30
  401. package/dist/assets/SettingsWindow-CM5fL8Ot.js +0 -1
  402. package/dist/assets/SubmoduleDiffSummary-CfsMcfiQ.js +0 -1
  403. package/dist/assets/ToolOutputDialog-mZrfSofz.js +0 -15
  404. package/dist/assets/ToolPartDiffPreview-BUGCRYEL.js +0 -8
  405. package/dist/assets/WalkthroughView-ClBo7PPd.js +0 -4
  406. package/dist/assets/appThemeRegistry-CxzMxR-H.js +0 -1
  407. package/dist/assets/de-CBla4hjd.js +0 -14
  408. package/dist/assets/es-BmnZtxTD.js +0 -20
  409. package/dist/assets/fileTreeStatus-BSHFIGEg.js +0 -1
  410. package/dist/assets/fr-OVKIBUZO.js +0 -18
  411. package/dist/assets/index-CKCFKmjF.js +0 -1
  412. package/dist/assets/index-Dbx4V4GN.css +0 -1
  413. package/dist/assets/index-vQ1Pg1cr.js +0 -61
  414. package/dist/assets/ja-6v-yr-gE.js +0 -20
  415. package/dist/assets/ko-I_ZLvgQ9.js +0 -20
  416. package/dist/assets/main-CmO4dRtR.js +0 -2
  417. package/dist/assets/main-b9vqhlYM.js +0 -999
  418. package/dist/assets/markdownCore-BPEZRQfP.js +0 -7
  419. package/dist/assets/miniChat-B5Oi7B-6.js +0 -2
  420. package/dist/assets/mobile-CVtShif9.js +0 -2
  421. package/dist/assets/nl-DcvIJf46.js +0 -20
  422. package/dist/assets/patchFileDiff-CW3wCH1O.js +0 -13
  423. package/dist/assets/pl-JxCUl0sd.js +0 -20
  424. package/dist/assets/pt-BR-L89mjja2.js +0 -20
  425. package/dist/assets/relocateRestoredSession-DYh_9oBa.js +0 -1
  426. package/dist/assets/renderElectronMiniChatApp-BD-1JHSg.js +0 -2
  427. package/dist/assets/renderMobileApp-7s5InXt1.js +0 -25
  428. package/dist/assets/runtimeEndpointReset-DqLJC6L1.js +0 -53
  429. package/dist/assets/shikiHighlight-CXgZnvHh.js +0 -1
  430. package/dist/assets/space-apply-1trGVIl8.js +0 -1
  431. package/dist/assets/space-close-y7j-NuBv.js +0 -1
  432. package/dist/assets/tr-ka5J1flt.js +0 -11
  433. package/dist/assets/uk-PyGe6Q_u.js +0 -20
  434. package/dist/assets/useInlineCommentController-CrjSGCpi.js +0 -2
  435. package/dist/assets/vendor-codemirror-lang-python-B7ClrMIT.js +0 -3
  436. package/dist/assets/vendor-codemirror-lang-xml-C8tGnS64.js +0 -1
  437. package/dist/assets/vendor-codemirror-language-data-q2bKO1EM.js +0 -2
  438. package/dist/assets/vendor-framer-motion-Bq3iTZ6r.js +0 -1
  439. package/dist/assets/vendor-katex-CHD9BdDO.js +0 -257
  440. package/dist/assets/vendor-katex-Cp579UoJ.css +0 -1
  441. package/dist/assets/zh-CN-B0P2tybJ.js +0 -20
  442. package/dist/assets/zh-TW-Bs9C5cN8.js +0 -20
@@ -42,6 +42,7 @@ const createOpenCode = () => {
42
42
  sent: [],
43
43
  switched: [],
44
44
  failNext: null,
45
+ failStatus: 500,
45
46
  htmlNext: null,
46
47
  };
47
48
  // v2 wraps `/api/*` payloads in `{ location, data }`; a message page is
@@ -52,7 +53,7 @@ const createOpenCode = () => {
52
53
  const method = init.method ?? 'GET';
53
54
  if (state.failNext && state.failNext.test(pathname)) {
54
55
  state.failNext = null;
55
- return new Response('boom', { status: 500 });
56
+ return new Response('boom', { status: state.failStatus });
56
57
  }
57
58
  if (method === 'POST' && state.htmlNext?.test(pathname)) {
58
59
  state.htmlNext = null;
@@ -80,7 +81,7 @@ const createOpenCode = () => {
80
81
  return { state, fetchImpl };
81
82
  };
82
83
 
83
- const createRuntime = ({ dataDir = makeDataDir(), openCode = createOpenCode(), knowledge = null, retryDelayMs, resolveAutoSelection, now } = {}) => {
84
+ const createRuntime = ({ dataDir = makeDataDir(), openCode = createOpenCode(), knowledge = null, retryDelayMs, resolveAutoSelection, now, beforeScheduledTaskSend, onScheduledTaskResult } = {}) => {
84
85
  let eventHandler = () => {};
85
86
  let statusHandler = () => {};
86
87
  const broadcasts = [];
@@ -99,6 +100,8 @@ const createRuntime = ({ dataDir = makeDataDir(), openCode = createOpenCode(), k
99
100
  fetchImpl: openCode.fetchImpl,
100
101
  dispatchQuietMs: 0,
101
102
  abortHoldMs: 50,
103
+ beforeScheduledTaskSend,
104
+ onScheduledTaskResult,
102
105
  };
103
106
  if (retryDelayMs) options.retryDelayMs = retryDelayMs;
104
107
  if (resolveAutoSelection) options.resolveAutoSelection = resolveAutoSelection;
@@ -120,6 +123,173 @@ const settle = async (ms = 30) => {
120
123
  await new Promise((resolve) => setTimeout(resolve, ms));
121
124
  };
122
125
 
126
+ describe('scheduled task items', () => {
127
+ const scheduled = (projectId = 'project-1', overrides = {}) => item({
128
+ scheduledTask: { projectId, taskId: 'task-1' }, sendConfig: {}, ...overrides,
129
+ });
130
+
131
+ it('deduplicates project and task across sessions, but not across projects', async () => {
132
+ const { runtime, openCode } = createRuntime();
133
+ openCode.state.active = { [SESSION]: { type: 'running' } };
134
+ await runtime.enqueue(SESSION, DIRECTORY, scheduled());
135
+ await expect(runtime.enqueue('ses_other', DIRECTORY, scheduled())).rejects.toMatchObject({ status: 409 });
136
+ await runtime.enqueue(SESSION, DIRECTORY, scheduled('project-2'));
137
+ expect(runtime.sessionSnapshot(SESSION).items).toHaveLength(2);
138
+ runtime.stop();
139
+ await runtime.flush();
140
+ });
141
+
142
+ it('inherits without a model POST and reports sent without waiting for completion', async () => {
143
+ const results = [];
144
+ const { runtime, openCode } = createRuntime({ onScheduledTaskResult: (task, status) => results.push({ task, status }) });
145
+ await runtime.enqueue(SESSION, DIRECTORY, scheduled());
146
+ await settle();
147
+ expect(openCode.state.switched).toEqual([]);
148
+ expect(openCode.state.sent).toHaveLength(1);
149
+ expect(openCode.state.sent[0].body).toEqual({ text: 'follow up' });
150
+ expect(runtime.sessionSnapshot(SESSION).items).toEqual([]);
151
+ expect(results).toEqual([{ task: { projectId: 'project-1', taskId: 'task-1' }, status: 'sent' }]);
152
+ runtime.stop();
153
+ await runtime.flush();
154
+ });
155
+
156
+ it('applies explicit selection and keeps the existing directory query', async () => {
157
+ const { runtime, openCode } = createRuntime();
158
+ await runtime.enqueue(SESSION, DIRECTORY, scheduled('project-1', { sendConfig: { providerID: 'openai', modelID: 'chosen', agent: 'plan' } }));
159
+ await settle();
160
+ expect(openCode.state.switched.map((entry) => entry.body)).toEqual([{ model: { providerID: 'openai', id: 'chosen' } }, { agent: 'plan' }]);
161
+ const promptCall = openCode.fetchImpl.mock.calls.find(([url]) => new URL(url).pathname.endsWith('/prompt'));
162
+ expect(new URL(promptCall[0]).searchParams.get('directory')).toBe(DIRECTORY);
163
+ runtime.stop();
164
+ await runtime.flush();
165
+ });
166
+
167
+ it('supports an agent override without switching the target model', async () => {
168
+ const { runtime, openCode } = createRuntime();
169
+ await runtime.enqueue(SESSION, DIRECTORY, scheduled('project-1', { sendConfig: { agent: 'plan' } }));
170
+ await settle();
171
+ expect(openCode.state.switched.map((entry) => entry.body)).toEqual([{ agent: 'plan' }]);
172
+ runtime.stop();
173
+ await runtime.flush();
174
+ });
175
+
176
+ it('keeps an agent-only override across persistence and restart', async () => {
177
+ const first = createRuntime();
178
+ first.openCode.state.active = { [SESSION]: { type: 'running' } };
179
+ await first.runtime.enqueue(SESSION, DIRECTORY, scheduled('project-1', { sendConfig: { agent: 'plan' } }));
180
+ first.runtime.stop();
181
+ await first.runtime.flush();
182
+ const second = createRuntime({ dataDir: first.dataDir });
183
+ await second.runtime.load();
184
+ expect(second.runtime.sessionSnapshot(SESSION).items[0].sendConfig).toEqual({ agent: 'plan' });
185
+ second.runtime.start();
186
+ await settle();
187
+ expect(second.openCode.state.switched.map((entry) => entry.body)).toEqual([{ agent: 'plan' }]);
188
+ expect(second.openCode.state.sent).toHaveLength(1);
189
+ second.runtime.stop();
190
+ await second.runtime.flush();
191
+ });
192
+
193
+ it.each([404, 409])('drops a target refusal %s without holding ordinary items behind it', async (statusCode) => {
194
+ const results = [];
195
+ const { runtime, openCode } = createRuntime({
196
+ beforeScheduledTaskSend: async () => { throw Object.assign(new Error('Scheduled target is unavailable'), { statusCode }); },
197
+ onScheduledTaskResult: (_task, status) => results.push(status),
198
+ });
199
+ await runtime.enqueue(SESSION, DIRECTORY, scheduled());
200
+ await runtime.enqueue(SESSION, DIRECTORY, item({ text: 'ordinary' }));
201
+ await settle();
202
+ expect(results).toEqual(['failed']);
203
+ expect(openCode.state.sent[0].body.text).toBe('ordinary');
204
+ runtime.stop();
205
+ await runtime.flush();
206
+ });
207
+
208
+ it.each([
209
+ { operation: 'prompt', status: 500 },
210
+ { operation: 'model', status: 500 },
211
+ { operation: 'model', status: 404 },
212
+ { operation: 'model', status: 409 },
213
+ { operation: 'target lookup', status: 503 },
214
+ ])('uses the ordinary retry policy for scheduled $operation failure $status', async ({ operation, status }) => {
215
+ const results = [];
216
+ let checks = 0;
217
+ const { runtime, openCode, emit } = createRuntime({
218
+ retryDelayMs: () => 100,
219
+ beforeScheduledTaskSend: async () => {
220
+ checks += 1;
221
+ if (operation === 'target lookup' && checks === 1) throw Object.assign(new Error('upstream unavailable'), { statusCode: 503 });
222
+ },
223
+ onScheduledTaskResult: (_task, status) => results.push(status),
224
+ });
225
+ runtime.start();
226
+ openCode.state.failStatus = status;
227
+ if (operation === 'prompt') openCode.state.failNext = /\/prompt$/;
228
+ if (operation === 'model') openCode.state.failNext = /\/model$/;
229
+ await runtime.enqueue(SESSION, DIRECTORY, operation === 'model'
230
+ ? scheduled('project-1', { sendConfig: { providerID: 'openai', modelID: 'chosen' } })
231
+ : scheduled());
232
+ await runtime.enqueue(SESSION, DIRECTORY, item({ text: 'ordinary' }));
233
+ await settle();
234
+ expect(runtime.sessionSnapshot(SESSION).items).toHaveLength(2);
235
+ expect(results).toEqual([]);
236
+ expect(openCode.state.sent).toEqual([]);
237
+ await expect(runtime.enqueue(SESSION, DIRECTORY, scheduled())).rejects.toMatchObject({ status: 409 });
238
+ await settle(130);
239
+ expect(checks).toBe(2);
240
+ expect(results).toEqual(['sent']);
241
+ expect(openCode.state.sent.map((entry) => entry.body.text)).toEqual(['follow up']);
242
+ emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
243
+ await settle();
244
+ expect(openCode.state.sent.map((entry) => entry.body.text)).toEqual(['follow up', 'ordinary']);
245
+ runtime.stop();
246
+ await runtime.flush();
247
+ });
248
+
249
+ it('rejects scheduled takes, leaves them in bulk take, and allows reorder and remove', async () => {
250
+ const results = [];
251
+ const { runtime, openCode } = createRuntime({ onScheduledTaskResult: (task, status) => results.push({ task, status }) });
252
+ openCode.state.active = { [SESSION]: { type: 'running' } };
253
+ const first = await runtime.enqueue(SESSION, DIRECTORY, scheduled());
254
+ const ordinary = await runtime.enqueue(SESSION, DIRECTORY, item());
255
+ const second = await runtime.enqueue(SESSION, DIRECTORY, scheduled('project-1', { scheduledTask: { projectId: 'project-1', taskId: 'task-2' } }));
256
+ await runtime.reorder(SESSION, [second.itemId, ordinary.itemId, first.itemId]);
257
+ await expect(runtime.take(SESSION, first.itemId)).rejects.toMatchObject({ status: 409 });
258
+ expect((await runtime.takeAll(SESSION)).items.map((entry) => entry.id)).toEqual([ordinary.itemId]);
259
+ expect(runtime.sessionSnapshot(SESSION).items.map((entry) => entry.id)).toEqual([second.itemId, first.itemId]);
260
+ expect(results).toEqual([]);
261
+ await runtime.remove(SESSION, second.itemId);
262
+ expect(results).toEqual([{ task: { projectId: 'project-1', taskId: 'task-2' }, status: 'cancelled' }]);
263
+ runtime.stop();
264
+ await runtime.flush();
265
+ });
266
+
267
+ it('ignores creation-only policy fields on stored scheduled provenance', () => {
268
+ const parsed = parseQueuedItemInput(scheduled('project-1', {
269
+ scheduledTask: { projectId: 'project-1', taskId: 'task-1', goalEnabled: true, goalTokenBudget: 5000, permissionAutoAccept: true },
270
+ }));
271
+ expect(parsed.scheduledTask).toEqual({ projectId: 'project-1', taskId: 'task-1' });
272
+ });
273
+
274
+ it('cancels only the requested task and survives a restart with provenance', async () => {
275
+ const results = [];
276
+ const first = createRuntime({ onScheduledTaskResult: (_task, status) => results.push(status) });
277
+ first.openCode.state.active = { [SESSION]: { type: 'running' } };
278
+ await first.runtime.enqueue(SESSION, DIRECTORY, scheduled());
279
+ await first.runtime.enqueue(SESSION, DIRECTORY, item());
280
+ first.runtime.stop();
281
+ await first.runtime.flush();
282
+ const second = createRuntime({ dataDir: first.dataDir, onScheduledTaskResult: (_task, status) => results.push(status) });
283
+ await second.runtime.load();
284
+ expect(second.runtime.sessionSnapshot(SESSION).items[0].scheduledTask).toEqual({ projectId: 'project-1', taskId: 'task-1' });
285
+ await second.runtime.cancelScheduledTask('project-1', 'task-1');
286
+ expect(second.runtime.sessionSnapshot(SESSION).items).toHaveLength(1);
287
+ expect(results).toEqual(['cancelled']);
288
+ second.runtime.stop();
289
+ await second.runtime.flush();
290
+ });
291
+ });
292
+
123
293
  describe('auto routing', () => {
124
294
  it('switches a queued prompt and a queued command onto the routed model and agent', async () => {
125
295
  const resolveAutoSelection = vi.fn(async ({ model }) => (model?.id === 'auto'
@@ -43,9 +43,8 @@ export const createApnsRuntime = (deps) => {
43
43
  http2,
44
44
  APNS_TOKENS_FILE_PATH,
45
45
  readSettingsFromDiskMigrated,
46
- writeSettingsToDisk,
47
- // Strict settings reader gating identity regeneration (see signing-key.js).
48
- readSettingsStrict,
46
+ // Host relay identity keys, shared with the private relay (relay/key-store.js).
47
+ relayKeyStore,
49
48
  } = deps;
50
49
 
51
50
  let persistLock = Promise.resolve();
@@ -55,8 +54,8 @@ export const createApnsRuntime = (deps) => {
55
54
  let warnedUnconfigured = false;
56
55
 
57
56
  // ---------------------------------------------------------------------------
58
- // Per-server relay signing identity (ECDSA P-256). Auto-generated + persisted in settings
59
- // (mirrors getOrCreateVapidKeys). The relay derives serverId = SHA-256(publicKey), verifies
57
+ // Per-server relay signing identity (ECDSA P-256). Auto-generated + persisted by
58
+ // relay/key-store.js. The relay derives serverId = SHA-256(publicKey), verifies
60
59
  // each request's signature, and only delivers to tokens this server registered — so a leaked
61
60
  // device token alone can't be used to push. Zero-config: the keypair generates on first use.
62
61
  // ---------------------------------------------------------------------------
@@ -65,7 +64,7 @@ export const createApnsRuntime = (deps) => {
65
64
  // relay identity — same keypair, same storage, same serverId derivation).
66
65
  const getOrCreateRelayKeypair = async () => {
67
66
  if (cachedRelayKey) return cachedRelayKey;
68
- cachedRelayKey = await getOrCreateRelaySigningKeypair({ crypto, readSettingsFromDiskMigrated, writeSettingsToDisk, readSettingsStrict });
67
+ cachedRelayKey = await getOrCreateRelaySigningKeypair({ crypto, relayKeyStore });
69
68
  return cachedRelayKey;
70
69
  };
71
70
 
@@ -28,9 +28,25 @@ const createMemoryFs = () => {
28
28
  };
29
29
  };
30
30
 
31
+ // In-memory relay key store: the auto-generated signing keypair persists and
32
+ // reads back; `generations` counts how many keypairs were minted.
33
+ const makeMemoryKeyStore = () => {
34
+ const slots = {};
35
+ const store = {
36
+ generations: 0,
37
+ getOrCreate: async (slot, generate) => {
38
+ if (!slots[slot]) {
39
+ store.generations += 1;
40
+ slots[slot] = await generate();
41
+ }
42
+ return slots[slot];
43
+ },
44
+ };
45
+ return store;
46
+ };
47
+
31
48
  const makeDeps = (overrides = {}) => {
32
- // Stateful settings so the auto-generated relay signing keypair persists + reads back.
33
- let settings = {};
49
+ const settings = {};
34
50
  return {
35
51
  fsPromises: createMemoryFs(),
36
52
  path: { dirname: () => '/tmp' },
@@ -38,7 +54,7 @@ const makeDeps = (overrides = {}) => {
38
54
  http2: { connect: vi.fn(() => { throw new Error('http2 must not be used in relay mode'); }) },
39
55
  APNS_TOKENS_FILE_PATH: '/tmp/apns-tokens.json',
40
56
  readSettingsFromDiskMigrated: vi.fn(async () => settings),
41
- writeSettingsToDisk: vi.fn(async (next) => { settings = next; }),
57
+ relayKeyStore: makeMemoryKeyStore(),
42
58
  ...overrides,
43
59
  };
44
60
  };
@@ -144,7 +160,7 @@ describe('apns runtime relay mode (default)', () => {
144
160
  expect(keys.length).toBeGreaterThanOrEqual(2);
145
161
  expect(keys.every((k) => k.x === keys[0].x && k.y === keys[0].y)).toBe(true);
146
162
  // Keypair was generated + persisted exactly once.
147
- expect(deps.writeSettingsToDisk).toHaveBeenCalledTimes(1);
163
+ expect(deps.relayKeyStore.generations).toBe(1);
148
164
  });
149
165
 
150
166
  it('honors an explicit sandbox environment override for every token', async () => {
@@ -13,8 +13,18 @@ other.
13
13
  scheduled-task action allowlist. `actions.js` marks CLI-only actions with
14
14
  `agentExposed: false` (currently `schedule.status`); the agent tool consumes
15
15
  the filtered `OPENCHAMBER_AGENT_TOOL_*` exports. `schedule.toggle` requires
16
- the `disabled` boolean and replaces separate enable/disable actions;
16
+ the `disabled` boolean and replaces separate enable/disable actions; its
17
+ `enabled` is the saved state, never the requested one. Loop tasks and the
18
+ refusal to enable a repository loop are the scheduled-task service's rules
19
+ (`../scheduled-tasks/DOCUMENTATION.md`, *Local approval*).
17
20
  `schedule.list` also returns scheduler status as `scheduler`.
21
+ `schedule.update` patches a task in place: only the fields the call names
22
+ change, the id and run state stay, and a task driven by a loop file is
23
+ refused (the file is its source).
24
+ `schedule.create` and `schedule.update` accept `targetSessionId`, validated
25
+ by the scheduled-task service. An empty update clears the target. Create
26
+ without a model inherits the target selection; an explicit agent overrides
27
+ it. A task without a target still requires a model.
18
28
  - `routes.js` is the authenticated CLI HTTP adapter. It forwards one action,
19
29
  preserves service status and partial-result details, and propagates request
20
30
  cancellation.
@@ -53,6 +63,14 @@ other.
53
63
  - Send and fork dispatches without an explicit model/agent/variant reuse the
54
64
  target session's last user-message selection before falling back to the
55
65
  configured defaults; only session creation resolves defaults directly.
66
+ - `session.create` with `pullRequest` passes the number, and `worktree` and
67
+ `branch` as folder and local-branch names, to the session service, which
68
+ creates the worktree through the injected `createChangeRequestWorktree`
69
+ (the Git routes' contributor pipeline) before any session exists. A
70
+ malformed number, or `startRef`/`setUpstream` beside it, is a 400; a
71
+ server without that pipeline answers 501 and never makes a plain branch
72
+ worktree instead; a provider or transfer refusal keeps its status and
73
+ message, and no session is created.
56
74
  - Default agents resolve from the owning project before global settings and
57
75
  OpenCode defaults. Directory-based requests identify the project before
58
76
  creating a worktree; existing linked worktrees resolve through Git's primary
@@ -19,7 +19,8 @@ export const OPENCHAMBER_CONTROL_ACTION_DEFINITIONS = Object.freeze([
19
19
  { action: 'session.link', title: 'Link a change or issue to a session', description: 'Link a change under review (pull or merge request) or an issue, on any service, to a session; requires link, sessionId defaults to the current session. Each link is added beside the others; linking one that is already there updates its title instead of adding it twice. Links are only added; the user removes them' },
20
20
  { action: 'schedule.status', title: 'Check scheduler status', description: 'Check scheduler status; no parameters', agentExposed: false },
21
21
  { action: 'schedule.list', title: 'List scheduled tasks', description: 'List tasks and scheduler status; scope with projectId or directory' },
22
- { action: 'schedule.create', title: 'Create a scheduled task', description: 'Create task; requires name, prompt, model, and one schedule selector' },
22
+ { action: 'schedule.create', title: 'Create a scheduled task', description: 'Create task; requires name, prompt, one schedule selector, and model unless targetSessionId is set. targetSessionId reuses a session; without model it keeps that session selection' },
23
+ { action: 'schedule.update', title: 'Change a scheduled task', description: 'Change taskId in place, keeping its id and run state; pass only what changes: name, prompt, model, variant, agent, targetSessionId (empty clears it), a schedule selector, timezone, disabled' },
23
24
  { action: 'schedule.run', title: 'Run a scheduled task', description: 'Run taskId; scope with projectId or directory' },
24
25
  { action: 'schedule.delete', title: 'Delete a scheduled task', description: 'Delete taskId; scope with projectId or directory' },
25
26
  { action: 'schedule.toggle', title: 'Enable or disable a scheduled task', description: 'Enable or disable taskId; requires the disabled boolean' },
@@ -1,3 +1,4 @@
1
+ import { asNonEmptyString } from '../shared/guards.js';
1
2
  import path from 'node:path';
2
3
  import { OpenCode } from '@opencode/client';
3
4
  import { OpenChamberControlError, asControlError } from './error.js';
@@ -10,17 +11,12 @@ const WAIT_POLL_INTERVAL_MS = 500;
10
11
  // One service, both capabilities: which tool asked is the caller's concern.
11
12
  const CONTROL_ACTIONS = new Set(OPENCHAMBER_ALL_ACTIONS);
12
13
  const SCHEDULE_TASK_ID_ACTIONS = new Set([
14
+ 'schedule.update',
13
15
  'schedule.run',
14
16
  'schedule.delete',
15
17
  'schedule.toggle',
16
18
  ]);
17
19
 
18
- const asNonEmptyString = (value) => {
19
- if (typeof value !== 'string') return null;
20
- const trimmed = value.trim();
21
- return trimmed.length > 0 ? trimmed : null;
22
- };
23
-
24
20
  const positiveInteger = (value, fallback, field) => {
25
21
  if (value === undefined || value === null) return fallback;
26
22
  const number = Number(value);
@@ -120,7 +116,8 @@ const buildScheduledTask = (input) => {
120
116
  const prompt = asNonEmptyString(input.prompt);
121
117
  if (!name) throw new OpenChamberControlError('name is required', 400);
122
118
  if (!prompt) throw new OpenChamberControlError('prompt is required', 400);
123
- const model = parseModel(input.model);
119
+ const targetSessionId = asNonEmptyString(input.targetSessionId);
120
+ const model = targetSessionId && input.model === undefined ? { useDefaults: !asNonEmptyString(input.agent) } : parseModel(input.model);
124
121
  const goalTokenBudget = input.goalTokenBudget;
125
122
  if (goalTokenBudget !== undefined && input.goal !== true) {
126
123
  throw new OpenChamberControlError('goalTokenBudget requires goal', 400);
@@ -128,7 +125,7 @@ const buildScheduledTask = (input) => {
128
125
  if (goalTokenBudget !== undefined && (!Number.isSafeInteger(goalTokenBudget) || goalTokenBudget < 1000 || goalTokenBudget > 100_000_000)) {
129
126
  throw new OpenChamberControlError('goalTokenBudget must be from 1000 to 100000000', 400);
130
127
  }
131
- return {
128
+ const task = {
132
129
  name,
133
130
  enabled: input.disabled !== true,
134
131
  schedule: buildSchedule(input),
@@ -141,6 +138,70 @@ const buildScheduledTask = (input) => {
141
138
  ...(goalTokenBudget !== undefined ? { goalTokenBudget } : {}),
142
139
  },
143
140
  };
141
+ if (input.targetSessionId !== undefined) task.targetSessionId = targetSessionId ?? input.targetSessionId;
142
+ return task;
143
+ };
144
+
145
+ /**
146
+ * `schedule.update`: the stored task with only the fields the call names
147
+ * changed. The id and the run state stay, so history and references survive;
148
+ * the scheduler re-arms from the new schedule on save.
149
+ */
150
+ const patchScheduledTask = (existing, input) => {
151
+ if (existing.loopFile) {
152
+ throw new OpenChamberControlError(`This task comes from ${existing.loopFile}; change that file instead`, 409);
153
+ }
154
+ const execution = { ...existing.execution };
155
+ if (input.prompt !== undefined) {
156
+ const prompt = asNonEmptyString(input.prompt);
157
+ if (!prompt) throw new OpenChamberControlError('prompt cannot be empty', 400);
158
+ execution.prompt = prompt;
159
+ }
160
+ if (input.model !== undefined) {
161
+ Object.assign(execution, parseModel(input.model));
162
+ // A thinking level belongs to the model it was chosen for.
163
+ delete execution.variant;
164
+ delete execution.useDefaults;
165
+ }
166
+ for (const field of ['variant', 'agent']) {
167
+ if (input[field] === undefined) continue;
168
+ const value = asNonEmptyString(input[field]);
169
+ if (value) execution[field] = value;
170
+ else delete execution[field];
171
+ if (field === 'agent' && value && (input.targetSessionId || existing.targetSessionId)) delete execution.useDefaults;
172
+ }
173
+ if (typeof input.goal === 'boolean') {
174
+ if (input.goal) execution.goalEnabled = true;
175
+ else {
176
+ delete execution.goalEnabled;
177
+ delete execution.goalTokenBudget;
178
+ }
179
+ }
180
+ if (input.goalTokenBudget !== undefined) {
181
+ if (!execution.goalEnabled) throw new OpenChamberControlError('goalTokenBudget requires goal', 400);
182
+ if (!Number.isSafeInteger(input.goalTokenBudget) || input.goalTokenBudget < 1000 || input.goalTokenBudget > 100_000_000) {
183
+ throw new OpenChamberControlError('goalTokenBudget must be from 1000 to 100000000', 400);
184
+ }
185
+ execution.goalTokenBudget = input.goalTokenBudget;
186
+ }
187
+ let name = existing.name;
188
+ if (input.name !== undefined) {
189
+ name = asNonEmptyString(input.name);
190
+ if (!name) throw new OpenChamberControlError('name cannot be empty', 400);
191
+ }
192
+ const replacesSchedule = ['daily', 'weekly', 'once', 'cron'].some((key) => asNonEmptyString(input[key]));
193
+ const timezone = asNonEmptyString(input.timezone);
194
+ const schedule = replacesSchedule
195
+ ? buildSchedule(input)
196
+ : (timezone ? { ...existing.schedule, timezone } : existing.schedule);
197
+ return {
198
+ id: existing.id,
199
+ targetSessionId: input.targetSessionId === undefined ? existing.targetSessionId : input.targetSessionId,
200
+ name,
201
+ enabled: typeof input.disabled === 'boolean' ? !input.disabled : existing.enabled,
202
+ schedule,
203
+ execution,
204
+ };
144
205
  };
145
206
 
146
207
  export const createOpenChamberControlService = (dependencies) => {
@@ -262,18 +323,36 @@ export const createOpenChamberControlService = (dependencies) => {
262
323
  return asPublicStatus(statuses[sessionID]);
263
324
  };
264
325
 
265
- // `order: 'desc'` puts the newest messages in the limited page;
266
- // extractTextMessages re-sorts them oldest-first for the caller.
326
+ // The message list is cursor-paginated and `order: 'desc'` puts the newest
327
+ // messages in the page, so a single call returns only the recent tail.
328
+ // `all: true` walks `cursor.next` until the history is exhausted; a bounded
329
+ // `limit` stops once enough text messages are collected. Later pages carry
330
+ // the order inside the cursor, and OpenCode rejects a cursor combined with
331
+ // `order`, so only the first page sends it. An empty page or a repeated
332
+ // cursor ends the walk. `extractTextMessages` sorts the gathered records
333
+ // oldest-first for the caller.
334
+ const MESSAGE_PAGE_LIMIT = 200;
267
335
  const sessionMessages = async (client, sessionID, role, limit) => {
268
336
  const fetchLimit = limit === undefined ? undefined : Math.max(100, limit * 4);
269
- let response = await client.message.list({ sessionID, ...(fetchLimit ? { limit: fetchLimit, order: 'desc' } : {}) });
270
- let raw = Array.isArray(response?.data) ? response.data : [];
271
- let messages = extractTextMessages(raw, role);
272
- if (limit !== undefined && messages.length < limit && raw.length >= fetchLimit) {
273
- response = await client.message.list({ sessionID });
274
- raw = Array.isArray(response?.data) ? response.data : [];
275
- messages = extractTextMessages(raw, role);
337
+ const collected = [];
338
+ const seenCursors = new Set();
339
+ let cursor;
340
+ for (;;) {
341
+ const response = await client.message.list({
342
+ sessionID,
343
+ limit: fetchLimit ?? MESSAGE_PAGE_LIMIT,
344
+ ...(cursor ? { cursor } : fetchLimit ? { order: 'desc' } : {}),
345
+ });
346
+ const raw = Array.isArray(response?.data) ? response.data : [];
347
+ collected.push(...raw);
348
+ const next = asNonEmptyString(response?.cursor?.next);
349
+ if (!next || raw.length === 0 || seenCursors.has(next)) break;
350
+ // A bounded read only needs enough pages to project `limit` text messages.
351
+ if (limit !== undefined && extractTextMessages(collected, role).length >= limit) break;
352
+ seenCursors.add(next);
353
+ cursor = next;
276
354
  }
355
+ const messages = extractTextMessages(collected, role);
277
356
  return limit === undefined ? messages : messages.slice(-limit);
278
357
  };
279
358
 
@@ -368,6 +447,10 @@ export const createOpenChamberControlService = (dependencies) => {
368
447
  if (input.timeout !== undefined && input.wait !== true) throw new OpenChamberControlError('timeout requires wait', 400);
369
448
  if (input.lastAssistant === true && input.wait !== true) throw new OpenChamberControlError('lastAssistant requires wait', 400);
370
449
  assertSingleScope(input);
450
+ // Only a new session gets a worktree; elsewhere the number would be ignored.
451
+ if (input.pullRequest !== undefined && action !== 'session.create') {
452
+ throw new OpenChamberControlError('pullRequest applies only to session.create', 400);
453
+ }
371
454
  const sessionID = asNonEmptyString(input.sessionId);
372
455
  const parentSessionID = asNonEmptyString(contextSessionId);
373
456
  const returnResult = input.returnResult === true;
@@ -395,6 +478,16 @@ export const createOpenChamberControlService = (dependencies) => {
395
478
  ...(typeof input.setUpstream === 'boolean' ? { setUpstream: input.setUpstream } : {}),
396
479
  ...(asNonEmptyString(input.messageId) ? { messageId: input.messageId.trim() } : {}),
397
480
  };
481
+ // A pull request makes a worktree of its own, so its branch name (and a
482
+ // start ref the session service refuses) travel without a worktree name.
483
+ if (input.pullRequest !== undefined) {
484
+ payload.pullRequest = input.pullRequest;
485
+ if (!payload.worktree) {
486
+ payload.worktree = {};
487
+ if (asNonEmptyString(input.branch)) payload.worktree.branchName = input.branch.trim();
488
+ if (asNonEmptyString(input.startRef)) payload.worktree.startRef = input.startRef.trim();
489
+ }
490
+ }
398
491
  const startedAt = now();
399
492
  let result;
400
493
  if (action === 'session.create') {
@@ -488,7 +581,6 @@ export const createOpenChamberControlService = (dependencies) => {
488
581
  parameters.url = parsed.toString();
489
582
  }
490
583
 
491
-
492
584
  if (action === 'browser.click') {
493
585
  const selector = asNonEmptyString(input.selector);
494
586
  const text = asNonEmptyString(input.text);
@@ -648,6 +740,12 @@ export const createOpenChamberControlService = (dependencies) => {
648
740
  const result = await scheduledTaskService.upsert(projectID, buildScheduledTask(input));
649
741
  return { task: result.task, created: result.created };
650
742
  }
743
+ case 'schedule.update': {
744
+ const existing = (await scheduledTaskService.list(projectID)).find((task) => task.id === taskID);
745
+ if (!existing) throw new OpenChamberControlError(`Scheduled task not found: ${taskID}`, 404);
746
+ const result = await scheduledTaskService.upsert(projectID, patchScheduledTask(existing, input));
747
+ return { task: result.task, updated: true };
748
+ }
651
749
  case 'schedule.run':
652
750
  return scheduledTaskService.run(projectID, taskID);
653
751
  case 'schedule.delete':
@@ -656,8 +754,9 @@ export const createOpenChamberControlService = (dependencies) => {
656
754
  if (typeof input.disabled !== 'boolean') {
657
755
  throw new OpenChamberControlError('disabled is required for schedule.toggle', 400);
658
756
  }
659
- const enabled = input.disabled === false;
660
- return { task: await scheduledTaskService.setEnabled(projectID, taskID, enabled), enabled };
757
+ const task = await scheduledTaskService.setEnabled(projectID, taskID, input.disabled === false);
758
+ // What was saved, which is what the scheduler acts on.
759
+ return { task, enabled: task?.enabled === true };
661
760
  }
662
761
  }
663
762
  }