@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
@@ -1,8 +1,14 @@
1
1
  import crypto from 'node:crypto';
2
- import { createOpencodeClient } from '@opencode-ai/sdk/v2';
3
2
  import { DateTime } from 'luxon';
4
3
  import parser from 'cron-parser';
4
+ import { makeOpenCodeV2Client } from '../opencode/v2-client.js';
5
5
  import { expandSnippets } from '../opencode/snippets.js';
6
+ import {
7
+ getSessionGoalCapability,
8
+ isSessionGoalSupported,
9
+ sessionGoalUnavailableMessage,
10
+ } from '../session-goal/capability.js';
11
+ import { buildScheduledTaskMetadata } from '../session-metadata/system-session.js';
6
12
  import { SCHEDULED_SLOT_CLAIMED_CODE } from './run-history-store.js';
7
13
 
8
14
  const DEFAULT_GLOBAL_CONCURRENCY = 4;
@@ -15,16 +21,20 @@ const TASK_DUE_SLACK_MS = 5_000;
15
21
  const MAX_TIMER_DELAY_MS = 2_147_483_647;
16
22
  const PROJECT_SYNC_RETRY_DELAY_MS = 1_000;
17
23
  const MAX_PROJECT_SYNC_RETRIES = 3;
18
- const ARCHIVE_404_RETRY_DELAY_MS = 250;
19
24
  /** Poll interval while waiting for the OpenCode session turn (or goal) to settle. */
20
25
  const SESSION_SETTLEMENT_POLL_MS = 1_000;
21
26
  /**
22
- * Incomplete assistant tails while idle need a couple of stable polls before we
23
- * treat them as settled (OpenCode sometimes leaves time.completed unset).
27
+ * Latest-side message page for settlement. order=desc + bounded limit — never
28
+ * order=asc page-0 + at(-1), which reads an old page once the session exceeds
29
+ * the limit.
24
30
  */
25
- const INCOMPLETE_ASSISTANT_SETTLE_PROBES = 2;
31
+ const SESSION_SETTLEMENT_MESSAGE_LIMIT = 50;
26
32
  /** Goal terminal statuses — active/paused mean the run is still open. */
27
33
  const GOAL_TERMINAL_STATUSES = new Set(['complete', 'blocked', 'budgetLimited']);
34
+ /** Assistant finish values that are real terminal success (not tool-calls mid-loop). */
35
+ const ASSISTANT_SUCCESS_FINISH = new Set(['stop']);
36
+ /** Assistant finish values that are terminal failure without an error object. */
37
+ const ASSISTANT_ERROR_FINISH = new Set(['error', 'length', 'content-filter']);
28
38
 
29
39
  const buildTaskKey = (projectID, taskID) => `${projectID}:${taskID}`;
30
40
 
@@ -235,22 +245,6 @@ export const formatScheduledSessionTitle = (task, nowMs = Date.now()) => {
235
245
  return `${SCHEDULED_TITLE_PREFIX}${trimmedName}${suffix}`;
236
246
  };
237
247
 
238
- const buildScheduledTaskMetadata = ({ projectID, taskID, runID, name }) => ({
239
- openchamber: {
240
- scheduledTask: {
241
- projectID,
242
- taskID,
243
- runID,
244
- name,
245
- },
246
- },
247
- });
248
-
249
- const isMissingSessionError = (result) => {
250
- const status = result?.error?.status ?? result?.error?.statusCode ?? result?.status;
251
- return status === 404 || result?.error?.code === 'not_found';
252
- };
253
-
254
248
  const sleep = (ms, signal) => new Promise((resolve, reject) => {
255
249
  if (signal?.aborted) {
256
250
  reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
@@ -273,9 +267,11 @@ const sleep = (ms, signal) => new Promise((resolve, reject) => {
273
267
 
274
268
  const readMessageInfo = (entry) => {
275
269
  if (!entry || typeof entry !== 'object') return null;
276
- const info = entry.info && typeof entry.info === 'object' ? entry.info : entry;
277
- if (!info || typeof info !== 'object') return null;
278
- return info;
270
+ // SessionMessageInfo is the entry itself; tolerate projected { info, parts }.
271
+ if (entry.info && typeof entry.info === 'object') {
272
+ return entry.info;
273
+ }
274
+ return entry;
279
275
  };
280
276
 
281
277
  const formatAssistantError = (error) => {
@@ -291,6 +287,79 @@ const formatAssistantError = (error) => {
291
287
  return 'assistant error';
292
288
  };
293
289
 
290
+ const isAssistantMessage = (info) => (
291
+ info
292
+ && typeof info === 'object'
293
+ && (info.type === 'assistant' || info.role === 'assistant')
294
+ );
295
+
296
+ const isUserMessage = (info) => (
297
+ info
298
+ && typeof info === 'object'
299
+ && (info.type === 'user' || info.role === 'user')
300
+ );
301
+
302
+ /**
303
+ * Classify the newest message on a latest-side page.
304
+ * - finish tool-calls is mid-loop, never success
305
+ * - incomplete (no time.completed) is never success
306
+ * - model-switched / non-assistant tails are not success
307
+ * - only a completed assistant with a real terminal finish succeeds
308
+ */
309
+ const classifyMessageTail = (messages) => {
310
+ if (!Array.isArray(messages) || messages.length === 0) {
311
+ return { outcome: 'unknown' };
312
+ }
313
+ // order=desc contract: index 0 is the newest message on this page.
314
+ // Do not re-rank by time.created — that reintroduces the old-page bug when
315
+ // a bounded page mixes ages (and fixtures / clock skew can invert created).
316
+ const latest = readMessageInfo(messages[0]);
317
+ if (!latest) {
318
+ return { outcome: 'unknown' };
319
+ }
320
+ if (isAssistantMessage(latest)) {
321
+ if (latest.error) {
322
+ return {
323
+ outcome: 'error',
324
+ error: formatAssistantError(latest.error),
325
+ };
326
+ }
327
+ const finish = typeof latest.finish === 'string' ? latest.finish.trim() : '';
328
+ if (finish === 'tool-calls') {
329
+ // Model paused for tools — not a successful run terminal.
330
+ return { outcome: 'busy' };
331
+ }
332
+ if (ASSISTANT_ERROR_FINISH.has(finish)) {
333
+ return {
334
+ outcome: 'error',
335
+ error: finish === 'error' ? 'assistant error' : `assistant finish ${finish}`,
336
+ };
337
+ }
338
+ if (latest.time?.completed && (!finish || ASSISTANT_SUCCESS_FINISH.has(finish))) {
339
+ return { outcome: 'success' };
340
+ }
341
+ // Incomplete assistant (no completed, or unknown finish) — not success.
342
+ return { outcome: 'busy' };
343
+ }
344
+ if (isUserMessage(latest)) {
345
+ return { outcome: 'awaiting_assistant', lastInfo: latest };
346
+ }
347
+ // model-switched, compaction, system, etc. — not a success terminal.
348
+ return { outcome: 'busy' };
349
+ };
350
+
351
+ const listLatestMessages = async (client, sessionID, requestOptions) => {
352
+ if (typeof client?.message?.list !== 'function') {
353
+ return null;
354
+ }
355
+ const messagesResult = await client.message.list({
356
+ sessionID,
357
+ limit: SESSION_SETTLEMENT_MESSAGE_LIMIT,
358
+ order: 'desc',
359
+ }, requestOptions);
360
+ return Array.isArray(messagesResult?.data) ? messagesResult.data : null;
361
+ };
362
+
294
363
  const extractGoalFromSession = (session) => {
295
364
  const metadata = session?.metadata;
296
365
  if (!metadata || typeof metadata !== 'object') return null;
@@ -308,9 +377,13 @@ const extractGoalFromSession = (session) => {
308
377
 
309
378
  /**
310
379
  * Single-shot session outcome for post-run continuation (no polling).
311
- * Goal-enabled: terminal complete → success; blocked/budgetLimited → error;
312
- * otherwise not-yet-success. Non-goal: busy/retry, assistant error, or
313
- * completed assistant; incomplete/empty tails are not treated as success.
380
+ * Goal-enabled + supported: terminal complete → success; blocked/budgetLimited
381
+ * → error; otherwise not-yet-success. Goal unsupported: never invent success.
382
+ * Prefer Host `readSessionMetadata` over OpenCode `session.get` (direct clients
383
+ * do not see the Host store). Store read failure is never empty-goal success.
384
+ * Non-goal: session.active failure stays unknown; latest-side assistant tail
385
+ * must be a real terminal (finish tool-calls / incomplete / model-switch ≠
386
+ * success). History rows are never rewritten here.
314
387
  */
315
388
  const snapshotSessionOutcome = async ({
316
389
  client,
@@ -318,74 +391,90 @@ const snapshotSessionOutcome = async ({
318
391
  projectPath,
319
392
  goalEnabled,
320
393
  signal,
394
+ readSessionMetadata = null,
321
395
  }) => {
322
396
  const requestOptions = signal ? { signal } : undefined;
323
397
 
324
- if (goalEnabled && typeof client?.session?.get === 'function') {
325
- try {
326
- const sessionResult = await client.session.get({
327
- sessionID,
328
- directory: projectPath,
329
- }, requestOptions);
330
- if (!sessionResult?.error) {
331
- const goal = extractGoalFromSession(sessionResult?.data);
332
- if (goal && GOAL_TERMINAL_STATUSES.has(goal.status)) {
333
- if (goal.status === 'complete') {
334
- return { outcome: 'success' };
335
- }
336
- return {
337
- outcome: 'error',
338
- error: goal.note || `goal ${goal.status}`,
339
- };
398
+ if (goalEnabled) {
399
+ if (!isSessionGoalSupported()) {
400
+ // Host goal state unavailable — do not promote assistant-tail idle to success.
401
+ return { outcome: 'busy' };
402
+ }
403
+
404
+ const classifyGoal = (goal) => {
405
+ if (goal && GOAL_TERMINAL_STATUSES.has(goal.status)) {
406
+ if (goal.status === 'complete') {
407
+ return { outcome: 'success' };
340
408
  }
409
+ return {
410
+ outcome: 'error',
411
+ error: goal.note || `goal ${goal.status}`,
412
+ };
341
413
  }
342
- } catch (error) {
343
- if (signal?.aborted) throw error;
414
+ return null;
415
+ };
416
+
417
+ // Prefer Host store: OpenCode session.get cannot see openchamber.goal.
418
+ if (typeof readSessionMetadata === 'function') {
419
+ try {
420
+ const metadata = await readSessionMetadata(sessionID);
421
+ const classified = classifyGoal(extractGoalFromSession({ metadata }));
422
+ if (classified) return classified;
423
+ } catch (error) {
424
+ if (signal?.aborted) throw error;
425
+ // Store read failure must not masquerade as empty goal / success.
426
+ return { outcome: 'busy' };
427
+ }
428
+ return { outcome: 'busy' };
344
429
  }
345
- return { outcome: 'busy' };
346
- }
347
430
 
348
- if (typeof client?.session?.status === 'function') {
349
- try {
350
- const statusResult = await client.session.status({
351
- directory: projectPath,
352
- }, requestOptions);
353
- if (!statusResult?.error && statusResult?.data && typeof statusResult.data === 'object') {
354
- const statusValue = statusResult.data[sessionID];
355
- const type = statusValue?.type ?? statusValue?.status;
356
- if (type === 'busy' || type === 'retry') {
357
- return { outcome: 'busy' };
431
+ if (typeof client?.session?.get === 'function') {
432
+ try {
433
+ const sessionResult = await client.session.get({
434
+ sessionID,
435
+ directory: projectPath,
436
+ }, requestOptions);
437
+ if (!sessionResult?.error) {
438
+ const classified = classifyGoal(
439
+ extractGoalFromSession(sessionResult?.data ?? sessionResult),
440
+ );
441
+ if (classified) return classified;
358
442
  }
443
+ } catch (error) {
444
+ if (signal?.aborted) throw error;
359
445
  }
360
- } catch (error) {
361
- if (signal?.aborted) throw error;
362
446
  }
447
+ return { outcome: 'busy' };
363
448
  }
364
449
 
365
- if (typeof client?.session?.messages === 'function') {
450
+ if (typeof client?.session?.active === 'function') {
366
451
  try {
367
- const messagesResult = await client.session.messages({
368
- sessionID,
369
- directory: projectPath,
370
- limit: 50,
371
- }, requestOptions);
372
- if (!messagesResult?.error && Array.isArray(messagesResult?.data)) {
373
- const lastInfo = readMessageInfo(messagesResult.data.at(-1));
374
- if (lastInfo?.role === 'assistant') {
375
- if (lastInfo.error) {
376
- return {
377
- outcome: 'error',
378
- error: formatAssistantError(lastInfo.error),
379
- };
380
- }
381
- if (lastInfo.time?.completed) {
382
- return { outcome: 'success' };
383
- }
384
- }
452
+ const activeMap = await client.session.active(requestOptions);
453
+ if (!(activeMap && typeof activeMap === 'object' && !Array.isArray(activeMap))) {
454
+ // Malformed active payload is unknown — never idle-success.
455
+ return { outcome: 'busy' };
456
+ }
457
+ if (Object.prototype.hasOwnProperty.call(activeMap, sessionID)) {
458
+ return { outcome: 'busy' };
385
459
  }
386
460
  } catch (error) {
387
461
  if (signal?.aborted) throw error;
462
+ // active failure = unknown; do not fall through to message.list success.
463
+ return { outcome: 'busy' };
464
+ }
465
+ }
466
+
467
+ try {
468
+ const messages = await listLatestMessages(client, sessionID, requestOptions);
469
+ if (!messages) {
470
+ return { outcome: 'busy' };
471
+ }
472
+ const classified = classifyMessageTail(messages);
473
+ if (classified.outcome === 'success' || classified.outcome === 'error') {
474
+ return classified;
388
475
  }
476
+ } catch (error) {
477
+ if (signal?.aborted) throw error;
389
478
  }
390
479
 
391
480
  return { outcome: 'busy' };
@@ -393,12 +482,26 @@ const snapshotSessionOutcome = async ({
393
482
 
394
483
  const parseSessionEventPhase = (event) => {
395
484
  const payload = event?.payload?.payload ?? event?.payload;
396
- const properties = payload?.properties;
485
+ // Accept bridge `{ properties }` and native v2 `{ data }` envelopes.
486
+ const properties = (payload?.properties && typeof payload.properties === 'object')
487
+ ? payload.properties
488
+ : ((payload?.data && typeof payload.data === 'object') ? payload.data : null);
397
489
  const sessionID = typeof properties?.sessionID === 'string' ? properties.sessionID : '';
398
490
  if (!sessionID) {
399
491
  return { phase: '', sessionID: '' };
400
492
  }
401
- if (payload?.type === 'session.idle') {
493
+ // Ticket 07: shutdown interrupt is not an idle terminal — keep run open.
494
+ if (payload?.type === 'session.execution.interrupted' && properties?.reason === 'shutdown') {
495
+ return { phase: 'busy', sessionID };
496
+ }
497
+ if (payload?.type === 'session.idle' || payload?.type === 'session.execution.succeeded') {
498
+ return { phase: 'idle', sessionID };
499
+ }
500
+ if (payload?.type === 'session.execution.failed') {
501
+ return { phase: 'error', sessionID };
502
+ }
503
+ if (payload?.type === 'session.execution.interrupted') {
504
+ // user / superseded / inactivity — claim released; treat as idle for settlement.
402
505
  return { phase: 'idle', sessionID };
403
506
  }
404
507
  if (payload?.type === 'session.status') {
@@ -428,6 +531,25 @@ export const createScheduledTasksRuntime = (deps) => {
428
531
  maxGlobalConcurrency = DEFAULT_GLOBAL_CONCURRENCY,
429
532
  maxProjectConcurrency = DEFAULT_PROJECT_CONCURRENCY,
430
533
  maxRunDurationMs = DEFAULT_MAX_RUN_MS,
534
+ /** Host store write: (sessionId, directory, goal) => Promise */
535
+ persistSessionGoal = null,
536
+ /** Host store write: (sessionId, patch) => Promise — required for sidebar isolation */
537
+ persistSessionMetadata = null,
538
+ /**
539
+ * After Host isolation metadata is committed. Used to drop the session
540
+ * from the sidebar index immediately (do not wait for the next list sync).
541
+ * @type {null | ((event: { sessionID: string, directory?: string, metadata: object }) => void)}
542
+ */
543
+ onSystemSessionPersisted = null,
544
+ /** Host store read: (sessionId) => Promise<metadata|null> */
545
+ readSessionMetadata = null,
546
+ /**
547
+ * After a scheduled goal is first persisted, arm the session-goal loop.
548
+ * (session-goal runtime's own progress writes only broadcast — do not
549
+ * feed those back here.)
550
+ * @type {null | ((event: { sessionID: string, directory: string, metadata: object }) => void)}
551
+ */
552
+ onGoalPersisted = null,
431
553
  } = deps;
432
554
 
433
555
  let started = false;
@@ -702,44 +824,56 @@ export const createScheduledTasksRuntime = (deps) => {
702
824
  + '\n</system-reminder>';
703
825
  };
704
826
 
705
- const buildPromptAsyncPayload = (task, projectPath) => ({
827
+ const buildSessionCreateInput = (task, projectPath, title, metadata) => ({
828
+ title,
829
+ location: { directory: projectPath },
830
+ metadata,
831
+ ...(task.execution.agent ? { agent: task.execution.agent } : {}),
706
832
  model: {
833
+ id: task.execution.modelID,
707
834
  providerID: task.execution.providerID,
708
- modelID: task.execution.modelID,
835
+ ...(task.execution.variant ? { variant: task.execution.variant } : {}),
709
836
  },
710
- ...(task.execution.agent ? { agent: task.execution.agent } : {}),
711
- ...(task.execution.variant ? { variant: task.execution.variant } : {}),
712
- parts: [
713
- {
714
- type: 'text',
715
- text: expandSnippets(task.execution.prompt, projectPath),
716
- },
717
- ...(task.execution.goalEnabled
718
- ? [{ type: 'text', text: buildGoalIntroText(task.execution.goalTokenBudget), synthetic: true }]
719
- : []),
720
- ],
721
837
  });
722
838
 
723
- // Scheduled goal runs: stamp the goal onto the fresh session's metadata
724
- // before the prompt goes out; the session-goal runtime picks the loop up
725
- // from session events like any other goal.
839
+ const persistScheduledTaskIsolation = async (sessionID, directory, metadata) => {
840
+ if (typeof persistSessionMetadata !== 'function') {
841
+ throw new Error('scheduled-task session metadata persist is not configured');
842
+ }
843
+ await persistSessionMetadata(sessionID, metadata);
844
+ try {
845
+ onSystemSessionPersisted?.({ sessionID, directory, metadata });
846
+ } catch {
847
+ // Index/live hide is compensatory; Host persist is the authority.
848
+ }
849
+ };
850
+
851
+ const buildPromptText = (task, projectPath) => {
852
+ const promptText = expandSnippets(task.execution.prompt, projectPath);
853
+ if (!task.execution.goalEnabled) {
854
+ return promptText;
855
+ }
856
+ return `${promptText}\n${buildGoalIntroText(task.execution.goalTokenBudget)}`;
857
+ };
858
+
859
+ // Scheduled goal runs: write the file-backed objective, then persist a
860
+ // trackable goal record into the Host session-metadata store (same seam as
861
+ // manual UI goals). OpenCode SessionInfo has no openchamber.goal channel.
726
862
  const createTaskGoal = async ({
727
- baseUrl,
728
- authHeaders,
729
863
  sessionID,
730
864
  projectPath,
731
865
  task,
732
- scheduledTaskMarker,
733
866
  signal,
734
867
  }) => {
735
868
  signal?.throwIfAborted?.();
736
- const now = Date.now();
737
- // File-backed objective keyed by session id: metadata stays light, the
738
- // full expanded prompt lives under the OpenChamber data dir. If the file
739
- // write fails, fall back to an inline (clamped) objective.
740
- // Oversized prompts are distilled into audit criteria by the small model
741
- // (the working agent gets the full prompt in chat anyway); on distill
742
- // failure a head+tail excerpt keeps intent and acceptance criteria.
869
+ if (typeof persistSessionGoal !== 'function') {
870
+ throw new Error('session goal persist is not configured');
871
+ }
872
+ // File-backed objective keyed by session id: the full expanded prompt
873
+ // lives under the OpenChamber data dir. If the file write fails, fall
874
+ // back to an inline objective so parseGoalMetadata still accepts the
875
+ // record. Oversized prompts are distilled into audit criteria by the
876
+ // small model; on distill failure a head+tail excerpt keeps intent.
743
877
  let objectiveText = expandSnippets(task.execution.prompt, projectPath);
744
878
  if (objectiveText.length > 5000) {
745
879
  let distilled = null;
@@ -788,205 +922,142 @@ export const createScheduledTasksRuntime = (deps) => {
788
922
  try {
789
923
  const { writeObjective } = await import('../session-goal/objectives.js');
790
924
  await writeObjective(sessionID, objectiveText);
791
- signal?.throwIfAborted?.();
792
925
  objectiveFile = true;
926
+ signal?.throwIfAborted?.();
793
927
  } catch (error) {
794
928
  if (signal?.aborted) {
795
929
  throw error;
796
930
  }
797
- console.warn('[scheduled-tasks] goal objective file write failed, falling back to inline:', error?.message || error);
931
+ console.warn('[scheduled-tasks] goal objective file write failed, continuing with inline objective:', error?.message || error);
798
932
  }
799
933
  signal?.throwIfAborted?.();
934
+
935
+ const budgetRaw = task.execution?.goalTokenBudget;
936
+ const tokenBudget = Number.isFinite(budgetRaw) && budgetRaw > 0
937
+ ? Math.floor(budgetRaw)
938
+ : null;
939
+ const now = Date.now();
800
940
  const goal = {
801
- id: `${now.toString(36)}${Math.random().toString(36).slice(2, 8)}`,
802
- objective: objectiveFile ? '' : objectiveText.slice(0, 5000),
803
- objectiveFile,
941
+ id: crypto.randomUUID(),
804
942
  status: 'active',
805
- tokenBudget: task.execution.goalTokenBudget || null,
943
+ // File-backed: flag only (text lives under goals/<sessionId>.md).
944
+ // Inline fallback: full objective text when the file write failed.
945
+ objective: objectiveFile ? '' : objectiveText,
946
+ objectiveFile,
947
+ tokenBudget,
806
948
  tokensUsed: 0,
807
949
  turnsUsed: 0,
808
950
  blockedStreak: 0,
809
951
  note: '',
810
952
  statusReason: '',
811
953
  lastAccountedMessageID: '',
954
+ executionGeneration: 0,
812
955
  createdAt: now,
813
956
  updatedAt: now,
814
957
  };
815
- const url = new URL(`${baseUrl}/session/${encodeURIComponent(sessionID)}`);
816
- url.searchParams.set('directory', projectPath);
817
- // Preserve the scheduledTask marker when goal metadata is patched; a full
818
- // openchamber replace would otherwise drop ownership for history/UI.
819
- const response = await fetch(url.toString(), {
820
- method: 'PATCH',
821
- headers: {
822
- ...authHeaders,
823
- 'content-type': 'application/json',
824
- accept: 'application/json',
825
- },
826
- body: JSON.stringify({
827
- metadata: {
828
- openchamber: {
829
- ...(scheduledTaskMarker ? { scheduledTask: scheduledTaskMarker } : {}),
830
- goal,
831
- },
832
- },
833
- }),
834
- ...(signal ? { signal } : {}),
835
- });
836
- if (!response.ok) {
837
- throw new Error(`goal metadata patch failed (${response.status})`);
838
- }
839
- };
840
958
 
841
- const archiveSessionBeforePrompt = async ({ client, sessionID, projectPath, signal }) => {
842
- const archivePayload = {
843
- sessionID,
844
- directory: projectPath,
845
- time: { archived: Date.now() },
846
- };
847
- const requestOptions = signal ? { signal } : undefined;
848
- let result = await client.session.update(archivePayload, requestOptions);
849
- if (isMissingSessionError(result)) {
850
- await sleep(ARCHIVE_404_RETRY_DELAY_MS, signal);
851
- signal?.throwIfAborted?.();
852
- result = await client.session.update(archivePayload, requestOptions);
853
- }
854
- if (result?.error || (result?.data == null && result?.response?.ok === false)) {
855
- const status = result?.error?.status ?? result?.error?.statusCode ?? result?.status;
856
- const message = result?.error?.message || result?.error?.data?.message || 'session archive failed';
857
- throw new Error(`session archive failed${status ? ` (${status})` : ''}: ${message}`);
959
+ // Persist failure must throw — never prompt as if the goal was registered.
960
+ const persisted = await persistSessionGoal(sessionID, projectPath, goal);
961
+ signal?.throwIfAborted?.();
962
+
963
+ if (typeof onGoalPersisted === 'function') {
964
+ try {
965
+ // Prefer full metadata when the seam returns the store object; otherwise
966
+ // synthesize the openchamber.goal shape the session-goal loop expects.
967
+ const metadata = persisted
968
+ && typeof persisted === 'object'
969
+ && !Array.isArray(persisted)
970
+ && persisted.openchamber
971
+ ? persisted
972
+ : { openchamber: { goal } };
973
+ onGoalPersisted({
974
+ sessionID,
975
+ directory: projectPath,
976
+ metadata,
977
+ });
978
+ } catch (error) {
979
+ console.warn('[scheduled-tasks] onGoalPersisted failed:', error?.message || error);
980
+ }
858
981
  }
859
982
  };
860
983
 
861
- const runPromptAsync = async ({ baseUrl, authHeaders, sessionID, projectPath, task, signal }) => {
984
+ const runSessionPrompt = async ({ client, sessionID, projectPath, task, signal }) => {
862
985
  signal?.throwIfAborted?.();
863
- const promptUrl = new URL(`${baseUrl}/session/${encodeURIComponent(sessionID)}/prompt_async`);
864
- promptUrl.searchParams.set('directory', projectPath);
865
- const response = await fetch(promptUrl.toString(), {
866
- method: 'POST',
867
- headers: {
868
- ...authHeaders,
869
- 'content-type': 'application/json',
870
- accept: 'application/json',
871
- },
872
- body: JSON.stringify(buildPromptAsyncPayload(task, projectPath)),
873
- ...(signal ? { signal } : {}),
874
- });
875
-
876
- if (!response.ok) {
877
- const body = await response.text().catch(() => '');
878
- throw new Error(`prompt_async failed (${response.status})${body ? `: ${body}` : ''}`);
879
- }
986
+ const requestOptions = signal ? { signal } : undefined;
987
+ await client.session.prompt({
988
+ sessionID,
989
+ text: buildPromptText(task, projectPath),
990
+ }, requestOptions);
880
991
  };
881
992
 
882
993
  /**
883
- * prompt_async / command return when the turn is *admitted*, not when the
884
- * agent finishes. History must reflect the real session (or goal) outcome and
885
- * wall-clock duration — poll OpenCode until idle+settled or a terminal goal.
994
+ * session.prompt / command return when the turn is *admitted*, not when the
995
+ * agent finishes. History must reflect the real session outcome and
996
+ * wall-clock duration — poll until idle+settled on a latest-side message
997
+ * page (order=desc). finish tool-calls and incomplete assistants never
998
+ * succeed; two incomplete idle probes must not invent success.
999
+ * session.active / message.list failures stay unknown — never idle or empty
1000
+ * success. Goal-enabled runs that passed admission still settle on the
1001
+ * assistant tail here when Host goal state is unavailable.
886
1002
  */
887
1003
  const waitForRunOutcome = async ({
888
1004
  client,
889
1005
  sessionID,
890
- projectPath,
891
- goalEnabled,
892
1006
  signal,
893
1007
  }) => {
894
1008
  const requestOptions = signal ? { signal } : undefined;
895
- let incompleteAssistantProbes = 0;
896
1009
  let emptyIdleProbes = 0;
897
1010
 
898
1011
  for (;;) {
899
1012
  signal?.throwIfAborted?.();
900
1013
 
901
- if (goalEnabled && typeof client?.session?.get === 'function') {
1014
+ let sessionBusy = null;
1015
+ if (typeof client?.session?.active === 'function') {
902
1016
  try {
903
- const sessionResult = await client.session.get({
904
- sessionID,
905
- directory: projectPath,
906
- }, requestOptions);
907
- if (!sessionResult?.error) {
908
- const goal = extractGoalFromSession(sessionResult?.data);
909
- if (goal && GOAL_TERMINAL_STATUSES.has(goal.status)) {
910
- if (goal.status === 'complete') {
911
- return { outcome: 'success' };
912
- }
913
- return {
914
- outcome: 'error',
915
- error: goal.note || `goal ${goal.status}`,
916
- };
917
- }
1017
+ const activeMap = await client.session.active(requestOptions);
1018
+ if (activeMap && typeof activeMap === 'object' && !Array.isArray(activeMap)) {
1019
+ sessionBusy = Object.prototype.hasOwnProperty.call(activeMap, sessionID);
918
1020
  }
1021
+ // Malformed / thrown active stays null → unknown, keep polling.
919
1022
  } catch (error) {
920
1023
  if (signal?.aborted) throw error;
921
- // Transient get failures: keep polling until watchdog aborts.
922
1024
  }
923
1025
  }
924
1026
 
925
- let sessionBusy = false;
926
- if (typeof client?.session?.status === 'function') {
927
- try {
928
- const statusResult = await client.session.status({
929
- directory: projectPath,
930
- }, requestOptions);
931
- if (!statusResult?.error && statusResult?.data && typeof statusResult.data === 'object') {
932
- const statusValue = statusResult.data[sessionID];
933
- const type = statusValue?.type ?? statusValue?.status;
934
- sessionBusy = type === 'busy' || type === 'retry';
935
- }
936
- } catch (error) {
937
- if (signal?.aborted) throw error;
1027
+ if (sessionBusy !== false) {
1028
+ if (sessionBusy === true) {
1029
+ emptyIdleProbes = 0;
938
1030
  }
939
- }
940
-
941
- if (sessionBusy) {
942
- incompleteAssistantProbes = 0;
943
- emptyIdleProbes = 0;
944
1031
  await sleep(SESSION_SETTLEMENT_POLL_MS, signal);
945
1032
  continue;
946
1033
  }
947
1034
 
948
- // Non-goal runs settle on the first idle assistant tail. Goal runs only
949
- // finish via terminal goal status above — idle between goal turns is normal.
950
- if (!goalEnabled && typeof client?.session?.messages === 'function') {
951
- try {
952
- const messagesResult = await client.session.messages({
953
- sessionID,
954
- directory: projectPath,
955
- limit: 50,
956
- }, requestOptions);
957
- if (!messagesResult?.error && Array.isArray(messagesResult?.data)) {
958
- const lastInfo = readMessageInfo(messagesResult.data.at(-1));
959
- if (lastInfo?.role === 'assistant') {
960
- emptyIdleProbes = 0;
961
- if (lastInfo.error) {
962
- return {
963
- outcome: 'error',
964
- error: formatAssistantError(lastInfo.error),
965
- };
966
- }
967
- if (lastInfo.time?.completed) {
968
- return { outcome: 'success' };
969
- }
970
- incompleteAssistantProbes += 1;
971
- if (incompleteAssistantProbes >= INCOMPLETE_ASSISTANT_SETTLE_PROBES) {
972
- return { outcome: 'success' };
973
- }
974
- } else {
975
- incompleteAssistantProbes = 0;
976
- emptyIdleProbes += 1;
977
- // Prompt admitted but no assistant yet, or aborted before reply.
978
- // Allow several idle probes so we don't race the first token.
979
- if (emptyIdleProbes >= 5 && lastInfo?.role === 'user') {
980
- return {
981
- outcome: 'error',
982
- error: 'session ended without assistant response',
983
- };
984
- }
1035
+ try {
1036
+ const messages = await listLatestMessages(client, sessionID, requestOptions);
1037
+ if (messages) {
1038
+ const classified = classifyMessageTail(messages);
1039
+ if (classified.outcome === 'success' || classified.outcome === 'error') {
1040
+ return classified;
1041
+ }
1042
+ if (classified.outcome === 'awaiting_assistant') {
1043
+ emptyIdleProbes += 1;
1044
+ // Prompt admitted but no assistant yet, or aborted before reply.
1045
+ // Allow several idle probes so we don't race the first token.
1046
+ if (emptyIdleProbes >= 5) {
1047
+ return {
1048
+ outcome: 'error',
1049
+ error: 'session ended without assistant response',
1050
+ };
985
1051
  }
1052
+ } else {
1053
+ // busy / unknown tails (tool-calls, incomplete, model-switch, …)
1054
+ // keep polling — never promote incomplete idle to success.
1055
+ emptyIdleProbes = 0;
986
1056
  }
987
- } catch (error) {
988
- if (signal?.aborted) throw error;
989
1057
  }
1058
+ } catch (error) {
1059
+ if (signal?.aborted) throw error;
1060
+ // message.list failure stays unknown — keep polling under the watchdog.
990
1061
  }
991
1062
 
992
1063
  await sleep(SESSION_SETTLEMENT_POLL_MS, signal);
@@ -1003,7 +1074,9 @@ export const createScheduledTasksRuntime = (deps) => {
1003
1074
  const requestOptions = signal ? { signal } : undefined;
1004
1075
  let commands = [];
1005
1076
  try {
1006
- const response = await client.command.list({ directory: projectPath }, requestOptions);
1077
+ const response = await client.command.list({
1078
+ location: { directory: projectPath },
1079
+ }, requestOptions);
1007
1080
  commands = Array.isArray(response?.data) ? response.data : [];
1008
1081
  } catch (error) {
1009
1082
  if (signal?.aborted) {
@@ -1020,28 +1093,30 @@ export const createScheduledTasksRuntime = (deps) => {
1020
1093
 
1021
1094
  await client.session.command({
1022
1095
  sessionID,
1023
- directory: projectPath,
1024
1096
  command: parsed.command,
1025
1097
  arguments: parsed.arguments,
1026
1098
  ...(task.execution.agent ? { agent: task.execution.agent } : {}),
1027
- model: `${task.execution.providerID}/${task.execution.modelID}`,
1028
- ...(task.execution.variant ? { variant: task.execution.variant } : {}),
1099
+ model: {
1100
+ id: task.execution.modelID,
1101
+ providerID: task.execution.providerID,
1102
+ ...(task.execution.variant ? { variant: task.execution.variant } : {}),
1103
+ },
1029
1104
  }, requestOptions);
1030
1105
 
1031
1106
  return true;
1032
1107
  };
1033
1108
 
1034
- const abortCreatedSessionBestEffort = async ({ client, sessionID, projectPath }) => {
1035
- if (!sessionID || !client?.session?.abort) {
1109
+ const interruptCreatedSessionBestEffort = async ({ client, sessionID }) => {
1110
+ if (!sessionID || typeof client?.session?.interrupt !== 'function') {
1036
1111
  return;
1037
1112
  }
1038
1113
  try {
1039
- await client.session.abort({
1114
+ await client.session.interrupt({
1040
1115
  sessionID,
1041
- directory: projectPath,
1116
+ continue: false,
1042
1117
  });
1043
1118
  } catch {
1044
- // Never let upstream abort failure replace the watchdog timeout error.
1119
+ // Never let upstream interrupt failure replace the watchdog timeout error.
1045
1120
  }
1046
1121
  };
1047
1122
 
@@ -1055,6 +1130,24 @@ export const createScheduledTasksRuntime = (deps) => {
1055
1130
 
1056
1131
  signal?.throwIfAborted?.();
1057
1132
 
1133
+ // Goal-enabled tasks: refuse before session create / objective write /
1134
+ // prompt when Host goal state is unavailable OR persist is not wired.
1135
+ // Config + run-history stay; only this run is recorded as error (no
1136
+ // false first-turn success / no create·distill·prompt side effects).
1137
+ if (task.execution?.goalEnabled) {
1138
+ const capability = getSessionGoalCapability();
1139
+ if (!capability.supported) {
1140
+ throw new Error(sessionGoalUnavailableMessage(capability));
1141
+ }
1142
+ if (typeof persistSessionGoal !== 'function') {
1143
+ throw new Error('session goal persist is not configured');
1144
+ }
1145
+ }
1146
+
1147
+ if (typeof persistSessionMetadata !== 'function') {
1148
+ throw new Error('scheduled-task session metadata persist is not configured');
1149
+ }
1150
+
1058
1151
  if (typeof waitForOpenCodeReady === 'function') {
1059
1152
  await waitForOpenCodeReady(10_000, 250);
1060
1153
  signal?.throwIfAborted?.();
@@ -1062,26 +1155,16 @@ export const createScheduledTasksRuntime = (deps) => {
1062
1155
 
1063
1156
  const baseUrl = buildOpenCodeUrl('/', '').replace(/\/$/, '');
1064
1157
  const authHeaders = getOpenCodeAuthHeaders();
1065
- const client = createOpencodeClient({
1158
+ const client = makeOpenCodeV2Client({
1066
1159
  baseUrl,
1067
- headers: authHeaders,
1160
+ authHeaders,
1068
1161
  });
1069
1162
  const requestOptions = signal ? { signal } : undefined;
1070
1163
 
1071
- const taskName = typeof task?.name === 'string' && task.name.trim().length > 0
1072
- ? task.name.trim()
1073
- : 'Schedule';
1074
- const scheduledTaskMarker = {
1075
- projectID,
1076
- taskID: task.id,
1077
- runID,
1078
- name: taskName,
1079
- };
1080
-
1081
1164
  let sessionID;
1082
- // Watchdog abort must fire session.abort even while awaiting non-cancellable
1083
- // work (small-model distill / writeObjective). Register once after create;
1084
- // never on ordinary non-timeout failures or success.
1165
+ // Watchdog abort must fire session.interrupt even while awaiting
1166
+ // non-cancellable work (small-model distill / writeObjective). Register
1167
+ // once after create; never on ordinary non-timeout failures or success.
1085
1168
  let sessionAbortStarted = false;
1086
1169
  let onWatchdogAbort = null;
1087
1170
  const triggerSessionAbortOnce = () => {
@@ -1089,7 +1172,7 @@ export const createScheduledTasksRuntime = (deps) => {
1089
1172
  return;
1090
1173
  }
1091
1174
  sessionAbortStarted = true;
1092
- void abortCreatedSessionBestEffort({ client, sessionID, projectPath });
1175
+ void interruptCreatedSessionBestEffort({ client, sessionID });
1093
1176
  };
1094
1177
  const clearWatchdogAbortListener = () => {
1095
1178
  if (onWatchdogAbort && signal) {
@@ -1099,15 +1182,21 @@ export const createScheduledTasksRuntime = (deps) => {
1099
1182
  };
1100
1183
 
1101
1184
  try {
1102
- const sessionResponse = await client.session.create({
1103
- directory: projectPath,
1104
- title,
1105
- metadata: buildScheduledTaskMetadata(scheduledTaskMarker),
1106
- }, requestOptions);
1107
- sessionID = sessionResponse?.data?.id;
1185
+ const isolationMetadata = buildScheduledTaskMetadata({
1186
+ projectID,
1187
+ taskID: task.id,
1188
+ runID,
1189
+ name: task.name,
1190
+ });
1191
+ const session = await client.session.create(
1192
+ buildSessionCreateInput(task, projectPath, title, isolationMetadata),
1193
+ requestOptions,
1194
+ );
1195
+ sessionID = session?.id;
1108
1196
  if (!sessionID) {
1109
1197
  throw new Error('failed to create session');
1110
1198
  }
1199
+ await persistScheduledTaskIsolation(sessionID, projectPath, isolationMetadata);
1111
1200
 
1112
1201
  if (signal) {
1113
1202
  if (signal.aborted) {
@@ -1130,19 +1219,6 @@ export const createScheduledTasksRuntime = (deps) => {
1130
1219
  }
1131
1220
  }
1132
1221
 
1133
- try {
1134
- // Archive must succeed before any goal/prompt/command work. A 404 gets
1135
- // one short bounded retry; any other failure aborts without prompting.
1136
- await archiveSessionBeforePrompt({ client, sessionID, projectPath, signal });
1137
- } catch (archiveError) {
1138
- if (attachFailed) {
1139
- throw new Error(
1140
- `attachSession failed (${safeErrorMessage(attachFailed)}); archive also failed: ${safeErrorMessage(archiveError)}`,
1141
- );
1142
- }
1143
- throw archiveError;
1144
- }
1145
-
1146
1222
  if (attachFailed) {
1147
1223
  throw new Error(`attachSession failed: ${safeErrorMessage(attachFailed)}`);
1148
1224
  }
@@ -1162,12 +1238,9 @@ export const createScheduledTasksRuntime = (deps) => {
1162
1238
 
1163
1239
  if (task.execution.goalEnabled) {
1164
1240
  await createTaskGoal({
1165
- baseUrl,
1166
- authHeaders,
1167
1241
  sessionID,
1168
1242
  projectPath,
1169
1243
  task,
1170
- scheduledTaskMarker,
1171
1244
  signal,
1172
1245
  });
1173
1246
  }
@@ -1182,9 +1255,8 @@ export const createScheduledTasksRuntime = (deps) => {
1182
1255
  signal,
1183
1256
  });
1184
1257
  if (!executedAsCommand) {
1185
- await runPromptAsync({
1186
- baseUrl,
1187
- authHeaders,
1258
+ await runSessionPrompt({
1259
+ client,
1188
1260
  sessionID,
1189
1261
  projectPath,
1190
1262
  task,
@@ -1192,13 +1264,11 @@ export const createScheduledTasksRuntime = (deps) => {
1192
1264
  });
1193
1265
  }
1194
1266
 
1195
- // Do not finalize on admission alone — wait for the real agent turn (or
1196
- // goal loop) so history status/duration match what the user sees in chat.
1267
+ // Do not finalize on admission alone — wait for the real agent turn so
1268
+ // history status/duration match what the user sees in chat.
1197
1269
  const settlement = await waitForRunOutcome({
1198
1270
  client,
1199
1271
  sessionID,
1200
- projectPath,
1201
- goalEnabled: Boolean(task.execution?.goalEnabled),
1202
1272
  signal,
1203
1273
  });
1204
1274
 
@@ -1369,8 +1439,8 @@ export const createScheduledTasksRuntime = (deps) => {
1369
1439
 
1370
1440
  if (raced.timedOut) {
1371
1441
  // Bound: do not await uncancellable steps (distill / writeObjective).
1372
- // session.abort is started by the signal listener on create; later
1373
- // throwIfAborted gates stop goal PATCH / command / prompt.
1442
+ // session.interrupt is started by the signal listener on create;
1443
+ // later throwIfAborted gates stop goal write / command / prompt.
1374
1444
  throw timeoutError;
1375
1445
  }
1376
1446
 
@@ -1701,15 +1771,16 @@ export const createScheduledTasksRuntime = (deps) => {
1701
1771
 
1702
1772
  const baseUrl = buildOpenCodeUrl('/', '').replace(/\/$/, '');
1703
1773
  const authHeaders = getOpenCodeAuthHeaders();
1704
- const client = createOpencodeClient({
1774
+ const client = makeOpenCodeV2Client({
1705
1775
  baseUrl,
1706
- headers: authHeaders,
1776
+ authHeaders,
1707
1777
  });
1708
1778
  const snapshot = await snapshotSessionOutcome({
1709
1779
  client,
1710
1780
  sessionID,
1711
1781
  projectPath,
1712
1782
  goalEnabled: Boolean(task.execution?.goalEnabled),
1783
+ readSessionMetadata,
1713
1784
  });
1714
1785
  if (snapshot.outcome !== 'success') {
1715
1786
  return;