@openchambery/web 1.19.22-beta.1 → 2.0.0-beta.3

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 (311) hide show
  1. package/README.md +13 -7
  2. package/bin/cli-opencode.test.js +208 -0
  3. package/bin/cli.js +62 -9
  4. package/bin/lib/cli-startup.js +105 -2
  5. package/bin/lib/cli-startup.test.js +179 -0
  6. package/dist/assets/{AgentAvatar-CRaBJXKs.js → AgentAvatar-B_JQoreg.js} +1 -1
  7. package/dist/assets/AssistantView-ClQAfGOT.js +3 -0
  8. package/dist/assets/{DiagramView-D1LGK_s2.js → DiagramView-BNnda8qO.js} +1 -1
  9. package/dist/assets/{FadeInOnReveal-B0kJo9Ii.js → FadeInOnReveal-VYQTSAkr.js} +1 -1
  10. package/dist/assets/{Icon-BgdfPhNs.js → Icon-BiEQEcih.js} +1 -1
  11. package/dist/assets/MarkdownRenderer-CJHSsWin.js +2 -0
  12. package/dist/assets/{MarkdownRendererImpl-DfoCJ1Mq.js → MarkdownRendererImpl-DwrHFGUp.js} +3 -3
  13. package/dist/assets/{MarkstreamRendererImpl-D2dsW5Lg.js → MarkstreamRendererImpl-CSnbQMu3.js} +1 -1
  14. package/dist/assets/MobileDetailNavigation-BXr6AGLT.js +1 -0
  15. package/dist/assets/{MobileShareBridge-2Yjn6wXg.js → MobileShareBridge-D7OE9OP_.js} +2 -2
  16. package/dist/assets/MobileSurface-D2jUkX9l.js +2 -0
  17. package/dist/assets/{MultiRunWindow-tTbMFowS.js → MultiRunWindow-DF3iIFCd.js} +1 -1
  18. package/dist/assets/{OnboardingScreen-CD2KtlF3.js → OnboardingScreen-BjHmlwlw.js} +1 -1
  19. package/dist/assets/OverlayScrollbar-DLtWO_Gp.js +2 -0
  20. package/dist/assets/SettingsView-BRxELjow.js +25 -0
  21. package/dist/assets/{SettingsWindow-zIa76s4t.js → SettingsWindow-BI5swspb.js} +1 -1
  22. package/dist/assets/{TerminalView-CDdzF4yi.js → TerminalView-BU3x8-fr.js} +1 -1
  23. package/dist/assets/{ToolOutputDialog-DNqA4uBl.js → ToolOutputDialog-BEWhBcRd.js} +2 -2
  24. package/dist/assets/appThemeRegistry-4MR0F7Jz.js +1 -0
  25. package/dist/assets/{button-CI7Q-Zw_.js → button-DKuz3L8l.js} +1 -1
  26. package/dist/assets/collapsible-DXbDynN4.js +3 -0
  27. package/dist/assets/context-menu-CqolsLCm.js +1 -0
  28. package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-CoDkOnyS.js} +1 -1
  29. package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-DDlVKyOE.js} +1 -1
  30. package/dist/assets/{diagram-DdLO8HZ9.js → diagram-D7T8HQgj.js} +1 -1
  31. package/dist/assets/{dialog-B6pnIYmp.js → dialog-DQYl3W_o.js} +1 -1
  32. package/dist/assets/es-CN3E_zXj.js +15 -0
  33. package/dist/assets/{fileMentionTouchSelection-DNeiqns-.js → fileMentionTouchSelection-BZ6ovBYH.js} +1 -1
  34. package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-JrpcsOCs.js} +1 -1
  35. package/dist/assets/fr-9zZvQZD2.js +13 -0
  36. package/dist/assets/gitApi-CM42qUed.js +10 -0
  37. package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-CIl0FL10.js} +1 -1
  38. package/dist/assets/{input-D-__fwRi.js → input-CzQy8UhV.js} +1 -1
  39. package/dist/assets/insertionBoundaries-CtkZ9720.js +1 -0
  40. package/dist/assets/ja-Dnjk_82N.js +15 -0
  41. package/dist/assets/ko-BxCEE99C.js +15 -0
  42. package/dist/assets/main-BILgBI3B.js +169 -0
  43. package/dist/assets/main-BeveiW6F.js +2 -0
  44. package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
  45. package/dist/assets/{markdownHeightCache-BhxqJOvf.js → markdownHeightCache-Cj1I5NDw.js} +1 -1
  46. package/dist/assets/{markdownTheme-DOfGg-A7.js → markdownTheme-CKac3wKC.js} +1 -1
  47. package/dist/assets/mcpQueries-D5mQymgM.js +1 -0
  48. package/dist/assets/miniChat-BgrzM3u7.js +2 -0
  49. package/dist/assets/mobile-DtYoQE8C.js +2 -0
  50. package/dist/assets/multirun-jy8dEI96.js +4 -0
  51. package/dist/assets/{persistence-Ce3AnpEr.js → persistence-C9LQpiOS.js} +2 -2
  52. package/dist/assets/pl-CNNsWnPM.js +15 -0
  53. package/dist/assets/platform-BQghHRfp.js +6 -0
  54. package/dist/assets/pt-BR-BZB2DJMp.js +15 -0
  55. package/dist/assets/renderElectronMiniChatApp-BGrrmeME.js +2 -0
  56. package/dist/assets/renderMobileApp-u3c4GtFC.js +28 -0
  57. package/dist/assets/{runtimeConfig-CMkwoQ_X.js → runtimeConfig-BIGBRGQI.js} +1 -1
  58. package/dist/assets/{runtimeEndpointReset-ChOxt-J7.js → runtimeEndpointReset-D2savkuw.js} +12 -12
  59. package/dist/assets/{sessionChangeSummary-NnxcqCTI.js → sessionChangeSummary-eIbpSfrO.js} +1 -1
  60. package/dist/assets/{sessionLookup-BrU3rFHJ.js → sessionLookup-myHe5ltu.js} +1 -1
  61. package/dist/assets/{sortable-tabs-strip-Fnfhf3K9.js → sortable-tabs-strip-BQtmHx9r.js} +1 -1
  62. package/dist/assets/src-CLN7Z-zx.css +2 -0
  63. package/dist/assets/store-DLCiZaA6.js +15 -0
  64. package/dist/assets/{terminalApi-xjqssKke.js → terminalApi-BxcdiduX.js} +1 -1
  65. package/dist/assets/{textarea-bHYxZtfH.js → textarea-CakKvqFw.js} +1 -1
  66. package/dist/assets/{tooltip-0T5jv0cu.js → tooltip-Cx5gTbdY.js} +1 -1
  67. package/dist/assets/uk-rwQrxeYM.js +15 -0
  68. package/dist/assets/{url-_z7evzdK.js → url-CDHnjlsm.js} +1 -1
  69. package/dist/assets/{useAppFontEffects-C02YFyAv.js → useAppFontEffects-CNeo0aZs.js} +59 -45
  70. package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-asNG6K7n.js} +1 -1
  71. package/dist/assets/useDesktopWindowControlsLayout-DBJ56qIo.js +1 -0
  72. package/dist/assets/useEffectiveDirectory-B4LvR8Gt.js +1 -0
  73. package/dist/assets/{useFontPreferences-B1JYyzpK.js → useFontPreferences-CUnQ4TVm.js} +1 -1
  74. package/dist/assets/useMobileNavigationStore-ZHJPxlHt.js +414 -0
  75. package/dist/assets/{useRuntimeAPIs-DSLXJybW.js → useRuntimeAPIs-DwLtO0c_.js} +1 -1
  76. package/dist/assets/useSessionAutoCleanup-BIVmrnYU.js +2 -0
  77. package/dist/assets/{useThemeSystem-DMzWVJaW.js → useThemeSystem-BlNUWH47.js} +1 -1
  78. package/dist/assets/useUIStore-CQYKJnrg.js +1 -0
  79. package/dist/assets/{useWorkerHighlightedLines-C9z0ML1z.js → useWorkerHighlightedLines-CJZbJDW3.js} +2 -2
  80. package/dist/assets/{vendor-base-ui-CnHfUmDN.js → vendor-base-ui-CDjyofeD.js} +1 -1
  81. package/dist/assets/{vendor-cmdk-CeAGfrLo.js → vendor-cmdk-BFH0UpXX.js} +1 -1
  82. package/dist/assets/{vendor-dnd-kit-accessibility-BY5LhhUB.js → vendor-dnd-kit-accessibility-BrR3AKvM.js} +1 -1
  83. package/dist/assets/{vendor-dnd-kit-core-CvWxUPdb.js → vendor-dnd-kit-core-CNfdvk6H.js} +1 -1
  84. package/dist/assets/{vendor-dnd-kit-sortable-hHdP9lLb.js → vendor-dnd-kit-sortable-DQUaTeGj.js} +1 -1
  85. package/dist/assets/{vendor-framer-motion-DFy_BSVd.js → vendor-framer-motion-4dogsUk3.js} +1 -1
  86. package/dist/assets/{vendor-legendapp-list-WE32lwNk.js → vendor-legendapp-list-BU7MdPZR.js} +1 -1
  87. package/dist/assets/{vendor-markstream-react-DX55XLGO.js → vendor-markstream-react-qHC2FykU.js} +3 -3
  88. package/dist/assets/vendor-opencode-client-BOe2qpQb.js +12 -0
  89. package/dist/assets/{vendor-pierre-diffs-DX7XTPG3.js → vendor-pierre-diffs-CUfqC003.js} +1 -1
  90. package/dist/assets/{vendor-react-DLPElpoQ.js → vendor-react-C94A0AHl.js} +1 -1
  91. package/dist/assets/{vendor-react-drawio-ECtEwvbc.js → vendor-react-drawio-FVlWqKVE.js} +1 -1
  92. package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-C2A3EjUi.js} +1 -1
  93. package/dist/assets/{vendor-sonner-DVzioF3d.js → vendor-sonner-DNqHmUQs.js} +1 -1
  94. package/dist/assets/{vendor-stream-diffs-DWy3xSmp.js → vendor-stream-diffs-CwbKsmlv.js} +2 -2
  95. package/dist/assets/{vendor-tanstack-react-query-D7pys_c9.js → vendor-tanstack-react-query-D0iFK57C.js} +1 -1
  96. package/dist/assets/{vendor-tanstack-react-virtual-tlDgH6PG.js → vendor-tanstack-react-virtual-B2kV6cnT.js} +1 -1
  97. package/dist/assets/{vendor-yaml-DIcAK3EC.js → vendor-yaml-C9KlJAlj.js} +37 -37
  98. package/dist/assets/{vendor-zustand-B4dlGEM2.js → vendor-zustand-BqLKf6ZY.js} +1 -1
  99. package/dist/assets/zh-CN-BvmBDnkD.js +15 -0
  100. package/dist/assets/zh-TW-9wcHTX0y.js +15 -0
  101. package/dist/index.html +13 -13
  102. package/dist/mini-chat.html +13 -13
  103. package/dist/mobile.html +13 -13
  104. package/package.json +2 -2
  105. package/server/index.js +392 -20
  106. package/server/lib/assistants/DOCUMENTATION.md +26 -21
  107. package/server/lib/assistants/assign.js +2 -1
  108. package/server/lib/assistants/contact-store.js +37 -19
  109. package/server/lib/assistants/contracts.js +11 -1
  110. package/server/lib/assistants/projection-history.test.js +454 -0
  111. package/server/lib/assistants/routes.js +11 -5
  112. package/server/lib/assistants/service.js +850 -294
  113. package/server/lib/assistants/service.test.js +588 -274
  114. package/server/lib/conversations/DOCUMENTATION.md +5 -5
  115. package/server/lib/conversations/service.js +87 -57
  116. package/server/lib/conversations/service.test.js +132 -100
  117. package/server/lib/conversations/validation.js +11 -1
  118. package/server/lib/event-stream/directory-ws-bridge.js +13 -2
  119. package/server/lib/event-stream/global-hub.js +51 -5
  120. package/server/lib/event-stream/runtime.js +57 -20
  121. package/server/lib/event-stream/runtime.test.js +112 -0
  122. package/server/lib/llm/DOCUMENTATION.md +95 -84
  123. package/server/lib/llm/catalog.js +111 -45
  124. package/server/lib/llm/catalog.test.js +121 -59
  125. package/server/lib/llm/completions.js +41 -12
  126. package/server/lib/llm/completions.test.js +45 -4
  127. package/server/lib/llm/generate.js +516 -453
  128. package/server/lib/llm/generate.test.js +705 -484
  129. package/server/lib/llm/routes.js +8 -4
  130. package/server/lib/message-queue/DOCUMENTATION.md +5 -5
  131. package/server/lib/message-queue/opencode-adapter.js +191 -104
  132. package/server/lib/message-queue/opencode-adapter.test.js +328 -58
  133. package/server/lib/message-queue/runtime.js +15 -2
  134. package/server/lib/message-queue/runtime.test.js +6 -6
  135. package/server/lib/message-queue/worker.js +7 -1
  136. package/server/lib/notifications/runtime.js +5 -0
  137. package/server/lib/opencode/DOCUMENTATION.md +103 -16
  138. package/server/lib/opencode/agent-document.d.ts +28 -0
  139. package/server/lib/opencode/agent-document.js +280 -0
  140. package/server/lib/opencode/agent-document.test.js +137 -0
  141. package/server/lib/opencode/agents.js +96 -0
  142. package/server/lib/opencode/auth-state-runtime.js +5 -0
  143. package/server/lib/opencode/bootstrap-runtime.js +2 -0
  144. package/server/lib/opencode/config-entity-routes.js +111 -102
  145. package/server/lib/opencode/config-entity-routes.test.js +123 -10
  146. package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
  147. package/server/lib/opencode/ensure-cli.js +238 -0
  148. package/server/lib/opencode/ensure-cli.test.js +119 -0
  149. package/server/lib/opencode/env-runtime.js +208 -111
  150. package/server/lib/opencode/env-runtime.test.js +211 -14
  151. package/server/lib/opencode/feature-routes-runtime.js +80 -5
  152. package/server/lib/opencode/feature-routes-runtime.test.js +18 -0
  153. package/server/lib/opencode/http-server-connections.js +98 -0
  154. package/server/lib/opencode/http-server-connections.test.js +321 -0
  155. package/server/lib/opencode/lifecycle.js +539 -95
  156. package/server/lib/opencode/lifecycle.test.js +698 -6
  157. package/server/lib/opencode/managed-capabilities-runtime.js +15 -2
  158. package/server/lib/opencode/managed-capabilities-runtime.test.js +1 -0
  159. package/server/lib/opencode/network-runtime.js +28 -16
  160. package/server/lib/opencode/network-runtime.test.js +68 -4
  161. package/server/lib/opencode/openchamber-routes.js +7 -1
  162. package/server/lib/opencode/opencode2-pin.js +139 -0
  163. package/server/lib/opencode/opencode2-pin.test.js +88 -0
  164. package/server/lib/opencode/opencode2-sidecar.e2e.test.js +326 -0
  165. package/server/lib/opencode/owned-runtime-upgrade.js +431 -0
  166. package/server/lib/opencode/owned-runtime-upgrade.test.js +160 -0
  167. package/server/lib/opencode/plugin-compatibility.js +15 -0
  168. package/server/lib/opencode/plugin-compatibility.test.js +21 -0
  169. package/server/lib/opencode/plugin-routes.js +3 -18
  170. package/server/lib/opencode/plugin-routes.test.js +9 -9
  171. package/server/lib/opencode/plugins.js +4 -3
  172. package/server/lib/opencode/plugins.test.js +12 -0
  173. package/server/lib/opencode/provider-catalog.js +58 -12
  174. package/server/lib/opencode/provider-catalog.test.js +48 -0
  175. package/server/lib/opencode/proxy.js +368 -28
  176. package/server/lib/opencode/proxy.test.js +338 -0
  177. package/server/lib/opencode/routes.js +303 -76
  178. package/server/lib/opencode/routes.test.js +315 -50
  179. package/server/lib/opencode/runtime-contract.js +401 -0
  180. package/server/lib/opencode/runtime-contract.test.js +214 -0
  181. package/server/lib/opencode/server-opencode-fetch.js +242 -0
  182. package/server/lib/opencode/server-opencode-fetch.test.js +130 -0
  183. package/server/lib/opencode/server-utils-runtime.js +35 -0
  184. package/server/lib/opencode/shared-service.integration.test.js +66 -0
  185. package/server/lib/opencode/shared-service.js +147 -0
  186. package/server/lib/opencode/shared-service.test.js +93 -0
  187. package/server/lib/opencode/shared.js +3 -3
  188. package/server/lib/opencode/shutdown-runtime.js +15 -6
  189. package/server/lib/opencode/shutdown-runtime.test.js +49 -1
  190. package/server/lib/opencode/skill-routes.js +26 -40
  191. package/server/lib/opencode/skill-routes.test.js +71 -7
  192. package/server/lib/opencode/skills.test.js +15 -0
  193. package/server/lib/opencode/startup-pipeline-runtime.js +4 -0
  194. package/server/lib/opencode/static-routes-runtime.js +3 -1
  195. package/server/lib/opencode/static-routes-runtime.test.js +33 -0
  196. package/server/lib/opencode/v1-migration-gate.js +157 -0
  197. package/server/lib/opencode/v1-migration-gate.test.js +111 -0
  198. package/server/lib/opencode/v2-client.js +60 -0
  199. package/server/lib/opencode/v2-plugin-host-shim/index.mjs +155 -0
  200. package/server/lib/opencode/v2-plugin-host-shim/package.json +4 -0
  201. package/server/lib/opencode/watcher.js +2 -1
  202. package/server/lib/opencode/watcher.test.js +1 -1
  203. package/server/lib/permission-auto-accept/DOCUMENTATION.md +5 -5
  204. package/server/lib/permission-auto-accept/runtime.js +14 -7
  205. package/server/lib/permission-auto-accept/runtime.test.js +27 -12
  206. package/server/lib/relay/reasoning-projection.e2e.test.ts +22 -7
  207. package/server/lib/scheduled-tasks/DOCUMENTATION.md +79 -39
  208. package/server/lib/scheduled-tasks/managed-tool-route.js +31 -1
  209. package/server/lib/scheduled-tasks/managed-tool-route.test.js +23 -0
  210. package/server/lib/scheduled-tasks/routes.js +27 -0
  211. package/server/lib/scheduled-tasks/routes.test.js +94 -0
  212. package/server/lib/scheduled-tasks/runtime.js +381 -310
  213. package/server/lib/scheduled-tasks/runtime.test.js +600 -309
  214. package/server/lib/session-goal/DOCUMENTATION.md +173 -22
  215. package/server/lib/session-goal/capability.js +57 -0
  216. package/server/lib/session-goal/capability.test.js +33 -0
  217. package/server/lib/session-goal/routes.js +240 -2
  218. package/server/lib/session-goal/routes.test.js +211 -0
  219. package/server/lib/session-goal/runtime.js +1617 -113
  220. package/server/lib/session-goal/runtime.test.js +1500 -70
  221. package/server/lib/session-index/DOCUMENTATION.md +18 -15
  222. package/server/lib/session-index/event-ingest.js +89 -16
  223. package/server/lib/session-index/event-ingest.test.js +73 -0
  224. package/server/lib/session-index/host-authority-snapshot.js +92 -0
  225. package/server/lib/session-index/routes.host-archive.test.js +201 -0
  226. package/server/lib/session-index/routes.js +63 -10
  227. package/server/lib/session-index/routes.test.js +8 -8
  228. package/server/lib/session-index/service.js +7 -1
  229. package/server/lib/session-index/sync-runtime.js +171 -41
  230. package/server/lib/session-index/sync-runtime.test.js +164 -9
  231. package/server/lib/session-metadata/DOCUMENTATION.md +86 -0
  232. package/server/lib/session-metadata/routes.js +193 -0
  233. package/server/lib/session-metadata/routes.test.js +156 -0
  234. package/server/lib/session-metadata/session-archive.d.ts +73 -0
  235. package/server/lib/session-metadata/session-archive.js +654 -0
  236. package/server/lib/session-metadata/session-archive.test.js +359 -0
  237. package/server/lib/session-metadata/session-metadata-store.d.ts +36 -0
  238. package/server/lib/session-metadata/session-metadata-store.js +375 -0
  239. package/server/lib/session-metadata/session-metadata-store.test.js +379 -0
  240. package/server/lib/session-metadata/session-projection.d.ts +41 -0
  241. package/server/lib/session-metadata/session-projection.js +255 -0
  242. package/server/lib/session-metadata/session-projection.test.js +64 -0
  243. package/server/lib/session-metadata/system-session.js +55 -0
  244. package/server/lib/session-metadata/system-session.test.js +55 -0
  245. package/server/lib/session-title/DOCUMENTATION.md +40 -7
  246. package/server/lib/session-title/native-runtime.test.js +167 -0
  247. package/server/lib/session-title/runtime.js +67 -42
  248. package/server/lib/session-title/session-access.js +46 -0
  249. package/server/lib/session-title/session-access.test.js +91 -0
  250. package/server/lib/session-turn-pages/DOCUMENTATION.md +12 -11
  251. package/server/lib/session-turn-pages/reconcile.service.js +1 -1
  252. package/server/lib/session-turn-pages/reconcile.service.test.js +16 -0
  253. package/server/lib/session-turn-pages/routes.js +123 -75
  254. package/server/lib/session-turn-pages/routes.test.js +566 -0
  255. package/server/lib/session-turn-pages/service.js +6 -2
  256. package/server/lib/session-turn-pages/service.test.js +34 -0
  257. package/server/lib/session-turn-pages/session-message-projection.d.ts +13 -0
  258. package/server/lib/session-turn-pages/session-message-projection.js +281 -0
  259. package/server/lib/small-model/DOCUMENTATION.md +101 -146
  260. package/server/lib/small-model/catalog.js +84 -54
  261. package/server/lib/small-model/catalog.test.js +111 -9
  262. package/server/lib/small-model/custom-api.js +68 -0
  263. package/server/lib/small-model/custom-api.test.js +64 -0
  264. package/server/lib/small-model/index.js +125 -241
  265. package/server/lib/small-model/resolve.js +35 -120
  266. package/server/lib/small-model/resolve.test.js +20 -110
  267. package/server/lib/small-model/routes.js +1 -3
  268. package/server/lib/small-model/summary-settings.test.js +157 -253
  269. package/server/opencode-proxy.test.js +63 -12
  270. package/dist/assets/AssistantView-BUveAgc4.js +0 -3
  271. package/dist/assets/MarkdownRenderer-BbrbDH6-.js +0 -2
  272. package/dist/assets/MobileDetailNavigation-B0aqKXd7.js +0 -1
  273. package/dist/assets/MobileSurface-DW7WfPsy.js +0 -2
  274. package/dist/assets/OverlayScrollbar-DrA0Vsg5.js +0 -2
  275. package/dist/assets/SettingsView-PkXIavr7.js +0 -25
  276. package/dist/assets/appThemeRegistry-D5Yxg4YF.js +0 -1
  277. package/dist/assets/collapsible-D5Phaykh.js +0 -1
  278. package/dist/assets/context-menu-CubSDW2g.js +0 -1
  279. package/dist/assets/es-DA3MqOo0.js +0 -15
  280. package/dist/assets/fr-Dbz0CdIA.js +0 -13
  281. package/dist/assets/gitApi-DXyC4MiV.js +0 -7
  282. package/dist/assets/insertionBoundaries-KlOVrHHt.js +0 -1
  283. package/dist/assets/ja-DumanVQA.js +0 -15
  284. package/dist/assets/ko-C0IkiUjQ.js +0 -15
  285. package/dist/assets/main-BPS5ylib.js +0 -2
  286. package/dist/assets/main-Cpd9VWYs.js +0 -169
  287. package/dist/assets/markdown-worker-CptoICmq.js +0 -2
  288. package/dist/assets/miniChat-B8_KLWiY.js +0 -2
  289. package/dist/assets/mobile-CtktKrXn.js +0 -2
  290. package/dist/assets/multirun-CYfP9Cfm.js +0 -4
  291. package/dist/assets/pl-B9No6MQp.js +0 -15
  292. package/dist/assets/platform-0w_Q9_ji.js +0 -2
  293. package/dist/assets/projectMeta-S1hJ7oBp.js +0 -3
  294. package/dist/assets/pt-BR-DyQ0WTDh.js +0 -15
  295. package/dist/assets/renderElectronMiniChatApp-iK14LWm8.js +0 -2
  296. package/dist/assets/renderMobileApp-DORLPWnb.js +0 -28
  297. package/dist/assets/src-rS3zCcsC.css +0 -2
  298. package/dist/assets/store-5B_LDnLM.js +0 -15
  299. package/dist/assets/uk-C_1KkqyP.js +0 -15
  300. package/dist/assets/useDesktopWindowControlsLayout-Dnr-jX0C.js +0 -1
  301. package/dist/assets/useEffectiveDirectory-CZLLyarc.js +0 -1
  302. package/dist/assets/useMobileNavigationStore-CoCiqNgm.js +0 -413
  303. package/dist/assets/useSessionAutoCleanup-xQyETQkG.js +0 -2
  304. package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
  305. package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
  306. package/dist/assets/zh-CN-uWILgBdt.js +0 -15
  307. package/dist/assets/zh-TW-MYfdvhbD.js +0 -15
  308. package/server/lib/small-model/call.js +0 -427
  309. package/server/lib/small-model/call.test.js +0 -382
  310. package/server/lib/small-model/opencode-session.js +0 -339
  311. package/server/lib/small-model/opencode-session.test.js +0 -177
@@ -31,6 +31,18 @@ const flush = async () => {
31
31
  };
32
32
 
33
33
  describe('permission auto-accept runtime', () => {
34
+ it('approves a session with no explicit policy', async () => {
35
+ const fetchImpl = vi.fn(async (url, init = {}) => {
36
+ const path = new URL(url).pathname;
37
+ if (path === '/session/root') return Response.json({ id: 'root' });
38
+ if (init.method === 'POST') return Response.json({});
39
+ return new Response('', { status: 404 });
40
+ });
41
+ const { runtime } = createRuntime({ fetchImpl });
42
+ await expect(runtime.processPermission({ id: 'perm', sessionID: 'root' }, '/project')).resolves.toBe(true);
43
+ expect(fetchImpl.mock.calls.some(([url, init]) => new URL(url).pathname === '/session/root/permission/perm/reply' && JSON.parse(init.body).decision === 'always')).toBe(true);
44
+ });
45
+
34
46
  it('persists explicit session policies across runtime restarts', async () => {
35
47
  const first = createRuntime();
36
48
  await first.runtime.setSessionPolicy('root', true);
@@ -55,7 +67,7 @@ describe('permission auto-accept runtime', () => {
55
67
  it('fetches missing subagent lineage before replying', async () => {
56
68
  const fetchImpl = vi.fn(async (url, init = {}) => {
57
69
  const path = new URL(url).pathname;
58
- if (path === '/permission') return new Response('[]');
70
+ if (path === '/permission/request') return new Response('[]');
59
71
  if (path === '/session/child') return Response.json({ id: 'child', parentID: 'root', directory: '/project' });
60
72
  if (init.method === 'POST') return Response.json({});
61
73
  return new Response('', { status: 404 });
@@ -65,15 +77,15 @@ describe('permission auto-accept runtime', () => {
65
77
  fetchImpl,
66
78
  });
67
79
  await expect(runtime.processPermission({ id: 'perm', sessionID: 'child' }, '/project')).resolves.toBe(true);
68
- expect(fetchImpl.mock.calls.some(([url, init]) => new URL(url).pathname === '/permission/perm/reply' && init.method === 'POST')).toBe(true);
80
+ expect(fetchImpl.mock.calls.some(([url, init]) => new URL(url).pathname === '/session/child/permission/perm/reply' && init.method === 'POST' && JSON.parse(init.body).decision === 'always')).toBe(true);
69
81
  });
70
82
 
71
83
  it('retries a transient reply failure and deduplicates concurrent events', async () => {
72
84
  let replyAttempts = 0;
73
85
  const fetchImpl = vi.fn(async (url, init = {}) => {
74
86
  const path = new URL(url).pathname;
75
- if (path === '/permission') return new Response('[]');
76
- if (path === '/permission/perm/reply' && init.method === 'POST') {
87
+ if (path === '/permission/request') return new Response('[]');
88
+ if (path === '/session/root/permission/perm/reply' && init.method === 'POST') {
77
89
  replyAttempts += 1;
78
90
  return replyAttempts === 1 ? new Response('', { status: 503 }) : Response.json({});
79
91
  }
@@ -94,8 +106,8 @@ describe('permission auto-accept runtime', () => {
94
106
  it('reconciles pending permissions after reconnect', async () => {
95
107
  const fetchImpl = vi.fn(async (url, init = {}) => {
96
108
  const path = new URL(url).pathname;
97
- if (path === '/permission') return Response.json([{ id: 'pending', sessionID: 'root' }]);
98
- if (path === '/permission/pending/reply' && init.method === 'POST') return Response.json({});
109
+ if (path === '/permission/request') return Response.json([{ id: 'pending', sessionID: 'root' }]);
110
+ if (path === '/session/root/permission/pending/reply' && init.method === 'POST') return Response.json({});
99
111
  return Response.json({ id: 'root' });
100
112
  });
101
113
  const { connect } = createRuntime({
@@ -104,14 +116,14 @@ describe('permission auto-accept runtime', () => {
104
116
  });
105
117
  connect();
106
118
  await flush();
107
- expect(fetchImpl.mock.calls.some(([url]) => new URL(url).pathname === '/permission/pending/reply')).toBe(true);
119
+ expect(fetchImpl.mock.calls.some(([url]) => new URL(url).pathname === '/session/root/permission/pending/reply')).toBe(true);
108
120
  });
109
121
 
110
122
  it('accepts existing pending permissions when a session policy is enabled', async () => {
111
123
  const fetchImpl = vi.fn(async (url, init = {}) => {
112
124
  const parsed = new URL(url);
113
125
  const path = parsed.pathname;
114
- if (path === '/permission') {
126
+ if (path === '/permission/request') {
115
127
  return parsed.searchParams.get('directory') === '/project'
116
128
  ? Response.json([
117
129
  { id: 'root-pending', sessionID: 'root' },
@@ -119,19 +131,22 @@ describe('permission auto-accept runtime', () => {
119
131
  ])
120
132
  : Response.json([]);
121
133
  }
122
- if (path === '/permission/root-pending/reply' && init.method === 'POST') return Response.json({});
134
+ if (init.method === 'POST' && path.endsWith('/reply')) return Response.json({});
123
135
  if (path === '/session/other') return Response.json({ id: 'other' });
124
136
  return new Response('', { status: 404 });
125
137
  });
126
- const { runtime } = createRuntime({ fetchImpl });
138
+ const { runtime } = createRuntime({
139
+ stored: { permissionAutoAccept: { sessions: { other: false } } },
140
+ fetchImpl,
141
+ });
127
142
 
128
143
  await runtime.setSessionPolicy('root', true, '/project');
129
144
 
130
145
  const replyPaths = fetchImpl.mock.calls
131
146
  .filter(([, init]) => init?.method === 'POST')
132
147
  .map(([url]) => new URL(url).pathname);
133
- expect(replyPaths).toEqual(['/permission/root-pending/reply']);
148
+ expect(replyPaths).toEqual(['/session/root/permission/root-pending/reply']);
134
149
  expect(fetchImpl.mock.calls.some(([url]) => new URL(url).searchParams.get('directory') === '/project')).toBe(true);
135
- expect(await runtime.load()).toEqual({ sessions: { root: true } });
150
+ expect(await runtime.load()).toEqual({ sessions: { other: false, root: true } });
136
151
  });
137
152
  });
@@ -147,7 +147,11 @@ const startFakeOpenCode = async (): Promise<{
147
147
  next();
148
148
  });
149
149
 
150
- app.get('/global/event', (req, res) => {
150
+ // Production proxy forwards official v2 paths with `/api` restored
151
+ // (`toUpstreamOpenCodeApiPath`). Host mounts both `/api/global/event` and
152
+ // `/api/event`; fake upstream must match those upstream targets — not the
153
+ // pre-v2 bare `/global/event` or `/event`.
154
+ const writeSseFixture = (res: express.Response) => {
151
155
  res.writeHead(200, {
152
156
  'content-type': 'text/event-stream; charset=utf-8',
153
157
  'cache-control': 'no-cache',
@@ -243,9 +247,16 @@ const startFakeOpenCode = async (): Promise<{
243
247
  );
244
248
 
245
249
  res.end();
250
+ };
251
+
252
+ app.get('/api/global/event', (_req, res) => {
253
+ writeSseFixture(res);
254
+ });
255
+ app.get('/api/event', (_req, res) => {
256
+ writeSseFixture(res);
246
257
  });
247
258
 
248
- app.get('/session/:sessionID/message', (req, res) => {
259
+ app.get('/api/session/:sessionID/message', (_req, res) => {
249
260
  res.json([
250
261
  {
251
262
  info: {
@@ -513,11 +524,15 @@ describe('relay reasoning projection e2e (Host filter over private relay)', () =
513
524
  for (const url of upstream.observation.urls) {
514
525
  expect(url).not.toContain('includeReasoning');
515
526
  }
516
- // Sanity: upstream still saw the event + message routes.
517
- expect(upstream.observation.urls.some((u) => u.includes('/global/event'))).toBe(true);
518
- expect(upstream.observation.urls.some((u) => u.includes('/session/ses_1/message'))).toBe(
519
- true,
520
- );
527
+ // Sanity: upstream still saw official v2 event + message routes.
528
+ expect(
529
+ upstream.observation.urls.some(
530
+ (u) => u.includes('/api/global/event') || u.includes('/api/event'),
531
+ ),
532
+ ).toBe(true);
533
+ expect(
534
+ upstream.observation.urls.some((u) => u.includes('/api/session/ses_1/message')),
535
+ ).toBe(true);
521
536
 
522
537
  // Unauthenticated tunnel request still hits real auth gate (not open).
523
538
  const denied = await client.fetch('/api/global/event?includeReasoning=false', {
@@ -15,7 +15,7 @@ Server-owned scheduled task runtime and routes for OpenChamber-only automation.
15
15
  - Next-run computation (daily/weekly/cron compatibility)
16
16
  - Timer scheduling and queueing
17
17
  - Concurrency controls
18
- - Session create → archive → goal/prompt/command execution
18
+ - Session create → goal/prompt/command execution
19
19
  - Emits OpenChamber task-run events
20
20
  - Isolates project sync failures, retries each failed project up to three times,
21
21
  and clears pending retries during shutdown
@@ -69,6 +69,14 @@ Server-owned scheduled task runtime and routes for OpenChamber-only automation.
69
69
  - `runNow(projectId, taskId)`
70
70
  - `observeSessionEvent(event)`
71
71
 
72
+ Isolation dependencies (required for sidebar hide):
73
+
74
+ - `persistSessionMetadata(sessionID, patch)` — Host store write. Missing
75
+ persist refuses the run **before** `session.create`. Persist failure after
76
+ create fails the run before prompt.
77
+ - `onSystemSessionPersisted({ sessionID, directory, metadata })` — drop the
78
+ session from the sidebar index immediately after Host commit.
79
+
72
80
  Dependencies include optional `runHistoryStore` with:
73
81
 
74
82
  - `startRun(record)`
@@ -186,44 +194,55 @@ Every actual run (timer or manual):
186
194
  4. Create an OpenCode session with:
187
195
  - title: `[Scheduled] ${taskName} yyyy-LL-dd HH:mm` (total length ≤ 120)
188
196
  - metadata: `openchamber.scheduledTask = { projectID, taskID, runID, name }`
189
- - SDK/HTTP calls receive `{ signal }` as the second request-options argument
190
- (`client.session.create(params, { signal })`, same for `update` /
191
- `command` / `command.list`; raw `fetch` for goal PATCH and `prompt_async`
192
- pass `signal` in `RequestInit`).
193
- 5. `attachSession(runID, sessionID)` immediately after create.
194
- 6. Archive the session via SDK `client.session.update({ sessionID, directory, time: { archived } })`.
195
- - 404 receives one short bounded retry (also signal-aware); other failures
196
- abort without prompt.
197
- - If attach fails, the runtime still attempts archive, then fails the run.
198
- 7. Only after a successful archive: goal metadata PATCH (preserving the
199
- `scheduledTask` marker together with `goal`), then command or `prompt_async`.
200
- Non-cancellable async gaps (small-model distill, objective file write) check
201
- `signal.throwIfAborted()` before continuing so a timed-out run never prompts.
202
- **Admission is not completion:** `prompt_async` / command return when the
197
+ - SDK/HTTP calls receive `{ signal }` as the second request-options argument
198
+ (`client.session.create(params, { signal })`, same for `prompt` /
199
+ `command` / `command.list`; raw `fetch` for goal PATCH
200
+ pass `signal` in `RequestInit`).
201
+ 5. `attachSession(runID, sessionID)` immediately after create.
202
+ - If attach fails, the runtime fails the run without prompting.
203
+ 6. When `goalEnabled`: refuse before OpenCode ready / session create if
204
+ `persistSessionGoal` is not injected; otherwise write objective file,
205
+ persist an active goal into the Host session-metadata store, optionally
206
+ notify `onGoalPersisted` to arm the goal loop, then command or
207
+ `session.prompt`. Non-cancellable async gaps (small-model distill,
208
+ objective file write) check `signal.throwIfAborted()` before continuing so
209
+ a timed-out run never prompts.
210
+ **Admission is not completion:** `session.prompt` / command return when the
203
211
  turn is accepted, not when the agent finishes.
204
- 8. Wait for the real session outcome (bounded by the same watchdog):
205
- - Poll `session.status` + `session.messages` until the session is idle and
206
- the tail is a settled assistant turn (completed, or incomplete-but-stable
207
- while idle). Assistant `error` (including abort) finalizes as `error`.
208
- - Goal-enabled runs poll `session.get` for terminal goal status
209
- (`complete` → success; `blocked` / `budgetLimited` → error) and do not
210
- finish on the first idle between goal turns.
211
- - `durationMs` / `finishedAt` are wall-clock from run start through this
212
- settlement — not the prompt admission latency.
213
- 9. On watchdog timeout after a session exists: immediately throw the canonical
212
+ 7. Wait for the real session outcome (bounded by the same watchdog):
213
+ - Poll `session.active` + `message.list` until the session is idle and
214
+ the **latest-side** message page settles. Settlement always uses
215
+ `message.list({ limit: 50, order: 'desc' })` (newest first) — never
216
+ `order: 'asc'` page-0 + `at(-1)`, which reads an old page once the
217
+ session exceeds the limit.
218
+ - Real terminal success requires an assistant tail with `time.completed`
219
+ and finish not `tool-calls` (prefer `finish: 'stop'`). `finish:
220
+ 'tool-calls'`, incomplete assistants (no `time.completed`), and
221
+ non-assistant tails such as `model-switched` never succeed — including
222
+ after multiple incomplete idle probes.
223
+ - `session.active` / `message.list` failures stay **unknown** (keep
224
+ polling under the watchdog); they never masquerade as idle success.
225
+ - Assistant `error` (and terminal finish `error` / `length` /
226
+ `content-filter`) finalizes as `error`.
227
+ - Goal-enabled runs: settle via Host goal terminal status when the store
228
+ seam is available (`readSessionMetadata` preferred over OpenCode
229
+ `session.get`). Store read failure is never empty-goal success.
230
+ - `durationMs` / `finishedAt` are wall-clock from run start through this
231
+ settlement — not the prompt admission latency.
232
+ 8. On watchdog timeout after a session exists: immediately throw the canonical
214
233
  `schedule run timed out` without awaiting non-cancellable helper work. A
215
234
  once-only abort listener registered after session create starts best-effort
216
- `client.session.abort({ sessionID, directory })` as soon as the watchdog
217
- aborts the signal (or right after create if the signal already aborted).
218
- Abort failures must not replace the timeout error. Successful runs and
219
- ordinary non-timeout failures never call session abort.
220
- 10. Finalize history with the ultimate run status (timeout → `error` with
235
+ `client.session.interrupt({ sessionID, continue: false })` as soon as the watchdog
236
+ aborts the signal (or right after create if the signal already aborted).
237
+ Interrupt failures must not replace the timeout error. Successful runs and
238
+ ordinary non-timeout failures never call session interrupt.
239
+ 9. Finalize history with the ultimate run status (timeout → `error` with
221
240
  `schedule run timed out`; assistant/goal failure → `error` with the
222
241
  recorded reason). An already-attached `session_id` remains on the history
223
242
  row so the session stays openable from history. Task state persistence
224
243
  failures also finalize history as `error`. A failed history finalize
225
244
  returns error and may leave a `running` row for next-start convergence.
226
- 11. `lastSessionId` is written on the task state whenever the run has a
245
+ 10. `lastSessionId` is written on the task state whenever the run has a
227
246
  session (success or error). If the run has no session, omit
228
247
  `lastSessionId` from the patch so a previous value is preserved. Full
229
248
  association still lives in the history table; history rows are not
@@ -239,14 +258,35 @@ that same history session. `observeSessionEvent` corrects **only task state**
239
258
 
240
259
  - `session.status` busy/retry while `lastStatus` is `error` or `success` →
241
260
  `lastStatus` `running`, clear `lastError`, keep/set `lastSessionId`
242
- - later `session.idle` (or idle `session.status`) with a success snapshot →
243
- `lastStatus` `success`
261
+ - later `session.idle` (or idle `session.status`) with a success snapshot from
262
+ `session.active` + latest-side `message.list` → `lastStatus` `success`
244
263
  - do not change `lastRunAt` / `lastDurationMs` / `nextRunAt`
264
+ - history rows are never rewritten; `finishRun` is not called again
245
265
 
246
266
  A live run (`runningTaskKeys`) owns settlement; the observer is a no-op while
247
267
  the task is in `runningTaskKeys`. Idle correction is a no-op if `lastStatus`
248
- is already `success`, or if the snapshot is still error/busy/unknown. Observer
249
- failures are logged and must not throw out of the event bus.
268
+ is already `success`, or if the snapshot is still error/busy/unknown
269
+ (`session.active` failure and goal-unsupported goal tasks stay unknown).
270
+ Observer failures are logged and must not throw out of the event bus.
271
+
272
+ ## Goal safety gating
273
+
274
+ Session goal Host state is advertised by
275
+ `packages/web/server/lib/session-goal/capability.js` as `{ supported: true }`.
276
+ Goal records live in the OpenChamber session-metadata store (same seam as
277
+ manual UI / `session-goal` runtime). Scheduled runs inject
278
+ `persistSessionGoal`, `readSessionMetadata`, and optional `onGoalPersisted`
279
+ from `server/index.js`.
280
+
281
+ | Path | Behavior |
282
+ |---|---|
283
+ | `PUT` scheduled task / managed-tool create·update with `execution.goalEnabled` | Allowed when capability supported |
284
+ | Existing `goalEnabled` task due or manual run | Requires `persistSessionGoal`; without it fail **before** OpenCode ready / session create / objective write / prompt; with it write objective + active goal then prompt |
285
+ | Ordinary non-goal task | Unchanged |
286
+ | Post-run observer on a goal-enabled task | Prefer Host store goal terminal status; never invent success from assistant-tail idle alone when goal is active |
287
+
288
+ Do not treat objective-file hints or first-turn assistant idle as a registered
289
+ goal success without a store-backed goal record.
250
290
 
251
291
  Run start also emits `scheduled-task-ran` with `status: running` as soon as
252
292
  task state is persisted, so the outer task list can show in-progress before
@@ -255,21 +295,21 @@ the session is attached.
255
295
  ## Watchdog cancel / upstream abort
256
296
 
257
297
  - Timeout path: abort the per-run signal → cancel in-flight cancellable
258
- SDK/HTTP → start best-effort OpenCode `session.abort` via the create-time
298
+ SDK/HTTP → start best-effort OpenCode `session.interrupt` via the create-time
259
299
  abort listener (independent of whether the current await is cancellable) →
260
300
  **do not wait** for non-cancellable helpers (small-model distill /
261
301
  `writeObjective`) to finish → history and task state finalize as `error`
262
302
  with `schedule run timed out` within the watchdog bound. The detached
263
303
  `runPromise` is settled with a catch so late rejections are not unhandled.
264
304
  - When uncancellable helper work eventually finishes, post-await
265
- `throwIfAborted` gates stop goal PATCH / command / `prompt_async`.
266
- - Non-timeout failures: no timeout abort; no forced `session.abort` from the
305
+ `throwIfAborted` gates stop goal PATCH / command / `session.prompt`.
306
+ - Non-timeout failures: no timeout abort; no forced `session.interrupt` from the
267
307
  watchdog.
268
308
  - Runtime `stop()` clears timers, project-sync retries, and the queue only; it
269
309
  does not abort in-flight run controllers.
270
310
  - Remaining non-cancellable edges: work already fully admitted and completed
271
311
  HTTP-wise before timeout cannot be rolled back by aborting the HTTP request
272
- alone; `session.abort` is the upstream stop for admitted command/prompt
312
+ alone; `session.interrupt` is the upstream stop for admitted command/prompt
273
313
  execution. Small-model distillation and objective file writes are not passed
274
314
  a signal; the watchdog does not await them, and abort gates block later
275
315
  stages after they complete.
@@ -1,3 +1,8 @@
1
+ import {
2
+ getSessionGoalCapability,
3
+ isSessionGoalSupported,
4
+ sessionGoalUnavailableMessage,
5
+ } from '../session-goal/capability.js';
1
6
  import { MANAGED_SCHEDULED_TASK_TOOL_PATH } from './managed-tool-contract.js';
2
7
 
3
8
  const MAX_ID_LENGTH = 512;
@@ -263,6 +268,17 @@ export const registerScheduledTaskToolRoute = (app, dependencies) => {
263
268
  : new Set(['taskId', 'name', 'enabled', 'schedule', 'execution']), 'input');
264
269
  validateTaskInputFields(input);
265
270
 
271
+ const refuseGoalExecution = (execution) => {
272
+ if (execution?.goalEnabled !== true || isSessionGoalSupported()) {
273
+ return null;
274
+ }
275
+ const capability = getSessionGoalCapability();
276
+ const error = new Error(sessionGoalUnavailableMessage(capability));
277
+ error.statusCode = 501;
278
+ error.capability = capability;
279
+ return error;
280
+ };
281
+
266
282
  if (operation === 'update') {
267
283
  const patch = {
268
284
  ...(input.name === undefined ? {} : { name: input.name }),
@@ -270,6 +286,8 @@ export const registerScheduledTaskToolRoute = (app, dependencies) => {
270
286
  ...(input.schedule === undefined ? {} : { schedule: input.schedule }),
271
287
  ...(input.execution === undefined ? {} : { execution: input.execution }),
272
288
  };
289
+ const goalRefusal = refuseGoalExecution(patch.execution);
290
+ if (goalRefusal) throw goalRefusal;
273
291
  const patched = await projectConfigRuntime.patchScheduledTask(project.id, taskID, patch);
274
292
  if (!patched?.task) return res.status(404).json({ error: 'Schedule not found' });
275
293
  const schedulerSynced = await syncAfterMutation(project.id, operation);
@@ -285,6 +303,8 @@ export const registerScheduledTaskToolRoute = (app, dependencies) => {
285
303
  if (operation === 'create' && (!execution.providerID || !execution.modelID)) {
286
304
  throw new Error('execution.providerID and execution.modelID are required from the user message or input');
287
305
  }
306
+ const goalRefusal = refuseGoalExecution(execution);
307
+ if (goalRefusal) throw goalRefusal;
288
308
  const taskInput = {
289
309
  ...(input.name === undefined ? {} : { name: input.name }),
290
310
  ...(input.enabled === undefined ? {} : { enabled: input.enabled }),
@@ -296,7 +316,17 @@ export const registerScheduledTaskToolRoute = (app, dependencies) => {
296
316
  return res.status(201).json({ projectId: project.id, created: upserted.created, task: upserted.task, tasks: upserted.tasks, schedulerSynced });
297
317
  } catch (error) {
298
318
  const statusCode = error?.statusCode || (isValidationError(error?.message || '') ? 400 : 500);
299
- if (statusCode >= 500) logger.error?.('[ScheduledTaskTool] request failed', { operation: req.body?.operation, statusCode });
319
+ if (statusCode >= 500 && statusCode !== 501) {
320
+ logger.error?.('[ScheduledTaskTool] request failed', { operation: req.body?.operation, statusCode });
321
+ }
322
+ if (statusCode === 501) {
323
+ const capability = error?.capability || getSessionGoalCapability();
324
+ return res.status(501).json({
325
+ error: error?.message || sessionGoalUnavailableMessage(capability),
326
+ reason: capability.reason,
327
+ capability,
328
+ });
329
+ }
300
330
  return res.status(statusCode).json({ error: statusCode >= 500 ? 'Failed to manage schedule' : (error?.message || 'Invalid schedule request') });
301
331
  }
302
332
  });
@@ -265,4 +265,27 @@ describe('managed scheduled task tool route', () => {
265
265
  expect(oversized.status).toBe(413);
266
266
  } finally { fixture.restore(); }
267
267
  });
268
+
269
+ it('allows create/update with goalEnabled when Host goal state is supported', async () => {
270
+ const fixture = createApp();
271
+ try {
272
+ const created = await request(fixture.app).post(MANAGED_SCHEDULED_TASK_TOOL_PATH).send(body('create', {
273
+ name: 'Goal task',
274
+ schedule: { kind: 'daily', time: '10:00', timezone: 'UTC' },
275
+ execution: { prompt: 'review', goalEnabled: true },
276
+ }));
277
+ expect(created.status).toBe(201);
278
+ expect(fixture.projectConfigRuntime.upsertScheduledTask).toHaveBeenCalled();
279
+ expect(fixture.projectConfigRuntime.upsertScheduledTask.mock.calls[0][1]).toMatchObject({
280
+ execution: expect.objectContaining({ goalEnabled: true }),
281
+ });
282
+
283
+ const updated = await request(fixture.app).post(MANAGED_SCHEDULED_TASK_TOOL_PATH).send(body('update', {
284
+ taskId: 'task_1',
285
+ execution: { goalEnabled: true },
286
+ }));
287
+ expect(updated.status).toBe(200);
288
+ expect(fixture.projectConfigRuntime.patchScheduledTask).toHaveBeenCalled();
289
+ } finally { fixture.restore(); }
290
+ });
268
291
  });
@@ -1,3 +1,9 @@
1
+ import {
2
+ getSessionGoalCapability,
3
+ isSessionGoalSupported,
4
+ sessionGoalUnavailableMessage,
5
+ } from '../session-goal/capability.js';
6
+
1
7
  const asNonEmptyString = (value) => {
2
8
  if (typeof value !== 'string') {
3
9
  return null;
@@ -6,6 +12,22 @@ const asNonEmptyString = (value) => {
6
12
  return trimmed.length > 0 ? trimmed : null;
7
13
  };
8
14
 
15
+ /** Reject create/edit that enables goal when Host goal state is unavailable. */
16
+ const rejectUnsupportedGoalExecution = (taskInput) => {
17
+ if (taskInput?.execution?.goalEnabled !== true) {
18
+ return null;
19
+ }
20
+ if (isSessionGoalSupported()) {
21
+ return null;
22
+ }
23
+ const capability = getSessionGoalCapability();
24
+ return {
25
+ error: sessionGoalUnavailableMessage(capability),
26
+ reason: capability.reason,
27
+ capability,
28
+ };
29
+ };
30
+
9
31
  const parseProjectID = (req) => asNonEmptyString(req?.params?.projectId);
10
32
  const parseTaskID = (req) => asNonEmptyString(req?.params?.taskId);
11
33
 
@@ -182,6 +204,11 @@ export const registerScheduledTaskRoutes = (app, dependencies) => {
182
204
  return res.status(404).json({ error: 'Project not found' });
183
205
  }
184
206
 
207
+ const goalRefusal = rejectUnsupportedGoalExecution(taskInput);
208
+ if (goalRefusal) {
209
+ return res.status(501).json(goalRefusal);
210
+ }
211
+
185
212
  const upserted = await projectConfigRuntime.upsertScheduledTask(projectID, taskInput);
186
213
  const schedulerSynced = await syncAfterMutation(projectID);
187
214
 
@@ -359,4 +359,98 @@ describe('scheduled task mutation routes', () => {
359
359
  await Promise.resolve();
360
360
  expect(scheduledTasksRuntime.syncProject).toHaveBeenCalledTimes(4);
361
361
  });
362
+
363
+ it('allows create/edit with goalEnabled when Host goal state is supported', async () => {
364
+ const savedTask = {
365
+ id: 'goal-task',
366
+ name: 'Goal task',
367
+ execution: { prompt: 'do the work', goalEnabled: true },
368
+ };
369
+ const projectConfigRuntime = {
370
+ upsertScheduledTask: vi.fn(async () => ({ task: savedTask, tasks: [savedTask], created: true })),
371
+ deleteScheduledTask: vi.fn(),
372
+ listScheduledTasks: vi.fn(),
373
+ };
374
+ const routes = registerMutationRoutes({
375
+ projectConfigRuntime,
376
+ scheduledTasksRuntime: { syncProject: vi.fn(async () => {}) },
377
+ scheduleSyncRetry: vi.fn(),
378
+ });
379
+
380
+ const created = createResponse();
381
+ await routes.put({
382
+ params: { projectId: 'project-a' },
383
+ body: {
384
+ task: {
385
+ name: 'Goal task',
386
+ execution: {
387
+ prompt: 'do the work',
388
+ providerID: 'openai',
389
+ modelID: 'gpt',
390
+ goalEnabled: true,
391
+ },
392
+ },
393
+ },
394
+ }, created);
395
+
396
+ expect(created.statusCode).toBe(200);
397
+ expect(projectConfigRuntime.upsertScheduledTask).toHaveBeenCalledTimes(1);
398
+ expect(projectConfigRuntime.upsertScheduledTask.mock.calls[0][1]).toMatchObject({
399
+ execution: expect.objectContaining({ goalEnabled: true }),
400
+ });
401
+
402
+ const updatedTask = { ...savedTask, id: 'existing', name: 'Existing' };
403
+ projectConfigRuntime.upsertScheduledTask.mockResolvedValueOnce({
404
+ task: updatedTask,
405
+ tasks: [updatedTask],
406
+ created: false,
407
+ });
408
+ const updated = createResponse();
409
+ await routes.put({
410
+ params: { projectId: 'project-a' },
411
+ body: {
412
+ task: {
413
+ id: 'existing',
414
+ name: 'Existing',
415
+ execution: {
416
+ prompt: 'do the work',
417
+ providerID: 'openai',
418
+ modelID: 'gpt',
419
+ goalEnabled: true,
420
+ },
421
+ },
422
+ },
423
+ }, updated);
424
+
425
+ expect(updated.statusCode).toBe(200);
426
+ expect(projectConfigRuntime.upsertScheduledTask).toHaveBeenCalledTimes(2);
427
+ });
428
+
429
+ it('allows ordinary non-goal task create when goal is unsupported', async () => {
430
+ const savedTask = { id: 'plain', name: 'Plain' };
431
+ const projectConfigRuntime = {
432
+ upsertScheduledTask: vi.fn(async () => ({ task: savedTask, tasks: [savedTask], created: true })),
433
+ deleteScheduledTask: vi.fn(),
434
+ listScheduledTasks: vi.fn(),
435
+ };
436
+ const routes = registerMutationRoutes({
437
+ projectConfigRuntime,
438
+ scheduledTasksRuntime: { syncProject: vi.fn(async () => {}) },
439
+ scheduleSyncRetry: vi.fn(),
440
+ });
441
+
442
+ const response = createResponse();
443
+ await routes.put({
444
+ params: { projectId: 'project-a' },
445
+ body: {
446
+ task: {
447
+ name: 'Plain',
448
+ execution: { prompt: 'hello', providerID: 'openai', modelID: 'gpt' },
449
+ },
450
+ },
451
+ }, response);
452
+
453
+ expect(response.statusCode).toBe(200);
454
+ expect(projectConfigRuntime.upsertScheduledTask).toHaveBeenCalledTimes(1);
455
+ });
362
456
  });